diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs index a3a11c8ce..49f13c78b 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioInboundController.cs @@ -52,28 +52,22 @@ public async Task InitiateStreamConversation(ConversationalVoiceReq instruction.SpeechPaths.Add(request.InitAudioFile); } - // Load agent profile - var agentService = _services.GetRequiredService(); - var agent = await agentService.LoadAgent(request.AgentId); - await HookEmitter.Emit(_services, async hook => { await hook.OnSessionCreating(request, instruction); }); - request.ConversationId = await InitConversation(request, agent); + var (agent, conversationId) = await InitConversation(request); + request.ConversationId = conversationId.Id; instruction.AgentId = request.AgentId; instruction.ConversationId = request.ConversationId; - if (request.AnsweredBy == "machine_start" && - request.Direction == "outbound-api") + if (twilio.MachineDetected(request)) { response = new VoiceResponse(); - await HookEmitter.Emit(_services, async hook => - { - await hook.OnVoicemailStarting(request); - }); + await HookEmitter.Emit(_services, + async hook => await hook.OnVoicemailStarting(request)); var url = twilio.GetSpeechPath(request.ConversationId, "voicemail.mp3"); response.Play(new Uri(url)); @@ -141,7 +135,7 @@ protected Dictionary ParseStates(List states) return result; } - private async Task InitConversation(ConversationalVoiceRequest request, Agent agent) + private async Task<(Agent, Conversation)> InitConversation(ConversationalVoiceRequest request) { var convService = _services.GetRequiredService(); var conversation = await convService.GetConversation(request.ConversationId); @@ -167,20 +161,25 @@ private async Task InitConversation(ConversationalVoiceRequest request, new("twilio_call_sid", request.CallSid), }; - // Enable lazy routing mode to optimize realtime experience - if (agent.Profiles.Contains("realtime") && agent.Type == AgentType.Routing) - { - states.Add(new(StateConst.ROUTING_MODE, "lazy")); - } - if (request.InitAudioFile != null) { states.Add(new("init_audio_file", request.InitAudioFile)); } convService.SetConversationId(conversation.Id, states); + + // Load agent profile + var agentService = _services.GetRequiredService(); + var agent = await agentService.LoadAgent(request.AgentId); + + // Enable lazy routing mode to optimize realtime experience + if (agent.Profiles.Contains("realtime") && agent.Type == AgentType.Routing) + { + states.Add(new(StateConst.ROUTING_MODE, "lazy")); + } + convService.SaveStates(); - return conversation.Id; + return (agent, conversation); } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs index 66418761b..4796c02d2 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioOutboundController.cs @@ -29,15 +29,12 @@ public async Task InitiateOutboundCall(ConversationalVoiceRequest r var twilio = _services.GetRequiredService(); VoiceResponse response = default!; - if (request.AnsweredBy == "machine_start" && - request.Direction == "outbound-api") + if (twilio.MachineDetected(request)) { response = new VoiceResponse(); - await HookEmitter.Emit(_services, async hook => - { - await hook.OnVoicemailStarting(request); - }); + await HookEmitter.Emit(_services, + async hook => await hook.OnVoicemailStarting(request)); var url = twilio.GetSpeechPath(request.ConversationId, "voicemail.mp3"); response.Play(new Uri(url)); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index e6cd56123..4f8b7806e 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -332,38 +332,41 @@ public async Task TransferCall(ConversationalVoiceRequest request) [HttpPost("twilio/voice/status")] public async Task PhoneCallStatus(ConversationalVoiceRequest request) { + var twilio = _services.GetRequiredService(); if (request.CallStatus == "completed") { - if (request.AnsweredBy == "machine_start" && - request.Direction == "outbound-api") + if (twilio.MachineDetected(request)) { // voicemail - await HookEmitter.Emit(_services, async hook => - { - await hook.OnVoicemailLeft(request); - }); + await HookEmitter.Emit(_services, + async hook => await hook.OnVoicemailLeft(request)); } else { // phone call completed - await HookEmitter.Emit(_services, x => x.OnUserDisconnected(request)); + await HookEmitter.Emit(_services, + async x => await x.OnUserDisconnected(request)); } } else if (request.CallStatus == "busy") { - await HookEmitter.Emit(_services, x => x.OnCallBusyStatus(request)); + await HookEmitter.Emit(_services, + async x => await x.OnCallBusyStatus(request)); } else if (request.CallStatus == "no-answer") { - await HookEmitter.Emit(_services, x => x.OnCallNoAnswerStatus(request)); + await HookEmitter.Emit(_services, + async x => await x.OnCallNoAnswerStatus(request)); } else if (request.CallStatus == "canceled") { - await HookEmitter.Emit(_services, x => x.OnCallCanceledStatus(request)); + await HookEmitter.Emit(_services, + async x => await x.OnCallCanceledStatus(request)); } else if (request.CallStatus == "failed") { - await HookEmitter.Emit(_services, x => x.OnCallFailedStatus(request)); + await HookEmitter.Emit(_services, + async x => await x.OnCallFailedStatus(request)); } return Ok(); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs index fb637677f..4b074a432 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs @@ -309,6 +309,19 @@ await HookEmitter.Emit(_services, async hook => return response; } + /// + /// https://www.twilio.com/docs/voice/answering-machine-detection + /// + /// + /// + public bool MachineDetected(ConversationalVoiceRequest request) + { + var answeredBy = request.AnsweredBy ?? "unknown"; + var isOutboundCall = request.Direction == "outbound-api"; + var isMachine = answeredBy.StartsWith("machine_") || answeredBy == "fax"; + return isOutboundCall && isMachine; + } + public string GetSpeechPath(string conversationId, string speechPath) { if (speechPath.StartsWith("twilio/"))