Skip to content

Commit a49b75f

Browse files
author
Haiping Chen
committed
Add Twilio settings.
1 parent 934aa24 commit a49b75f

File tree

11 files changed

+125
-65
lines changed

11 files changed

+125
-65
lines changed

src/Infrastructure/BotSharp.Abstraction/Conversations/ConversationHookBase.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,4 @@ public virtual Task OnBreakpointUpdated(string conversationId, bool resetStates)
7878

7979
public virtual Task OnNotificationGenerated(RoleDialogModel message)
8080
=> Task.CompletedTask;
81-
82-
public virtual Task OnUserDisconnected(Conversation conversation)
83-
=> Task.CompletedTask;
8481
}

src/Infrastructure/BotSharp.Abstraction/Conversations/IConversationHook.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,6 @@ public interface IConversationHook
2525
/// <returns></returns>
2626
Task OnUserAgentConnectedInitially(Conversation conversation);
2727

28-
/// <summary>
29-
/// Triggered when user disconnects with agent.
30-
/// </summary>
31-
/// <param name="conversation"></param>
32-
/// <returns></returns>
33-
Task OnUserDisconnected(Conversation conversation);
34-
3528
/// <summary>
3629
/// Triggered once for every new conversation.
3730
/// </summary>

src/Infrastructure/BotSharp.Core.Realtime/BotSharp.Core.Realtime.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>$(TargetFramework)</TargetFramework>
5+
<LangVersion>$(LangVersion)</LangVersion>
6+
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
7+
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
8+
<OutputPath>$(SolutionDir)packages</OutputPath>
59
<ImplicitUsings>enable</ImplicitUsings>
610
<Nullable>enable</Nullable>
711
</PropertyGroup>

src/Infrastructure/BotSharp.Core.Realtime/Services/RealtimeHub.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
using BotSharp.Abstraction.Utilities;
2-
using BotSharp.Core.Infrastructures;
3-
using Microsoft.AspNetCore.Cors.Infrastructure;
4-
51
namespace BotSharp.Core.Realtime.Services;
62

73
public class RealtimeHub : IRealtimeHub
@@ -257,9 +253,7 @@ private async Task HandleUserDtmfReceived()
257253

258254
private async Task HandleUserDisconnected()
259255
{
260-
var convService = _services.GetRequiredService<IConversationService>();
261-
var conversation = await convService.GetConversation(_conn.ConversationId);
262-
await HookEmitter.Emit<IConversationHook>(_services, x => x.OnUserDisconnected(conversation));
256+
263257
}
264258

265259
private async Task SendEventToUser(WebSocket webSocket, object message)

src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioStreamController.cs

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public async Task<TwiMLResult> InitiateStreamConversation(ConversationalVoiceReq
4242
request.InitAudioFile != null)
4343
{
4444
response = new VoiceResponse();
45-
response.Play(new Uri($"{_settings.CallbackHost}/twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}"));
45+
response.Play(new Uri(request.InitAudioFile));
4646
return TwiML(response);
4747
}
4848

@@ -54,7 +54,7 @@ public async Task<TwiMLResult> InitiateStreamConversation(ConversationalVoiceReq
5454

5555
if (request.InitAudioFile != null)
5656
{
57-
instruction.SpeechPaths.Add(request.InitAudioFile);
57+
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}");
5858
}
5959

6060
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
@@ -82,24 +82,6 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
8282
return TwiML(response);
8383
}
8484

85-
[ValidateRequest]
86-
[HttpPost("twilio/stream/status")]
87-
public async Task<ActionResult> StreamConversationStatus(ConversationalVoiceRequest request)
88-
{
89-
if (request.AnsweredBy == "machine_start" &&
90-
request.Direction == "outbound-api" &&
91-
request.InitAudioFile != null &&
92-
request.CallStatus == "completed")
93-
{
94-
// voicemail
95-
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
96-
{
97-
await hook.OnVoicemailLeft(request.ConversationId);
98-
});
99-
}
100-
return Ok();
101-
}
102-
10385
private async Task<string> InitConversation(ConversationalVoiceRequest request)
10486
{
10587
var convService = _services.GetRequiredService<IConversationService>();

src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
using BotSharp.Abstraction.Files;
22
using BotSharp.Abstraction.Infrastructures;
3-
using BotSharp.Abstraction.Repositories;
43
using BotSharp.Core.Infrastructures;
54
using BotSharp.Plugin.Twilio.Interfaces;
65
using BotSharp.Plugin.Twilio.Models;
76
using BotSharp.Plugin.Twilio.Services;
87
using Microsoft.AspNetCore.Http;
98
using Microsoft.AspNetCore.Mvc;
10-
using System.ComponentModel.DataAnnotations;
119
using Twilio.Http;
1210

1311
namespace BotSharp.Plugin.Twilio.Controllers;
@@ -382,21 +380,21 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
382380
}
383381

