Outbound call phone_recording_url
This commit is contained in:
parent
a49b75f495
commit
addb1b1bba
|
|
@ -0,0 +1,44 @@
|
|||
using BotSharp.Core.Infrastructures;
|
||||
using BotSharp.Plugin.Twilio.Interfaces;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Controllers;
|
||||
|
||||
public class TwilioRecordController : TwilioController
|
||||
{
|
||||
private readonly TwilioSetting _settings;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public TwilioRecordController(TwilioSetting settings, IServiceProvider services, IHttpContextAccessor context, ILogger<TwilioRecordController> logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_services = services;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[ValidateRequest]
|
||||
[HttpPost("twilio/record/status")]
|
||||
public async Task<ActionResult> PhoneRecordingStatus(ConversationalVoiceRequest request)
|
||||
{
|
||||
if (request.RecordingStatus == "completed")
|
||||
{
|
||||
_logger.LogInformation($"Recording completed for {request.CallSid}, the record URL is {request.RecordingUrl}");
|
||||
|
||||
// Set the recording URL to the conversation state
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
convService.SetConversationId(request.ConversationId, new List<MessageState>
|
||||
{
|
||||
new("phone_recording_url", request.RecordingUrl)
|
||||
});
|
||||
convService.SaveStates();
|
||||
|
||||
// recording completed
|
||||
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnRecordingCompleted(request));
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ public class TwilioStreamController : TwilioController
|
|||
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
ConversationId = request.ConversationId,
|
||||
SpeechPaths = [],
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ public class TwilioVoiceController : TwilioController
|
|||
VoiceResponse response = null;
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
ConversationId = request.ConversationId,
|
||||
SpeechPaths = ["twilio/welcome.mp3"],
|
||||
ActionOnEmptyResult = true
|
||||
};
|
||||
|
|
@ -170,6 +171,7 @@ public class TwilioVoiceController : TwilioController
|
|||
{
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
ConversationId = request.ConversationId,
|
||||
SpeechPaths = new List<string>(),
|
||||
CallbackPath = $"twilio/voice/receive/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
|
||||
ActionOnEmptyResult = true
|
||||
|
|
@ -273,6 +275,7 @@ public class TwilioVoiceController : TwilioController
|
|||
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
ConversationId = request.ConversationId,
|
||||
SpeechPaths = speechPaths,
|
||||
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
|
||||
ActionOnEmptyResult = true
|
||||
|
|
@ -312,6 +315,7 @@ public class TwilioVoiceController : TwilioController
|
|||
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
ConversationId = request.ConversationId,
|
||||
SpeechPaths = instructions,
|
||||
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
|
||||
ActionOnEmptyResult = true
|
||||
|
|
@ -358,6 +362,7 @@ public class TwilioVoiceController : TwilioController
|
|||
{
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
ConversationId = request.ConversationId,
|
||||
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
|
||||
CallbackPath = $"twilio/voice/receive/{nextSeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}",
|
||||
ActionOnEmptyResult = true,
|
||||
|
|
@ -383,8 +388,19 @@ public class TwilioVoiceController : TwilioController
|
|||
[HttpPost("twilio/voice/init-outbound-call")]
|
||||
public TwiMLResult InitiateOutboundCall(ConversationalVoiceRequest request)
|
||||
{
|
||||
VoiceResponse response = default!;
|
||||
if (request.AnsweredBy == "machine_start" &&
|
||||
request.Direction == "outbound-api" &&
|
||||
request.InitAudioFile != null)
|
||||
{
|
||||
response = new VoiceResponse();
|
||||
response.Play(new Uri(request.InitAudioFile));
|
||||
return TwiML(response);
|
||||
}
|
||||
|
||||
var instruction = new ConversationalVoiceResponse
|
||||
{
|
||||
ConversationId = request.ConversationId,
|
||||
ActionOnEmptyResult = true,
|
||||
CallbackPath = $"twilio/voice/receive/1?conversation-id={request.ConversationId}",
|
||||
};
|
||||
|
|
@ -396,7 +412,7 @@ public class TwilioVoiceController : TwilioController
|
|||
}
|
||||
|
||||
var twilio = _services.GetRequiredService<TwilioService>();
|
||||
var response = twilio.ReturnNoninterruptedInstructions(instruction);
|
||||
response = twilio.ReturnNoninterruptedInstructions(instruction);
|
||||
return TwiML(response);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ public interface ITwilioCallStatusHook
|
|||
{
|
||||
Task OnVoicemailLeft(ConversationalVoiceRequest request);
|
||||
Task OnUserDisconnected(ConversationalVoiceRequest request);
|
||||
Task OnRecordingCompleted(ConversationalVoiceRequest request);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ namespace BotSharp.Plugin.Twilio.Models;
|
|||
|
||||
public class ConversationalVoiceResponse
|
||||
{
|
||||
public string ConversationId { get; set; } = null!;
|
||||
public List<string> SpeechPaths { get; set; } = [];
|
||||
public string CallbackPath { get; set; }
|
||||
public bool ActionOnEmptyResult { get; set; }
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ public class OutboundPhoneCallFn : IFunctionCallback
|
|||
|
||||
var processUrl = $"{_twilioSetting.CallbackHost}/twilio";
|
||||
var statusUrl = $"{_twilioSetting.CallbackHost}/twilio/voice/status?conversation-id={newConversationId}";
|
||||
var recordingStatusUrl = $"{_twilioSetting.CallbackHost}/twilio/recording/status?conversation-id={newConversationId}";
|
||||
|
||||
// Generate initial assistant audio
|
||||
string initAudioUrl = null;
|
||||
|
|
@ -102,7 +103,9 @@ public class OutboundPhoneCallFn : IFunctionCallback
|
|||
from: new PhoneNumber(_twilioSetting.PhoneNumber),
|
||||
statusCallback: new Uri(statusUrl),
|
||||
// https://www.twilio.com/docs/voice/answering-machine-detection
|
||||
machineDetection: _twilioSetting.MachineDetection);
|
||||
machineDetection: _twilioSetting.MachineDetection,
|
||||
record: _twilioSetting.RecordingEnabled,
|
||||
recordingStatusCallback: $"{_twilioSetting.CallbackHost}/twilio/record/status?conversation-id={newConversationId}");
|
||||
|
||||
var convService = _services.GetRequiredService<IConversationService>();
|
||||
var routing = _services.GetRequiredService<IRoutingContext>();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using BotSharp.Abstraction.Utilities;
|
||||
using BotSharp.Plugin.Twilio.Models;
|
||||
using Twilio.Jwt.AccessToken;
|
||||
using Twilio.TwiML.Messaging;
|
||||
using Token = Twilio.Jwt.AccessToken.Token;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Services;
|
||||
|
|
@ -101,13 +100,13 @@ public class TwilioService
|
|||
return response;
|
||||
}
|
||||
|
||||
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse conversationalVoiceResponse)
|
||||
public VoiceResponse ReturnNoninterruptedInstructions(ConversationalVoiceResponse voiceResponse)
|
||||
{
|
||||
var response = new VoiceResponse();
|
||||
response.Pause(2);
|
||||
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
|
||||
|
||||
if (voiceResponse.SpeechPaths != null && voiceResponse.SpeechPaths.Any())
|
||||
{
|
||||
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
|
||||
foreach (var speechPath in voiceResponse.SpeechPaths)
|
||||
{
|
||||
if (speechPath.StartsWith(_settings.CallbackHost))
|
||||
{
|
||||
|
|
@ -119,6 +118,7 @@ public class TwilioService
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var gather = new Gather()
|
||||
{
|
||||
Input = new List<Gather.InputEnum>()
|
||||
|
|
@ -126,14 +126,15 @@ public class TwilioService
|
|||
Gather.InputEnum.Speech,
|
||||
Gather.InputEnum.Dtmf
|
||||
},
|
||||
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
|
||||
Action = new Uri($"{_settings.CallbackHost}/{voiceResponse.CallbackPath}"),
|
||||
Enhanced = true,
|
||||
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
|
||||
SpeechTimeout = "auto", // conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout.ToString() : "3",
|
||||
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
|
||||
ActionOnEmptyResult = conversationalVoiceResponse.ActionOnEmptyResult
|
||||
Timeout = voiceResponse.Timeout > 0 ? voiceResponse.Timeout : 3,
|
||||
ActionOnEmptyResult = voiceResponse.ActionOnEmptyResult,
|
||||
};
|
||||
response.Append(gather);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +193,7 @@ public class TwilioService
|
|||
public VoiceResponse ReturnBidirectionalMediaStreamsInstructions(string conversationId, ConversationalVoiceResponse conversationalVoiceResponse)
|
||||
{
|
||||
var response = new VoiceResponse();
|
||||
|
||||
if (conversationalVoiceResponse.SpeechPaths != null && conversationalVoiceResponse.SpeechPaths.Any())
|
||||
{
|
||||
foreach (var speechPath in conversationalVoiceResponse.SpeechPaths)
|
||||
|
|
|
|||
|
|
@ -33,5 +33,4 @@ public class TwilioSetting
|
|||
public string? MachineDetection { get; set; }
|
||||
|
||||
public bool RecordingEnabled { get; set; } = false;
|
||||
public bool RecordingTranscribe { get; set; } = false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue