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 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-03-11 10:34:30 +01:00
Ursprung 445f645936
Commit 91d412a797

Datei anzeigen

@@ -301,17 +301,17 @@ class AnalyzerAgent:
return None, None return None, None
def _parse_response(self, response: str) -> dict | 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 # Markdown-Code-Fences entfernen
cleaned = response.strip() cleaned = response.strip()
if cleaned.startswith("```"): if cleaned.startswith("```"):
# Erste Zeile (```json oder ```) entfernen
first_nl = cleaned.find(chr(10)) first_nl = cleaned.find(chr(10))
if first_nl != -1: if first_nl != -1:
cleaned = cleaned[first_nl + 1:] cleaned = cleaned[first_nl + 1:]
if cleaned.endswith("```"): if cleaned.endswith("```"):
cleaned = cleaned[:-3].rstrip() cleaned = cleaned[:-3].rstrip()
# Versuch 1: Direkt parsen
try: try:
data = json.loads(cleaned) data = json.loads(cleaned)
if isinstance(data, dict): if isinstance(data, dict):
@@ -319,8 +319,8 @@ class AnalyzerAgent:
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
# Fallback: aeusserstes JSON-Objekt per Regex finden # Versuch 2: Aeusserstes JSON-Objekt per Regex
match = re.search(r"""\{.*\}""", response, re.DOTALL) match = re.search(r"\{.*\}", response, re.DOTALL)
if match: if match:
try: try:
data = json.loads(match.group()) data = json.loads(match.group())
@@ -329,5 +329,80 @@ class AnalyzerAgent:
except json.JSONDecodeError: except json.JSONDecodeError:
pass 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 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": []}