Add Twilio Hook

This commit is contained in:
Haiping Chen 2024-11-12 16:51:33 -06:00
parent 421fbb5956
commit 8b645a084a
13 changed files with 374 additions and 82 deletions

View file

@ -10,6 +10,13 @@ public interface IConversationHook
Conversation Conversation { get; }
IConversationHook SetConversation(Conversation conversation);
/// <summary>
/// Get the predifined intent for the conversation.
/// It will send to the conversation context to help LLM to understand the user's intent.
/// </summary>
/// <returns></returns>
Task<string> GetConversationIntent() => Task.FromResult(string.Empty);
/// <summary>
/// Triggered when user connects with agent first time.
/// This hook is the good timing to show welcome infomation.

View file

@ -0,0 +1,6 @@
namespace BotSharp.Abstraction.Infrastructures;
public class HookEmitOption
{
public bool OnlyOnce { get; set; }
}

View file

@ -4,11 +4,12 @@ namespace BotSharp.Core.Infrastructures;
public static class HookEmitter
{
public static HookEmittedResult Emit<T>(IServiceProvider services, Action<T> action)
public static HookEmittedResult Emit<T>(IServiceProvider services, Action<T> action, HookEmitOption? option = null)
{
var logger = services.GetRequiredService<ILogger<T>>();
var result = new HookEmittedResult();
var hooks = services.GetServices<T>();
option = option ?? new();
foreach (var hook in hooks)
{
@ -16,6 +17,11 @@ public static class HookEmitter
{
logger.LogInformation($"Emit hook action on {action.Method.Name}({hook.GetType().Name})");
action(hook);
if (option.OnlyOnce)
{
break;
}
}
catch (Exception ex)
{
@ -26,11 +32,12 @@ public static class HookEmitter
return result;
}
public static async Task<HookEmittedResult> Emit<T>(IServiceProvider services, Func<T, Task> action)
public static async Task<HookEmittedResult> Emit<T>(IServiceProvider services, Func<T, Task> action, HookEmitOption? option = null)
{
var logger = services.GetRequiredService<ILogger<T>>();
var result = new HookEmittedResult();
var hooks = services.GetServices<T>();
option = option ?? new();
foreach (var hook in hooks)
{
@ -38,6 +45,11 @@ public static class HookEmitter
{
logger.LogInformation($"Emit hook action on {action.Method.Name}({hook.GetType().Name})");
await action(hook);
if (option.OnlyOnce)
{
break;
}
}
catch (Exception ex)
{

View file

@ -1,5 +1,7 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Infrastructures;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using BotSharp.Plugin.Twilio.Services;
using Microsoft.AspNetCore.Http;
@ -32,122 +34,202 @@ public class TwilioVoiceController : TwilioController
/// <exception cref="ArgumentNullException"></exception>
[ValidateRequest]
[HttpPost("twilio/voice/welcome")]
public async Task<TwiMLResult> InitiateConversation(VoiceRequest request, [FromQuery] string[] states, [FromQuery] string intent)
public async Task<TwiMLResult> InitiateConversation(ConversationalVoiceRequest request)
{
var text = JsonSerializer.Serialize(request);
if (request?.CallSid == null)
{
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
}
string conversationId = $"TwilioVoice_{request.CallSid}";
var twilio = _services.GetRequiredService<TwilioService>();
VoiceResponse response;
if (string.IsNullOrWhiteSpace(intent))
VoiceResponse response = null;
request.ConversationId = $"TwilioVoice_{request.CallSid}";
var instruction = new ConversationalVoiceResponse
{
var url = $"twilio/voice/{conversationId}/receive/0?{GenerateStatesParameter(states)}";
response = twilio.ReturnNoninterruptedInstructions(new List<string> { "twilio/welcome.mp3" }, url, true, timeout: 2);
SpeechPaths = ["twilio/welcome.mp3"],
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/0?{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true,
Timeout = 2
};
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreating(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
var twilio = _services.GetRequiredService<TwilioService>();
if (string.IsNullOrWhiteSpace(request.Intent))
{
response = twilio.ReturnNoninterruptedInstructions(instruction);
}
else
{
int seqNum = 0;
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, intent);
await sessionManager.StageCallerMessageAsync(request.ConversationId, seqNum, request.Intent);
var callerMessage = new CallerMessage()
{
ConversationId = conversationId,
ConversationId = request.ConversationId,
SeqNumber = seqNum,
Content = intent,
Content = request.Intent,
From = request.From,
States = ParseStates(states)
States = ParseStates(request.States)
};
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
response = new VoiceResponse();
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{seqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
}
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreated(request);
}, new HookEmitOption
{
OnlyOnce = true
});
return TwiML(response);
}
/// <summary>
/// Wait for caller's response
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
public async Task<TwiMLResult> ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string[] states, VoiceRequest request, [FromQuery] int attempts = 1)
public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest request)
{
var twilio = _services.GetRequiredService<TwilioService>();
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(conversationId, seqNum);
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(request.ConversationId, request.SeqNum);
string text = (request.SpeechResult + "\r\n" + request.Digits).Trim();
if (!string.IsNullOrWhiteSpace(text))
{
messages.Add(text);
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, text);
await sessionManager.StageCallerMessageAsync(request.ConversationId, request.SeqNum, text);
}
VoiceResponse response;
VoiceResponse response = null;
if (messages.Any())
{
var messageContent = string.Join("\r\n", messages);
var callerMessage = new CallerMessage()
{
ConversationId = conversationId,
SeqNumber = seqNum,
ConversationId = request.ConversationId,
SeqNumber = request.SeqNum,
Content = messageContent,
Digits = request.Digits,
From = request.From,
States = ParseStates(states)
States = ParseStates(request.States)
};
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
response = new VoiceResponse();
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnReceivedUserMessage(request);
}, new HookEmitOption
{
OnlyOnce = true
});
}
else
{
if (attempts >= 2)
// keep waiting for user response
if (request.Attempts > 2)
{
var speechPaths = new List<string>();
if (seqNum == 0)
var instruction = new ConversationalVoiceResponse
{
speechPaths.Add("twilio/welcome.mp3");
SpeechPaths = new List<string>(),
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true
};
// prompt user to speak clearly
if (request.SeqNum == 0)
{
instruction.SpeechPaths.Add("twilio/welcome.mp3");
}
else
{
var lastRepy = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum - 1);
speechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{lastRepy.SpeechFileName}");
var lastRepy = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.SeqNum - 1);
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{lastRepy.SpeechFileName}");
}
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}", true);
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnWaitingUserResponse(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
}
else
{
response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}&attempts={++attempts}", true);
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = new List<string>(),
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
ActionOnEmptyResult = true
};
if (request.Attempts == 2)
{
instruction.SpeechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
}
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnWaitingUserResponse(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
}
}
return TwiML(response);
}
/// <summary>
/// Polling for assistant reply after user responsed
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
public async Task<TwiMLResult> ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum,
[FromQuery] string[] states, VoiceRequest request)
public async Task<TwiMLResult> ReplyCallerMessage(ConversationalVoiceRequest request)
{
var nextSeqNum = seqNum + 1;
var nextSeqNum = request.SeqNum + 1;
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
var twilio = _services.GetRequiredService<TwilioService>();
var fileStorage = _services.GetRequiredService<IFileStorageService>();
if (request.SpeechResult != null)
{
await sessionManager.StageCallerMessageAsync(conversationId, nextSeqNum, request.SpeechResult);
await sessionManager.StageCallerMessageAsync(request.ConversationId, nextSeqNum, request.SpeechResult);
}
var reply = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum);
var reply = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.SeqNum);
VoiceResponse response;
if (reply == null)
{
var indication = await sessionManager.GetReplyIndicationAsync(conversationId, seqNum);
var indication = await sessionManager.GetReplyIndicationAsync(request.ConversationId, request.SeqNum);
if (indication != null)
{
_logger.LogWarning($"Indication: {indication}");
@ -172,9 +254,9 @@ public class TwilioVoiceController : TwilioController
speechPaths.Add($"twilio/hold-on-short-{holdOnIndex}.mp3");
}
var fileName = $"indication_{seqNum}_{segIndex}.mp3";
fileStorage.SaveSpeechFile(conversationId, fileName, data);
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}");
var fileName = $"indication_{request.SeqNum}_{segIndex}.mp3";
fileStorage.SaveSpeechFile(request.ConversationId, fileName, data);
speechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{fileName}");
// add typing
var typingIndex = Random.Shared.Next(1, 7);
@ -185,8 +267,25 @@ public class TwilioVoiceController : TwilioController
segIndex++;
}
}
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
await sessionManager.RemoveReplyIndicationAsync(conversationId, seqNum);
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = speechPaths,
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true
};
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnIndicationGenerated(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
await sessionManager.RemoveReplyIndicationAsync(request.ConversationId, request.SeqNum);
}
else
{
@ -208,25 +307,69 @@ public class TwilioVoiceController : TwilioController
instructions.Add($"twilio/typing-{typingIndex}.mp3");
}
response = twilio.ReturnInstructions(instructions, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = instructions,
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true
};
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnWaitingAgentResponse(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
}
}
else
{
if (reply.HumanIntervationNeeded)
{
response = twilio.DialCsrAgent($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}");
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnAgentTransferring(request, _settings);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.DialCsrAgent($"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}");
}
else if (reply.ConversationEnd)
{
response = twilio.HangUp($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}");
response = twilio.HangUp($"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}");
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnAgentHangUp(request);
}, new HookEmitOption
{
OnlyOnce = true
});
}
else
{
response = twilio.ReturnInstructions(new List<string>
var instruction = new ConversationalVoiceResponse
{
$"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}"
}, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(states)}", true, hints: reply.Hints);
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true,
Hints = reply.Hints
};
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnAgentResponsing(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
}
}
@ -246,7 +389,7 @@ public class TwilioVoiceController : TwilioController
return result;
}
private Dictionary<string, string> ParseStates(string[] states)
private Dictionary<string, string> ParseStates(List<string> states)
{
var result = new Dictionary<string, string>();
if (states is null || !states.Any())
@ -264,9 +407,9 @@ public class TwilioVoiceController : TwilioController
return result;
}
private string GenerateStatesParameter(string[] states)
private string GenerateStatesParameter(List<string> states)
{
if (states is null || states.Length == 0)
if (states is null || states.Count == 0)
{
return null;
}

View file

@ -0,0 +1,88 @@
using BotSharp.Plugin.Twilio.Models;
using Task = System.Threading.Tasks.Task;
namespace BotSharp.Plugin.Twilio.Interfaces;
public interface ITwilioSessionHook
{
/// <summary>
/// Before session creating
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnSessionCreating(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
=> Task.CompletedTask;
/// <summary>
/// On session created
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnSessionCreated(ConversationalVoiceRequest request)
=> Task.CompletedTask;
/// <summary>
/// On received user message
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnReceivedUserMessage(ConversationalVoiceRequest request)
=> Task.CompletedTask;
/// <summary>
/// Waiting user response
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnWaitingUserResponse(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
=> Task.CompletedTask;
/// <summary>
/// On agent generated indication
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnIndicationGenerated(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
=> Task.CompletedTask;
/// <summary>
/// Waiting agent response
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnWaitingAgentResponse(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
=> Task.CompletedTask;
/// <summary>
/// Before agent responsing
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnAgentResponsing(ConversationalVoiceRequest request, ConversationalVoiceResponse response)
=> Task.CompletedTask;
/// <summary>
/// On agent hang up
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnAgentHangUp(ConversationalVoiceRequest request)
=> Task.CompletedTask;
/// <summary>
/// Before agent transferred
/// </summary>
/// <param name="request"></param>
/// <param name="response"></param>
/// <returns></returns>
Task OnAgentTransferring(ConversationalVoiceRequest request, TwilioSetting settings)
=> Task.CompletedTask;
}

View file

@ -0,0 +1,15 @@
using BotSharp.Plugin.Twilio.Models;
using Task = System.Threading.Tasks.Task;
namespace BotSharp.Plugin.Twilio.Interfaces;
public interface ITwilioSessionManager
{
Task SetAssistantReplyAsync(string conversationId, int seqNum, AssistantMessage message);
Task<AssistantMessage> GetAssistantReplyAsync(string conversationId, int seqNum);
Task StageCallerMessageAsync(string conversationId, int seqNum, string message);
Task<List<string>> RetrieveStagedCallerMessagesAsync(string conversationId, int seqNum);
Task SetReplyIndicationAsync(string conversationId, int seqNum, string indication);
Task<string> GetReplyIndicationAsync(string conversationId, int seqNum);
Task RemoveReplyIndicationAsync(string conversationId, int seqNum);
}

View file

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;
namespace BotSharp.Plugin.Twilio.Models;
public class ConversationalVoiceRequest : VoiceRequest
{
[FromRoute]
public string ConversationId { get; set; }
[FromRoute]
public int SeqNum { get; set; }
public int Attempts { get; set; } = 1;
public string Intent { get; set; }
public List<string> States { get; set; } = [];
}

View file

@ -0,0 +1,15 @@
namespace BotSharp.Plugin.Twilio.Models;
public class ConversationalVoiceResponse
{
public List<string> SpeechPaths { get; set; } = [];
public string CallbackPath { get; set; }
public bool ActionOnEmptyResult { get; set; }
/// <summary>
/// Timeout in seconds
/// </summary>
public int Timeout { get; set; } = 3;
public string Hints { get; set; }
}

View file

@ -1,16 +0,0 @@
using BotSharp.Plugin.Twilio.Models;
using Task = System.Threading.Tasks.Task;
namespace BotSharp.Plugin.Twilio.Services
{
public interface ITwilioSessionManager
{
Task SetAssistantReplyAsync(string conversationId, int seqNum, AssistantMessage message);
Task<AssistantMessage> GetAssistantReplyAsync(string conversationId, int seqNum);
Task StageCallerMessageAsync(string conversationId, int seqNum, string message);
Task<List<string>> RetrieveStagedCallerMessagesAsync(string conversationId, int seqNum);
Task SetReplyIndicationAsync(string conversationId, int seqNum, string indication);
Task<string> GetReplyIndicationAsync(string conversationId, int seqNum);
Task RemoveReplyIndicationAsync(string conversationId, int seqNum);
}
}

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Routing;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Utilities;
using BotSharp.Plugin.Twilio.Models;
using Twilio.Jwt.AccessToken;
using Token = Twilio.Jwt.AccessToken.Token;
@ -66,7 +67,7 @@ public class TwilioService
return response;
}
public VoiceResponse ReturnInstructions(List<string> speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 3, string hints = null)
public VoiceResponse ReturnInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
{
var response = new VoiceResponse();
var gather = new Gather()
@ -76,17 +77,17 @@ public class TwilioService
Gather.InputEnum.Speech,
Gather.InputEnum.Dtmf
},
Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"),
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
SpeechTimeout = "auto", // timeout > 0 ? timeout.ToString() : "3",
Timeout = timeout > 0 ? timeout : 3,
ActionOnEmptyResult = actionOnEmptyResult,
Hints = hints
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
ActionOnEmptyResult = conversationalVoiceResponse.ActionOnEmptyResult,
Hints = conversationalVoiceResponse.Hints
};
if (!speechPaths.IsNullOrEmpty())
if (!conversationalVoiceResponse.SpeechPaths.IsNullOrEmpty())
{
foreach (var speechPath in speechPaths)
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
{
gather.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
}
@ -95,12 +96,12 @@ public class TwilioService
return response;
}
public VoiceResponse ReturnNoninterruptedInstructions(List<string> speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 3)
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
{
var response = new VoiceResponse();
if (speechPaths != null && speechPaths.Any())
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
{
foreach (var speechPath in speechPaths)
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
{
response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
}
@ -112,11 +113,11 @@ public class TwilioService
Gather.InputEnum.Speech,
Gather.InputEnum.Dtmf
},
Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"),
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
SpeechTimeout = timeout > 0 ? timeout.ToString() : "3",
Timeout = timeout > 0 ? timeout : 3,
ActionOnEmptyResult = actionOnEmptyResult
SpeechTimeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout.ToString() : "3",
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
ActionOnEmptyResult = conversationalVoiceResponse.ActionOnEmptyResult
};
response.Append(gather);
return response;

View file

@ -1,3 +1,4 @@
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
using StackExchange.Redis;
using Task = System.Threading.Tasks.Task;

View file

@ -1,4 +1,5 @@
using BotSharp.Abstraction.Settings;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Services;
using StackExchange.Redis;