diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 91ba71e..4ea0b8c 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -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? } } + /// + /// 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. + /// + private static void FixOrphanedAskUserChoice(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) + { + var blockJson = JsonSerializer.Serialize(block); + JsonElement el; + try { el = JsonSerializer.Deserialize(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 + { + ["role"] = "user", + ["content"] = syntheticResults + }); + } + /// /// Baut einen SubAgentContext für Preparer/Review/Finalizer. /// Gemeinsame Logik: Agent laden, Model auflösen, SystemPrompt bauen, ToolChoice bestimmen.