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

+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>