fix(researcher): Robusteres JSON-Parsing der Claude-Antworten

Behebt das Symptom, dass Recherche-Lagen wie staging Lage 6 "Friedrich Merz"
trotz erfolgreichem Refresh leer blieben. Claude lieferte nicht-leere Antworten
(1226-2125 Zeichen), die der bisherige Regex-Parser nicht extrahieren konnte —
die Recherche meldete "0 Artikel" und der Refresh wurde stumm als Erfolg
verbucht.

Aenderungen:
- _parse_response, select_relevant_feeds, extract_dynamic_keywords und
  select_relevant_telegram_channels nutzen jetzt json.JSONDecoder.raw_decode
  ueber Modul-Helper _extract_json_array/_extract_json_object. Damit werden
  auch JSON-Bloecke mit Vor-/Nachtext, Markdown-Fences oder verschachtelten
  Objekten zuverlaessig erkannt.
- Bei Parse-Fehlschlag wird jetzt ein gekuerztes Sample der Claude-Antwort
  geloggt, damit kuenftige Faelle direkt debuggbar sind.
- Neue ResearcherParseError-Exception unterscheidet "echt 0 Treffer" von
  "Antwort kaputt". search() gibt zusaetzlich ein parse_failed-Flag zurueck.
- Orchestrator-Multi-Pass: wenn alle 3 research-Durchlaeufe 0 neue Artikel
  ergeben UND mindestens einer am Parser scheiterte, wird der Refresh als
  Fehler markiert (statt als stiller Erfolg). Der WebSocket-refresh_error
  loest dann die sichtbare UI-Meldung aus.

Adhoc-Lagen sind unveraendert: dort fangen RSS und Telegram die kaputte
Claude-Antwort auf, dafuer ist nur die Diagnose im Log neu.
Dieser Commit ist enthalten in:
Claude Code
2026-04-30 20:45:41 +00:00
Ursprung 682828ea58
Commit 88b18d0775
2 geänderte Dateien mit 168 neuen und 84 gelöschten Zeilen

Datei anzeigen

@@ -796,13 +796,16 @@ class AgentOrchestrator:
"source_url": row["source_url"]}
for row in existing_db_articles_full
]
results, usage = await researcher.search(
results, usage, parse_failed = await researcher.search(
title, description, incident_type,
international=international, user_id=user_id,
existing_articles=existing_for_context,
)
logger.info(f"Claude-Recherche: {len(results)} Ergebnisse")
return results, usage
logger.info(
f"Claude-Recherche: {len(results)} Ergebnisse"
+ (" (Parser fehlgeschlagen)" if parse_failed else "")
)
return results, usage, parse_failed
async def _podcast_pipeline():
"""Podcast-Episoden-Suche (nur adhoc-Lagen, nur mit vorhandenen Transkripten)."""
@@ -885,7 +888,7 @@ class AgentOrchestrator:
pipeline_results = await asyncio.gather(*pipelines)
(rss_articles, rss_feed_usage) = pipeline_results[0]
(search_results, search_usage) = pipeline_results[1]
(search_results, search_usage, search_parse_failed) = pipeline_results[1]
(podcast_articles, _podcast_usage) = pipeline_results[2]
telegram_articles = pipeline_results[3][0] if include_telegram else []
@@ -1568,6 +1571,11 @@ class AgentOrchestrator:
logger.info(f"Refresh für Lage {incident_id} abgeschlossen: {new_count} neue Artikel")
# Multi-Pass-Diagnose: Pass-Ergebnis zurueck an Multi-Pass-Caller geben
if _pass_info is not None:
_pass_info["new_count"] = new_count
_pass_info["parse_failed"] = search_parse_failed
# Executive Summary im Hintergrund vorab generieren (fuer schnelleren Export)
if new_count > 0:
async def _pregenerate_exec_summary():
@@ -1622,6 +1630,7 @@ class AgentOrchestrator:
Durchlauf 3: Konsolidierung (letzte Lücken, Fakten-Upgrade)
"""
total = RESEARCH_MULTI_PASS_COUNT
pass_results = []
for pass_nr in range(1, total + 1):
# Cancel zwischen Durchläufen prüfen
@@ -1662,12 +1671,27 @@ class AgentOrchestrator:
if is_last:
raise
# Nicht-letzter Durchlauf: weiter mit nächstem, bisherige Ergebnisse bleiben
finally:
pass_results.append(pass_info)
logger.info(
f"Research Multi-Pass abgeschlossen für Lage {incident_id}: "
f"{total} Durchläufe"
)
# Diagnose: Wenn ALLE Passes 0 neue Artikel hatten UND mindestens einer
# an einem Parser-Fehler scheiterte, ist die Recherche faktisch fehlgeschlagen —
# Claude lieferte zwar Antworten, aber kein verwertbares JSON. Sonst bliebe
# die Lage ohne sichtbare Fehlermeldung leer (siehe staging Lage "Friedrich Merz").
total_new = sum(p.get("new_count", 0) for p in pass_results)
any_parse_failed = any(p.get("parse_failed") for p in pass_results)
if total_new == 0 and any_parse_failed:
raise RuntimeError(
"Recherche fehlgeschlagen: Claude lieferte keine verwertbaren Quellen "
"(JSON-Parsing schlug bei mindestens einem Durchlauf fehl). "
"Bitte Logs prüfen und Refresh erneut starten."
)
# Singleton-Instanz
orchestrator = AgentOrchestrator()