| | | 1 | | using Elsa.Authorization; |
| | | 2 | | using System.Text.Json; |
| | | 3 | | using Elsa.Abstractions; |
| | | 4 | | using Elsa.AI.Abstractions.Contracts; |
| | | 5 | | using Elsa.AI.Abstractions.Models; |
| | | 6 | | using Elsa.AI.Host.Endpoints.AI; |
| | | 7 | | using Elsa.AI.Host.Options; |
| | | 8 | | using Elsa.AI.Host.Permissions; |
| | | 9 | | using Elsa.AI.Host.Streaming; |
| | | 10 | | using JetBrains.Annotations; |
| | | 11 | | using Microsoft.AspNetCore.Http; |
| | | 12 | | using Microsoft.Extensions.Options; |
| | | 13 | | |
| | | 14 | | namespace Elsa.AI.Host.Endpoints.AI.Chat; |
| | | 15 | | |
| | | 16 | | [PublicAPI] |
| | 8 | 17 | | public class Endpoint( |
| | 8 | 18 | | IAIOrchestrator orchestrator, |
| | 8 | 19 | | AIStreamSessionManager sessionManager, |
| | 8 | 20 | | IOptions<AIHostOptions> options) : ElsaEndpoint<AIChatRequest> |
| | | 21 | | { |
| | | 22 | | public override void Configure() |
| | | 23 | | { |
| | 0 | 24 | | Post("/ai/chat"); |
| | 0 | 25 | | RequirePermission(Elsa.AI.Host.Permissions.AIResourcePermissions.Chat, CoreVerbs.Execute); |
| | 0 | 26 | | } |
| | | 27 | | |
| | | 28 | | public override async Task HandleAsync(AIChatRequest request, CancellationToken cancellationToken) |
| | | 29 | | { |
| | 8 | 30 | | var conversationId = string.IsNullOrWhiteSpace(request.ConversationId) ? Guid.NewGuid().ToString("N") : request. |
| | 8 | 31 | | var userPermissions = AIHttpContextIdentity.GetPermissions(HttpContext); |
| | 8 | 32 | | request = request with |
| | 8 | 33 | | { |
| | 8 | 34 | | ConversationId = conversationId, |
| | 8 | 35 | | Message = request.Message ?? "", |
| | 8 | 36 | | Attachments = request.Attachments ?? [], |
| | 8 | 37 | | IsReconnect = sessionManager.CanReconnect(conversationId), |
| | 8 | 38 | | TenantId = AIHttpContextIdentity.GetTenantId(HttpContext), |
| | 8 | 39 | | UserId = AIHttpContextIdentity.GetActorId(HttpContext), |
| | 8 | 40 | | UserPermissions = userPermissions, |
| | 8 | 41 | | Agent = AIHttpContextIdentity.GetAuthorizedAgent(request.Agent, options.Value, HttpContext.User), |
| | 8 | 42 | | ProviderName = null |
| | 8 | 43 | | }; |
| | 8 | 44 | | var response = HttpContext.Response; |
| | 8 | 45 | | response.ContentType = "text/event-stream"; |
| | 8 | 46 | | response.Headers["Cache-Control"] = "no-cache"; |
| | | 47 | | |
| | 8 | 48 | | var completed = false; |
| | 8 | 49 | | var reconnectAccepted = request.IsReconnect; |
| | 8 | 50 | | var requestedReconnectConversationId = request.ConversationId; |
| | 8 | 51 | | var reconnectConnected = false; |
| | 8 | 52 | | var disconnectedConversationId = request.ConversationId; |
| | | 53 | | try |
| | | 54 | | { |
| | 32 | 55 | | await foreach (var streamEvent in orchestrator.ExecuteChatAsync(request, cancellationToken)) |
| | | 56 | | { |
| | 8 | 57 | | disconnectedConversationId = streamEvent.ConversationId; |
| | 8 | 58 | | if (reconnectAccepted && !reconnectConnected) |
| | | 59 | | { |
| | 1 | 60 | | if (!string.Equals(requestedReconnectConversationId, disconnectedConversationId, StringComparison.Or |
| | 1 | 61 | | sessionManager.ReleaseReconnect(requestedReconnectConversationId); |
| | | 62 | | |
| | 1 | 63 | | sessionManager.MarkConnected(disconnectedConversationId); |
| | 1 | 64 | | reconnectConnected = true; |
| | | 65 | | } |
| | | 66 | | |
| | 8 | 67 | | await response.WriteAsync($"event: {streamEvent.Type}\n", cancellationToken); |
| | 8 | 68 | | await response.WriteAsync($"data: {JsonSerializer.Serialize(streamEvent)}\n\n", cancellationToken); |
| | 8 | 69 | | await response.Body.FlushAsync(cancellationToken); |
| | 8 | 70 | | } |
| | | 71 | | |
| | 8 | 72 | | completed = true; |
| | 8 | 73 | | } |
| | 0 | 74 | | catch (OperationCanceledException) when (HttpContext.RequestAborted.IsCancellationRequested) |
| | | 75 | | { |
| | | 76 | | // Expected when the client disconnects; the finally block records reconnect state. |
| | 0 | 77 | | return; |
| | | 78 | | } |
| | | 79 | | finally |
| | | 80 | | { |
| | 8 | 81 | | if (!completed) |
| | | 82 | | { |
| | 0 | 83 | | sessionManager.MarkDisconnected(disconnectedConversationId, options.Value.ReconnectGrace); |
| | | 84 | | } |
| | 8 | 85 | | else if (reconnectAccepted && !reconnectConnected) |
| | | 86 | | { |
| | 1 | 87 | | sessionManager.ReleaseReconnect(requestedReconnectConversationId); |
| | | 88 | | } |
| | | 89 | | } |
| | 8 | 90 | | } |
| | | 91 | | } |