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:
@@ -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()
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren