From e374c70c8963e1d109fb79a8dce21ae6b2e825b8 Mon Sep 17 00:00:00 2001 From: Meleki Date: Wed, 11 Mar 2026 13:50:12 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20send=5Fto=5Fsession=20f=C3=BCr=20alle=20?= =?UTF-8?q?ExecuteTool-Pfade=20+=20generelle=20FixOrphanedToolUse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AnthropicService: db/chatService/anthropicService/userId/telegramService auch im RunAgentChat (Zeile 899) und ChatNonStreaming (Zeile 1105) ExecuteTool-Aufruf durchreichen – Telegram-Flow nutzt ChatNonStreaming, hatte zuvor null-Parameter - ChatService: FixOrphanedAskUserChoice → FixOrphanedToolUse ersetzt Prüft gesamte History auf orphaned tool_use-Blöcke (nicht nur ask_user_choice am Ende) Synthetische tool_results werden eingefügt um Anthropic API 400 zu verhindern - ToolService: Debug-Logging für send_to_session nach /tmp/gott_send_to_session.log --- Services/AnthropicService.cs | 4 +- Services/ChatService.cs | 118 ++++++++++++++++++++++++----------- Services/ToolService.cs | 16 +++++ 3 files changed, 98 insertions(+), 40 deletions(-) diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 6a6a021..748f33c 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -896,7 +896,7 @@ public class AnthropicService string toolResult; bool isError = false; - try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); } + try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, agentWorkspace, _memoryService, agentId, workCt, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, telegramService: _telegramService); } catch (OperationCanceledException) { throw; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } @@ -1102,7 +1102,7 @@ public class AnthropicService string toolResult; bool isError = false; - try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg); } + try { toolResult = await _toolService.ExecuteTool(toolName, toolInput, ctx.Workspace, _memoryService, ctx.AgentId, vectorSearch: _vectorSearch, embeddingBg: _embeddingBg, db: _db, chatService: _chatService, anthropicService: this, userId: ctx.Session?.UserId, telegramService: _telegramService); } catch (OperationCanceledException) { throw; } catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; } diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 4ea0b8c..f5da266 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -593,8 +593,8 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? } } - // Post-Processing: Offener ask_user_choice tool_use ohne tool_result → synthetisches tool_result einfügen - FixOrphanedAskUserChoice(history); + // Post-Processing: Offene tool_use-Blöcke ohne tool_result → synthetische tool_results einfügen + FixOrphanedToolUse(history); // Post-Processing: Orphaned web_search server_tool_use entfernen // Bei alten DB-Einträgen wurde web_search_tool_result nur als Sources-Array gespeichert, @@ -681,49 +681,91 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? } /// - /// Prüft ob die History mit einem offenen ask_user_choice tool_use endet (kein passendes tool_result). - /// Falls ja, wird ein synthetisches tool_result eingefügt – verhindert Anthropic API 400. - /// Passiert wenn der User einfach Text schreibt statt eine Choice zu wählen. + /// Prüft die gesamte History auf tool_use-Blöcke ohne passendes tool_result in der nächsten user-Message. + /// Falls gefunden, werden synthetische tool_results eingefügt – verhindert Anthropic API 400. + /// Betrifft alle Tools, nicht nur ask_user_choice (z.B. wenn eine Chain abbricht ohne tool_result zu speichern). /// - private static void FixOrphanedAskUserChoice(List history) + private static void FixOrphanedToolUse(List history) { if (history.Count == 0) return; - var last = history[history.Count - 1]; - if (last is not Dictionary lastDict) return; - if (!lastDict.TryGetValue("role", out var lastRole) || lastRole?.ToString() != "assistant") return; - if (!lastDict.TryGetValue("content", out var lastContent) || lastContent is not List lastContentList) return; - - // Sammle alle ask_user_choice tool_use IDs die noch kein tool_result haben - var orphanedIds = new List(); - foreach (var block in lastContentList) + for (int i = 0; i < history.Count; i++) { - var blockJson = JsonSerializer.Serialize(block); - JsonElement el; - try { el = JsonSerializer.Deserialize(blockJson); } - catch { continue; } + if (history[i] is not Dictionary assistantDict) continue; + if (!assistantDict.TryGetValue("role", out var role) || role?.ToString() != "assistant") continue; + if (!assistantDict.TryGetValue("content", out var content) || content is not List contentList) continue; - if (!el.TryGetProperty("type", out var typeProp) || typeProp.GetString() != "tool_use") continue; - if (!el.TryGetProperty("name", out var nameProp) || nameProp.GetString() != ToolService.AskUserChoiceToolName) continue; - if (!el.TryGetProperty("id", out var idProp)) continue; - orphanedIds.Add(idProp.GetString() ?? ""); + // Alle tool_use IDs in dieser assistant-Message sammeln + var toolUseIds = new List(); + foreach (var block in contentList) + { + var json = JsonSerializer.Serialize(block); + JsonElement el; + try { el = JsonSerializer.Deserialize(json); } + catch { continue; } + + if (!el.TryGetProperty("type", out var t) || t.GetString() != "tool_use") continue; + if (!el.TryGetProperty("id", out var idProp)) continue; + var id = idProp.GetString(); + if (!string.IsNullOrEmpty(id)) + toolUseIds.Add(id); + } + + if (toolUseIds.Count == 0) continue; + + // Prüfen ob die nächste user-Message die tool_results enthält + var coveredIds = new HashSet(); + if (i + 1 < history.Count && + history[i + 1] is Dictionary nextDict && + nextDict.TryGetValue("role", out var nextRole) && nextRole?.ToString() == "user" && + nextDict.TryGetValue("content", out var nextContent) && nextContent is List nextContentList) + { + foreach (var block in nextContentList) + { + var json = JsonSerializer.Serialize(block); + JsonElement el; + try { el = JsonSerializer.Deserialize(json); } + catch { continue; } + + if (!el.TryGetProperty("type", out var t) || t.GetString() != "tool_result") continue; + if (!el.TryGetProperty("tool_use_id", out var tid)) continue; + var toolUseId = tid.GetString(); + if (!string.IsNullOrEmpty(toolUseId)) + coveredIds.Add(toolUseId); + } + } + + // Orphaned IDs = tool_use ohne passendes tool_result + var orphanedIds = toolUseIds.Where(id => !coveredIds.Contains(id)).ToList(); + if (orphanedIds.Count == 0) continue; + + // Synthetische tool_results bauen + var syntheticResults = orphanedIds.Select(id => (object)new + { + type = "tool_result", + tool_use_id = id, + content = "Tool-Ergebnis nicht verfügbar (Chain wurde unterbrochen)." + }).ToList(); + + // Falls nächste Message eine user-Message ist, synthetische results dort prependen + if (i + 1 < history.Count && + history[i + 1] is Dictionary existingUser && + existingUser.TryGetValue("role", out var er) && er?.ToString() == "user" && + existingUser.TryGetValue("content", out var ec) && ec is List existingContent) + { + existingContent.InsertRange(0, syntheticResults); + } + else + { + // Neue user-Message mit synthetischen results einfügen + history.Insert(i + 1, new Dictionary + { + ["role"] = "user", + ["content"] = syntheticResults + }); + i++; // Eingefügte Message überspringen + } } - - if (orphanedIds.Count == 0) return; - - // Synthetisches tool_result einfügen - var syntheticResults = orphanedIds.Select(id => (object)new - { - type = "tool_result", - tool_use_id = id, - content = "Der User hat keine Option gewählt und stattdessen eine neue Nachricht geschrieben." - }).ToList(); - - history.Add(new Dictionary - { - ["role"] = "user", - ["content"] = syntheticResults - }); } /// diff --git a/Services/ToolService.cs b/Services/ToolService.cs index 079b52b..b515d1e 100644 --- a/Services/ToolService.cs +++ b/Services/ToolService.cs @@ -729,10 +729,19 @@ public class ToolService } case "send_to_session": { + var debugLog = "/tmp/gott_send_to_session.log"; + void Log(string msg) { try { File.AppendAllText(debugLog, $"[{DateTime.Now:HH:mm:ss}] {msg}\n"); } catch {} } + Log($"Tool aufgerufen – sessionId={sessionId}, agentId={agentId}, userId={userId}"); + Log($"db={db != null}, chatService={chatService != null}, anthropicService={anthropicService != null}, telegramService={telegramService != null}"); + if (db == null || chatService == null || anthropicService == null) + { + Log("ABBRUCH: db/chatService/anthropicService ist null"); return "Fehler: send_to_session nicht verfügbar"; + } var sessionIdOrName = input.GetProperty("session_id_or_name").GetString() ?? ""; var message = input.GetProperty("message").GetString() ?? ""; + Log($"sessionIdOrName={sessionIdOrName}, message={message}"); if (string.IsNullOrEmpty(sessionIdOrName)) return "Fehler: session_id_or_name fehlt"; if (string.IsNullOrEmpty(message)) @@ -740,8 +749,10 @@ public class ToolService // Ziel-Session per ID oder Name suchen (instanzweit, kein User-Check) var targetSession = await db.GetSession(sessionIdOrName); + Log($"GetSession={targetSession?.SessionId ?? "null"}"); if (targetSession == null) targetSession = await db.GetSessionByName(sessionIdOrName); + Log($"GetSessionByName={targetSession?.SessionId ?? "null"}"); if (targetSession == null) return $"Fehler: Session '{sessionIdOrName}' nicht gefunden"; @@ -751,12 +762,14 @@ public class ToolService // Context für Ziel-Session vorbereiten (analog zu CronjobService) var ctx = await chatService.PrepareContext(targetSessionId, userId: null); + Log($"PrepareContext={ctx != null}"); if (ctx == null) return $"Fehler: Session '{targetSession.Name}' ist nicht konfiguriert (kein Agent zugewiesen?)"; // History aufbauen – User-Message NICHT in DB speichern (wie Cronjob) // Stattdessen als kontextualisierte Nachricht in die History injizieren var history = await chatService.BuildHistoryFromDb(targetSessionId, promptBlocks: ctx.PromptBlocks); + Log($"History geladen, {history.Count} Einträge"); var senderAgent = agentId != null ? await db.GetAgent(agentId) : null; var senderName = senderAgent?.Name ?? agentId ?? "Unbekannter Agent"; var senderInfo = $"[Nachricht von Agent \"{senderName}\"]: {message}"; @@ -765,10 +778,13 @@ public class ToolService try { + Log("Rufe ChatNonStreaming auf..."); await anthropicService.ChatNonStreaming(ctx, history); + Log("ChatNonStreaming erfolgreich"); } catch (Exception ex) { + Log($"ChatNonStreaming FEHLER: {ex}"); Console.WriteLine($"[send_to_session] ChatNonStreaming Fehler: {ex}"); return $"Fehler beim Senden an Session \"{targetSession.Name}\": {ex.Message}"; }