"""Robustes JSON-Parsen von LLM-Antworten: Modelle liefern Fences, Prosa drumherum oder Präfixe — wir suchen das erste vollständige JSON-Objekt/-Array.""" import json import re _FENCE = re.compile(r"```(?:json)?\s*(.*?)```", re.DOTALL) def parse(text: str): """→ Objekt oder None. Nie werfen — der Aufrufer entscheidet über Retry.""" if not text: return None m = _FENCE.search(text) if m: text = m.group(1) text = text.strip() # Das FRÜHESTE Klammerzeichen entscheidet: sonst gewinnt ein {…} im Array-Inneren. erste = sorted((("{", "}"), ("[", "]")), key=lambda p: text.find(p[0]) if p[0] in text else len(text)) for start_ch, end_ch in erste: start = text.find(start_ch) if start < 0: continue depth = 0 in_str = False esc = False for i in range(start, len(text)): c = text[i] if esc: esc = False continue if c == "\\": esc = in_str continue if c == '"': in_str = not in_str continue if in_str: continue if c == start_ch: depth += 1 elif c == end_ch: depth -= 1 if depth == 0: try: return json.loads(text[start:i + 1]) except ValueError: break try: return json.loads(text) except ValueError: return None