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 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-30 21:08:34 +00:00
Ursprung 676741b1c4
Commit b32c54b22c
4 geänderte Dateien mit 206 neuen und 7 gelöschten Zeilen

Datei anzeigen

@@ -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 +

Datei anzeigen

@@ -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)

Datei anzeigen

@@ -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

Datei anzeigen

@@ -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