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

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