Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,22 @@ public async Task<TwiMLResult> InitiateStreamConversation(ConversationalVoiceReq
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));
Expand Down Expand Up @@ -141,7 +135,7 @@ protected Dictionary<string, string> ParseStates(List<string> states)
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);
Expand All @@ -167,20 +161,25 @@ private async Task<string> InitConversation(ConversationalVoiceRequest request,
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,12 @@ public async Task<TwiMLResult> InitiateOutboundCall(ConversationalVoiceRequest r
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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,38 +332,41 @@ public async Task<TwiMLResult> TransferCall(ConversationalVoiceRequest request)
[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();
Expand Down
13 changes: 13 additions & 0 deletions src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,19 @@ await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
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/"))
Expand Down
Loading