384382
[ValidateRequest]
385-
[HttpPost("twilio/voice/init-call")]
386-
public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQuery] string conversationId)
383+
[HttpPost("twilio/voice/init-outbound-call")]
384+
public TwiMLResult InitiateOutboundCall(ConversationalVoiceRequest request)
387385
{
388386
var instruction = new ConversationalVoiceResponse
389387
{
390388
ActionOnEmptyResult = true,
391-
CallbackPath = $"twilio/voice/receive/1?conversation-id={conversationId}",
392-
SpeechPaths = new List<string>
393-
{
394-
$"twilio/voice/speeches/{conversationId}/intial.mp3"
395-
}
389+
CallbackPath = $"twilio/voice/receive/1?conversation-id={request.ConversationId}",
396390
};
397-
string tag = $"twilio:{Request.Form["AnsweredBy"]}";
398-
var db = _services.GetRequiredService<IBotSharpRepository>();
399-
db.AppendConversationTags(conversationId, new List<string> { tag });
391+
392+
if (request.InitAudioFile != null)
393+
{
394+
instruction.CallbackPath += $"&init-audio-file={request.InitAudioFile}";
395+
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}");
396+
}
397+
400398
var twilio = _services.GetRequiredService<TwilioService>();
401399
var response = twilio.ReturnNoninterruptedInstructions(instruction);
402400
return TwiML(response);
@@ -415,6 +413,32 @@ public async Task<FileContentResult> GetSpeechFile([FromRoute] string conversati
415413
return result;
416414
}
417415

416+
[ValidateRequest]
417+
[HttpPost("twilio/voice/status")]
418+
public async Task<ActionResult> PhoneCallStatus(ConversationalVoiceRequest request)
419+
{
420+
if (request.CallStatus == "completed")
421+
{
422+
if (request.AnsweredBy == "machine_start" &&
423+
request.Direction == "outbound-api" &&
424+
request.InitAudioFile != null)
425+
{
426+
// voicemail
427+
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
428+
{
429+
await hook.OnVoicemailLeft(request);
430+
});
431+
}
432+
else
433+
{
434+
// phone call completed
435+
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnUserDisconnected(request));
436+
}
437+
}
438+
439+
return Ok();
440+
}
441+
418442
private Dictionary<string, string> ParseStates(List<string> states)
419443
{
420444
var result = new Dictionary<string, string>();
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
using BotSharp.Plugin.Twilio.Models;
12
using Task = System.Threading.Tasks.Task;
23

34
namespace BotSharp.Plugin.Twilio.Interfaces;
45

56
public interface ITwilioCallStatusHook
67
{
7-
Task OnVoicemailLeft(string conversationId);
8+
Task OnVoicemailLeft(ConversationalVoiceRequest request);
9+
Task OnUserDisconnected(ConversationalVoiceRequest request);
810
}

src/Plugins/BotSharp.Plugin.Twilio/Models/AssistantMessage.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public class AssistantMessage
66
public bool HumanIntervationNeeded { get; set; }
77
public string Content { get; set; }
88
public string MessageId { get; set; }
9-
public string SpeechFileName { get; set; }
9+
public string? SpeechFileName { get; set; }
1010
public string Hints { get; set; }
1111
}
1212
}

src/Plugins/BotSharp.Plugin.Twilio/OutboundPhoneCallHandler/Functions/OutboundPhoneCallFn.cs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
using BotSharp.Abstraction.Files;
2+
using BotSharp.Abstraction.Files.Models;
23
using BotSharp.Abstraction.Infrastructures.Enums;
34
using BotSharp.Abstraction.Options;
45
using BotSharp.Abstraction.Routing;
56
using BotSharp.Core.Infrastructures;
7+
using BotSharp.Plugin.Twilio.Interfaces;
8+
using BotSharp.Plugin.Twilio.Models;
69
using BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.LlmContexts;
710
using Twilio.Rest.Api.V2010.Account;
811
using Twilio.Types;
@@ -58,20 +61,48 @@ public async Task<bool> Execute(RoleDialogModel message)
5861
var newConversationId = Guid.NewGuid().ToString();
5962
states.SetState(StateConst.SUB_CONVERSATION_ID, newConversationId);
6063

