fix: send_to_session für alle ExecuteTool-Pfade + generelle FixOrphanedToolUse

- 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
This commit is contained in:
Meleki committed 2026-03-11 13:50:12 +01:00
1 parent 2486eee047
commit e374c70c89
3 files changed
+98 -40

No files matched your search

+2 -2
View File
@@ -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; }
+80 -38
View File
@@ -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?
}
/// <summary>
/// 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).
/// </summary>
private static void FixOrphanedAskUserChoice(List<object> history)
private static void FixOrphanedToolUse(List<object> history)
{
if (history.Count == 0) return;
var last = history[history.Count - 1];
if (last is not Dictionary<string, object> lastDict) return;
if (!lastDict.TryGetValue("role", out var lastRole) || lastRole?.ToString() != "assistant") return;
if (!lastDict.TryGetValue("content", out var lastContent) || lastContent is not List<object> lastContentList) return;
// Sammle alle ask_user_choice tool_use IDs die noch kein tool_result haben
var orphanedIds = new List<string>();
foreach (var block in lastContentList)
for (int i = 0; i < history.Count; i++)
{
var blockJson = JsonSerializer.Serialize(block);
JsonElement el;
try { el = JsonSerializer.Deserialize<JsonElement>(blockJson); }
catch { continue; }
if (history[i] is not Dictionary<string, object> assistantDict) continue;
if (!assistantDict.TryGetValue("role", out var role) || role?.ToString() != "assistant") continue;
if (!assistantDict.TryGetValue("content", out var content) || content is not List<object> 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<string>();
foreach (var block in contentList)
{
var json = JsonSerializer.Serialize(block);
JsonElement el;
try { el = JsonSerializer.Deserialize<JsonElement>(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<string>();
if (i + 1 < history.Count &&
history[i + 1] is Dictionary<string, object> nextDict &&
nextDict.TryGetValue("role", out var nextRole) && nextRole?.ToString() == "user" &&
nextDict.TryGetValue("content", out var nextContent) && nextContent is List<object> nextContentList)
{
foreach (var block in nextContentList)
{
var json = JsonSerializer.Serialize(block);
JsonElement el;
try { el = JsonSerializer.Deserialize<JsonElement>(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<string, object> existingUser &&
existingUser.TryGetValue("role", out var er) && er?.ToString() == "user" &&
existingUser.TryGetValue("content", out var ec) && ec is List<object> existingContent)
{
existingContent.InsertRange(0, syntheticResults);
}
else
{
// Neue user-Message mit synthetischen results einfügen
history.Insert(i + 1, new Dictionary<string, object>
{
["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<string, object>
{
["role"] = "user",
["content"] = syntheticResults
});
}
/// <summary>
+16
View File
@@ -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}";
}