Fix conversation states before loading agent for init-outbound-call

This commit is contained in:
Haiping Chen 2025-04-10 13:38:46 -05:00
parent 69f0138593
commit aba857c925
4 changed files with 48 additions and 36 deletions

View file

@ -52,28 +52,22 @@ public class TwilioInboundController : TwilioController
instruction.SpeechPaths.Add(request.InitAudioFile);
}
// Load agent profile
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(request.AgentId);
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreating(request, instruction);
});
request.ConversationId = await InitConversation(request, agent);
var (agent, conversationId) = await InitConversation(request);
request.ConversationId = conversationId.Id;
instruction.AgentId = request.AgentId;
instruction.ConversationId = request.ConversationId;
if (request.AnsweredBy == "machine_start" &&
request.Direction == "outbound-api")
if (twilio.MachineDetected(request))
{
response = new VoiceResponse();
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
{
await hook.OnVoicemailStarting(request);
});
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook => await hook.OnVoicemailStarting(request));
var url = twilio.GetSpeechPath(request.ConversationId, "voicemail.mp3");
response.Play(new Uri(url));
@ -141,7 +135,7 @@ public class TwilioInboundController : TwilioController
return result;
}
private async Task<string> InitConversation(ConversationalVoiceRequest request, Agent agent)
private async Task<(Agent, Conversation)> InitConversation(ConversationalVoiceRequest request)
{
var convService = _services.GetRequiredService<IConversationService>();
var conversation = await convService.GetConversation(request.ConversationId);
@ -167,20 +161,25 @@ public class TwilioInboundController : TwilioController
new("twilio_call_sid", request.CallSid),
};
// Enable lazy routing mode to optimize realtime experience
if (agent.Profiles.Contains("realtime") && agent.Type == AgentType.Routing)
{
states.Add(new(StateConst.ROUTING_MODE, "lazy"));
}
if (request.InitAudioFile != null)
{
states.Add(new("init_audio_file", request.InitAudioFile));
}
convService.SetConversationId(conversation.Id, states);
// Load agent profile
var agentService = _services.GetRequiredService<IAgentService>();
var agent = await agentService.LoadAgent(request.AgentId);
// Enable lazy routing mode to optimize realtime experience
if (agent.Profiles.Contains("realtime") && agent.Type == AgentType.Routing)
{
states.Add(new(StateConst.ROUTING_MODE, "lazy"));
}
convService.SaveStates();
return conversation.Id;
return (agent, conversation);
}
}

View file

@ -29,15 +29,12 @@ public class TwilioOutboundController : TwilioController
var twilio = _services.GetRequiredService<TwilioService>();
VoiceResponse response = default!;
if (request.AnsweredBy == "machine_start" &&
request.Direction == "outbound-api")
if (twilio.MachineDetected(request))
{
response = new VoiceResponse();
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
{
await hook.OnVoicemailStarting(request);
});
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook => await hook.OnVoicemailStarting(request));
var url = twilio.GetSpeechPath(request.ConversationId, "voicemail.mp3");
response.Play(new Uri(url));

View file

@ -332,38 +332,41 @@ public class TwilioVoiceController : TwilioController
[HttpPost("twilio/voice/status")]
public async Task<ActionResult> PhoneCallStatus(ConversationalVoiceRequest request)
{
var twilio = _services.GetRequiredService<TwilioService>();
if (request.CallStatus == "completed")
{
if (request.AnsweredBy == "machine_start" &&
request.Direction == "outbound-api")
if (twilio.MachineDetected(request))
{
// voicemail
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, async hook =>
{
await hook.OnVoicemailLeft(request);
});
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async hook => await hook.OnVoicemailLeft(request));
}
else
{
// phone call completed
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnUserDisconnected(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnUserDisconnected(request));
}
}
else if (request.CallStatus == "busy")
{
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnCallBusyStatus(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnCallBusyStatus(request));
}
else if (request.CallStatus == "no-answer")
{
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnCallNoAnswerStatus(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnCallNoAnswerStatus(request));
}
else if (request.CallStatus == "canceled")
{
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnCallCanceledStatus(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnCallCanceledStatus(request));
}
else if (request.CallStatus == "failed")
{
await HookEmitter.Emit<ITwilioCallStatusHook>(_services, x => x.OnCallFailedStatus(request));
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
async x => await x.OnCallFailedStatus(request));
}
return Ok();

View file

@ -309,6 +309,19 @@ public class TwilioService
return response;
}
/// <summary>
/// https://www.twilio.com/docs/voice/answering-machine-detection
/// </summary>
/// <param name="answeredBy"></param>
/// <returns></returns>
public bool MachineDetected(ConversationalVoiceRequest request)
{
var answeredBy = request.AnsweredBy ?? "unknown";
var isOutboundCall = request.Direction == "outbound-api";
var isMachine = answeredBy.StartsWith("machine_") || answeredBy == "fax";
return isOutboundCall && isMachine;
}
public string GetSpeechPath(string conversationId, string speechPath)
{
if (speechPath.StartsWith("twilio/"))