commit
2a54764c93
|
|
@ -26,7 +26,6 @@ public class RealtimeHub : IRealtimeHub
|
|||
{
|
||||
var buffer = new byte[1024 * 16];
|
||||
WebSocketReceiveResult result;
|
||||
|
||||
|
||||
|
||||
do
|
||||
|
|
@ -98,24 +97,33 @@ public class RealtimeHub : IRealtimeHub
|
|||
|
||||
routing.Context.SetDialogs(dialogs);
|
||||
|
||||
var states = _services.GetRequiredService<IConversationStateService>();
|
||||
|
||||
await _completer.Connect(_conn,
|
||||
onModelReady: async () =>
|
||||
{
|
||||
// Control initial session, prevent initial response interruption
|
||||
await _completer.UpdateSession(_conn, turnDetection: false);
|
||||
|
||||
if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
|
||||
if (states.ContainsState("init_audio_file"))
|
||||
{
|
||||
await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
|
||||
await _completer.UpdateSession(_conn, turnDetection: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _completer.TriggerModelInference("Reply based on the conversation context.");
|
||||
}
|
||||
// Control initial session, prevent initial response interruption
|
||||
await _completer.UpdateSession(_conn, turnDetection: false);
|
||||
|
||||
// Start turn detection
|
||||
await Task.Delay(1000 * 8);
|
||||
await _completer.UpdateSession(_conn, turnDetection: true);
|
||||
if (dialogs.LastOrDefault()?.Role == AgentRole.Assistant)
|
||||
{
|
||||
await _completer.TriggerModelInference($"Rephase your last response:\r\n{dialogs.LastOrDefault()?.Content}");
|
||||
}
|
||||
else
|
||||
{
|
||||
await _completer.TriggerModelInference("Reply based on the conversation context.");
|
||||
}
|
||||
|
||||
// Start turn detection
|
||||
await Task.Delay(1000 * 8);
|
||||
await _completer.UpdateSession(_conn, turnDetection: true);
|
||||
}
|
||||
},
|
||||
onModelAudioDeltaReceived: async (audioDeltaData, itemId) =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -35,21 +35,26 @@ 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)
|
||||
{
|
||||
instruction.SpeechPaths.Add(_context.HttpContext.Request.Query["init_audio_file"]);
|
||||
}
|
||||
|
||||
if (_context.HttpContext.Request.Query.ContainsKey("conversation_id"))
|
||||
{
|
||||
request.ConversationId = _context.HttpContext.Request.Query["conversation_id"];
|
||||
instruction.SpeechPaths.Add(request.InitAudioFile);
|
||||
}
|
||||
|
||||
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
||||
|
|
@ -77,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>();
|
||||
|
|
@ -104,6 +127,11 @@ public class TwilioStreamController : TwilioController
|
|||
new(StateConst.ROUTING_MODE, "lazy"),
|
||||
};
|
||||
|
||||
if (request.InitAudioFile != null)
|
||||
{
|
||||
states.Add(new("init_audio_file", request.InitAudioFile));
|
||||
}
|
||||
|
||||
convService.SetConversationId(conversation.Id, states);
|
||||
convService.SaveStates();
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace BotSharp.Plugin.Twilio.Interfaces;
|
||||
|
||||
public interface ITwilioCallStatusHook
|
||||
{
|
||||
Task OnVoicemailLeft(string conversationId);
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
@ -19,5 +19,26 @@ public class ConversationalVoiceRequest : VoiceRequest
|
|||
|
||||
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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
Loading…
Reference in a new issue