fix: synthetisches tool_result bei übergangenem ask_user_choice

Wenn der User Text schreibt statt eine Choice zu wählen, endet die
vorherige Chain mit einem offenen ask_user_choice tool_use ohne
passendes tool_result. Anthropic API wirft dann 400.

FixOrphanedAskUserChoice() erkennt diesen Fall in BuildHistoryFromDb
und fügt ein synthetisches tool_result ein.
This commit is contained in:
Meleki committed 2026-03-11 11:26:52 +01:00
1 parent 148d1459a3
commit bdb7f54c74
1 file changed
+49
+49
View File
@@ -593,6 +593,9 @@ 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: Orphaned web_search server_tool_use entfernen
// Bei alten DB-Einträgen wurde web_search_tool_result nur als Sources-Array gespeichert,
// nicht als Raw-JSON → BuildHistoryFromDb überspringt sie (Array-Check) →
@@ -677,6 +680,52 @@ 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.
/// </summary>
private static void FixOrphanedAskUserChoice(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)
{
var blockJson = JsonSerializer.Serialize(block);
JsonElement el;
try { el = JsonSerializer.Deserialize<JsonElement>(blockJson); }
catch { 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() ?? "");
}
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>
/// Baut einen SubAgentContext für Preparer/Review/Finalizer.
/// Gemeinsame Logik: Agent laden, Model auflösen, SystemPrompt bauen, ToolChoice bestimmen.