using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Gott.Models; namespace Gott.Services; public class ChatService { private readonly DatabaseService _db; private readonly ToolService _toolService; private readonly MemoryService _memoryService; private readonly SkillService _skillService; public ChatService(DatabaseService db, ToolService toolService, MemoryService memoryService, SkillService skillService) { _db = db; _toolService = toolService; _memoryService = memoryService; _skillService = skillService; } public static string ResolveModelId(string model) { return 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" }; } /// /// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc. /// public async Task PrepareContext(string sessionId, string userId, int maxIterations = 100) { var session = await _db.GetSession(sessionId); if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId)) return null; var agent = await _db.GetAgent(session.AgentId); if (agent == null) return null; var modelId = ResolveModelId(agent.Model); var markedMessages = await _db.GetMarkedMessages(sessionId); var agentMemoryDir = _memoryService.GetAgentMemoryDir(agent.AgentId); var agentSkills = await _db.GetAgentSkills(agent.AgentId); var user = userId != null ? await _db.GetUser(userId) : null; var systemPromptText = SystemPromptBuilder.Build( agent, session, markedMessages, _memoryService, agentMemoryDir, agentSkills, _skillService, userPersona: user?.GetEffectivePersona()); var workspace = session.Workspace; 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(); var toolNames = toolDefs.Length > 0 ? JsonSerializer.Serialize(toolDefs.Select(td => { var json = JsonSerializer.Serialize(td); var el = JsonSerializer.Deserialize(json); return el.GetProperty("name").GetString(); }).ToArray()) : null; var chainId = Guid.NewGuid().ToString("N"); await _db.CreateChain(new ChainRecord { ChainId = chainId, SessionId = sessionId, AgentId = agent.AgentId, Model = modelId, SystemPrompt = systemPromptText, ToolChoice = (hasWorkspace || includeMemory) ? toolChoice : null, ToolNames = toolNames }); await _db.UpdateSessionTimestamp(sessionId); if (!string.IsNullOrEmpty(workspace)) Directory.CreateDirectory(workspace); var systemPrompt = new List(); if (!string.IsNullOrWhiteSpace(systemPromptText)) systemPrompt.Add(new { type = "text", text = systemPromptText }); return new ChatContext { ChainId = chainId, SessionId = sessionId, ModelId = modelId, Workspace = workspace, AgentId = agent.AgentId, SystemPromptText = systemPromptText, SystemPrompt = systemPrompt, ToolChoice = toolChoice, UseMemory = agent.UseMemory, WebSearchVersion = agent.WebSearchVersion, MaxIterations = maxIterations, Agent = agent, Session = session }; } /// /// Baut die Message-History aus der DB (aktive Chains). /// Optimiert: Nur Text-Messages (user/assistant) für ältere Chains, /// Tool-Calls nur für die aktuelle Chain, Attachments nur für die letzte User-Message. /// public async Task> BuildHistoryFromDb(string sessionId) { var chainsList = (await _db.GetActiveChains(sessionId)).ToList(); // chronologisch sortieren (älteste zuerst) chainsList.Reverse(); var history = new List(); var isLastChain = false; long? lastUserMsgId = null; for (int i = 0; i < chainsList.Count; i++) { var chain = chainsList[i]; isLastChain = (i == chainsList.Count - 1); var msgs = (await _db.GetChainMessages(chain.ChainId)).ToList(); // Letzte User-Message-ID der letzten Chain merken (für Attachments) if (isLastChain) { var lastUserMsg = msgs.LastOrDefault(m => m.Role == "user" && m.Type == "text"); lastUserMsgId = lastUserMsg?.Id; } // Für ältere Chains: nur Text-Messages (wie Frontend es vorher gemacht hat) if (!isLastChain) { foreach (var msg in msgs) { if (msg.Role == "user" && msg.Type == "text" && !string.IsNullOrEmpty(msg.Content)) { history.Add(new Dictionary { ["role"] = "user", ["content"] = new List { new { type = "text", text = msg.Content } } }); } else if (msg.Role == "assistant" && msg.Type == "text" && !string.IsNullOrEmpty(msg.Content)) { history.Add(new Dictionary { ["role"] = "assistant", ["content"] = new List { new { type = "text", text = msg.Content } } }); } } continue; } // Letzte Chain: vollständig mit Tool-Calls var grouped = new Dictionary>(); foreach (var msg in msgs) { if (!grouped.ContainsKey(msg.Iteration)) grouped[msg.Iteration] = new List(); grouped[msg.Iteration].Add(msg); } foreach (var kvp in grouped.OrderBy(x => x.Key)) { foreach (var msg in kvp.Value) { if (msg.Role == "user" && msg.Type == "text") { var content = new List(); // Attachments nur für die letzte User-Message laden if (msg.Id == lastUserMsgId) { var attachments = await _db.GetAttachments(msg.Id); foreach (var att in attachments) { if (att.Type == "image") { content.Add(new { type = "image", source = new { type = "base64", media_type = att.MediaType, data = att.Data } }); } else if (att.Type == "document") { content.Add(new { type = "document", source = new { type = "base64", media_type = att.MediaType, data = att.Data } }); } } } if (!string.IsNullOrEmpty(msg.Content)) content.Add(new { type = "text", text = msg.Content }); if (content.Count > 0) history.Add(new Dictionary { ["role"] = "user", ["content"] = content }); } else if (msg.Role == "assistant" && msg.Type == "text") { if (!string.IsNullOrEmpty(msg.Content)) { history.Add(new Dictionary { ["role"] = "assistant", ["content"] = new List { new { type = "text", text = msg.Content } } }); } } else if (msg.Role == "assistant" && msg.Type == "tool_use") { JsonElement inputEl; try { inputEl = JsonSerializer.Deserialize(msg.Content ?? "{}"); } catch { inputEl = JsonSerializer.Deserialize("{}"); } var lastAssistant = history.Count > 0 ? history[history.Count - 1] : null; if (lastAssistant is Dictionary dict && dict.TryGetValue("role", out var role) && role.ToString() == "assistant" && dict.TryGetValue("content", out var existingContent) && existingContent is List contentList) { contentList.Add(new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl }); } else { history.Add(new Dictionary { ["role"] = "assistant", ["content"] = new List { new { type = "tool_use", id = msg.ToolCallId, name = msg.ToolName, input = inputEl } } }); } } else if (msg.Role == "user" && msg.Type == "tool_result") { var lastUser = history.Count > 0 ? history[history.Count - 1] : null; if (lastUser is Dictionary dict && dict.TryGetValue("role", out var role) && role.ToString() == "user" && dict.TryGetValue("content", out var existingContent) && existingContent is List contentList) { contentList.Add(new { type = "tool_result", tool_use_id = msg.ToolCallId, content = msg.Content ?? "", is_error = msg.IsError }); } else { history.Add(new Dictionary { ["role"] = "user", ["content"] = new List { new { type = "tool_result", tool_use_id = msg.ToolCallId, content = msg.Content ?? "", is_error = msg.IsError } } }); } } } } } return history; } }