Backend: - ChatEndpoint empfängt nur noch content + sessionId (nicht mehr die gesamte History) - History wird komplett aus DB aufgebaut via BuildHistoryFromDb - Gelöschte Nachrichten (deleted_on) werden aus History gefiltert - Leere Nachrichten werden übersprungen (kein API 400 mehr) - BuildHistoryFromDb optimiert: nur Text für ältere Chains, Tool-Calls nur für aktuelle Chain - GetChainMessages: includeDeleted-Parameter (Default: false) - Archiv-Export nutzt includeDeleted: true Frontend: - messages[]-Array komplett entfernt (kein Client-seitiger History-State mehr) - apiChat() sendet nur content + sessionId - Inline-Bilder: [Bild: name] wird durch <img> ersetzt (live mit DataURL, History per API-Nachladen) - _autoLoadImageAttachments() für History/Archiv/ältere Nachrichten - Klick auf Inline-Bild öffnet Vollbild-Modal
92 lines
3.3 KiB
C#
92 lines
3.3 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Gott.Extensions;
|
|
using Gott.Models;
|
|
using Gott.Services;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
namespace Gott.Endpoints;
|
|
|
|
public static class ChatEndpoint
|
|
{
|
|
public static void MapChatEndpoint(this IEndpointRouteBuilder app)
|
|
{
|
|
app.MapPost("/api/chat", HandleChat);
|
|
}
|
|
|
|
private static async Task HandleChat(HttpRequest req, HttpResponse res, IConfiguration config, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, CancellationToken cancellationToken)
|
|
{
|
|
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
|
if (body == null || body.Content.ValueKind == JsonValueKind.Undefined)
|
|
{
|
|
res.StatusCode = 400;
|
|
await res.WriteAsJsonAsync(new { error = "content required" });
|
|
return;
|
|
}
|
|
|
|
var sessionId = body.SessionId;
|
|
if (string.IsNullOrEmpty(sessionId))
|
|
{
|
|
res.StatusCode = 400;
|
|
await res.WriteAsJsonAsync(new { error = "sessionId required" });
|
|
return;
|
|
}
|
|
|
|
var userId = req.HttpContext.GetUserId();
|
|
var maxIterations = config.GetValue<int>("MaxIterations", 100);
|
|
|
|
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations);
|
|
if (ctx == null)
|
|
{
|
|
res.StatusCode = 400;
|
|
await res.WriteAsJsonAsync(new { error = "Session/Agent nicht gefunden oder kein Agent ausgewählt" });
|
|
return;
|
|
}
|
|
|
|
// Neue User-Message aus Request-Content aufbereiten und speichern
|
|
var userMessage = new ChatMessage { Role = "user", Content = body.Content };
|
|
var textContent = userMessage.GetTextContent();
|
|
var attachmentBlocks = userMessage.GetAttachmentBlocks();
|
|
|
|
var attachmentRecords = attachmentBlocks.Select(b => new AttachmentRecord
|
|
{
|
|
Type = b.Type,
|
|
MediaType = b.MediaType,
|
|
FileName = b.FileName,
|
|
Data = b.Data
|
|
}).ToList();
|
|
|
|
await db.SaveMessageWithAttachments(new MessageRecord
|
|
{
|
|
ChainId = ctx.ChainId,
|
|
Iteration = 0,
|
|
Role = "user",
|
|
Type = "text",
|
|
Content = textContent
|
|
}, attachmentRecords);
|
|
|
|
// History komplett aus DB aufbauen (inkl. gerade gespeicherter Message)
|
|
var messages = await chatService.BuildHistoryFromDb(sessionId);
|
|
|
|
res.Headers["Content-Type"] = "text/event-stream";
|
|
res.Headers["Cache-Control"] = "no-cache";
|
|
res.Headers["Connection"] = "keep-alive";
|
|
|
|
streamingTracker.Start(sessionId, ctx.ChainId);
|
|
try
|
|
{
|
|
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: ctx.ToolChoice, cancellationToken: cancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
streamingTracker.Stop(sessionId);
|
|
}
|
|
}
|
|
}
|