- MemoryService: Verwaltet data/agents/{agentId}/ Verzeichnisse
- MEMORY.md als Haupt-Memory-File
- memory/ Unterordner für Tages-Logs und thematische Notizen
- Sandbox-Prüfung verhindert Zugriff außerhalb des Agent-Dirs
- Memory-Tools für den Agent:
- memory_read: Liest Memory-Dateien
- memory_write: Schreibt/überschreibt oder hängt an (append)
- memory_list: Listet alle Memory-Dateien auf
- SystemPromptBuilder: Memory-Abschnitt mit Regeln und aktuellem
MEMORY.md-Inhalt wird in den System-Prompt injiziert
- ToolService/AnthropicService: Memory-Tools werden automatisch
eingebunden wenn MemoryService und AgentId verfügbar sind
- TODO.md: Embedding-basierte Suche und Subagents als Features
170 lines
5.8 KiB
C#
170 lines
5.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Gott.Models;
|
|
using Gott.Services;
|
|
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
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, IHttpClientFactory httpFactory, IConfiguration config, ToolService toolService, DatabaseService db, StreamingTracker streamingTracker, MemoryService memoryService, CancellationToken cancellationToken)
|
|
{
|
|
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
|
if (body == null || body.Messages == null || body.Messages.Count == 0)
|
|
{
|
|
res.StatusCode = 400;
|
|
await res.WriteAsJsonAsync(new { error = "messages required" });
|
|
return;
|
|
}
|
|
|
|
var token = config["AnthropicToken"];
|
|
if (string.IsNullOrEmpty(token))
|
|
{
|
|
res.StatusCode = 500;
|
|
await res.WriteAsJsonAsync(new { error = "AnthropicToken not configured" });
|
|
return;
|
|
}
|
|
|
|
var sessionId = body.SessionId;
|
|
if (string.IsNullOrEmpty(sessionId))
|
|
{
|
|
res.StatusCode = 400;
|
|
await res.WriteAsJsonAsync(new { error = "sessionId required" });
|
|
return;
|
|
}
|
|
|
|
// Agent aus Session auflösen
|
|
var session = await db.GetSession(sessionId);
|
|
if (session == null || string.IsNullOrEmpty(session.AgentId))
|
|
{
|
|
res.StatusCode = 400;
|
|
await res.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
|
|
return;
|
|
}
|
|
|
|
var agent = await db.GetAgent(session.AgentId);
|
|
if (agent == null)
|
|
{
|
|
res.StatusCode = 400;
|
|
await res.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
|
|
return;
|
|
}
|
|
|
|
var modelId = agent.Model switch
|
|
{
|
|
"opus" => "claude-opus-4-6",
|
|
"sonnet" => "claude-sonnet-4-6",
|
|
_ => "claude-sonnet-4-6"
|
|
};
|
|
|
|
res.Headers["Content-Type"] = "text/event-stream";
|
|
res.Headers["Cache-Control"] = "no-cache";
|
|
res.Headers["Connection"] = "keep-alive";
|
|
|
|
var markedMessages = await db.GetMarkedMessages(sessionId);
|
|
var systemPromptText = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService);
|
|
|
|
var chainId = Guid.NewGuid().ToString("N");
|
|
|
|
await db.CreateChain(new ChainRecord
|
|
{
|
|
ChainId = chainId,
|
|
SessionId = sessionId,
|
|
AgentId = agent.AgentId,
|
|
Model = modelId,
|
|
SystemPrompt = systemPromptText
|
|
});
|
|
|
|
// Session-Timestamp aktualisieren
|
|
await db.UpdateSessionTimestamp(sessionId);
|
|
|
|
var messages = new List<object>();
|
|
foreach (var msg in body.Messages)
|
|
{
|
|
// Content bereinigen: interne Felder (_file_name) entfernen
|
|
JsonElement content = msg.Content;
|
|
if (msg.Content.ValueKind == System.Text.Json.JsonValueKind.Array)
|
|
{
|
|
var node = System.Text.Json.Nodes.JsonNode.Parse(msg.Content.GetRawText());
|
|
foreach (var block in node.AsArray())
|
|
{
|
|
if (block is System.Text.Json.Nodes.JsonObject obj)
|
|
obj.Remove("_file_name");
|
|
}
|
|
content = JsonSerializer.Deserialize<JsonElement>(node.ToJsonString());
|
|
}
|
|
|
|
messages.Add(new Dictionary<string, JsonElement>
|
|
{
|
|
["role"] = JsonSerializer.Deserialize<JsonElement>($"\"{msg.Role}\""),
|
|
["content"] = content
|
|
});
|
|
}
|
|
|
|
// Nur die letzte User-Message speichern (= die neue)
|
|
var lastUserMsg = body.Messages.LastOrDefault(m => m.Role == "user");
|
|
if (lastUserMsg != null)
|
|
{
|
|
var textContent = lastUserMsg.GetTextContent();
|
|
var attachmentInfos = lastUserMsg.GetAttachments();
|
|
var attachmentBlocks = lastUserMsg.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 = chainId,
|
|
Iteration = 0,
|
|
Role = "user",
|
|
Type = "text",
|
|
Content = textContent
|
|
}, attachmentRecords);
|
|
}
|
|
|
|
var systemPrompt = new List<object>
|
|
{
|
|
new { type = "text", text = systemPromptText }
|
|
};
|
|
|
|
var workspace = session.Workspace;
|
|
if (!string.IsNullOrEmpty(workspace))
|
|
Directory.CreateDirectory(workspace);
|
|
|
|
var maxIterations = config.GetValue<int>("MaxIterations", 100);
|
|
var toolChoice = agent.ToolChoice ?? "any";
|
|
|
|
var anthropic = new AnthropicService(httpFactory, token, toolService, db, memoryService);
|
|
streamingTracker.Start(sessionId, chainId);
|
|
try
|
|
{
|
|
await anthropic.StreamChat(res, modelId, systemPrompt, messages, chainId, workspace, sessionId, agentId: agent.AgentId, maxIterations: maxIterations, toolChoice: toolChoice, cancellationToken: cancellationToken);
|
|
}
|
|
finally
|
|
{
|
|
streamingTracker.Stop(sessionId);
|
|
}
|
|
}
|
|
}
|