2025-04-02 15:35:03 +00:00
|
|
|
using BotSharp.Abstraction.Agents.Models;
|
|
|
|
|
using BotSharp.Abstraction.Infrastructures.Enums;
|
|
|
|
|
using BotSharp.Core.Infrastructures;
|
|
|
|
|
using BotSharp.Plugin.Twilio.Interfaces;
|
|
|
|
|
using BotSharp.Plugin.Twilio.Models;
|
|
|
|
|
using BotSharp.Plugin.Twilio.Services;
|
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using Twilio.Http;
|
|
|
|
|
using Conversation = BotSharp.Abstraction.Conversations.Models.Conversation;
|
|
|
|
|
using Task = System.Threading.Tasks.Task;
|
|
|
|
|
|
|
|
|
|
namespace BotSharp.Plugin.Twilio.Controllers;
|
|
|
|
|
|
|
|
|
|
public class TwilioInboundController : TwilioController
|
|
|
|
|
{
|
|
|
|
|
private readonly TwilioSetting _settings;
|
|
|
|
|
private readonly IServiceProvider _services;
|
|
|
|
|
private readonly IHttpContextAccessor _context;
|
|
|
|
|
private readonly ILogger _logger;
|
|
|
|
|
|
|
|
|
|
public TwilioInboundController(TwilioSetting settings, IServiceProvider services, IHttpContextAccessor context, ILogger<TwilioOutboundController> logger)
|
|
|
|
|
{
|
|
|
|
|
_settings = settings;
|
|
|
|
|
_services = services;
|
|
|
|
|
_context = context;
|
|
|
|
|
_logger = logger;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[ValidateRequest]
|
|
|
|
|
[HttpPost("twilio/inbound")]
|
|
|
|
|
public async Task<TwiMLResult> InitiateStreamConversation(ConversationalVoiceRequest request)
|
|
|
|
|
{
|
|
|
|
|
if (request?.CallSid == null)
|
|
|
|
|
{
|
|
|
|
|
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var twilio = _services.GetRequiredService<TwilioService>();
|
|
|
|
|
VoiceResponse response = default!;
|
|
|
|
|
|
|
|
|
|
var instruction = new ConversationalVoiceResponse
|
|
|
|
|
{
|
|
|
|
|
AgentId = request.AgentId,
|
|
|
|
|
ConversationId = request.ConversationId,
|
|
|
|
|
SpeechPaths = [],
|
|
|
|
|
ActionOnEmptyResult = true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (request.InitAudioFile != null)
|
|
|
|
|
{
|
|
|
|
|
instruction.SpeechPaths.Add(request.InitAudioFile);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
|
|
|
|
{
|
|
|
|
|
await hook.OnSessionCreating(request, instruction);
|
|
|
|
|
});
|
|
|
|
|
|
2025-04-10 18:38:46 +00:00
|
|
|
var (agent, conversationId) = await InitConversation(request);
|
|
|
|
|
request.ConversationId = conversationId.Id;
|
2025-04-02 15:35:03 +00:00
|
|
|
instruction.AgentId = request.AgentId;
|
|
|
|
|
instruction.ConversationId = request.ConversationId;
|
|
|
|
|
|
2025-04-10 18:38:46 +00:00
|
|
|
if (twilio.MachineDetected(request))
|
2025-04-02 15:35:03 +00:00
|
|
|
{
|
|
|
|
|
response = new VoiceResponse();
|
|
|
|
|
|
2025-04-10 18:38:46 +00:00
|
|
|
await HookEmitter.Emit<ITwilioCallStatusHook>(_services,
|
|
|
|
|
async hook => await hook.OnVoicemailStarting(request));
|
2025-04-02 15:35:03 +00:00
|
|
|
|
|
|
|
|
var url = twilio.GetSpeechPath(request.ConversationId, "voicemail.mp3");
|
|
|
|
|
response.Play(new Uri(url));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (agent.Profiles.Contains("realtime"))
|
|
|
|
|
{
|
|
|
|
|
response = twilio.ReturnBidirectionalMediaStreamsInstructions(instruction, agent);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(request.Intent))
|
|
|
|
|
{
|
|
|
|
|
instruction.CallbackPath = $"twilio/voice/receive/0?agent-id={request.AgentId}&conversation-id={request.ConversationId}&{twilio.GenerateStatesParameter(request.States)}";
|
|
|
|
|
response = twilio.ReturnNoninterruptedInstructions(instruction);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
int seqNum = 0;
|
|
|
|
|
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
|
|
|
|
|
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
|
|
|
|
|
await sessionManager.StageCallerMessageAsync(request.ConversationId, seqNum, request.Intent);
|
|
|
|
|
var callerMessage = new CallerMessage()
|
|
|
|
|
{
|
|
|
|
|
AgentId = request.AgentId,
|
|
|
|
|
ConversationId = request.ConversationId,
|
|
|
|
|
SeqNumber = seqNum,
|
|
|
|
|
Content = request.Intent,
|
|
|
|
|
From = request.From,
|
|
|
|
|
States = ParseStates(request.States)
|
|
|
|
|
};
|
|
|
|
|
await messageQueue.EnqueueAsync(callerMessage);
|
|
|
|
|
response = new VoiceResponse();
|
|
|
|
|
// delay 3 seconds to wait for the first message reply and caller is listening dudu sound
|
|
|
|
|
await Task.Delay(1000 * 3);
|
|
|
|
|
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/reply/{seqNum}?agent-id={request.AgentId}&conversation-id={request.ConversationId}&{twilio.GenerateStatesParameter(request.States)}"), HttpMethod.Post);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
|
|
|
|
|
{
|
|
|
|
|
await hook.OnSessionCreated(request);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return TwiML(response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected Dictionary<string, string> ParseStates(List<string> states)
|
|
|
|
|
{
|
|
|
|
|
var result = new Dictionary<string, string>();
|
|
|
|
|
if (states is null || !states.Any())
|
|
|
|
|
{
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
foreach (var kvp in states)
|
|
|
|
|
{
|
|
|
|
|
var parts = kvp.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
if (parts.Length == 2)
|
|
|
|
|
{
|
|
|
|
|
result.Add(parts[0], parts[1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-10 18:38:46 +00:00
|
|
|
private async Task<(Agent, Conversation)> InitConversation(ConversationalVoiceRequest request)
|
2025-04-02 15:35:03 +00:00
|
|
|
{
|
|
|
|
|
var convService = _services.GetRequiredService<IConversationService>();
|
|
|
|
|
var conversation = await convService.GetConversation(request.ConversationId);
|
|
|
|
|
if (conversation == null)
|
|
|
|
|
{
|
|
|
|
|
var conv = new Conversation
|
|
|
|
|
{
|
|
|
|
|
AgentId = request.AgentId,
|
|
|
|
|
Channel = ConversationChannel.Phone,
|
|
|
|
|
ChannelId = request.CallSid,
|
|
|
|
|
Title = $"Incoming phone call from {request.From}",
|
|
|
|
|
Tags = [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
conversation = await convService.NewConversation(conv);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var states = new List<MessageState>
|
|
|
|
|
{
|
|
|
|
|
new("channel", ConversationChannel.Phone),
|
|
|
|
|
new("calling_phone", request.From),
|
|
|
|
|
new("phone_direction", request.Direction),
|
|
|
|
|
new("twilio_call_sid", request.CallSid),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (request.InitAudioFile != null)
|
|
|
|
|
{
|
|
|
|
|
states.Add(new("init_audio_file", request.InitAudioFile));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
convService.SetConversationId(conversation.Id, states);
|
2025-04-10 18:38:46 +00:00
|
|
|
|
|
|
|
|
// 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"));
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-02 15:35:03 +00:00
|
|
|
convService.SaveStates();
|
|
|
|
|
|
2025-04-10 18:38:46 +00:00
|
|
|
return (agent, conversation);
|
2025-04-02 15:35:03 +00:00
|
|
|
}
|
|
|
|
|
}
|