using Dapper; using FastEndpoints; using KBX.Shared.Experience; using Npgsql; using System.Diagnostics; namespace KBX.Modules.Common.Ai.Ask; public sealed record Request(string Question, AiScreenContext Context); public sealed class Endpoint(IKbxAiAssistantProvider assistant, NpgsqlDataSource dataSource) : Endpoint { public override void Configure() { Post("/api/common/ai/ask"); Permissions("common.ai.use"); } public override async Task HandleAsync(Request req, CancellationToken ct) { if (string.IsNullOrWhiteSpace(req.Question) || req.Question.Length > 2000) { AddError(r => r.Question, "질문을 1~2000자로 입력하세요."); await Send.ErrorsAsync(cancellation: ct); return; } // Host application replaces this with authenticated claims resolution. var tenantId = Guid.Empty; var userId = Guid.Empty; var sw = Stopwatch.StartNew(); var answer = await assistant.AskAsync(tenantId, userId, new AiAskCommand(req.Question.Trim(), req.Context), ct); sw.Stop(); await using var connection = await dataSource.OpenConnectionAsync(ct); await connection.ExecuteAsync(new CommandDefinition(""" insert into kbx.ai_interactions (id, tenant_id, user_id, screen_id, screen_version, capability, result_kind, provider, duration_ms) values (@Id, @TenantId, @UserId, @ScreenId, @ScreenVersion, 'explain', @ResultKind, @Provider, @DurationMs) """, new { Id = Guid.NewGuid(), TenantId = tenantId, UserId = userId, req.Context.ScreenId, req.Context.ScreenVersion, ResultKind = answer.Proposal is null ? "answer" : "proposal", Provider = assistant.GetType().Name, DurationMs = (int)sw.ElapsedMilliseconds, }, cancellationToken: ct)); await Send.OkAsync(answer, ct); } }