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:
@@ -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": []}
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren