feat(eu): Faktencheck der EU-Fassung sucht breiter, gezielter und hakt nach
Der Faktencheck ueber Bedrock und staan lieferte spuerbar weniger belegte Fakten als der bisherige Weg ueber die Anthropic-CLI. Ursache war die Bauart. Der bisherige Weg sucht waehrend der Pruefung selbst und kann bei duenner Beleglage nachfassen, die EU-Fassung bekam dagegen einen festen, vorab beschafften Stapel Belege und hatte genau einen Versuch. Drei Aenderungen schliessen die Luecke. 1. Mehr Belege je Pruefung. Statt 3 Anfragen mit je 3 Treffern laufen jetzt 7 Anfragen mit je 6 Treffern, und bis zu 14 statt 8 Pruefpunkte gehen in die Planung ein. Gleiche Quellen werden nur einmal in den Kontext gelegt. 2. Punktbezogene Planung. Das planende Modell ordnet jeder Suchanfrage zu, welche Behauptung sie belegen soll, statt allgemein zum Thema zu suchen. 3. Echte Nachhak-Runde. Bleiben nach der Pruefung mindestens zwei Behauptungen unbelegt, wird fuer genau diese noch einmal gesucht, mit anderen Anfragen als zuvor, und nur diese Punkte werden neu bewertet. Ein Status wird dabei nur angehoben, nie gesenkt, und faellt die Runde aus, bleiben die Fakten unveraendert. Die Nachhak-Runde greift in allen drei Pruefwegen, also bei der Erstpruefung, der inkrementellen Pruefung und der Pruefung in Themengruppen. Gruppen nutzen kleinere Suchmengen, da sie parallel laufen und je nur ein Teilthema abdecken. Alle Mengen sind ueber Umgebungsvariablen einstellbar. Der Weg ueber die Anthropic-CLI bleibt unveraendert, das sichern sechs Regressionstests ab. Neu sind zwei Testdateien mit zusammen 55 Pruefungen, die ohne Netzzugriff und ohne Kosten laufen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -26,6 +26,9 @@ from config import (
|
||||
CLAUDE_MODEL_STANDARD,
|
||||
EU_RESEARCH_MAX_ROUNDS_ADHOC,
|
||||
EU_RESEARCH_MAX_ROUNDS_RESEARCH,
|
||||
EU_VERIFY_HITS_PER_QUERY,
|
||||
EU_VERIFY_MAX_ITEMS,
|
||||
EU_VERIFY_MAX_QUERIES,
|
||||
STAAN_COST_PER_QUERY_USD,
|
||||
TIMEZONE,
|
||||
)
|
||||
@@ -390,15 +393,25 @@ _SEARCH_NOTE_NO_RESULTS = (
|
||||
_VERIFY_QUERY_PROMPT = """Du planst Verifikations-Suchanfragen für einen OSINT-Faktencheck.
|
||||
Heute ist der {today}. Lage: {title}
|
||||
|
||||
ZU PRÜFENDE PUNKTE:
|
||||
ZU PRÜFENDE PUNKTE (nummeriert):
|
||||
{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.
|
||||
AUFGABE:
|
||||
Erzeuge bis zu {max_queries} Websuche-Anfragen. Plane sie GEZIELT je Punkt, nicht
|
||||
pauschal für alle zusammen. Eine gute Anfrage macht genau einen dieser Punkte
|
||||
überprüfbar, mit seinen konkreten Zahlen, Namen und Orten.
|
||||
|
||||
Antworte NUR mit JSON: {{"queries": [{{"q": "suchbegriffe", "market": "{default_market}"}}]}}
|
||||
"market" ist "de-de", "en-us" oder "fr-fr"."""
|
||||
REGELN:
|
||||
- Ordne jeder Anfrage über "for_items" zu, welche Punkte sie belegen soll.
|
||||
- Decke möglichst viele verschiedene Punkte ab, statt denselben mehrfach zu suchen.
|
||||
- Priorisiere Punkte mit harten, überprüfbaren Angaben (Zahlen, Daten, Zitate, Beschlüsse).
|
||||
- Nutze die Sprache, in der die Berichterstattung zu erwarten ist. Für Ereignisse in
|
||||
einem Land sind Quellen in dessen Landessprache oft ergiebiger.
|
||||
- Keine Jahreszahlen anhängen, keine Wiederholung derselben Anfrage.
|
||||
{avoid_block}
|
||||
Antworte NUR mit JSON:
|
||||
{{"queries": [{{"q": "suchbegriffe", "market": "{default_market}", "for_items": [1, 2]}}]}}
|
||||
"market" ist "de-de", "en-us" oder "fr-fr". Fremdsprachige Anfragen funktionieren mit "en-us"."""
|
||||
|
||||
|
||||
def _iso_from_display(output_language: str) -> str:
|
||||
@@ -407,98 +420,163 @@ def _iso_from_display(output_language: str) -> str:
|
||||
return mapping.get((output_language or "").strip().lower(), "en")
|
||||
|
||||
|
||||
def _add_usage(total: ClaudeUsage, u: ClaudeUsage | None) -> None:
|
||||
"""Zaehlt einen Einzelverbrauch auf die Gesamtsumme."""
|
||||
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
|
||||
|
||||
|
||||
async def plan_verification_searches(
|
||||
*,
|
||||
title: str,
|
||||
search_items: list[str],
|
||||
output_language: str = "Deutsch",
|
||||
max_queries: int = EU_VERIFY_MAX_QUERIES,
|
||||
avoid_queries: list[str] | None = None,
|
||||
label: str = "Stützsuche",
|
||||
) -> tuple[str, ClaudeUsage, list[str]]:
|
||||
"""Plant gezielte Belegsuchen zu den uebergebenen Punkten und fuehrt sie aus.
|
||||
|
||||
Die Planung erfolgt punktbezogen, das planende Modell ordnet jeder Anfrage
|
||||
zu, welche Punkte sie belegen soll. avoid_queries verhindert, dass eine
|
||||
Nachrunde dieselben Anfragen noch einmal stellt.
|
||||
|
||||
Gibt (kontextblock, verbrauch, ausgefuehrte_anfragen) zurueck. Der
|
||||
Kontextblock ist leer, wenn nichts gefunden wurde.
|
||||
"""
|
||||
from config import CLAUDE_MODEL_FAST
|
||||
|
||||
total = ClaudeUsage()
|
||||
items = [str(s).strip() for s in (search_items or []) if str(s).strip()][:EU_VERIFY_MAX_ITEMS]
|
||||
if not items:
|
||||
return "", total, []
|
||||
|
||||
default_market = market_for_language(_iso_from_display(output_language))
|
||||
avoid_block = ""
|
||||
if avoid_queries:
|
||||
avoid_block = (
|
||||
"- Diese Anfragen wurden bereits gestellt und brachten nicht genug, stelle ANDERE:\n"
|
||||
+ "\n".join(f" {q[:120]}" for q in avoid_queries[:10]) + "\n"
|
||||
)
|
||||
|
||||
plan_prompt = _VERIFY_QUERY_PROMPT.format(
|
||||
today=datetime.now(TIMEZONE).strftime("%d.%m.%Y"),
|
||||
title=title or "(ohne Titel)",
|
||||
items_block="\n".join(f"{i}. {s[:220]}" for i, s in enumerate(items, 1)),
|
||||
max_queries=max_queries,
|
||||
default_market=default_market,
|
||||
avoid_block=avoid_block,
|
||||
)
|
||||
|
||||
try:
|
||||
plan_text, plan_usage = await call_bedrock(plan_prompt, model=CLAUDE_MODEL_FAST, timeout=120)
|
||||
_add_usage(total, plan_usage)
|
||||
except Exception as e:
|
||||
logger.warning("EU-%s: Planung der Anfragen fehlgeschlagen (%s), fahre ohne Suche fort", label, e)
|
||||
return "", total, []
|
||||
|
||||
from agents.researcher import _extract_json_object
|
||||
obj = _extract_json_object(plan_text or "")
|
||||
queries: list[dict] = []
|
||||
gesehen: set[str] = {(q or "").strip().lower() for q in (avoid_queries or [])}
|
||||
for q in (obj or {}).get("queries", []) or []:
|
||||
if not isinstance(q, dict):
|
||||
continue
|
||||
text = str(q.get("q", "")).strip()[:400]
|
||||
if not text or text.lower() in gesehen:
|
||||
continue
|
||||
gesehen.add(text.lower())
|
||||
queries.append({"q": text, "market": str(q.get("market", "")).strip().lower()})
|
||||
if len(queries) >= max_queries:
|
||||
break
|
||||
|
||||
if not queries:
|
||||
logger.warning("EU-%s: Planung lieferte keine brauchbaren Anfragen", label)
|
||||
return "", total, []
|
||||
|
||||
lines: list[str] = []
|
||||
ausgefuehrt: list[str] = []
|
||||
gesehene_urls: set[str] = set()
|
||||
treffer = 0
|
||||
for q in queries:
|
||||
try:
|
||||
res = await staan_search(
|
||||
q["q"],
|
||||
market=q["market"] if q["market"] in ("de-de", "en-us", "fr-fr") else default_market,
|
||||
extra_snippets=True, max_snippets=3, full_content=False,
|
||||
)
|
||||
ausgefuehrt.append(q["q"])
|
||||
except StaanError as e:
|
||||
logger.warning("EU-%s: Suche fehlgeschlagen (%s)", label, e)
|
||||
continue
|
||||
for r in res[:EU_VERIFY_HITS_PER_QUERY]:
|
||||
# Mehrere Anfragen finden oft dieselbe Seite, jede Quelle nur einmal
|
||||
# in den Kontextblock legen.
|
||||
url = (r.get("url") or "").strip()
|
||||
if url and url in gesehene_urls:
|
||||
continue
|
||||
if url:
|
||||
gesehene_urls.add(url)
|
||||
treffer += 1
|
||||
lines.append(f"[S{treffer}] {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 += len(ausgefuehrt) * STAAN_COST_PER_QUERY_USD
|
||||
logger.info("EU-%s: %d Suchen, %d Treffer im Kontextblock", label, len(ausgefuehrt), treffer)
|
||||
|
||||
if not lines:
|
||||
return "", total, ausgefuehrt
|
||||
|
||||
block = (
|
||||
"\n\nERGEBNISSE DER EUROPÄISCHEN WEBSUCHE "
|
||||
"(von unserem System ausgeführt, als unabhängige Zweitquellen nutzbar):\n"
|
||||
+ "\n".join(lines)
|
||||
)
|
||||
return block, total, ausgefuehrt
|
||||
|
||||
|
||||
def build_eu_prompt(prompt: str, results_block: str = "") -> str:
|
||||
"""Haengt den Werkzeug-Hinweis und einen etwaigen Belegblock an."""
|
||||
hint = _EU_TOOL_HINT.format(
|
||||
search_note=_SEARCH_NOTE_WITH_RESULTS if results_block else _SEARCH_NOTE_NO_RESULTS
|
||||
)
|
||||
return prompt + hint + results_block
|
||||
|
||||
|
||||
async def eu_call_with_search(
|
||||
prompt: str,
|
||||
*,
|
||||
title: str = "",
|
||||
search_items: list[str] | None = None,
|
||||
output_language: str = "Deutsch",
|
||||
max_queries: int = 3,
|
||||
max_queries: int = EU_VERIFY_MAX_QUERIES,
|
||||
) -> 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
|
||||
Mit search_items läuft vorab die gezielte staan-Belegsuche, 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,
|
||||
results_block, such_usage, _ = await plan_verification_searches(
|
||||
title=title, search_items=search_items,
|
||||
output_language=output_language, max_queries=max_queries,
|
||||
)
|
||||
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)
|
||||
_add_usage(total, such_usage)
|
||||
|
||||
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)
|
||||
result, usage = await call_claude(build_eu_prompt(prompt, results_block), tools=None)
|
||||
_add_usage(total, usage)
|
||||
return result, total
|
||||
|
||||
@@ -330,6 +330,38 @@ STATUS_PRIORITY = {
|
||||
MAX_FACTS_PER_VERIFY_GROUP = 8 # Max Fakten pro Verifikationsgruppe
|
||||
TWOPHASE_MIN_FACTS = 25 # Ab dieser Anzahl bestehender Fakten wird der Zwei-Phasen-Ansatz genutzt
|
||||
|
||||
# Status, die als "noch nicht ausreichend belegt" gelten und im EU-Modus eine
|
||||
# Nachhak-Runde ausloesen koennen.
|
||||
OFFENE_STATUS = ("unconfirmed", "unverified", "developing")
|
||||
|
||||
EU_FOLLOWUP_PROMPT_TEMPLATE = """Du bist ein OSINT-Faktenchecker.
|
||||
AUSGABESPRACHE: {output_language}
|
||||
- KEINE Gedankenstriche verwenden, stattdessen Kommas oder neue Saetze.
|
||||
- Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß), NIEMALS Umschreibungen.
|
||||
|
||||
LAGE: {title}
|
||||
|
||||
Die folgenden Behauptungen konnten im ersten Durchgang NICHT ausreichend belegt werden.
|
||||
Fuer genau diese Punkte wurden zusaetzliche Suchergebnisse beschafft, sie stehen unten.
|
||||
|
||||
OFFENE BEHAUPTUNGEN:
|
||||
{open_claims_text}
|
||||
|
||||
AUFGABE:
|
||||
Pruefe jede dieser Behauptungen erneut, ausschliesslich gegen die unten angehaengten
|
||||
Suchergebnisse.
|
||||
|
||||
REGELN:
|
||||
- Stufe eine Behauptung nur dann hoch, wenn die neuen Belege sie tatsaechlich stuetzen.
|
||||
- Widersprechen die Belege der Behauptung, setze den Status auf "contradicted".
|
||||
- Findest du keinen tragfaehigen Beleg, lass den Status unveraendert.
|
||||
- Nenne in "evidence" die stuetzende Quelle mit ihrer [S..]-Nummer UND ihrer vollstaendigen
|
||||
URL, damit der Beleg nachpruefbar bleibt, und fasse kurz zusammen, was sie belegt.
|
||||
- Erfinde nichts und uebernimm nichts aus deinem Gedaechtnis.
|
||||
|
||||
Antworte NUR mit einem JSON-Array, ein Objekt je oben genannter Behauptung:
|
||||
[{{"claim": "Behauptung im Wortlaut wie oben", "status": "{status_werte}", "evidence": "Begruendung mit [S..]"}}]"""
|
||||
|
||||
|
||||
def normalize_claim(claim: str) -> str:
|
||||
"""Normalisiert einen Claim fuer Aehnlichkeitsvergleich."""
|
||||
@@ -480,6 +512,157 @@ class FactCheckerAgent:
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _ist_eu_modus() -> bool:
|
||||
from agents.claude_client import _ai_backend_var
|
||||
from config import AI_BACKEND
|
||||
return (_ai_backend_var.get(None) or AI_BACKEND) == "bedrock"
|
||||
|
||||
async def _eu_pruefen(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
title: str,
|
||||
articles: list[dict],
|
||||
output_language: str,
|
||||
search_items: list[str],
|
||||
incident_type: str = "adhoc",
|
||||
max_queries: int | None = None,
|
||||
followup_queries: int | None = None,
|
||||
) -> tuple[list[dict], ClaudeUsage]:
|
||||
"""Faktencheck im EU-Modus, mit gezielter Belegsuche und Nachhak-Runde.
|
||||
|
||||
Ablauf. Erstens werden zu den uebergebenen Punkten gezielt Belege
|
||||
gesucht und in den Auftrag gelegt. Zweitens laeuft der eigentliche
|
||||
Faktencheck. Drittens, falls danach noch Behauptungen unbelegt sind,
|
||||
wird fuer genau diese noch einmal gesucht und nur sie werden erneut
|
||||
bewertet. Das bildet das Nachhaken nach, das der heutige Weg mit seiner
|
||||
eingebauten Websuche von sich aus macht.
|
||||
"""
|
||||
from agents.claude_client import call_claude
|
||||
from agents.eu_researcher import (
|
||||
plan_verification_searches, build_eu_prompt, _add_usage,
|
||||
)
|
||||
from config import (
|
||||
EU_VERIFY_MAX_QUERIES, EU_VERIFY_FOLLOWUP_QUERIES, EU_VERIFY_FOLLOWUP_MIN_OPEN,
|
||||
)
|
||||
|
||||
gesamt = ClaudeUsage()
|
||||
anzahl_anfragen = EU_VERIFY_MAX_QUERIES if max_queries is None else max_queries
|
||||
anzahl_nachhak = EU_VERIFY_FOLLOWUP_QUERIES if followup_queries is None else followup_queries
|
||||
|
||||
block, such_usage, gestellte_anfragen = await plan_verification_searches(
|
||||
title=title, search_items=search_items,
|
||||
output_language=output_language, max_queries=anzahl_anfragen,
|
||||
)
|
||||
_add_usage(gesamt, such_usage)
|
||||
|
||||
result, usage = await call_claude(build_eu_prompt(prompt, block), tools=None)
|
||||
_add_usage(gesamt, usage)
|
||||
facts = self._parse_response(result, articles=articles)
|
||||
|
||||
offene = [f for f in facts if f.get("status") in OFFENE_STATUS]
|
||||
if anzahl_nachhak <= 0 or len(offene) < EU_VERIFY_FOLLOWUP_MIN_OPEN:
|
||||
return facts, gesamt
|
||||
|
||||
logger.info("EU-Faktencheck: %d von %d Behauptungen offen, starte Nachhak-Runde", len(offene), len(facts))
|
||||
facts, nachhak_usage = await self._eu_nachhaken(
|
||||
facts=facts, offene=offene, title=title,
|
||||
output_language=output_language, incident_type=incident_type,
|
||||
avoid_queries=gestellte_anfragen, followup_queries=anzahl_nachhak,
|
||||
)
|
||||
_add_usage(gesamt, nachhak_usage)
|
||||
return facts, gesamt
|
||||
|
||||
async def _eu_nachhaken(
|
||||
self,
|
||||
*,
|
||||
facts: list[dict],
|
||||
offene: list[dict],
|
||||
title: str,
|
||||
output_language: str,
|
||||
incident_type: str,
|
||||
avoid_queries: list[str],
|
||||
followup_queries: int | None = None,
|
||||
) -> tuple[list[dict], ClaudeUsage]:
|
||||
"""Sucht gezielt Belege fuer die offenen Behauptungen und bewertet nur
|
||||
diese erneut. Gibt die zusammengefuehrte Faktenliste zurueck."""
|
||||
from agents.claude_client import call_claude
|
||||
from agents.eu_researcher import (
|
||||
plan_verification_searches, build_eu_prompt, _add_usage,
|
||||
)
|
||||
from config import EU_VERIFY_FOLLOWUP_QUERIES
|
||||
from json_utils import extract_json_array
|
||||
|
||||
gesamt = ClaudeUsage()
|
||||
kandidaten = offene[:MAX_FACTS_PER_VERIFY_GROUP]
|
||||
claims = [f.get("claim", "") for f in kandidaten if f.get("claim")]
|
||||
if not claims:
|
||||
return facts, gesamt
|
||||
|
||||
block, such_usage, _ = await plan_verification_searches(
|
||||
title=title, search_items=claims, output_language=output_language,
|
||||
max_queries=(EU_VERIFY_FOLLOWUP_QUERIES if followup_queries is None else followup_queries),
|
||||
avoid_queries=avoid_queries, label="Nachhak-Suche",
|
||||
)
|
||||
_add_usage(gesamt, such_usage)
|
||||
if not block:
|
||||
logger.info("EU-Nachhak-Runde: keine zusaetzlichen Belege gefunden, Fakten bleiben unveraendert")
|
||||
return facts, gesamt
|
||||
|
||||
status_werte = ("established|unverified|disputed|developing"
|
||||
if incident_type == "research"
|
||||
else "confirmed|unconfirmed|contradicted|developing")
|
||||
prompt = EU_FOLLOWUP_PROMPT_TEMPLATE.format(
|
||||
output_language=output_language,
|
||||
title=title,
|
||||
open_claims_text="\n".join(f"- {c}" for c in claims),
|
||||
status_werte=status_werte,
|
||||
)
|
||||
|
||||
try:
|
||||
result, usage = await call_claude(build_eu_prompt(prompt, block), tools=None)
|
||||
_add_usage(gesamt, usage)
|
||||
except TimeoutError:
|
||||
logger.warning("EU-Nachhak-Runde: Timeout, Fakten bleiben unveraendert")
|
||||
return facts, gesamt
|
||||
except Exception as e:
|
||||
logger.warning("EU-Nachhak-Runde fehlgeschlagen (%s), Fakten bleiben unveraendert", e)
|
||||
return facts, gesamt
|
||||
|
||||
aktualisierungen = extract_json_array(result or "")
|
||||
if not isinstance(aktualisierungen, list):
|
||||
logger.warning("EU-Nachhak-Runde: Antwort nicht auswertbar, Fakten bleiben unveraendert")
|
||||
return facts, gesamt
|
||||
|
||||
hochgestuft = 0
|
||||
for eintrag in aktualisierungen:
|
||||
if not isinstance(eintrag, dict):
|
||||
continue
|
||||
claim = str(eintrag.get("claim", "")).strip()
|
||||
neuer_status = str(eintrag.get("status", "")).strip().lower()
|
||||
if not claim or neuer_status not in STATUS_PRIORITY:
|
||||
continue
|
||||
treffer = find_matching_claim(claim, kandidaten)
|
||||
if not treffer:
|
||||
continue
|
||||
alt = treffer.get("status", "developing")
|
||||
if STATUS_PRIORITY.get(neuer_status, 0) <= STATUS_PRIORITY.get(alt, 0):
|
||||
continue
|
||||
treffer["status"] = neuer_status
|
||||
belege = str(eintrag.get("evidence", "")).strip()
|
||||
if belege:
|
||||
bisher = (treffer.get("evidence") or "").strip()
|
||||
treffer["evidence"] = f"{bisher} Nachrecherche: {belege}".strip() if bisher else belege
|
||||
hochgestuft += 1
|
||||
logger.info("EU-Nachhak-Runde: '%s...' %s -> %s", claim[:60], alt, neuer_status)
|
||||
|
||||
logger.info(
|
||||
"EU-Nachhak-Runde abgeschlossen: %d von %d offenen Behauptungen belegt",
|
||||
hochgestuft, len(kandidaten),
|
||||
)
|
||||
return facts, gesamt
|
||||
|
||||
async def check(self, title: str, articles: list[dict], incident_type: str = "adhoc", output_language: str = "Deutsch") -> tuple[list[dict], ClaudeUsage | None]:
|
||||
"""Führt vollständigen Faktencheck durch (erster Refresh)."""
|
||||
if not articles:
|
||||
@@ -495,19 +678,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,
|
||||
# EU-Modus: gezielte staan-Belegsuche plus Nachhak-Runde statt WebSearch.
|
||||
if self._ist_eu_modus():
|
||||
headlines = [a.get("headline_de") or a.get("headline", "") for a in articles[:10]]
|
||||
facts, usage = await self._eu_pruefen(
|
||||
prompt, title=title, articles=articles,
|
||||
output_language=output_language, incident_type=incident_type,
|
||||
search_items=[title] + headlines,
|
||||
)
|
||||
else:
|
||||
result, usage = await call_claude(prompt)
|
||||
facts = self._parse_response(result, articles=articles)
|
||||
facts = self._parse_response(result, articles=articles)
|
||||
logger.info(f"Faktencheck: {len(facts)} Fakten geprüft")
|
||||
return facts, usage
|
||||
except TimeoutError:
|
||||
@@ -548,23 +729,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
|
||||
# EU-Modus: gezielte Belegsuche zu offenen Fakten plus Nachhak-Runde.
|
||||
if self._ist_eu_modus():
|
||||
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,
|
||||
headlines = [a.get("headline_de") or a.get("headline", "") for a in new_articles[:6]]
|
||||
facts, usage = await self._eu_pruefen(
|
||||
prompt, title=title, articles=new_articles,
|
||||
output_language=output_language, incident_type=incident_type,
|
||||
search_items=open_claims + headlines,
|
||||
)
|
||||
else:
|
||||
result, usage = await call_claude(prompt)
|
||||
facts = self._parse_response(result, articles=new_articles)
|
||||
facts = self._parse_response(result, articles=new_articles)
|
||||
logger.info(f"Inkrementeller Faktencheck: {len(facts)} Fakten (neu + aktualisiert)")
|
||||
return facts, usage
|
||||
except TimeoutError:
|
||||
@@ -671,19 +850,24 @@ 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
|
||||
# EU-Modus: gezielte Belegsuche je Faktengruppe plus Nachhak-Runde.
|
||||
if self._ist_eu_modus():
|
||||
from config import (
|
||||
EU_VERIFY_GROUP_QUERIES, EU_VERIFY_GROUP_FOLLOWUP_QUERIES,
|
||||
)
|
||||
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,
|
||||
# Kleinere Suchmenge je Gruppe, da mehrere Gruppen parallel
|
||||
# laufen und jede nur ein Teilthema abdeckt.
|
||||
facts, v_usage = await self._eu_pruefen(
|
||||
prompt, title=f"{title} ({theme})", articles=None,
|
||||
output_language=output_language, incident_type=incident_type,
|
||||
search_items=claims[:8],
|
||||
max_queries=EU_VERIFY_GROUP_QUERIES,
|
||||
followup_queries=EU_VERIFY_GROUP_FOLLOWUP_QUERIES,
|
||||
)
|
||||
else:
|
||||
result, v_usage = await call_claude(prompt)
|
||||
facts = self._parse_response(result)
|
||||
facts = self._parse_response(result)
|
||||
logger.info(f"Gruppe '{theme}': {len(facts)} Fakten geprüft")
|
||||
return facts, v_usage
|
||||
except TimeoutError:
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren