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

431 lines
16 KiB
C#
Raw Normal View History

2024-08-08 19:54:32 +00:00
using BotSharp.Abstraction.Files;
2024-11-12 22:51:33 +00:00
using BotSharp.Abstraction.Infrastructures;
2025-01-14 16:45:54 +00:00
using BotSharp.Abstraction.Repositories;
2024-08-08 19:54:32 +00:00
using BotSharp.Core.Infrastructures;
2024-11-12 22:51:33 +00:00
using BotSharp.Plugin.Twilio.Interfaces;
2024-08-08 19:54:32 +00:00
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-11-19 19:05:12 +00:00
using System.ComponentModel.DataAnnotations;
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>
2025-02-11 23:27:07 +00:00
// [ValidateRequest]
2024-08-22 21:13:38 +00:00
[HttpPost("twilio/voice/welcome")]
2024-11-12 22:51:33 +00:00
public async Task<TwiMLResult> InitiateConversation(ConversationalVoiceRequest request)
2024-08-08 19:54:32 +00:00
{
2025-02-07 02:30:55 +00:00
foreach(var header in Request.Headers)
{
_logger.LogWarning($"{header.Key}: {header.Value}");
}
2025-02-07 03:27:13 +00:00
_logger.LogWarning($"{Request.Path}{Request.QueryString}");
2024-11-12 22:51:33 +00:00
var text = JsonSerializer.Serialize(request);
2024-08-28 20:34:41 +00:00
if (request?.CallSid == null)
{
throw new ArgumentNullException(nameof(VoiceRequest.CallSid));
}
2024-11-12 22:51:33 +00:00
VoiceResponse response = null;
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = ["twilio/welcome.mp3"],
2024-11-13 21:09:39 +00:00
ActionOnEmptyResult = true
2024-11-12 22:51:33 +00:00
};
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreating(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
2024-11-13 21:09:39 +00:00
request.ConversationId = $"TwilioVoice_{request.CallSid}";
instruction.CallbackPath = $"twilio/voice/{request.ConversationId}/receive/0?{GenerateStatesParameter(request.States)}";
2024-11-13 15:34:21 +00:00
2024-08-08 19:54:32 +00:00
var twilio = _services.GetRequiredService<TwilioService>();
2024-11-12 22:51:33 +00:00
if (string.IsNullOrWhiteSpace(request.Intent))
2024-10-29 18:55:55 +00:00
{
2024-11-12 22:51:33 +00:00
response = twilio.ReturnNoninterruptedInstructions(instruction);
2024-10-29 18:55:55 +00:00
}
else
{
int seqNum = 0;
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
2024-11-12 22:51:33 +00:00
await sessionManager.StageCallerMessageAsync(request.ConversationId, seqNum, request.Intent);
2024-10-29 18:55:55 +00:00
var callerMessage = new CallerMessage()
{
2024-11-12 22:51:33 +00:00
ConversationId = request.ConversationId,
2024-10-29 18:55:55 +00:00
SeqNumber = seqNum,
2024-11-12 22:51:33 +00:00
Content = request.Intent,
2024-10-29 18:55:55 +00:00
From = request.From,
2024-11-12 22:51:33 +00:00
States = ParseStates(request.States)
2024-10-29 18:55:55 +00:00
};
await messageQueue.EnqueueAsync(callerMessage);
2024-11-12 22:51:33 +00:00
response = new VoiceResponse();
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{seqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
2024-10-29 18:55:55 +00:00
}
2024-11-12 22:51:33 +00:00
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnSessionCreated(request);
}, new HookEmitOption
{
OnlyOnce = true
});
2024-08-08 19:54:32 +00:00
return TwiML(response);
}
2024-11-12 22:51:33 +00:00
/// <summary>
/// Wait for caller's response
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
2025-02-07 02:12:00 +00:00
// [ValidateRequest]
2024-08-22 21:13:38 +00:00
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
2024-11-12 22:51:33 +00:00
public async Task<TwiMLResult> ReceiveCallerMessage(ConversationalVoiceRequest request)
2024-08-08 19:54:32 +00:00
{
var twilio = _services.GetRequiredService<TwilioService>();
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
2024-11-12 22:51:33 +00:00
var messages = await sessionManager.RetrieveStagedCallerMessagesAsync(request.ConversationId, request.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);
2024-11-12 22:51:33 +00:00
await sessionManager.StageCallerMessageAsync(request.ConversationId, request.SeqNum, text);
2024-08-08 19:54:32 +00:00
}
2024-08-28 20:03:23 +00:00
2024-11-12 22:51:33 +00:00
VoiceResponse response = null;
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-11-12 22:51:33 +00:00
ConversationId = request.ConversationId,
SeqNumber = request.SeqNum,
2024-08-08 19:54:32 +00:00
Content = messageContent,
2024-10-29 18:55:55 +00:00
Digits = request.Digits,
2025-01-14 15:28:18 +00:00
From = string.Equals(request.Direction, "inbound") ? request.From : request.To,
2024-11-12 22:51:33 +00:00
States = ParseStates(request.States)
2024-08-08 19:54:32 +00:00
};
2025-01-14 15:28:18 +00:00
callerMessage.RequestHeaders = new KeyValuePair<string, Microsoft.Extensions.Primitives.StringValues>[Request.Headers.Count];
Request.Headers.CopyTo(callerMessage.RequestHeaders, 0);
2024-09-05 15:42:48 +00:00
await messageQueue.EnqueueAsync(callerMessage);
2024-11-12 22:51:33 +00:00
response = new VoiceResponse();
response.Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}"), HttpMethod.Post);
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnReceivedUserMessage(request);
}, new HookEmitOption
{
OnlyOnce = true
});
2024-08-08 19:54:32 +00:00
}
2024-08-28 20:03:23 +00:00
else
{
2025-01-23 16:57:21 +00:00
if (request.Attempts > _settings.MaxGatherAttempts)
2024-08-29 19:36:07 +00:00
{
2024-11-12 22:51:33 +00:00
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
2025-01-23 16:57:21 +00:00
await hook.OnAgentHangUp(request);
2024-11-12 22:51:33 +00:00
}, new HookEmitOption
{
OnlyOnce = true
});
2025-01-23 16:57:21 +00:00
response = twilio.HangUp(null);
2024-08-29 19:36:07 +00:00
}
2025-01-23 16:57:21 +00:00
// keep waiting for user response
2024-08-30 18:18:55 +00:00
else
{
2024-11-12 22:51:33 +00:00
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = new List<string>(),
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{request.SeqNum}?{GenerateStatesParameter(request.States)}&attempts={++request.Attempts}",
ActionOnEmptyResult = true
};
2024-11-13 21:09:39 +00:00
if (request.Attempts == 3)
2024-11-12 22:51:33 +00:00
{
instruction.SpeechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
}
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnWaitingUserResponse(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
2024-09-06 15:14:10 +00:00
}
2024-08-28 20:03:23 +00:00
}
2024-11-12 22:51:33 +00:00
2024-08-08 19:54:32 +00:00
return TwiML(response);
}
2024-11-12 22:51:33 +00:00
/// <summary>
/// Polling for assistant reply after user responsed
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
2025-02-07 02:12:00 +00:00
// [ValidateRequest]
2024-08-22 21:13:38 +00:00
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
2024-11-12 22:51:33 +00:00
public async Task<TwiMLResult> ReplyCallerMessage(ConversationalVoiceRequest request)
2024-08-08 19:54:32 +00:00
{
2024-11-12 22:51:33 +00:00
var nextSeqNum = request.SeqNum + 1;
2024-08-08 19:54:32 +00:00
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-08 19:54:32 +00:00
if (request.SpeechResult != null)
{
2024-11-12 22:51:33 +00:00
await sessionManager.StageCallerMessageAsync(request.ConversationId, nextSeqNum, request.SpeechResult);
2024-08-08 19:54:32 +00:00
}
2024-08-28 20:34:41 +00:00
2024-11-12 22:51:33 +00:00
var reply = await sessionManager.GetAssistantReplyAsync(request.ConversationId, request.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-11-12 22:51:33 +00:00
var indication = await sessionManager.GetReplyIndicationAsync(request.ConversationId, request.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-11-12 22:51:33 +00:00
var fileName = $"indication_{request.SeqNum}_{segIndex}.mp3";
fileStorage.SaveSpeechFile(request.ConversationId, fileName, data);
speechPaths.Add($"twilio/voice/speeches/{request.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-11-12 22:51:33 +00:00
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = speechPaths,
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true
};
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnIndicationGenerated(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
await sessionManager.RemoveReplyIndicationAsync(request.ConversationId, request.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");
}
2024-11-12 22:51:33 +00:00
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = instructions,
CallbackPath = $"twilio/voice/{request.ConversationId}/reply/{request.SeqNum}?{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true
};
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnWaitingAgentResponse(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
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)
{
2024-11-12 22:51:33 +00:00
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnAgentTransferring(request, _settings);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.DialCsrAgent($"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}");
2024-09-06 15:14:10 +00:00
}
else if (reply.ConversationEnd)
2024-08-14 21:44:47 +00:00
{
2024-11-12 22:51:33 +00:00
response = twilio.HangUp($"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}");
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
{
await hook.OnAgentHangUp(request);
}, new HookEmitOption
{
OnlyOnce = true
});
2024-08-14 21:44:47 +00:00
}
else
{
2024-11-12 22:51:33 +00:00
var instruction = new ConversationalVoiceResponse
{
SpeechPaths = [$"twilio/voice/speeches/{request.ConversationId}/{reply.SpeechFileName}"],
CallbackPath = $"twilio/voice/{request.ConversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(request.States)}",
ActionOnEmptyResult = true,
Hints = reply.Hints
};
await HookEmitter.Emit<ITwilioSessionHook>(_services, async hook =>
2024-08-30 16:29:21 +00:00
{
2024-11-12 22:51:33 +00:00
await hook.OnAgentResponsing(request, instruction);
}, new HookEmitOption
{
OnlyOnce = true
});
response = twilio.ReturnInstructions(instruction);
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);
}
2025-02-07 02:12:00 +00:00
// [ValidateRequest]
2024-11-19 19:05:12 +00:00
[HttpPost("twilio/voice/init-call")]
public TwiMLResult InitiateOutboundCall(VoiceRequest request, [Required][FromQuery] string conversationId)
{
var instruction = new ConversationalVoiceResponse
{
ActionOnEmptyResult = true,
CallbackPath = $"twilio/voice/{conversationId}/receive/1",
SpeechPaths = new List<string>
{
$"twilio/voice/speeches/{conversationId}/intial.mp3"
}
};
2025-01-14 17:11:42 +00:00
string tag = $"twilio:{Request.Form["AnsweredBy"]}";
2025-01-14 16:45:54 +00:00
var db = _services.GetRequiredService<IBotSharpRepository>();
db.AppendConversationTags(conversationId, new List<string> { tag });
2024-11-19 19:05:12 +00:00
var twilio = _services.GetRequiredService<TwilioService>();
var response = twilio.ReturnNoninterruptedInstructions(instruction);
return TwiML(response);
}
2025-02-07 02:12:00 +00:00
// [ValidateRequest]
2024-08-22 21:13:38 +00:00
[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
2024-11-12 22:51:33 +00:00
private Dictionary<string, string> ParseStates(List<string> states)
2024-10-29 18:55:55 +00:00
{
var result = new Dictionary<string, string>();
2024-10-29 22:25:35 +00:00
if (states is null || !states.Any())
2024-10-29 18:55:55 +00:00
{
return result;
}
2024-10-29 22:25:35 +00:00
foreach (var kvp in states)
2024-10-29 18:55:55 +00:00
{
2024-10-29 22:25:35 +00:00
var parts = kvp.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
2024-10-29 18:55:55 +00:00
if (parts.Length == 2)
{
result.Add(parts[0], parts[1]);
}
}
return result;
}
2024-10-29 22:25:35 +00:00
2024-11-12 22:51:33 +00:00
private string GenerateStatesParameter(List<string> states)
2024-10-29 22:25:35 +00:00
{
2024-11-12 22:51:33 +00:00
if (states is null || states.Count == 0)
2024-10-29 22:25:35 +00:00
{
return null;
}
return string.Join("&", states.Select(x => $"states={x}"));
}
2023-11-09 20:02:57 +00:00
}