From 72abb965de500b331d27d37ba7121c61f474fdf0 Mon Sep 17 00:00:00 2001 From: Haiping Chen Date: Tue, 18 Mar 2025 13:10:35 -0500 Subject: [PATCH] Hang up phone with audio --- .../Controllers/TwilioVoiceController.cs | 14 ++++++- .../Functions/HangupPhoneCallFn.cs | 32 +++++++++------ .../Functions/TransferPhoneCallFn.cs | 39 +++++++------------ .../LlmContexts/HangupPhoneCallArgs.cs | 7 +++- .../Services/TwilioService.cs | 16 ++++++++ .../util-twilio-hangup_phone_call.json | 8 ++-- 6 files changed, 73 insertions(+), 43 deletions(-) diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index b5749e0f6..0a898651e 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -164,7 +164,7 @@ await HookEmitter.Emit(_services, async hook => OnlyOnce = true }); - response = twilio.HangUp(null); + response = twilio.HangUp(string.Empty); } // keep waiting for user response else @@ -433,8 +433,18 @@ public async Task GetSpeechFile([FromRoute] string conversati [HttpPost("twilio/voice/hang-up")] public async Task Hangup(ConversationalVoiceRequest request) { + var instruction = new ConversationalVoiceResponse + { + ConversationId = request.ConversationId + }; + + if (request.InitAudioFile != null) + { + instruction.SpeechPaths.Add(request.InitAudioFile); + } + var twilio = _services.GetRequiredService(); - var response = twilio.HangUp("twilio/bye.mp3"); + var response = twilio.HangUp(instruction); return TwiML(response); } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs index 9fa7d03c3..9d3463181 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/HangupPhoneCallFn.cs @@ -1,4 +1,6 @@ +using BotSharp.Abstraction.Files; using BotSharp.Abstraction.Routing; +using BotSharp.Core.Infrastructures; using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; using Twilio.Rest.Api.V2010.Account; @@ -27,6 +29,7 @@ public async Task Execute(RoleDialogModel message) { var args = JsonSerializer.Deserialize(message.FunctionArgs); + var fileStorage = _services.GetRequiredService(); var routing = _services.GetRequiredService(); var conversationId = routing.Context.ConversationId; var states = _services.GetRequiredService(); @@ -39,21 +42,28 @@ public async Task Execute(RoleDialogModel message) return false; } - if (args.AnythingElseToHelp) - { - message.Content = "Tell me how I can help."; - } - else + var processUrl = $"{_twilioSetting.CallbackHost}/twilio/voice/hang-up?conversation-id={conversationId}"; + + // Generate initial assistant audio + string initAudioFile = null; + if (!string.IsNullOrEmpty(args.ResponseContent)) { - var call = CallResource.Update( - url: new Uri($"{_twilioSetting.CallbackHost}/twilio/voice/hang-up?conversation-id={conversationId}"), - pathSid: callSid - ); + var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1"); + var data = await completion.GenerateAudioFromTextAsync(args.ResponseContent); + initAudioFile = "ending.mp3"; + fileStorage.SaveSpeechFile(conversationId, initAudioFile, data); - message.Content = "The call is ending."; - message.StopCompletion = true; + processUrl += $"&init-audio-file={initAudioFile}"; } + var call = CallResource.Update( + url: new Uri(processUrl), + pathSid: callSid + ); + + message.Content = args.Reason; + message.StopCompletion = true; + return true; } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/TransferPhoneCallFn.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/TransferPhoneCallFn.cs index cc8e160cf..4f60198b0 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/TransferPhoneCallFn.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/TransferPhoneCallFn.cs @@ -35,44 +35,35 @@ public async Task Execute(RoleDialogModel message) var fileStorage = _services.GetRequiredService(); var states = _services.GetRequiredService(); + var sid = states.GetState("twilio_call_sid"); + if (string.IsNullOrEmpty(sid)) + { + _logger.LogError("Twilio call sid is empty."); + message.Content = "There is an error when transferring the phone call."; + return false; + } + var routing = _services.GetRequiredService(); var conversationId = routing.Context.ConversationId; var processUrl = $"{_twilioSetting.CallbackHost}/twilio/voice/transfer-call?conversation-id={conversationId}&transfer-to={args.PhoneNumber}"; // Generate initial assistant audio - string initAudioFile = null; if (!string.IsNullOrEmpty(args.TransitionMessage)) { var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1"); var data = await completion.GenerateAudioFromTextAsync(args.TransitionMessage); - initAudioFile = "transfer.mp3"; + var initAudioFile = "transfer.mp3"; fileStorage.SaveSpeechFile(conversationId, initAudioFile, data); processUrl += $"&init-audio-file={initAudioFile}"; } - if (!string.IsNullOrEmpty(initAudioFile)) - { - processUrl += $"&init-audio-file={initAudioFile}"; - } - - // Forward call - var sid = states.GetState("twilio_call_sid"); + // Transfer call + var call = CallResource.Update( + pathSid: sid, + url: new Uri(processUrl)); - if (string.IsNullOrEmpty(sid)) - { - _logger.LogError("Twilio call sid is empty."); - message.Content = "There is an error when transferring the phone call."; - return false; - } - else - { - var call = CallResource.Update( - pathSid: sid, - url: new Uri(processUrl)); - - message.Content = args.TransitionMessage; - return true; - } + message.Content = args.TransitionMessage; + return true; } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs index efd8114a5..02242ccf6 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/LlmContexts/HangupPhoneCallArgs.cs @@ -4,6 +4,9 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts; public class HangupPhoneCallArgs { - [JsonPropertyName("anything_else_to_help")] - public bool AnythingElseToHelp { get; set; } = true; + [JsonPropertyName("reason")] + public string Reason { get; set; } = null!; + + [JsonPropertyName("response_content")] + public string ResponseContent { get; set; } = null!; } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs index 3358e8acf..81d878844 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs @@ -149,6 +149,22 @@ public VoiceResponse HangUp(string speechPath) return response; } + public VoiceResponse HangUp(ConversationalVoiceResponse voiceResponse) + { + var response = new VoiceResponse(); + var conversationId = voiceResponse.ConversationId; + if (voiceResponse.SpeechPaths != null && voiceResponse.SpeechPaths.Any()) + { + foreach (var speechPath in voiceResponse.SpeechPaths) + { + var uri = GetSpeechPath(conversationId, speechPath); + response.Play(new Uri(uri)); + } + } + response.Hangup(); + return response; + } + public VoiceResponse DialCsrAgent(string speechPath) { var response = new VoiceResponse(); diff --git a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json index be5f3ca63..f35686616 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json +++ b/src/Plugins/BotSharp.Plugin.Twilio/data/agents/6745151e-6d46-4a02-8de4-1c4f21c7da95/functions/util-twilio-hangup_phone_call.json @@ -9,11 +9,11 @@ "type": "string", "description": "The reason why user wants to end the phone call." }, - "anything_else_to_help": { - "type": "boolean", - "description": "Check if user has anything else to help." + "response_content": { + "type": "string", + "description": "A statement said to the user when politely ending a conversation." } }, - "required": [ "reason", "anything_else_to_help" ] + "required": [ "reason", "response_content" ] } } \ No newline at end of file