BotSharp/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs

268 lines
11 KiB
C#
Raw Normal View History

2024-08-08 19:54:32 +00:00
using BotSharp.Abstraction.Files;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Models;
using BotSharp.Plugin.Twilio.Services;
2024-08-22 21:13:38 +00:00
using Microsoft.AspNetCore.Http;
2023-11-09 20:02:57 +00:00
using Microsoft.AspNetCore.Mvc;
2024-08-28 20:03:23 +00:00
using Twilio.Http;
2023-11-09 20:02:57 +00:00
namespace BotSharp.Plugin.Twilio.Controllers;
public class TwilioVoiceController : TwilioController
{
private readonly TwilioSetting _settings;
private readonly IServiceProvider _services;
2024-08-22 21:13:38 +00:00
private readonly IHttpContextAccessor _context;
private readonly ILogger _logger;
2023-11-09 20:02:57 +00:00
public TwilioVoiceController(TwilioSetting settings, IServiceProvider services, IHttpContextAccessor context, ILogger<TwilioVoiceController> logger)
2023-11-09 20:02:57 +00:00
{
_settings = settings;
_services = services;
2024-08-22 21:13:38 +00:00
_context = context;
_logger = logger;
2023-11-09 20:02:57 +00:00
}
2023-12-06 22:55:55 +00:00
2024-08-22 21:13:38 +00:00
/// <summary>
/// https://github.com/twilio-labs/twilio-aspnet?tab=readme-ov-file#validate-twilio-http-requests
/// </summary>
/// <param name="request"></param>
/// <param name="states"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
[ValidateRequest]
[HttpPost("twilio/voice/welcome")]
2024-10-29 18:55:55 +00:00
public async Task<TwiMLResult> InitiateConversation(VoiceRequest request, [FromQuery] string states, [FromQuery] string intent)
2024-08-08 19:54:32 +00:00
{
2024-08-28 20:34:41 +00:00
if (request?.CallSid == null)
{
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
}
2024-08-14 21:44:47 +00:00
string conversationId = $"TwilioVoice_{request.CallSid}";
2024-08-08 19:54:32 +00:00
var twilio = _services.GetRequiredService<TwilioService>();
2024-10-29 18:55:55 +00:00
VoiceResponse response;
if (string.IsNullOrWhiteSpace(intent))
{
var url = $"twilio/voice/{conversationId}/receive/0?states={states}";
response = twilio.ReturnNoninterruptedInstructions(new List<string> { "twilio/welcome.mp3" }, url, true, timeout: 2);
}
else
{
int seqNum = 0;
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, intent);
var callerMessage = new CallerMessage()
{
ConversationId = conversationId,
SeqNumber = seqNum,
Content = intent,
From = request.From,
States = ParseStates(states)
};
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}"), HttpMethod.Post);
}
2024-08-08 19:54:32 +00:00
return TwiML(response);
}
2024-08-22 21:13:38 +00:00
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
2024-09-23 22:20:19 +00:00
public async Task<TwiMLResult> ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, VoiceRequest request, [FromQuery] int attempts = 1)
2024-08-08 19:54:32 +00:00
{
var twilio = _services.GetRequiredService<TwilioService>();
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
2024-08-28 20:34:41 +00:00
2024-08-14 21:44:47 +00:00
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(conversationId, seqNum);
2024-08-26 22:33:47 +00:00
string text = (request.SpeechResult + "\r\n" + request.Digits).Trim();
2024-08-28 20:34:41 +00:00
2024-08-26 22:33:47 +00:00
if (!string.IsNullOrWhiteSpace(text))
2024-08-08 19:54:32 +00:00
{
2024-08-26 22:33:47 +00:00
messages.Add(text);
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, text);
2024-08-08 19:54:32 +00:00
}
2024-08-28 20:03:23 +00:00
2024-08-08 19:54:32 +00:00
VoiceResponse response;
2024-08-28 20:03:23 +00:00
if (messages.Any())
2024-08-08 19:54:32 +00:00
{
2024-08-22 15:56:10 +00:00
var messageContent = string.Join("\r\n", messages);
2024-08-08 19:54:32 +00:00
var callerMessage = new CallerMessage()
{
2024-08-14 21:44:47 +00:00
ConversationId = conversationId,
2024-08-08 19:54:32 +00:00
SeqNumber = seqNum,
Content = messageContent,
2024-10-29 18:55:55 +00:00
Digits = request.Digits,
From = request.From,
States = ParseStates(states)
2024-08-08 19:54:32 +00:00
};
2024-09-05 15:42:48 +00:00
await messageQueue.EnqueueAsync(callerMessage);
2024-09-04 03:13:25 +00:00
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}"), HttpMethod.Post);
2024-08-08 19:54:32 +00:00
}
2024-08-28 20:03:23 +00:00
else
{
2024-09-04 17:00:27 +00:00
if (attempts >= 2)
2024-08-29 19:36:07 +00:00
{
var speechPaths = new List<string>();
2024-09-04 17:00:27 +00:00
2024-08-29 19:36:07 +00:00
if (seqNum == 0)
{
speechPaths.Add("twilio/welcome.mp3");
}
else
{
var lastRepy = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum - 1);
2024-09-04 17:00:27 +00:00
speechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
2024-08-29 19:36:07 +00:00
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{lastRepy.SpeechFileName}");
}
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true);
}
2024-08-30 18:18:55 +00:00
else
{
response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}&attempts={++attempts}", true);
2024-09-06 15:14:10 +00:00
}
2024-08-28 20:03:23 +00:00
}
2024-08-08 19:54:32 +00:00
return TwiML(response);
}
2024-08-22 21:13:38 +00:00
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
2024-09-06 15:14:10 +00:00
public async Task<TwiMLResult> ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum,
2024-08-29 19:36:07 +00:00
[FromQuery] string states, VoiceRequest request)
2024-08-08 19:54:32 +00:00
{
var nextSeqNum = seqNum + 1;
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
var twilio = _services.GetRequiredService<TwilioService>();
2024-08-30 16:29:21 +00:00
var fileStorage = _services.GetRequiredService<IFileStorageService>();
2024-08-28 20:34:41 +00:00
2024-08-08 19:54:32 +00:00
if (request.SpeechResult != null)
{
2024-08-14 21:44:47 +00:00
await sessionManager.StageCallerMessageAsync(conversationId, nextSeqNum, request.SpeechResult);
2024-08-08 19:54:32 +00:00
}
2024-08-28 20:34:41 +00:00
2024-08-14 21:44:47 +00:00
var reply = await sessionManager.GetAssistantReplyAsync(conversationId, seqNum);
2024-08-08 19:54:32 +00:00
VoiceResponse response;
2024-08-30 16:29:21 +00:00
2024-08-14 21:44:47 +00:00
if (reply == null)
2024-08-08 19:54:32 +00:00
{
2024-08-29 16:49:34 +00:00
var indication = await sessionManager.GetReplyIndicationAsync(conversationId, seqNum);
2024-08-14 21:44:47 +00:00
if (indication != null)
{
_logger.LogWarning($"Indication: {indication}");
2024-08-28 20:03:23 +00:00
var speechPaths = new List<string>();
2024-08-29 19:36:07 +00:00
int segIndex = 0;
2024-08-28 20:03:23 +00:00
foreach (var text in indication.Split('|'))
2024-08-22 15:56:10 +00:00
{
2024-08-28 20:03:23 +00:00
var seg = text.Trim();
if (seg.StartsWith('#'))
{
speechPaths.Add($"twilio/{seg.Substring(1)}.mp3");
}
else
{
2024-08-30 16:29:21 +00:00
var completion = CompletionProvider.GetAudioCompletion(_services, "openai", "tts-1");
var data = await completion.GenerateAudioFromTextAsync(seg);
2024-09-05 21:44:15 +00:00
// add hold-on
var holdOnIndex = Random.Shared.Next(1, 10);
if (holdOnIndex < 7)
{
speechPaths.Add($"twilio/hold-on-short-{holdOnIndex}.mp3");
}
2024-10-29 18:55:55 +00:00
2024-08-29 19:36:07 +00:00
var fileName = $"indication_{seqNum}_{segIndex}.mp3";
2024-08-30 16:29:21 +00:00
fileStorage.SaveSpeechFile(conversationId, fileName, data);
2024-08-28 20:03:23 +00:00
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}");
2024-09-04 17:00:27 +00:00
// add typing
var typingIndex = Random.Shared.Next(1, 7);
if (typingIndex < 4)
{
speechPaths.Add($"twilio/typing-{typingIndex}.mp3");
}
2024-08-29 19:36:07 +00:00
segIndex++;
2024-08-28 20:03:23 +00:00
}
2024-08-22 15:56:10 +00:00
}
2024-08-28 20:03:23 +00:00
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true);
2024-08-29 19:36:07 +00:00
await sessionManager.RemoveReplyIndicationAsync(conversationId, seqNum);
2024-08-14 21:44:47 +00:00
}
else
{
var instructions = new List<string>
{
};
// add hold-on
var holdOnIndex = Random.Shared.Next(1, 15);
if (holdOnIndex < 9)
{
instructions.Add($"twilio/hold-on-long-{holdOnIndex}.mp3");
}
// add typing
var typingIndex = Random.Shared.Next(1, 7);
if (typingIndex < 4)
2024-08-29 16:49:34 +00:00
{
instructions.Add($"twilio/typing-{typingIndex}.mp3");
}
response = twilio.ReturnInstructions(instructions, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true);
2024-08-14 21:44:47 +00:00
}
2024-08-08 19:54:32 +00:00
}
else
{
2024-09-06 15:14:10 +00:00
if (reply.HumanIntervationNeeded)
{
response = twilio.DialCsrAgent($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}");
}
else if (reply.ConversationEnd)
2024-08-14 21:44:47 +00:00
{
2024-08-23 20:16:55 +00:00
response = twilio.HangUp($"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}");
2024-08-14 21:44:47 +00:00
}
else
{
2024-08-30 16:29:21 +00:00
response = twilio.ReturnInstructions(new List<string>
{
$"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}"
2024-10-29 18:55:55 +00:00
}, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?states={states}", true, hints: reply.Hints);
2024-08-14 21:44:47 +00:00
}
2024-08-08 19:54:32 +00:00
}
2024-08-28 20:34:41 +00:00
2024-08-08 19:54:32 +00:00
return TwiML(response);
}
2024-08-22 21:13:38 +00:00
[ValidateRequest]
[HttpGet("twilio/voice/speeches/{conversationId}/{fileName}")]
2024-08-28 20:34:41 +00:00
public async Task<FileContentResult> GetSpeechFile([FromRoute] string conversationId, [FromRoute] string fileName)
2024-08-08 19:54:32 +00:00
{
2024-08-28 20:34:41 +00:00
var fileStorage = _services.GetRequiredService<IFileStorageService>();
var data = fileStorage.GetSpeechFile(conversationId, fileName);
2024-08-30 16:29:21 +00:00
var result = new FileContentResult(data.ToArray(), "audio/mpeg")
{
FileDownloadName = fileName
};
2024-08-08 19:54:32 +00:00
return result;
}
2024-10-29 18:55:55 +00:00
private Dictionary<string, string> ParseStates(string? states)
{
var result = new Dictionary<string, string>();
if (string.IsNullOrWhiteSpace(states))
{
return result;
}
var kvps = states.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
foreach (var kvp in kvps)
{
var parts = kvp.Split(':');
if (parts.Length == 2)
{
result.Add(parts[0], parts[1]);
}
}
return result;
}
2023-11-09 20:02:57 +00:00
}