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:
@@ -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 +
|
||||
|
||||
@@ -419,6 +419,15 @@ class AnalyzerAgent:
|
||||
)
|
||||
|
||||
try:
|
||||
# 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:
|
||||
@@ -484,6 +493,13 @@ class AnalyzerAgent:
|
||||
)
|
||||
|
||||
try:
|
||||
# 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -495,6 +495,17 @@ class FactCheckerAgent:
|
||||
)
|
||||
|
||||
try:
|
||||
# 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")
|
||||
@@ -537,6 +548,21 @@ class FactCheckerAgent:
|
||||
)
|
||||
|
||||
try:
|
||||
# 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)")
|
||||
@@ -645,6 +671,17 @@ class FactCheckerAgent:
|
||||
)
|
||||
|
||||
try:
|
||||
# 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")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren