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:
@@ -262,6 +262,8 @@ class FactCheckerAgent:
|
|||||||
facts = self._parse_response(result)
|
facts = self._parse_response(result)
|
||||||
logger.info(f"Faktencheck: {len(facts)} Fakten geprüft")
|
logger.info(f"Faktencheck: {len(facts)} Fakten geprüft")
|
||||||
return facts, usage
|
return facts, usage
|
||||||
|
except TimeoutError:
|
||||||
|
raise # Timeout nach oben durchreichen fuer Retry im Orchestrator
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Faktencheck-Fehler: {e}")
|
logger.error(f"Faktencheck-Fehler: {e}")
|
||||||
return [], None
|
return [], None
|
||||||
@@ -302,6 +304,8 @@ class FactCheckerAgent:
|
|||||||
facts = self._parse_response(result)
|
facts = self._parse_response(result)
|
||||||
logger.info(f"Inkrementeller Faktencheck: {len(facts)} Fakten (neu + aktualisiert)")
|
logger.info(f"Inkrementeller Faktencheck: {len(facts)} Fakten (neu + aktualisiert)")
|
||||||
return facts, usage
|
return facts, usage
|
||||||
|
except TimeoutError:
|
||||||
|
raise # Timeout nach oben durchreichen fuer Retry im Orchestrator
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Inkrementeller Faktencheck-Fehler: {e}")
|
logger.error(f"Inkrementeller Faktencheck-Fehler: {e}")
|
||||||
return [], None
|
return [], None
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ class AgentOrchestrator:
|
|||||||
logger.info(f"Starte Refresh für Lage {incident_id} (Trigger: {trigger_type})")
|
logger.info(f"Starte Refresh für Lage {incident_id} (Trigger: {trigger_type})")
|
||||||
|
|
||||||
RETRY_DELAYS = [0, 120, 300] # Sekunden: sofort, 2min, 5min
|
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
|
last_error = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -233,6 +233,8 @@ class ResearcherAgent:
|
|||||||
logger.info(f"Recherche ergab {len(filtered)} Artikel (von {len(articles)} gefundenen, international={international})")
|
logger.info(f"Recherche ergab {len(filtered)} Artikel (von {len(articles)} gefundenen, international={international})")
|
||||||
return filtered, usage
|
return filtered, usage
|
||||||
|
|
||||||
|
except TimeoutError:
|
||||||
|
raise # Timeout nach oben durchreichen fuer Retry im Orchestrator
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Recherche-Fehler: {e}")
|
logger.error(f"Recherche-Fehler: {e}")
|
||||||
return [], None
|
return [], None
|
||||||
@@ -255,11 +257,25 @@ class ResearcherAgent:
|
|||||||
data = json.loads(response)
|
data = json.loads(response)
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
return data
|
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:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Versuche JSON aus der Antwort zu extrahieren (zwischen [ und ])
|
# 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:
|
if match:
|
||||||
try:
|
try:
|
||||||
data = json.loads(match.group())
|
data = json.loads(match.group())
|
||||||
@@ -268,5 +284,20 @@ class ResearcherAgent:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
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 []
|
return []
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ JWT_EXPIRE_HOURS = 24
|
|||||||
|
|
||||||
# Claude CLI
|
# Claude CLI
|
||||||
CLAUDE_PATH = os.environ.get("CLAUDE_PATH", "/home/claude-dev/.claude/local/claude")
|
CLAUDE_PATH = os.environ.get("CLAUDE_PATH", "/home/claude-dev/.claude/local/claude")
|
||||||
CLAUDE_TIMEOUT = 300 # Sekunden (Claude mit WebSearch braucht oft 2-3 Min)
|
CLAUDE_TIMEOUT = 420 # Sekunden (Claude mit WebSearch braucht oft 2-4 Min)
|
||||||
# Claude Modelle
|
# Claude Modelle
|
||||||
CLAUDE_MODEL_FAST = "claude-haiku-4-5-20251001" # Für einfache Aufgaben (Feed-Selektion)
|
CLAUDE_MODEL_FAST = "claude-haiku-4-5-20251001" # Für einfache Aufgaben (Feed-Selektion)
|
||||||
|
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren