Skip to content

Commit be70c3c

Browse files
authored
Merge pull request #945 from hchen2020/master
Twilio improvement
2 parents c028593 + 9a9c1a3 commit be70c3c

File tree

18 files changed

+276
-76
lines changed

18 files changed

+276
-76
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/BotSharp.Plugin.Twilio.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-hangup_phone_call.json">
1414
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
1515
</Content>
16+
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-text_message.json">
17+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
18+
</Content>
1619
<Content Include="data\agents\6745151e-6d46-4a02-8de4-1c4f21c7da95\functions\util-twilio-outbound_phone_call.json">
1720
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
1821
</Content>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using BotSharp.Core.Infrastructures;
2+
using BotSharp.Plugin.Twilio.Interfaces;
3+
using BotSharp.Plugin.Twilio.Models;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.Mvc;
6+
7+
namespace BotSharp.Plugin.Twilio.Controllers;
8+
9+
public class TwilioRecordController : TwilioController
10+
{
11+
private readonly TwilioSetting _settings;
12+
private readonly IServiceProvider _services;
13+
private readonly ILogger _logger;
14+
15+
public TwilioRecordController(TwilioSetting settings, IServiceProvider services, IHttpContextAccessor context, ILogger<TwilioRecordController> logger)
16+
{
17+
_settings = settings;
18+
_services = services;
19+
_logger = logger;
20+
}
21+
22+
[ValidateRequest]
23+
[HttpPost("twilio/record/status")]
24+
public async Task<ActionResult> PhoneRecordingStatus(ConversationalVoiceRequest request)
25+
{
26+
if (request.RecordingStatus == "completed")
27+
{
28+
_logger.LogInformation($"Recording completed for {request.CallSid}, the record URL is {request.RecordingUrl}");
29+
30+
// Set the recording URL to the conversation state
31+
var convService = _services.GetRequiredService<IConversationService>();
32+
convService.SetConversationId(request.ConversationId, new List<MessageState>
33+
{
34+
new("phone_recording_url", request.RecordingUrl)
35+
});
36+
convService.SaveStates();
37+
38+
// recording completed
39+
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnRecordingCompleted(request));
40+
}
41+
42+
return Ok();
43+
}
44+
}

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

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,19 +42,20 @@ 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

4949
var instruction = new ConversationalVoiceResponse
5050
{
51+
ConversationId = request.ConversationId,
5152
SpeechPaths = [],
5253
ActionOnEmptyResult = true
5354
};
5455

5556
if (request.InitAudioFile != null)
5657
{
57-
instruction.SpeechPaths.Add(request.InitAudioFile);
58+
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}");
5859
}
5960

6061
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
@@ -82,24 +83,6 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
8283
return TwiML(response);
8384
}
8485

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-
10386
private async Task<string> InitConversation(ConversationalVoiceRequest request)
10487
{
10588
var convService = _services.GetRequiredService<IConversationService>();

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

Lines changed: 53 additions & 13 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;
@@ -54,6 +52,7 @@ public async Task<TwiMLResult> InitiateConversation(ConversationalVoiceRequest r
5452
VoiceResponse response = null;
5553
var instruction = new ConversationalVoiceResponse
5654
{
55+
ConversationId = request.ConversationId,
5756
SpeechPaths = ["twilio/welcome.mp3"],
5857
ActionOnEmptyResult = true
5958
};
@@ -172,6 +171,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
172171
{
173172
var instruction = new ConversationalVoiceResponse
174173
{
174+
ConversationId = request.ConversationId,
175175
SpeechPaths = new List<string>(),
176176
CallbackPath = $"twilio/voice/receive/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
177177
ActionOnEmptyResult = true
@@ -275,6 +275,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
275275

276276
var instruction = new ConversationalVoiceResponse
277277
{
278+
ConversationId = request.ConversationId,
278279
SpeechPaths = speechPaths,
279280
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
280281
ActionOnEmptyResult = true
@@ -314,6 +315,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
314315

315316
var instruction = new ConversationalVoiceResponse
316317
{
318+
ConversationId = request.ConversationId,
317319
SpeechPaths = instructions,
318320
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
319321
ActionOnEmptyResult = true
@@ -360,6 +362,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
360362
{
361363
var instruction = new ConversationalVoiceResponse
362364
{
365+
ConversationId = request.ConversationId,
363366
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
364367
CallbackPath = $"twilio/voice/receive/{nextSeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}",
365368
ActionOnEmptyResult = true,
@@ -382,23 +385,34 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
382385
}
383386

384387
[ValidateRequest]
385-
[HttpPost("twilio/voice/init-call")]
386-
public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQuery] string conversationId)
388+
[HttpPost("twilio/voice/init-outbound-call")]
389+
public TwiMLResult InitiateOutboundCall(ConversationalVoiceRequest request)
387390
{
391+
VoiceResponse response = default!;
392+
if (request.AnsweredBy == "machine_start" &&
393+
request.Direction == "outbound-api" &&
394+
request.InitAudioFile != null)
395+
{
396+
response = new VoiceResponse();
397+
response.Play(new Uri(request.InitAudioFile));
398+
return TwiML(response);
399+
}
400+
388401
var instruction = new ConversationalVoiceResponse
389402
{
403+
ConversationId = request.ConversationId,
390404
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-
}
405+
CallbackPath = $"twilio/voice/receive/1?conversation-id={request.ConversationId}",
396406
};
397-
string tag = $"twilio:{Request.Form["AnsweredBy"]}";
398-
var db = _services.GetRequiredService<IBotSharpRepository>();
399-
db.AppendConversationTags(conversationId, new List<string> { tag });
407+
408+
if (request.InitAudioFile != null)
409+
{
410+
instruction.CallbackPath += $"&init-audio-file={request.InitAudioFile}";
411+
instruction.SpeechPaths.Add($"twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}");
412+
}
413+
400414
var twilio = _services.GetRequiredService<TwilioService>();
401-
var response = twilio.ReturnNoninterruptedInstructions(instruction);
415+
response = twilio.ReturnNoninterruptedInstructions(instruction);
402416
return TwiML(response);
403417
}
404418

@@ -415,6 +429,32 @@ public async Task<FileContentResult> GetSpeechFile([FromRoute] string conversati
415429
return result;
416430
}
417431

432+
[ValidateRequest]
433+
[HttpPost("twilio/voice/status")]
434+
public async Task<ActionResult> PhoneCallStatus(ConversationalVoiceRequest request)
435+
{
436+
if (request.CallStatus == "completed")
437+
{
438+
if (request.AnsweredBy == "machine_start" &&
439+
request.Direction == "outbound-api" &&
440+
request.InitAudioFile != null)
441+
{
442+
// voicemail
443+
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
444+
{
445+
await hook.OnVoicemailLeft(request);
446+
});
447+
}
448+
else
449+
{
450+
// phone call completed
451+
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnUserDisconnected(request));
452+
}
453+
}
454+
455+
return Ok();
456+
}
457+
418458
private Dictionary<string, string> ParseStates(List<string> states)
419459
{
420460
var result = new Dictionary<string, string>();
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
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);
10+
Task OnRecordingCompleted(ConversationalVoiceRequest request);
811
}

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
}

0 commit comments

Comments
 (0)