Skip to content

Commit addb1b1

Browse files
author
Haiping Chen
committed
Outbound call phone_recording_url
1 parent a49b75f commit addb1b1

File tree

8 files changed

+78
-11
lines changed

8 files changed

+78
-11
lines changed
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public async Task<TwiMLResult> InitiateStreamConversation(ConversationalVoiceReq
4848

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

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public async Task<TwiMLResult> InitiateConversation(ConversationalVoiceRequest r
5252
VoiceResponse response = null;
5353
var instruction = new ConversationalVoiceResponse
5454
{
55+
ConversationId = request.ConversationId,
5556
SpeechPaths = ["twilio/welcome.mp3"],
5657
ActionOnEmptyResult = true
5758
};
@@ -170,6 +171,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
170171
{
171172
var instruction = new ConversationalVoiceResponse
172173
{
174+
ConversationId = request.ConversationId,
173175
SpeechPaths = new List<string>(),
174176
CallbackPath = $"twilio/voice/receive/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
175177
ActionOnEmptyResult = true
@@ -273,6 +275,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
273275

274276
var instruction = new ConversationalVoiceResponse
275277
{
278+
ConversationId = request.ConversationId,
276279
SpeechPaths = speechPaths,
277280
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
278281
ActionOnEmptyResult = true
@@ -312,6 +315,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
312315

313316
var instruction = new ConversationalVoiceResponse
314317
{
318+
ConversationId = request.ConversationId,
315319
SpeechPaths = instructions,
316320
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
317321
ActionOnEmptyResult = true
@@ -358,6 +362,7 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
358362
{
359363
var instruction = new ConversationalVoiceResponse
360364
{
365+
ConversationId = request.ConversationId,
361366
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
362367
CallbackPath = $"twilio/voice/receive/{nextSeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}",
363368
ActionOnEmptyResult = true,
@@ -383,8 +388,19 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
383388
[HttpPost("twilio/voice/init-outbound-call")]
384389
public TwiMLResult InitiateOutboundCall(ConversationalVoiceRequest request)
385390
{
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+
386401
var instruction = new ConversationalVoiceResponse
387402
{
403+
ConversationId = request.ConversationId,
388404
ActionOnEmptyResult = true,
389405
CallbackPath = $"twilio/voice/receive/1?conversation-id={request.ConversationId}",
390406
};
@@ -396,7 +412,7 @@ public TwiMLResult InitiateOutboundCall(ConversationalVoiceRequest request)
396412
}
397413

398414
var twilio = _services.GetRequiredService<TwilioService>();
399-
var response = twilio.ReturnNoninterruptedInstructions(instruction);
415+
response = twilio.ReturnNoninterruptedInstructions(instruction);
400416
return TwiML(response);
401417
}
402418

src/Plugins/BotSharp.Plugin.Twilio/Interfaces/ITwilioCallStatusHook.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ public interface ITwilioCallStatusHook
77
{
88
Task OnVoicemailLeft(ConversationalVoiceRequest request);
99
Task OnUserDisconnected(ConversationalVoiceRequest request);
10+
Task OnRecordingCompleted(ConversationalVoiceRequest request);
1011
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ namespace BotSharp.Plugin.Twilio.Models;
22

33
public class ConversationalVoiceResponse
44
{
5+
public string ConversationId { get; set; } = null!;
56
public List<string> SpeechPaths { get; set; } = [];
67
public string CallbackPath { get; set; }
78
public bool ActionOnEmptyResult { get; set; }

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public async Task<bool> Execute(RoleDialogModel message)
6363

6464
var processUrl = $"{_twilioSetting.CallbackHost}/twilio";
6565
var statusUrl = $"{_twilioSetting.CallbackHost}/twilio/voice/status?conversation-id={newConversationId}";
66+
var recordingStatusUrl = $"{_twilioSetting.CallbackHost}/twilio/recording/status?conversation-id={newConversationId}";
6667

6768
// Generate initial assistant audio
6869
string initAudioUrl = null;
@@ -102,7 +103,9 @@ public async Task<bool> Execute(RoleDialogModel message)
102103
from: new PhoneNumber(_twilioSetting.PhoneNumber),
103104
statusCallback: new Uri(statusUrl),
104105
// https://www.twilio.com/docs/voice/answering-machine-detection
105-
machineDetection: _twilioSetting.MachineDetection);
106+
machineDetection: _twilioSetting.MachineDetection,
107+
record: _twilioSetting.RecordingEnabled,
108+
recordingStatusCallback: $"{_twilioSetting.CallbackHost}/twilio/record/status?conversation-id={newConversationId}");
106109

107110
var convService = _services.GetRequiredService<IConversationService>();
108111
var routing = _services.GetRequiredService<IRoutingContext>();

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using BotSharp.Abstraction.Utilities;
22
using BotSharp.Plugin.Twilio.Models;
33
using Twilio.Jwt.AccessToken;
4-
using Twilio.TwiML.Messaging;
54
using Token = Twilio.Jwt.AccessToken.Token;
65

76
namespace BotSharp.Plugin.Twilio.Services;
@@ -101,13 +100,13 @@ public VoiceResponse ReturnInstructions(ConversationalVoiceResponse conversation
101100
return response;
102101
}
103102

104-
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
103+
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse voiceResponse)
105104
{
106105
var response = new VoiceResponse();
107-
response.Pause(2);
108-
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
106+
107+
if (voiceResponse.SpeechPaths != null && voiceResponse.SpeechPaths.Any())
109108
{
110-
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
109+
foreach (var speechPath in voiceResponse.SpeechPaths)
111110
{
112111
if (speechPath.StartsWith(_settings.CallbackHost))
113112
{
@@ -119,21 +118,23 @@ public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceRespons
119118
}
120119
}
121120
}
121+
122122
var gather = new Gather()
123123
{
124124
Input = new List<Gather.InputEnum>()
125125
{
126126
Gather.InputEnum.Speech,
127127
Gather.InputEnum.Dtmf
128128
},
129-
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
129+
Action = new Uri($"{_settings.CallbackHost}/{voiceResponse.CallbackPath}"),
130130
Enhanced = true,
131131
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
132132
SpeechTimeout = "auto", // conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout.ToString() : "3",
133-
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
134-
ActionOnEmptyResult = conversationalVoiceResponse.ActionOnEmptyResult
133+
Timeout = voiceResponse.Timeout > 0 ? voiceResponse.Timeout : 3,
134+
ActionOnEmptyResult = voiceResponse.ActionOnEmptyResult,
135135
};
136136
response.Append(gather);
137+
137138
return response;
138139
}
139140

@@ -192,6 +193,7 @@ public VoiceResponse HoldOn(int interval, string message = null)
192193
public VoiceResponse ReturnBidirectionalMediaStreamsInstructions(string conversationId, ConversationalVoiceResponse conversationalVoiceResponse)
193194
{
194195
var response = new VoiceResponse();
196+
195197
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
196198
{
197199
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)

src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,4 @@ public class TwilioSetting
3333
public string? MachineDetection { get; set; }
3434

3535
public bool RecordingEnabled { get; set; } = false;
36-
public bool RecordingTranscribe { get; set; } = false;
3736
}

0 commit comments

Comments
 (0)