From f446cfaaa0dcd6bc28c6d774866610cc9474d053 Mon Sep 17 00:00:00 2001 From: danbusse <81775558+danbusse@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:37:54 -0600 Subject: [PATCH] fix: add multi-stage JSON parsing with fallback extraction --- backend/app/services/ai/claude_client.py | 37 ++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/backend/app/services/ai/claude_client.py b/backend/app/services/ai/claude_client.py index 9523411..f9057fc 100644 --- a/backend/app/services/ai/claude_client.py +++ b/backend/app/services/ai/claude_client.py @@ -58,11 +58,48 @@ def _extract_text(message) -> str: def _parse_json(text: str) -> dict: + # Strip markdown fences text = re.sub(r"^```(?:json)?\s*", "", text, flags=re.MULTILINE) text = re.sub(r"\s*```$", "", text, flags=re.MULTILINE) text = text.strip() + + # First attempt: standard parse try: return json.loads(text) + except json.JSONDecodeError: + pass + + # Second attempt: try with json5/demjson-style fixes + # Fix unescaped apostrophes inside JSON strings heuristically + # Replace 's that appear mid-word inside a string context + fixed = re.sub(r"(?<=[a-zA-Z])'(?=[a-zA-Z])", "\\'", text) + try: + return json.loads(fixed) + except json.JSONDecodeError: + pass + + # Third attempt: extract just the cards array if top-level parse fails + # This handles truncated responses + cards_match = re.search(r'"cards"\s*:\s*(\[.*?\])\s*[,}]', text, re.DOTALL) + decklist_match = re.search(r'"decklist"\s*:\s*(\[.*?\])\s*[,}]', text, re.DOTALL) + deck_match = re.search(r'"deck"\s*:\s*(\[.*?\])\s*[,}]', text, re.DOTALL) + + cards_text = None + for match in [cards_match, decklist_match, deck_match]: + if match: + cards_text = match.group(1) + break + + if cards_text: + try: + cards = json.loads(cards_text) + return {"deck_name": "Generated Deck", "strategy_summary": "", "cards": cards} + except json.JSONDecodeError: + pass + + # Last resort: raise with original error + try: + json.loads(text) except json.JSONDecodeError as e: raise ValueError(f"Claude response was not valid JSON: {e}\n\nRaw: {text[:500]}")