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