- Model-Optionen: sonnet-4-5, sonnet-4-6, opus-4-5, opus-4-6, haiku-4-5 - ChatEndpoint + TelegramService: Model-Mapping für alle Varianten - Agent-Formular: Dropdown mit allen Model-Versionen - ui.js: prettyModelName() vereinfacht für neue Model-Werte
202 lines
7.7 KiB
C#
202 lines
7.7 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.Extensions;
|
||
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, SkillService skillService, 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 userId = req.HttpContext.GetUserId();
|
||
var session = await db.GetSession(sessionId);
|
||
if (session == null || session.UserId != userId || 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
|
||
{
|
||
"sonnet" => "claude-sonnet-4-6",
|
||
"sonnet-4-5" => "claude-sonnet-4-5",
|
||
"sonnet-4-6" => "claude-sonnet-4-6",
|
||
"opus" => "claude-opus-4-6",
|
||
"opus-4-5" => "claude-opus-4-5",
|
||
"opus-4-6" => "claude-opus-4-6",
|
||
"haiku" => "claude-haiku-4-5",
|
||
"haiku-4-5" => "claude-haiku-4-5",
|
||
_ => "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 agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||
var agentSkills = await db.GetAgentSkills(agent.AgentId);
|
||
var user = await db.GetUser(userId);
|
||
var systemPromptText = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona());
|
||
|
||
var chainId = Guid.NewGuid().ToString("N");
|
||
|
||
var workspace = session.Workspace;
|
||
|
||
// Skills brauchen Tool-Use (execute) – wenn Skills aktiv aber kein Workspace gesetzt,
|
||
// /tmp als Fallback-Workspace setzen damit die Tool-Definitionen generiert werden.
|
||
if (string.IsNullOrEmpty(workspace) && agentSkills.Any())
|
||
workspace = "/tmp";
|
||
|
||
var toolChoice = agent.ToolChoice ?? "any";
|
||
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
||
var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId);
|
||
var toolDefs = (hasWorkspace || includeMemory)
|
||
? toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory)
|
||
: Array.Empty<object>();
|
||
var toolNames = toolDefs.Length > 0
|
||
? JsonSerializer.Serialize(toolDefs.Select(td =>
|
||
{
|
||
var json = JsonSerializer.Serialize(td);
|
||
var el = JsonSerializer.Deserialize<JsonElement>(json);
|
||
return el.GetProperty("name").GetString();
|
||
}).ToArray())
|
||
: null;
|
||
|
||
await db.CreateChain(new ChainRecord
|
||
{
|
||
ChainId = chainId,
|
||
SessionId = sessionId,
|
||
AgentId = agent.AgentId,
|
||
Model = modelId,
|
||
SystemPrompt = systemPromptText,
|
||
ToolChoice = (hasWorkspace || includeMemory) ? toolChoice : null,
|
||
ToolNames = toolNames
|
||
});
|
||
|
||
// 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 = string.IsNullOrWhiteSpace(systemPromptText)
|
||
? new List<object>()
|
||
: new List<object> { new { type = "text", text = systemPromptText } };
|
||
|
||
if (!string.IsNullOrEmpty(workspace))
|
||
Directory.CreateDirectory(workspace);
|
||
|
||
var maxIterations = config.GetValue<int>("MaxIterations", 100);
|
||
|
||
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, useMemory: agent.UseMemory, webSearchVersion: agent.WebSearchVersion, maxIterations: maxIterations, toolChoice: toolChoice, cancellationToken: cancellationToken);
|
||
}
|
||
finally
|
||
{
|
||
streamingTracker.Stop(sessionId);
|
||
}
|
||
}
|
||
}
|