From 8b9b0683ebe8702e4bf3d66b35ebe0c253093b93 Mon Sep 17 00:00:00 2001 From: ktutak1337 Date: Tue, 11 Jun 2024 20:43:03 +0200 Subject: [PATCH] Code(API::ExecuteNativeAction): Implement feature for execute native action --- .../ExecuteNativeAction.cs | 3 + .../ExecuteNativeActionEndpoint.cs | 30 +++++++ .../ExecuteNativeActionHandler.cs | 89 +++++++++++++++++++ .../Actions/ExecuteNativeActionRequest.cs | 6 ++ .../Extensions.cs | 3 + 5 files changed, 131 insertions(+) create mode 100644 src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeAction.cs create mode 100644 src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeActionEndpoint.cs create mode 100644 src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeActionHandler.cs create mode 100644 src/Shared/StellarChat.Shared.Contracts/Actions/ExecuteNativeActionRequest.cs diff --git a/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeAction.cs b/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeAction.cs new file mode 100644 index 0000000..a8ba39a --- /dev/null +++ b/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeAction.cs @@ -0,0 +1,3 @@ +namespace StellarChat.Server.Api.Features.Actions.ExecuteNativeAction; + +internal sealed record ExecuteNativeAction([Required] Guid Id, [Required] Guid ChatId, string Message) : ICommand; diff --git a/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeActionEndpoint.cs b/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeActionEndpoint.cs new file mode 100644 index 0000000..9ae1f35 --- /dev/null +++ b/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeActionEndpoint.cs @@ -0,0 +1,30 @@ +namespace StellarChat.Server.Api.Features.Actions.ExecuteNativeAction; + +public class ExecuteNativeActionEndpoint : IEndpoint +{ + public void Expose(IEndpointRouteBuilder endpoints) + { + var actions = endpoints.MapGroup("/actions").WithTags("Actions"); + + actions.MapPost("execute", async ([FromBody] ExecuteNativeActionRequest request, IMediator mediator) => + { + var id = Guid.NewGuid(); + var command = request.Adapt(); + + command = command with { Id = id }; + var result = await mediator.Send(command); + + return Results.Ok(result); + }) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status400BadRequest) + .WithOpenApi(operation => new(operation) + { + Summary = "Executes a new action." + }); + } + + public void Register(IServiceCollection services, IConfiguration configuration) { } + + public void Use(IApplicationBuilder app) { } +} diff --git a/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeActionHandler.cs b/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeActionHandler.cs new file mode 100644 index 0000000..c864fda --- /dev/null +++ b/src/Server/StellarChat.Server.Api/Features/Actions/ExecuteNativeAction/ExecuteNativeActionHandler.cs @@ -0,0 +1,89 @@ +using Microsoft.SemanticKernel; +using StellarChat.Server.Api.Features.Actions.CreateNativeAction; +using StellarChat.Server.Api.Features.Actions.Webhooks.Services; +using StellarChat.Server.Api.Features.Chat.CarryConversation; + +namespace StellarChat.Server.Api.Features.Actions.ExecuteNativeAction; + +internal sealed class ExecuteNativeActionHandler : ICommandHandler +{ + private readonly INativeActionRepository _nativeActionRepository; + private readonly IHttpClientService _httpClientService; + private readonly IChatContext _chatContext; + private readonly Kernel _kernel; + private readonly IHubContext _hubContext; + private readonly TimeProvider _clock; + private readonly ILogger _logger; + + public ExecuteNativeActionHandler( + INativeActionRepository nativeActionRepository, + IHttpClientService httpClientService, + IChatContext chatContext, + Kernel kernel, + IHubContext hubContext, + TimeProvider clock, + ILogger logger) + { + _nativeActionRepository = nativeActionRepository; + _httpClientService = httpClientService; + _chatContext = chatContext; + _kernel = kernel; + _hubContext = hubContext; + _clock = clock; + _logger = logger; + } + + public async ValueTask Handle(ExecuteNativeAction command, CancellationToken cancellationToken) + { + var (id, chatId, message) = command; + + var action = await _nativeActionRepository.GetAsync(id) ?? throw new NativeActionNotFoundException(id); + + action.Metaprompt = action.Metaprompt.IsEmpty() + ? string.Empty + : _clock.ReplaceDatePlaceholder(action.Metaprompt); + + string semanticResponse = string.Empty; + + await _chatContext.SetChatInstructions(chatId, action.Metaprompt); + await _chatContext.ExtractChatHistoryAsync(chatId); + + var userMessage = CreateUserMessage(chatId, message); + await _chatContext.SaveChatMessageAsync(chatId, userMessage); + + var botMessage = CreateBotMessage(chatId, content: string.Empty); + var botResponseMessage = await _chatContext.StreamResponseToClientAsync(chatId, action.Model, botMessage, _hubContext); + + semanticResponse = botResponseMessage.Content; + + if (action.IsRemoteAction) + { + var response = await _httpClientService.PostAsync(action.Webhook!.Url, semanticResponse, action.Webhook.Headers, cancellationToken); + botResponseMessage.Content = response; + await _hubContext.Clients.All.ReceiveChatMessageChunk(response); + await _chatContext.SaveChatMessageAsync(chatId, botResponseMessage); + + return response; + } + + await _chatContext.SaveChatMessageAsync(chatId, botResponseMessage); + + return semanticResponse; + } + + private ChatMessage CreateUserMessage(Guid chatId, string message, ChatMessageType messageType = ChatMessageType.Message) + { + var now = _clock.GetLocalNow(); + + var userMessage = ChatMessage.Create(Guid.NewGuid(), chatId, messageType, Author.User, message, tokenCount: 0, now); + return userMessage; + } + + private ChatMessage CreateBotMessage(Guid chatId, string content, ChatMessageType messageType = ChatMessageType.Message) + { + var now = _clock.GetLocalNow(); + + var chatMessage = ChatMessage.Create(Guid.NewGuid(), chatId, messageType, Author.Bot, content, tokenCount: 0, now); + return chatMessage; + } +} diff --git a/src/Shared/StellarChat.Shared.Contracts/Actions/ExecuteNativeActionRequest.cs b/src/Shared/StellarChat.Shared.Contracts/Actions/ExecuteNativeActionRequest.cs new file mode 100644 index 0000000..17f4011 --- /dev/null +++ b/src/Shared/StellarChat.Shared.Contracts/Actions/ExecuteNativeActionRequest.cs @@ -0,0 +1,6 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace StellarChat.Shared.Contracts.Actions; + +public sealed record ExecuteNativeActionRequest([property: JsonIgnore][Required] Guid Id, [Required] Guid ChatId, string Message); diff --git a/src/Shared/StellarChat.Shared.Infrastructure/Extensions.cs b/src/Shared/StellarChat.Shared.Infrastructure/Extensions.cs index 979b61f..c54b8e0 100644 --- a/src/Shared/StellarChat.Shared.Infrastructure/Extensions.cs +++ b/src/Shared/StellarChat.Shared.Infrastructure/Extensions.cs @@ -81,6 +81,9 @@ public static string ToFormatSize(this long bytes) return $"{number:n2} {suffixes[counter]}"; } + public static string ReplaceDatePlaceholder(this TimeProvider timeProvider, string prompt) + => prompt.Replace("{DATE}", timeProvider.GetLocalNow().ToString()); + public static IApplicationBuilder UseCorrelationId(this IApplicationBuilder app) => app.Use((ctx, next) => {