From 91d412a797b52f537668a3a5e443b753e4d8ea52 Mon Sep 17 00:00:00 2001 From: claude-dev Date: Wed, 11 Mar 2026 10:34:30 +0100 Subject: [PATCH] fix: Robuster JSON-Parser fuer Analyse-Antworten Analyse-Antworten von Claude wurden bei langen Outputs nicht korrekt geparst, wodurch das Lagebild-Briefing eingefroren blieb. Neuer Parser mit 4-stufigem Fallback: direktes Parsen, Regex-Extraktion, Reparatur abgeschnittenes JSON, Regex-Fallback fuer Summary. Co-Authored-By: Claude Opus 4.6 --- src/agents/analyzer.py | 85 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 5cd7c22..019c030 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -301,17 +301,17 @@ class AnalyzerAgent: return None, None def _parse_response(self, response: str) -> dict | None: - """Parst die Claude-Antwort als JSON-Objekt.""" + """Parst die Claude-Antwort als JSON-Objekt mit robustem Fallback.""" # Markdown-Code-Fences entfernen cleaned = response.strip() if cleaned.startswith("```"): - # Erste Zeile (```json oder ```) entfernen first_nl = cleaned.find(chr(10)) if first_nl != -1: cleaned = cleaned[first_nl + 1:] if cleaned.endswith("```"): cleaned = cleaned[:-3].rstrip() + # Versuch 1: Direkt parsen try: data = json.loads(cleaned) if isinstance(data, dict): @@ -319,8 +319,8 @@ class AnalyzerAgent: except json.JSONDecodeError: pass - # Fallback: aeusserstes JSON-Objekt per Regex finden - match = re.search(r"""\{.*\}""", response, re.DOTALL) + # Versuch 2: Aeusserstes JSON-Objekt per Regex + match = re.search(r"\{.*\}", response, re.DOTALL) if match: try: data = json.loads(match.group()) @@ -329,5 +329,80 @@ class AnalyzerAgent: except json.JSONDecodeError: pass - logger.warning("Konnte Analyse-Antwort nicht als JSON parsen") + # Versuch 3: Abgeschnittenes JSON reparieren (haeufig bei langen Antworten) + candidate = match.group() if match else cleaned + repaired = self._repair_truncated_json(candidate) + if repaired: + return repaired + + # Versuch 4: summary per Regex extrahieren als letzter Fallback + fallback = self._extract_summary_fallback(response) + if fallback: + logger.warning("JSON-Parse fehlgeschlagen, nutze Regex-Fallback fuer summary") + return fallback + + logger.error( + "Konnte Analyse-Antwort nicht als JSON parsen " + f"(Laenge: {len(response)} Zeichen, Anfang: {response[:200]!r})" + ) return None + + def _repair_truncated_json(self, text: str) -> dict | None: + """Versucht abgeschnittenes JSON zu reparieren (fehlende Klammern am Ende).""" + start = text.find("{") + if start == -1: + return None + fragment = text[start:] + + open_braces = fragment.count("{") - fragment.count("}") + open_brackets = fragment.count("[") - fragment.count("]") + + if open_braces <= 0 and open_brackets <= 0: + return None # Nicht abgeschnitten + + trimmed = fragment.rstrip() + + # Entferne unvollstaendigen trailing content + for end_pattern in [ + r'"\s*$', + r',\s*"[^"]*$', + r',\s*$', + ]: + m = re.search(end_pattern, trimmed) + if m: + trimmed = trimmed[:m.start()] + break + + # Schliesse offene Strukturen + trimmed = trimmed.rstrip().rstrip(",") + open_braces = trimmed.count("{") - trimmed.count("}") + open_brackets = trimmed.count("[") - trimmed.count("]") + trimmed += "]" * max(0, open_brackets) + "}" * max(0, open_braces) + + try: + data = json.loads(trimmed) + if isinstance(data, dict) and "summary" in data: + logger.info(f"Abgeschnittenes JSON erfolgreich repariert ({len(trimmed)} Zeichen)") + return data + except json.JSONDecodeError: + pass + return None + + def _extract_summary_fallback(self, response: str) -> dict | None: + """Extrahiert summary und sources per Regex wenn JSON-Parse komplett fehlschlaegt.""" + m = re.search(r'"summary"\s*:\s*"((?:[^"\\]|\\.)*)"', response, re.DOTALL) + if not m: + return None + summary = m.group(1) + summary = summary.replace('\\n', chr(10)).replace('\\"', '"').replace('\\\\', '\\') + + sources = [] + sm = re.search(r'"sources"\s*:\s*(\[.*?\])', response, re.DOTALL) + if sm: + try: + sources = json.loads(sm.group(1)) + except json.JSONDecodeError: + pass + + return {"summary": summary, "sources": sources, "key_facts": [], "translations": []} +