diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index d2914b3b..25b817d5 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -36,13 +36,13 @@ public class TwilioVoiceController : TwilioController string conversationId = $"TwilioVoice_{request.CallSid}"; var twilio = _services.GetRequiredService(); var url = $"twilio/voice/{conversationId}/receive/0?states={states}"; - var response = twilio.ReturnInstructions(new List { "twilio/welcome.mp3" }, url, true, timeout: 1); + var response = twilio.ReturnNoninterruptedInstructions(new List { "twilio/welcome.mp3" }, url, true); return TwiML(response); } [ValidateRequest] [HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")] - public async Task ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, VoiceRequest request) + public async Task ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, [FromQuery] int attempts, VoiceRequest request) { var twilio = _services.GetRequiredService(); var messageQueue = _services.GetRequiredService(); @@ -81,7 +81,21 @@ public class TwilioVoiceController : TwilioController } else { - response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true); + if (attempts >= 3) + { + var speechPaths = new List(); + if (seqNum == 0) + { + speechPaths.Add("twilio/welcome.mp3"); + } + else + { + var lastRepy = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum - 1); + speechPaths.Add($"twilio/voice/speeches/{conversationId}/{lastRepy.SpeechFileName}"); + } + response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true); + } + response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}&attempts={++attempts}", true); } return TwiML(response); @@ -89,7 +103,8 @@ public class TwilioVoiceController : TwilioController [ValidateRequest] [HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")] - public async Task ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, VoiceRequest request) + public async Task ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, + [FromQuery] string states, VoiceRequest request) { var nextSeqNum = seqNum + 1; var sessionManager = _services.GetRequiredService(); @@ -106,6 +121,7 @@ public class TwilioVoiceController : TwilioController if (indication != null) { var speechPaths = new List(); + int segIndex = 0; foreach (var text in indication.Split('|')) { var seg = text.Trim(); @@ -118,17 +134,22 @@ public class TwilioVoiceController : TwilioController var textToSpeechService = CompletionProvider.GetTextToSpeech(_services, "openai", "tts-1"); var fileService = _services.GetRequiredService(); var data = await textToSpeechService.GenerateSpeechFromTextAsync(seg); - var fileName = $"indication_{seqNum}.mp3"; + var fileName = $"indication_{seqNum}_{segIndex}.mp3"; await fileService.SaveSpeechFileAsync(conversationId, fileName, data); speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}"); + segIndex++; } } response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true); + await sessionManager.RemoveReplyIndicationAsync(conversationId, seqNum); } else { - int audioIndex = Random.Shared.Next(1, 4); - response = twilio.ReturnInstructions(new List { $"{_settings.CallbackHost}/twilio/typing-{audioIndex}.mp3" }, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true, 1); + response = twilio.ReturnInstructions(new List + { + $"twilio/hold-on-{Random.Shared.Next(1, 5)}.mp3", + $"twilio/typing-{Random.Shared.Next(2, 4)}.mp3" + }, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true); } } else diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/ITwilioSessionManager.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/ITwilioSessionManager.cs index b1acd298..3651a6ec 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/ITwilioSessionManager.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/ITwilioSessionManager.cs @@ -11,5 +11,6 @@ namespace BotSharp.Plugin.Twilio.Services Task> RetrieveStagedCallerMessagesAsync(string conversationId, int seqNum); Task SetReplyIndicationAsync(string conversationId, int seqNum, string indication); Task GetReplyIndicationAsync(string conversationId, int seqNum); + Task RemoveReplyIndicationAsync(string conversationId, int seqNum); } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs index 19e0ce68..3338c46d 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioMessageQueueService.cs @@ -82,7 +82,7 @@ namespace BotSharp.Plugin.Twilio.Services { reply = new AssistantMessage() { - ConversationEnd = msg.Instruction.ConversationEnd, + ConversationEnd = msg.Instruction?.ConversationEnd ?? false, Content = msg.Content, MessageId = msg.MessageId }; diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs index 5ccdc7db..ca489682 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs @@ -76,8 +76,8 @@ public class TwilioService }, Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"), SpeechModel = Gather.SpeechModelEnum.PhoneCall, - SpeechTimeout = timeout > 0 ? timeout.ToString() : "3", - Timeout = timeout > 0 ? timeout : 3, + SpeechTimeout = timeout > 0 ? timeout.ToString() : "2", + Timeout = timeout > 0 ? timeout : 2, ActionOnEmptyResult = actionOnEmptyResult }; if (speechPaths != null && speechPaths.Any()) @@ -91,6 +91,33 @@ public class TwilioService return response; } + public VoiceResponse ReturnNoninterruptedInstructions(List speechPaths, string callbackPath, bool actionOnEmptyResult, int timeout = 2) + { + var response = new VoiceResponse(); + if (speechPaths != null && speechPaths.Any()) + { + foreach (var speechPath in speechPaths) + { + response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}")); + } + } + var gather = new Gather() + { + Input = new List() + { + Gather.InputEnum.Speech, + Gather.InputEnum.Dtmf + }, + Action = new Uri($"{_settings.CallbackHost}/{callbackPath}"), + SpeechModel = Gather.SpeechModelEnum.PhoneCall, + SpeechTimeout = timeout > 0 ? timeout.ToString() : "2", + Timeout = timeout > 0 ? timeout : 2, + ActionOnEmptyResult = actionOnEmptyResult + }; + response.Append(gather); + return response; + } + public VoiceResponse HangUp(string speechPath) { var response = new VoiceResponse(); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioSessionManager.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioSessionManager.cs index eae0b238..40924fba 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioSessionManager.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioSessionManager.cs @@ -59,5 +59,12 @@ namespace BotSharp.Plugin.Twilio.Services var key = $"{conversationId}:Indication:{seqNum}"; return await db.StringGetAsync(key); } + + public async Task RemoveReplyIndicationAsync(string conversationId, int seqNum) + { + var db = _redis.GetDatabase(); + var key = $"{conversationId}:Indication:{seqNum}"; + await db.KeyDeleteAsync(key); + } } }