From b32c54b22c8478a68c220629813cdaa2cfc95fd6 Mon Sep 17 00:00:00 2001 From: claude-dev Date: Thu, 30 Jul 2026 21:08:34 +0000 Subject: [PATCH] feat(eu-umbau): Faktencheck und Lagebild auf die EU-Suche umgestellt (Phase 3) Im EU-Modus laufen jetzt auch Faktencheck und Lagebild komplett ueber Bedrock, damit gibt es dort keine CLI-Aufrufe mehr. Der Faktenchecker bekommt vorab eine gezielte staan-Stuetzsuche (eu_call_with_search: Haiku plant Verifikations-Queries zu offenen Behauptungen, Treffer wandern als Kontextblock in den Prompt und werden als Zweitquellen zitiert). Das Lagebild arbeitet im EU-Modus rein auf dem Meldungsbestand, die EU-Recherche hat unmittelbar davor gesammelt. Alle fuenf Aufrufstellen (Erst-/inkrementeller Faktencheck, Gruppen-Verifikation, Erst-/inkrementelle Analyse) haben die Weiche, der CLI-Modus bleibt unveraendert. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 9 ++- src/agents/analyzer.py | 20 ++++- src/agents/eu_researcher.py | 141 ++++++++++++++++++++++++++++++++++++ src/agents/factchecker.py | 43 ++++++++++- 4 files changed, 206 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8763617..fcdb754 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,8 +36,13 @@ backend: EU-Modus auch die RECHERCHE europaeisch: agents/eu_researcher.py steuert eine Schleife aus Bedrock-Planung und staan-Suchen (services/staan_client.py) und liefert dasselbe JSON wie die CLI-WebSearch. Werkzeuglose Aufrufe gehen - direkt ueber bedrock_client; Faktenchecker/Analyzer nutzen im EU-Modus noch - die CLI-WebSearch (kommt in Phase 3). Doppelspur: eine Orchestrator-Lane je + direkt ueber bedrock_client. Seit Phase 3 (2026-07-30) laufen auch + Faktencheck und Lagebild im EU-Modus komplett ueber Bedrock: der + Faktenchecker bekommt vorab eine staan-Stuetzsuche (eu_call_with_search in + eu_researcher.py, Haiku plant Verifikations-Queries, Treffer als + Kontextblock im Prompt), das Lagebild arbeitet rein auf dem + Meldungsbestand. Im EU-Modus gibt es damit KEINE CLI-Aufrufe mehr. + Doppelspur: eine Orchestrator-Lane je (Organisation, Backend) — CLI- und EU-Lauf derselben Org laufen gleichzeitig, gleiche Backends seriell. Kosten aus Token x BEDROCK_PRICING plus STAAN_COST_PER_QUERY_USD, Credits-System unveraendert. AWS-Zugang + diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 63cea18..0cfec86 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -419,7 +419,16 @@ class AnalyzerAgent: ) try: - result, usage = await call_claude(prompt) + # EU-Modus (Phase 3): Lagebild rein auf dem Meldungsbestand, über Bedrock. + # Die EU-Recherche hat unmittelbar davor gesammelt, eigene Websuche + # braucht die Analyse deshalb nicht. + from agents.claude_client import _ai_backend_var + from config import AI_BACKEND + if (_ai_backend_var.get(None) or AI_BACKEND) == "bedrock": + from agents.eu_researcher import eu_call_with_search + result, usage = await eu_call_with_search(prompt, output_language=output_language) + else: + result, usage = await call_claude(prompt) analysis = self._parse_response(result) if analysis: analysis = self._sanitize_sources(analysis) @@ -484,7 +493,14 @@ class AnalyzerAgent: ) try: - result, usage = await call_claude(prompt) + # EU-Modus (Phase 3): wie bei der Erstanalyse, werkzeuglos über Bedrock. + from agents.claude_client import _ai_backend_var + from config import AI_BACKEND + if (_ai_backend_var.get(None) or AI_BACKEND) == "bedrock": + from agents.eu_researcher import eu_call_with_search + result, usage = await eu_call_with_search(prompt, output_language=output_language) + else: + result, usage = await call_claude(prompt) analysis = self._parse_response(result) if analysis: analysis = self._sanitize_sources(analysis) diff --git a/src/agents/eu_researcher.py b/src/agents/eu_researcher.py index 6dc6b9e..84f3261 100644 --- a/src/agents/eu_researcher.py +++ b/src/agents/eu_researcher.py @@ -361,3 +361,144 @@ async def run_eu_research( len(validated), len(collected), searches_done, round_no + 1, total.cost_usd, ) return json.dumps(validated, ensure_ascii=False), total + + +# --- Phase 3: Stützsuche für Faktencheck und Lagebild -------------------------- +# Faktenchecker und Analyzer verlangen in ihren Prompts die eingebaute Websuche +# (WebSearch/WebFetch), die es auf Bedrock nicht gibt. Im EU-Modus ersetzt +# eu_call_with_search diese Aufrufe. Optional läuft vorab eine gezielte +# staan-Stützsuche (Haiku plant Verifikations-Queries), deren Treffer als +# Kontextblock in den Prompt wandern, danach geht der Aufruf werkzeuglos über +# Bedrock. Der CLI-Modus bleibt in den Agenten unverändert. + +_EU_TOOL_HINT = """ + +HINWEIS ZUR WEBSUCHE (EU-Modus): +WebSearch und WebFetch stehen NICHT zur Verfügung. Ignoriere alle Anweisungen oben, +diese Werkzeuge zu benutzen.{search_note} Behauptungen, die sich so nicht belegen +lassen, bekommen den entsprechenden Unbestätigt-Status.""" + +_SEARCH_NOTE_WITH_RESULTS = ( + " Nutze stattdessen die unten angehängten ERGEBNISSE DER EUROPÄISCHEN WEBSUCHE " + "als unabhängige Zweitquellen und zitiere sie als Evidenz, wenn sie eine " + "Behauptung stützen oder widerlegen. Zusätzlich zählen die übergebenen Meldungen." +) +_SEARCH_NOTE_NO_RESULTS = ( + " Stütze dich ausschließlich auf die übergebenen Meldungen und Quellen." +) + +_VERIFY_QUERY_PROMPT = """Du planst Verifikations-Suchanfragen für einen OSINT-Faktencheck. +Heute ist der {today}. Lage: {title} + +ZU PRÜFENDE PUNKTE: +{items_block} + +Erzeuge maximal {max_queries} Websuche-Anfragen, mit denen sich die wichtigsten dieser +Punkte gegen unabhängige, aktuelle Quellen prüfen lassen. Konkrete Ereignisse, Zahlen +und Akteure zuerst. Keine Jahreszahlen anhängen. + +Antworte NUR mit JSON: {{"queries": [{{"q": "suchbegriffe", "market": "{default_market}"}}]}} +"market" ist "de-de", "en-us" oder "fr-fr".""" + + +def _iso_from_display(output_language: str) -> str: + """Anzeige-Sprachname der Agenten ('Deutsch') auf ISO für die Marktwahl.""" + mapping = {"deutsch": "de", "english": "en", "french": "fr"} + return mapping.get((output_language or "").strip().lower(), "en") + + +async def eu_call_with_search( + prompt: str, + *, + title: str = "", + search_items: list[str] | None = None, + output_language: str = "Deutsch", + max_queries: int = 3, +) -> tuple[str, ClaudeUsage]: + """EU-Ersatz für call_claude mit Default-Tools (Faktencheck/Lagebild). + + Mit search_items läuft vorab die staan-Stützsuche, ohne wird nur der + Werkzeug-Hinweis angehängt. Der Modellaufruf geht werkzeuglos über + call_claude und damit (aktives Bedrock-Backend vorausgesetzt) über + Frankfurt. Gibt (result_text, gesamt_usage) zurück.""" + from agents.claude_client import call_claude + from config import CLAUDE_MODEL_FAST + + total = ClaudeUsage() + + def _add(u: ClaudeUsage | None): + if not u: + return + total.input_tokens += u.input_tokens + total.output_tokens += u.output_tokens + total.cache_creation_tokens += u.cache_creation_tokens + total.cache_read_tokens += u.cache_read_tokens + total.cost_usd += u.cost_usd + total.duration_ms += u.duration_ms + + results_block = "" + if search_items: + items = [str(s).strip() for s in search_items if str(s).strip()][:12] + today = datetime.now(TIMEZONE).strftime("%d.%m.%Y") + default_market = market_for_language(_iso_from_display(output_language)) + plan_prompt = _VERIFY_QUERY_PROMPT.format( + today=today, + title=title or "(ohne Titel)", + items_block="\n".join(f"- {s[:200]}" for s in items), + max_queries=max_queries, + default_market=default_market, + ) + try: + plan_text, plan_usage = await call_bedrock(plan_prompt, model=CLAUDE_MODEL_FAST, timeout=120) + _add(plan_usage) + except Exception as e: + logger.warning("EU-Stützsuche: Query-Planung fehlgeschlagen (%s), fahre ohne Suche fort", e) + plan_text = "" + queries = [] + if plan_text: + from agents.researcher import _extract_json_object + obj = _extract_json_object(plan_text) + for q in (obj or {}).get("queries", []) or []: + if isinstance(q, dict) and str(q.get("q", "")).strip(): + queries.append({ + "q": str(q["q"]).strip()[:400], + "market": str(q.get("market", "")).strip().lower(), + }) + searches = 0 + lines = [] + n = 0 + for q in queries[:max_queries]: + try: + res = await staan_search( + q["q"], + market=q["market"] or market_for_language(_iso_from_display(output_language)), + extra_snippets=True, max_snippets=3, full_content=False, + ) + searches += 1 + except StaanError as e: + logger.warning("EU-Stützsuche: Suche fehlgeschlagen (%s)", e) + continue + for r in res[:6]: + n += 1 + lines.append(f"[S{n}] {r['title'] or '(ohne Titel)'} | {r['hostname']} | {r['url']}") + if r.get("snippet"): + lines.append(f" Auszug: {r['snippet'][:300]}") + for chunk in (r.get("extra_snippets") or [])[:1]: + lines.append(f" Auszug: {chunk[:300]}") + total.cost_usd += searches * STAAN_COST_PER_QUERY_USD + if lines: + results_block = ( + "\n\nERGEBNISSE DER EUROPÄISCHEN WEBSUCHE " + "(von unserem System ausgeführt, als unabhängige Zweitquellen nutzbar):\n" + + "\n".join(lines) + ) + logger.info("EU-Stützsuche: %d Suchen, %d Treffer im Kontextblock", searches, n) + + hint = _EU_TOOL_HINT.format( + search_note=_SEARCH_NOTE_WITH_RESULTS if results_block else _SEARCH_NOTE_NO_RESULTS + ) + final_prompt = prompt + hint + results_block + + result, usage = await call_claude(final_prompt, tools=None) + _add(usage) + return result, total diff --git a/src/agents/factchecker.py b/src/agents/factchecker.py index a33ff0e..9f83482 100644 --- a/src/agents/factchecker.py +++ b/src/agents/factchecker.py @@ -495,7 +495,18 @@ class FactCheckerAgent: ) try: - result, usage = await call_claude(prompt) + # EU-Modus (Phase 3): staan-Stützsuche statt WebSearch, Aufruf über Bedrock. + from agents.claude_client import _ai_backend_var + from config import AI_BACKEND + if (_ai_backend_var.get(None) or AI_BACKEND) == "bedrock": + from agents.eu_researcher import eu_call_with_search + headlines = [a.get("headline_de") or a.get("headline", "") for a in articles[:8]] + result, usage = await eu_call_with_search( + prompt, title=title, output_language=output_language, + search_items=[title] + headlines, max_queries=3, + ) + else: + result, usage = await call_claude(prompt) facts = self._parse_response(result, articles=articles) logger.info(f"Faktencheck: {len(facts)} Fakten geprüft") return facts, usage @@ -537,7 +548,22 @@ class FactCheckerAgent: ) try: - result, usage = await call_claude(prompt) + # EU-Modus (Phase 3): staan-Stützsuche zu offenen Fakten statt WebSearch. + from agents.claude_client import _ai_backend_var + from config import AI_BACKEND + if (_ai_backend_var.get(None) or AI_BACKEND) == "bedrock": + from agents.eu_researcher import eu_call_with_search + open_claims = [ + f.get("claim", "") for f in existing_facts + if f.get("status") in ("unconfirmed", "developing", "disputed", "unverified") + ][:8] + headlines = [a.get("headline_de") or a.get("headline", "") for a in new_articles[:4]] + result, usage = await eu_call_with_search( + prompt, title=title, output_language=output_language, + search_items=open_claims + headlines, max_queries=3, + ) + else: + result, usage = await call_claude(prompt) facts = self._parse_response(result, articles=new_articles) logger.info(f"Inkrementeller Faktencheck: {len(facts)} Fakten (neu + aktualisiert)") return facts, usage @@ -645,7 +671,18 @@ class FactCheckerAgent: ) try: - result, v_usage = await call_claude(prompt) + # EU-Modus (Phase 3): gezielte Stützsuche je Faktengruppe. + from agents.claude_client import _ai_backend_var + from config import AI_BACKEND + if (_ai_backend_var.get(None) or AI_BACKEND) == "bedrock": + from agents.eu_researcher import eu_call_with_search + claims = [f.get("claim", "") for f in group_facts] + list(group_claims) + result, v_usage = await eu_call_with_search( + prompt, title=f"{title} ({theme})", output_language=output_language, + search_items=claims[:8], max_queries=2, + ) + else: + result, v_usage = await call_claude(prompt) facts = self._parse_response(result) logger.info(f"Gruppe '{theme}': {len(facts)} Fakten geprüft") return facts, v_usage