Fix: TimeoutError wird nicht mehr verschluckt - Retry greift jetzt

- researcher.py/factchecker.py: TimeoutError wird nach oben durchgereicht
  statt vom breiten except Exception geschluckt zu werden
- orchestrator.py: Built-in TimeoutError zu TRANSIENT_ERRORS hinzugefuegt
  (war nur asyncio.TimeoutError, aber claude_client wirft TimeoutError)
- config.py: CLAUDE_TIMEOUT von 300s auf 420s erhoeht

Vorher: Timeout fuehrte zu "0 Artikel" ohne Retry (8 Timeouts seit 28.02.)
Nachher: Timeout loest bis zu 3 Retries aus (sofort, +2min, +5min)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-03-07 10:48:48 +01:00
Ursprung ac3291f608
Commit f7809ccc77
4 geänderte Dateien mit 39 neuen und 4 gelöschten Zeilen

Datei anzeigen

@@ -262,6 +262,8 @@ class FactCheckerAgent:
facts = self._parse_response(result)
logger.info(f"Faktencheck: {len(facts)} Fakten geprüft")
return facts, usage
except TimeoutError:
raise # Timeout nach oben durchreichen fuer Retry im Orchestrator
except Exception as e:
logger.error(f"Faktencheck-Fehler: {e}")
return [], None
@@ -302,6 +304,8 @@ class FactCheckerAgent:
facts = self._parse_response(result)
logger.info(f"Inkrementeller Faktencheck: {len(facts)} Fakten (neu + aktualisiert)")
return facts, usage
except TimeoutError:
raise # Timeout nach oben durchreichen fuer Retry im Orchestrator
except Exception as e:
logger.error(f"Inkrementeller Faktencheck-Fehler: {e}")
return [], None

Datei anzeigen

@@ -392,7 +392,7 @@ class AgentOrchestrator:
logger.info(f"Starte Refresh für Lage {incident_id} (Trigger: {trigger_type})")
RETRY_DELAYS = [0, 120, 300] # Sekunden: sofort, 2min, 5min
TRANSIENT_ERRORS = (asyncio.TimeoutError, ConnectionError, OSError)
TRANSIENT_ERRORS = (asyncio.TimeoutError, TimeoutError, ConnectionError, OSError)
last_error = None
try:

Datei anzeigen

@@ -233,6 +233,8 @@ class ResearcherAgent:
logger.info(f"Recherche ergab {len(filtered)} Artikel (von {len(articles)} gefundenen, international={international})")
return filtered, usage
except TimeoutError:
raise # Timeout nach oben durchreichen fuer Retry im Orchestrator
except Exception as e:
logger.error(f"Recherche-Fehler: {e}")
return [], None
@@ -255,11 +257,25 @@ class ResearcherAgent:
data = json.loads(response)
if isinstance(data, list):
return data
if isinstance(data, dict) and "articles" in data:
return data["articles"]
except json.JSONDecodeError:
pass
# JSON-Code-Block extrahieren
code_pat = r'`{3}(?:json)?\s*\n?(\[.*?\])\s*`{3}'
code_match = re.search(code_pat, response, re.DOTALL)
if code_match:
try:
data = json.loads(code_match.group(1))
if isinstance(data, list):
return data
except json.JSONDecodeError:
pass
# Versuche JSON aus der Antwort zu extrahieren (zwischen [ und ])
match = re.search(r'\[.*\]', response, re.DOTALL)
arr_pat = r'\[\s*\{.*\}\s*\]'
match = re.search(arr_pat, response, re.DOTALL)
if match:
try:
data = json.loads(match.group())
@@ -268,5 +284,20 @@ class ResearcherAgent:
except json.JSONDecodeError:
pass
logger.warning("Konnte Claude-Antwort nicht als JSON parsen")
# Letzter Versuch: einzelne JSON-Objekte mit headline
objects = re.findall(r'\{[^{}]*"headline"[^{}]*\}', response)
if objects:
results = []
for obj_str in objects:
try:
obj = json.loads(obj_str)
if "headline" in obj:
results.append(obj)
except json.JSONDecodeError:
continue
if results:
logger.info(f"JSON-Recovery: {len(results)} Artikel aus Einzelobjekten extrahiert")
return results
logger.warning(f"Konnte Claude-Antwort nicht als JSON parsen (Laenge: {len(response)})")
return []