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 workspace = WorkspaceHelper.ResolveWorkspace(session, user); if (string.IsNullOrEmpty(workspace) && agentSkills.Any()) workspace = "/tmp"; var systemPromptText = SystemPromptBuilder.Build( agent, session, markedMessages, _memoryService, agentMemoryDir, agentSkills, _skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace); 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 }); // Review-Agent vorbereiten (falls konfiguriert) ReviewContext reviewContext = null; if (!string.IsNullOrEmpty(session.ReviewAgentId)) { var reviewAgent = await _db.GetAgent(session.ReviewAgentId); if (reviewAgent != null) { var reviewModelId = ResolveModelId(reviewAgent.Model); var reviewMarked = await _db.GetMarkedMessages(sessionId); var reviewMemDir = _memoryService.GetAgentMemoryDir(reviewAgent.AgentId); var reviewSkills = await _db.GetAgentSkills(reviewAgent.AgentId); var reviewWorkspace = workspace; // gleicher Workspace wie Worker var reviewSystemText = SystemPromptBuilder.Build( reviewAgent, session, reviewMarked, _memoryService, reviewMemDir, reviewSkills, _skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: reviewWorkspace); var reviewPrompt = new List(); if (!string.IsNullOrWhiteSpace(reviewSystemText)) reviewPrompt.Add(new { type = "text", text = reviewSystemText }); reviewContext = new ReviewContext { ModelId = reviewModelId, SystemPrompt = reviewPrompt, AgentId = reviewAgent.AgentId, Workspace = reviewWorkspace, UseMemory = reviewAgent.UseMemory, WebSearchVersion = reviewAgent.WebSearchVersion, ToolChoice = reviewAgent.ToolChoice ?? "any", MaxReviewIterations = session.MaxReviewIterations > 0 ? session.MaxReviewIterations : 3, Agent = reviewAgent }; } } // Finalizer-Agent vorbereiten (falls konfiguriert) FinalizerContext finalizerContext = null; if (!string.IsNullOrEmpty(session.FinalizerAgentId)) { var finalizerAgent = await _db.GetAgent(session.FinalizerAgentId); if (finalizerAgent != null) { var finalizerModelId = ResolveModelId(finalizerAgent.Model); var finalizerMarked = await _db.GetMarkedMessages(sessionId); var finalizerMemDir = _memoryService.GetAgentMemoryDir(finalizerAgent.AgentId); var finalizerSkills = await _db.GetAgentSkills(finalizerAgent.AgentId); var finalizerWorkspace = workspace; var finalizerSystemText = SystemPromptBuilder.Build( finalizerAgent, session, finalizerMarked, _memoryService, finalizerMemDir, finalizerSkills, _skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: finalizerWorkspace); var finalizerPrompt = new List(); if (!string.IsNullOrWhiteSpace(finalizerSystemText)) finalizerPrompt.Add(new { type = "text", text = finalizerSystemText }); finalizerContext = new FinalizerContext { ModelId = finalizerModelId, SystemPrompt = finalizerPrompt, AgentId = finalizerAgent.AgentId, Workspace = finalizerWorkspace, UseMemory = finalizerAgent.UseMemory, WebSearchVersion = finalizerAgent.WebSearchVersion, ToolChoice = finalizerAgent.ToolChoice ?? "any", Agent = finalizerAgent }; } } 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, Review = reviewContext, Finalizer = finalizerContext }; } /// /// 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; } }