BotSharp/src/Plugins/BotSharp.Plugin.Twilio/Services/Stream/TwilioStreamMiddleware.cs

134 lines
4.9 KiB
C#
Raw Normal View History

2025-02-07 17:18:47 +00:00
using BotSharp.Abstraction.Realtime;
2025-02-07 22:40:57 +00:00
using BotSharp.Abstraction.Realtime.Models;
2025-02-10 21:52:10 +00:00
using BotSharp.Plugin.Twilio.Interfaces;
2025-02-07 17:18:47 +00:00
using BotSharp.Plugin.Twilio.Models.Stream;
using Microsoft.AspNetCore.Http;
using System.Net.WebSockets;
using Task = System.Threading.Tasks.Task;
namespace BotSharp.Plugin.Twilio.Services.Stream;
/// <summary>
/// Reference to https://github.com/twilio-samples/speech-assistant-openai-realtime-api-node/blob/main/index.js
2025-02-07 17:18:47 +00:00
/// </summary>
public class TwilioStreamMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<TwilioStreamMiddleware> _logger;
2025-02-07 17:18:47 +00:00
public TwilioStreamMiddleware(RequestDelegate next, ILogger<TwilioStreamMiddleware> logger)
2025-02-07 17:18:47 +00:00
{
_next = next;
_logger = logger;
2025-02-07 17:18:47 +00:00
}
public async Task Invoke(HttpContext httpContext)
{
var request = httpContext.Request;
if (request.Path.StartsWithSegments("/twilio/stream"))
{
if (httpContext.WebSockets.IsWebSocketRequest)
{
var services = httpContext.RequestServices;
2025-02-10 21:52:10 +00:00
var conversationId = request.Path.Value.Split("/").Last();
2025-02-07 17:18:47 +00:00
using WebSocket webSocket = await httpContext.WebSockets.AcceptWebSocketAsync();
2025-03-05 18:29:28 +00:00
try
{
await HandleWebSocket(services, conversationId, webSocket);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error in WebSocket communication: {ex.Message} for conversation {conversationId}");
}
2025-02-10 21:52:10 +00:00
return;
2025-02-07 17:18:47 +00:00
}
}
await _next(httpContext);
}
2025-02-10 21:52:10 +00:00
private async Task HandleWebSocket(IServiceProvider services, string conversationId, WebSocket webSocket)
2025-02-07 17:18:47 +00:00
{
2025-02-07 22:40:57 +00:00
var hub = services.GetRequiredService<IRealtimeHub>();
2025-03-06 21:10:17 +00:00
var conn = hub.SetHubConnection(conversationId);
var completer = hub.SetCompleter("openai");
2025-03-05 18:29:28 +00:00
// load conversation and state
var convService = services.GetRequiredService<IConversationService>();
convService.SetConversationId(conversationId, []);
2025-02-10 21:52:10 +00:00
var hooks = services.GetServices<ITwilioSessionHook>();
foreach (var hook in hooks)
{
await hook.OnStreamingStarted(conn);
}
convService.States.Save();
2025-02-07 17:18:47 +00:00
2025-03-05 18:29:28 +00:00
await hub.Listen(webSocket, (receivedText) =>
2025-02-07 17:18:47 +00:00
{
2025-03-05 18:29:28 +00:00
var response = JsonSerializer.Deserialize<StreamEventResponse>(receivedText);
conn.StreamId = response.StreamSid;
2025-03-06 09:03:30 +00:00
switch (response.Event)
2025-02-07 17:18:47 +00:00
{
2025-03-06 09:03:30 +00:00
case "start":
conn.Event = "user_connected";
var startResponse = JsonSerializer.Deserialize<StreamEventStartResponse>(receivedText);
conn.Data = JsonSerializer.Serialize(startResponse.Body.CustomParameters);
conn.ResetStreamState();
break;
case "media":
conn.Event = "user_data_received";
var mediaResponse = JsonSerializer.Deserialize<StreamEventMediaResponse>(receivedText);
conn.LatestMediaTimestamp = long.Parse(mediaResponse.Body.Timestamp);
conn.Data = mediaResponse.Body.Payload;
break;
case "stop":
conn.Event = "user_disconnected";
break;
case "mark":
conn.Event = "mark";
if (conn.MarkQueue.Count > 0) conn.MarkQueue.TryDequeue(out var _);
break;
case "dtmf":
var dtmfResponse = JsonSerializer.Deserialize<StreamEventDtmfResponse>(receivedText);
if (dtmfResponse.Body.Digit == "#")
{
conn.Event = "user_dtmf_received";
conn.Data = conn.KeypadInputBuffer;
conn.KeypadInputBuffer = string.Empty;
}
else
{
conn.KeypadInputBuffer += dtmfResponse.Body.Digit;
}
break;
default:
conn.Event = response.Event;
break;
2025-03-05 18:29:28 +00:00
}
2025-03-05 18:29:28 +00:00
conn.OnModelMessageReceived = message =>
new
{
2025-03-05 18:29:28 +00:00
@event = "media",
streamSid = response.StreamSid,
media = new { payload = message }
};
conn.OnModelAudioResponseDone = () =>
new
{
2025-03-05 18:29:28 +00:00
@event = "mark",
streamSid = response.StreamSid,
mark = new { name = "responsePart" }
};
conn.OnModelUserInterrupted = () =>
new
{
@event = "clear",
streamSid = response.StreamSid
};
});
2025-02-07 17:18:47 +00:00
}
}