64+
var processUrl = $"{_twilioSetting.CallbackHost}/twilio";
65+
var statusUrl = $"{_twilioSetting.CallbackHost}/twilio/voice/status?conversation-id={newConversationId}";
66+
6167
// Generate initial assistant audio
62-
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
63-
var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage);
64-
var fileName = $"intial.mp3";
65-
fileStorage.SaveSpeechFile(newConversationId, fileName, data);
68+
string initAudioUrl = null;
69+
if (!string.IsNullOrEmpty(args.InitialMessage))
70+
{
71+
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
72+
var data = await completion.GenerateAudioFromTextAsync(args.InitialMessage);
73+
initAudioUrl = "intial.mp3";
74+
fileStorage.SaveSpeechFile(newConversationId, initAudioUrl, data);
75+
76+
statusUrl += $"&init-audio-file={initAudioUrl}";
77+
}
78+
79+
// Set up process URL streaming or synchronous
80+
if (_twilioSetting.StreamingEnabled)
81+
{
82+
processUrl += "/stream";
83+
}
84+
else
85+
{
86+
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
87+
await sessionManager.SetAssistantReplyAsync(newConversationId, 0, new AssistantMessage
88+
{
89+
Content = args.InitialMessage,
90+
SpeechFileName = initAudioUrl
91+
});
92+
93+
processUrl += "/voice/init-outbound-call";
94+
}
95+
96+
processUrl += $"?conversation-id={newConversationId}&init-audio-file={initAudioUrl}";
6697

6798
// Make outbound call
6899
var call = await CallResource.CreateAsync(
69-
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation-id={newConversationId}&init-audio-file={fileName}"),
100+
url: new Uri(processUrl),
70101
to: new PhoneNumber(args.PhoneNumber),
71102
from: new PhoneNumber(_twilioSetting.PhoneNumber),
72-
statusCallback: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream/status?conversation-id={newConversationId}&init-audio-file={fileName}"),
103+
statusCallback: new Uri(statusUrl),
73104
// https://www.twilio.com/docs/voice/answering-machine-detection
74-
machineDetection: "Enable");
105+
machineDetection: _twilioSetting.MachineDetection);
75106

76107
var convService = _services.GetRequiredService<IConversationService>();
77108
var routing = _services.GetRequiredService<IRoutingContext>();
@@ -80,7 +111,7 @@ public async Task<bool> Execute(RoleDialogModel message)
80111

81112
await ForkConversation(args, entryAgentId, originConversationId, newConversationId, call);
82113

83-
message.Content = $"The generated phone message: \"{args.InitialMessage}.\" [NEW CONVERSATION ID: {newConversationId}, TWILIO CALL SID: {call.Sid}]";
114+
message.Content = $"The generated phone initial message: \"{args.InitialMessage}.\" [NEW CONVERSATION ID: {newConversationId}, TWILIO CALL SID: {call.Sid}, STREAMING: {_twilioSetting.StreamingEnabled}, RECORDING: {_twilioSetting.RecordingEnabled}]";
84115
message.StopCompletion = true;
85116
return true;
86117
}

src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,14 @@ public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceRespons
109109
{
110110
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
111111
{
112-
response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
112+
if (speechPath.StartsWith(_settings.CallbackHost))
113+
{
114+
response.Play(new Uri(speechPath));
115+
}
116+
else
117+
{
118+
response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
119+
}
113120
}
114121
}
115122
var gather = new Gather()
@@ -193,9 +200,13 @@ public VoiceResponse ReturnBidirectionalMediaStreamsInstructions(string conversa
193200
{
194201
response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
195202
}
203+
else if (speechPath.StartsWith(_settings.CallbackHost))
204+
{
205+
response.Play(new Uri(speechPath));
206+
}
196207
else
197208
{
198-
response.Play(new Uri($"{_settings.CallbackHost}/twilio/voice/speeches/{conversationId}/{speechPath}"));
209+
response.Play(new Uri($"{_settings.CallbackHost}/{speechPath}"));
199210
}
200211
}
201212
}

0 commit comments

Comments
 (0)