ITwilioCallStatusHook

This commit is contained in:
Haiping Chen 2025-03-13 14:05:06 -05:00
parent 28279eebe3
commit f6994a3a6f
7 changed files with 75 additions and 22 deletions

View file

@ -26,7 +26,6 @@ public class RealtimeHub : IRealtimeHub
{
var buffer = new byte[1024 * 16];
WebSocketReceiveResult result;
do

View file

@ -35,24 +35,28 @@ public class TwilioStreamController : TwilioController
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
}
VoiceResponse response = null;
VoiceResponse response = default!;
if (request.AnsweredBy == "machine_start" &&
request.Direction == "outbound-api" &&
request.InitAudioFile != null)
{
response = new VoiceResponse();
response.Play(new Uri($"{_settings.CallbackHost}/twilio/voice/speeches/{request.ConversationId}/{request.InitAudioFile}"));
return TwiML(response);
}
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = [],
ActionOnEmptyResult = true
};
if (_context.HttpContext.Request.Query.ContainsKey("init_audio_file"))
if (request.InitAudioFile != null)
{
request.InitAudioFile = _context.HttpContext.Request.Query["init_audio_file"];
instruction.SpeechPaths.Add(request.InitAudioFile);
}
if (_context.HttpContext.Request.Query.ContainsKey("conversation_id"))
{
request.ConversationId = _context.HttpContext.Request.Query["conversation_id"];
}
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreating(request, instruction);
@ -78,6 +82,24 @@ public class TwilioStreamController : TwilioController
return TwiML(response);
}
[ValidateRequest]
[HttpPost("twilio/stream/status")]
public async Task<ActionResult> StreamConversationStatus(ConversationalVoiceRequest request)
{
if (request.AnsweredBy == "machine_start" &&
request.Direction == "outbound-api" &&
request.InitAudioFile != null &&
request.CallStatus == "completed")
{
// voicemail
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
{
await hook.OnVoicemailLeft(request.ConversationId);
});
}
return Ok();
}
private async Task<string> InitConversation(ConversationalVoiceRequest request)
{
var convService = _services.GetRequiredService<IConversationService>();

View file

@ -66,7 +66,7 @@ public class TwilioVoiceController : TwilioController
});
request.ConversationId = $"TwilioVoice_{request.CallSid}";
instruction.CallbackPath = $"twilio/voice/{request.ConversationId}/receive/0?{GenerateStatesParameter(request.States)}";
instruction.CallbackPath = $"twilio/voice/receive/0?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}";
var twilio = _services.GetRequiredService<TwilioService>();
if (string.IsNullOrWhiteSpace(request.Intent))
@ -89,7 +89,7 @@ public class TwilioVoiceController : TwilioController
};
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse();
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{seqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{seqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
}
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
@ -109,7 +109,7 @@ public class TwilioVoiceController : TwilioController
/// <param name="request"></param>
/// <returns></returns>
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
[HttpPost("twilio/voice/receive/{seqNum}")]
public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest request)
{
var twilio = _services.GetRequiredService<TwilioService>();
@ -142,7 +142,7 @@ public class TwilioVoiceController : TwilioController
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse();
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime=0"), HttpMethod.Post);
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime=0"), HttpMethod.Post);
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
@ -173,7 +173,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = new List<string>(),
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
CallbackPath = $"twilio/voice/receive/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
ActionOnEmptyResult = true
};
@ -203,7 +203,7 @@ public class TwilioVoiceController : TwilioController
/// <param name="request"></param>
/// <returns></returns>
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
[HttpPost("twilio/voice/reply/{seqNum}")]
public async Task<TwiMLResult> ReplyCallerMessage(ConversationalVoiceRequest request)
{
var nextSeqNum = request.SeqNum + 1;
@ -276,7 +276,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = speechPaths,
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
ActionOnEmptyResult = true
};
@ -315,7 +315,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = instructions,
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
CallbackPath = $"twilio/voice/reply/{request.SeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}&AIResponseWaitTime={++request.AIResponseWaitTime}",
ActionOnEmptyResult = true
};
@ -361,7 +361,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(request.States)}",
CallbackPath = $"twilio/voice/receive/{nextSeqNum}?conversation-id={request.ConversationId}&{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true,
Hints = reply.Hints
};
@ -388,7 +388,7 @@ public class TwilioVoiceController : TwilioController
var instruction = new ConversationalVoiceResponse
{
ActionOnEmptyResult = true,
CallbackPath = $"twilio/voice/{conversationId}/receive/1",
CallbackPath = $"twilio/voice/receive/1?conversation-id={conversationId}",
SpeechPaths = new List<string>
{
$"twilio/voice/speeches/{conversationId}/intial.mp3"

View file

@ -0,0 +1,8 @@
using Task = System.Threading.Tasks.Task;
namespace BotSharp.Plugin.Twilio.Interfaces;
public interface ITwilioCallStatusHook
{
Task OnVoicemailLeft(string conversationId);
}

View file

@ -7,7 +7,7 @@ public class ConversationalVoiceRequest : VoiceRequest
[FromQuery(Name = "agent-id")]
public string AgentId { get; set; } = string.Empty;
[FromRoute]
[FromQuery(Name = "conversation-id")]
public string ConversationId { get; set; } = string.Empty;
[FromRoute]
@ -18,7 +18,27 @@ public class ConversationalVoiceRequest : VoiceRequest
public string? AIResponseErrorMessage { get; set; } = string.Empty;
public string Intent { get; set; } = string.Empty;
[FromQuery(Name = "init-audio-file")]
public string? InitAudioFile { get; set; }
public List<string> States { get; set; } = [];
[FromForm]
public string? CallbackSource { get; set; }
/// <summary>
/// machine_start
/// </summary>
[FromForm]
public string? AnsweredBy { get; set; }
[FromForm]
public int MachineDetectionDuration { get; set; }
[FromForm]
public int Duration { get; set; }
[FromForm]
public int CallDuration { get; set; }
}

View file

@ -66,9 +66,12 @@ public class OutboundPhoneCallFn : IFunctionCallback
// Make outbound call
var call = await CallResource.CreateAsync(
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation_id={newConversationId}&init_audio_file={fileName}"),
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream?conversation-id={newConversationId}&init-audio-file={fileName}"),
to: new PhoneNumber(args.PhoneNumber),
from: new PhoneNumber(_twilioSetting.PhoneNumber));
from: new PhoneNumber(_twilioSetting.PhoneNumber),
statusCallback: new Uri($"{_twilioSetting.CallbackHost}/twilio/stream/status?conversation-id={newConversationId}&init-audio-file={fileName}"),
// https://www.twilio.com/docs/voice/answering-machine-detection
machineDetection: "Enable");
var convService = _services.GetRequiredService<IConversationService>();
var routing = _services.GetRequiredService<IRoutingContext>();

View file

@ -199,6 +199,7 @@ public class TwilioService
}
}
}
var connect = new Connect();
var host = _settings.CallbackHost.Split("://").Last();
connect.Stream(url: $"wss://{host}/twilio/stream/{conversationId}");