"""JSON parsing helpers used by every AI call site.""" import json import re def parse_json_safe(raw: str) -> dict: """Best-effort JSON parser for raw LLM output. Handles three common failure modes: 1. The model wraps the object in ```json ... ``` fences. 2. The model adds prose before/after the object. 3. The model emits literal control characters (newlines, tabs) inside a string value that ``json.loads`` rejects. """ cleaned = raw.strip() fence_match = re.search(r"```(?:json)?\s*([\s\S]*?)```", cleaned, re.IGNORECASE) if fence_match: cleaned = fence_match.group(1).strip() start = cleaned.find("{") end = cleaned.rfind("}") if start != -1 and end != -1 and end > start: cleaned = cleaned[start:end + 1] cleaned = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", cleaned) try: # Use strict=False to allow literal control characters (like newlines) in strings return json.loads(cleaned, strict=False) except json.JSONDecodeError as exc: # One last ditch effort: replace literal newlines with escaped ones try: # This is risky but sometimes helps with unescaped newlines in middle of strings cleaned_fix = cleaned.replace('\n', '\\n').replace('\r', '\\r') # But the start/end might be messed up now if it was already formatted. # Let's just try strict=False first as it's the standard solution for "control character" errors. return json.loads(cleaned, strict=False) except Exception: preview = raw[:500].replace("\n", " ") raise ValueError(f"Invalid JSON from model: {exc}. Raw preview: {preview}") from exc