File size: 1,736 Bytes
80a4a65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
"""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