diff --git a/CLAUDE.md b/CLAUDE.md index 9d54f0f..9c5ec8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -93,7 +93,7 @@ src/: stage_runners.py: "Studio-Bausteine: analyze/factcheck einzeln auf vorhandenem Datenbestand, bewusst NEBEN _run_refresh gebaut" researcher.py: "WebSearch-Recherche (Standard + 4-Phasen-Tiefenrecherche), Feed-/Kanal-/Account-Selektion, Keyword-Extraktion, Google-News-Suchfeeds je Sprache" analyzer.py: "Analyse-Agent (Lagebild/Briefing, Erst- + inkrementell, Inline-Zitate, Topic-Filter, Neueste Entwicklungen, Stimmungsbild)" - factchecker.py: "Faktencheck (Erst/Inkrementell/Zwei-Phasen mit Haiku-Triage ab 25 Fakten), Claim-Matching, Dedup, Schutz gegen Massen-Downgrades" + factchecker.py: "Faktencheck (Erst/Inkrementell/Zwei-Phasen mit Haiku-Triage ab 25 Fakten), Claim-Matching, Dedup, Schutz gegen Massen-Downgrades. Belegtiefe: zaehle_medienhaeuser zaehlt unabhaengige Verlage statt Fundstellen, pruefe_belegtiefe stuft Fakten ab, deren Status mehr Beleg behauptet als vorliegt (confirmed 2, established 3, Primaerbeleg 1), bewertungsregeln() haengt den verbindlichen Regelblock an jeden Auftrag" geoparsing.py: "Haiku-basierte Ortsextraktion, Geocoding offline via geonamescache" entity_extractor.py: "Netzwerkanalyse (Sonnet). ACHTUNG: aktuell ohne Aufrufer in der App, nur regenerate_relations.py im Repo-Root nutzt es" translator.py: "Haiku-Uebersetzung in Batches a 5 (groessere Batches rissen den JSON-Output ab)" @@ -164,6 +164,7 @@ tests/: test_eu_belegsuche.py: "Gezielte Belegsuche ueber staan, Planung, Grenzen, Entdopplung, Ausfallverhalten" test_qc_und_runden.py: "Absicherung der Duplikatpruefung und Abbruch der Suchrunden bei erreichter Treffergrenze" test_mehrfachantwort.py: "Antworten, in denen sich das Modell selbst korrigiert und mehrere Fassungen liefert, die ausfuehrlichste muss gewinnen" + test_faktencheck_belegtiefe.py: "Belegzaehlung nach Medienhaus, Nachbedingung fuer bestaetigte Fakten, Sperre gegen das Hochstufen von developing, getrennte Bezeichnungen fuer Widerspruch und Widerlegung" test_quellenausgabe.py: "Ausgabetreue des Quellenverzeichnisses: Nummern aus dem Lagebild statt Zeilennummern, keine Kappung, ein Verlag ein Name, Statistik deckungsgleich mit dem Verzeichnis" ``` @@ -175,13 +176,13 @@ incident_typen: label: "Live-Monitoring" quellen: "RSS + WebSearch + optional Telegram" analyse: "Fliesstext-Lagebild" - faktencheck_status: "confirmed/unconfirmed/contradicted/developing" + faktencheck_status: "confirmed/unconfirmed/contradicted/false/developing (contradicted = Belege widersprechen einander, false = nachweislich widerlegt)" refresh: "Manuell oder automatisch (Intervall konfigurierbar)" research: label: "Recherche" quellen: "Nur WebSearch 4-Phasen-Tiefenrecherche (kein RSS)" analyse: "Strukturiertes Briefing (Ueberblick, Hintergrund, Akteure, Lage, Einschaetzung, Quellenqualitaet)" - faktencheck_status: "established/unverified/disputed/developing" + faktencheck_status: "established/unverified/disputed/false/developing" refresh: "Immer manuell, erster Refresh automatisch 3 Durchlaeufe (Multi-Pass)" multi_pass: durchlaeufe: 3 diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 1f89f18..22ec294 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -279,6 +279,8 @@ _FACT_STATUS_GROUPS = [ {"confirmed", "established"}), ("Umstritten (Quellen widersprechen sich oder Faktencheck hat Widersprüche dokumentiert):", {"contradicted", "disputed"}), + ("Widerlegt (ein Beleg entkräftet die Aussage, sie darf im Lagebild nicht als Tatsache stehen):", + {"false"}), ("Unbestätigt (nur eine einzelne Quelle, eine unabhängige Bestätigung steht aus):", {"unconfirmed", "unverified"}), ("In Entwicklung (laufender Sachverhalt, Stand offen):", @@ -287,7 +289,7 @@ _FACT_STATUS_GROUPS = [ _FACT_STATUS_PRIORITY = { "confirmed": 5, "established": 5, - "contradicted": 4, "disputed": 4, + "contradicted": 4, "disputed": 4, "false": 4, "unconfirmed": 3, "unverified": 3, "developing": 1, } diff --git a/src/agents/factchecker.py b/src/agents/factchecker.py index 585dbc4..e848cd2 100644 --- a/src/agents/factchecker.py +++ b/src/agents/factchecker.py @@ -32,16 +32,18 @@ AUFTRAG: 1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Meldungen 2. Prüfe jeden Claim aktiv per WebSearch gegen mindestens eine weitere unabhängige Quelle 3. Kategorisiere jede Aussage: - - "confirmed": Durch 2+ unabhängige seriöse Quellen mit überprüfbarer URL bestätigt - - "unconfirmed": Nur 1 Quelle oder nicht unabhängig verifizierbar - - "contradicted": Widersprüchliche Informationen aus verschiedenen Quellen + - "confirmed": Mindestens 2 unabhängige Medienhäuser mit überprüfbarer URL bestätigen die Aussage + - "unconfirmed": Nur ein Medienhaus, oder nicht unabhängig verifizierbar + - "contradicted": Die Belege widersprechen EINANDER in der Sache, keine Seite ist entschieden + - "false": Ein Beleg widerlegt die Behauptung nachweislich - "developing": Situation noch unklar, entwickelt sich 4. Markiere WICHTIGE NEUE Entwicklungen mit is_notification: true Antworte AUSSCHLIESSLICH als JSON-Array. Jedes Element hat: - "claim": Die Faktenaussage auf {output_language} -- "status": "confirmed" | "unconfirmed" | "contradicted" | "developing" -- "sources_count": Anzahl unabhängiger Quellen mit überprüfbarer URL +- "status": "confirmed" | "unconfirmed" | "contradicted" | "false" | "developing" +- "sources_count": Anzahl unabhängiger Medienhäuser mit überprüfbarer URL +- "evidence_type": "primary" | "secondary" | "agency" (siehe Bewertungsregeln) - "evidence": Begründung MIT konkreten Quellen-URLs als Beleg (z.B. "Bestätigt durch: tagesschau.de (URL), Reuters (URL)") - "is_notification": true/false (nur bei wichtigen Entwicklungen true) @@ -72,16 +74,18 @@ Fokus: "Was sind die gesicherten Fakten zu diesem Thema?" 1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Quellen 2. Prüfe jeden Claim aktiv per WebSearch gegen weitere unabhängige Quellen 3. Kategorisiere jede Aussage: - - "established": Breit dokumentierter, gesicherter Fakt (3+ unabhängige Quellen mit URL) - - "disputed": Umstrittener Sachverhalt, verschiedene Positionen dokumentiert + - "established": Breit dokumentierter Fakt, 3+ unabhängige Medienhäuser mit URL + - "disputed": Die Belege widersprechen EINANDER, verschiedene Positionen dokumentiert + - "false": Ein Beleg widerlegt die Behauptung nachweislich - "unverified": Einzelbehauptung, nicht unabhängig verifizierbar - "developing": Aktuelle Entwicklung, Faktenlage noch im Fluss 4. Markiere WICHTIGE Erkenntnisse mit is_notification: true Antworte AUSSCHLIESSLICH als JSON-Array. Jedes Element hat: - "claim": Die Faktenaussage auf {output_language} -- "status": "established" | "disputed" | "unverified" | "developing" -- "sources_count": Anzahl unabhängiger Quellen mit überprüfbarer URL +- "status": "established" | "disputed" | "false" | "unverified" | "developing" +- "sources_count": Anzahl unabhängiger Medienhäuser mit überprüfbarer URL +- "evidence_type": "primary" | "secondary" | "agency" (siehe Bewertungsregeln) - "evidence": Begründung MIT konkreten Quellen-URLs als Beleg - "is_notification": true/false @@ -116,16 +120,18 @@ AUFTRAG: 5. Markiere wichtige Statusänderungen und neue Entwicklungen mit is_notification: true Status-Kategorien: -- "confirmed": 2+ unabhängige seriöse Quellen mit URL -- "unconfirmed": Nur 1 Quelle -- "contradicted": Widersprüchliche Informationen +- "confirmed": 2+ unabhängige Medienhäuser mit URL +- "unconfirmed": Nur ein Medienhaus +- "contradicted": Die Belege widersprechen einander +- "false": Ein Beleg widerlegt die Behauptung nachweislich - "developing": Situation unklar Antworte AUSSCHLIESSLICH als JSON-Array mit ALLEN Fakten (bestehende aktualisiert + neue). Jedes Element hat: - "claim": Die Faktenaussage auf {output_language} -- "status": "confirmed" | "unconfirmed" | "contradicted" | "developing" -- "sources_count": Anzahl unabhängiger Quellen +- "status": "confirmed" | "unconfirmed" | "contradicted" | "false" | "developing" +- "sources_count": Anzahl unabhängiger Medienhäuser +- "evidence_type": "primary" | "secondary" | "agency" - "evidence": Begründung MIT konkreten Quellen-URLs - "is_notification": true/false @@ -157,16 +163,18 @@ AUFTRAG: 4. Prüfe neue Claims per WebSearch Status-Kategorien: -- "established": 3+ unabhängige Quellen mit URL -- "disputed": Verschiedene Positionen dokumentiert +- "established": 3+ unabhängige Medienhäuser mit URL +- "disputed": Die Belege widersprechen einander, verschiedene Positionen dokumentiert +- "false": Ein Beleg widerlegt die Behauptung nachweislich - "unverified": Nicht unabhängig verifizierbar - "developing": Faktenlage im Fluss Antworte AUSSCHLIESSLICH als JSON-Array mit ALLEN Fakten (bestehende aktualisiert + neue). Jedes Element hat: - "claim": Die Faktenaussage auf {output_language} -- "status": "established" | "disputed" | "unverified" | "developing" -- "sources_count": Anzahl unabhängiger Quellen +- "status": "established" | "disputed" | "false" | "unverified" | "developing" +- "sources_count": Anzahl unabhängiger Medienhäuser +- "evidence_type": "primary" | "secondary" | "agency" - "evidence": Begründung MIT konkreten Quellen-URLs - "is_notification": true/false @@ -237,14 +245,15 @@ ANWEISUNGEN: Für JEDE Faktenaussage (bestehend UND neu): 1. Suche per WebSearch nach unabhängigen Bestätigungen oder Widerlegungen 2. Bewerte den Status: - - "confirmed": Mindestens 2 unabhängige Quellen mit verifizierter URL bestätigen + - "confirmed": Mindestens 2 unabhängige Medienhäuser mit verifizierter URL bestätigen - "unconfirmed": Nicht genug unabhängige Bestätigung - - "contradicted": Glaubwürdige Quellen widersprechen + - "contradicted": Die Belege widersprechen einander in der Sache + - "false": Ein Beleg widerlegt die Behauptung nachweislich - "developing": Lage noch im Fluss 3. Dokumentiere die Evidenz mit konkreten, verifizierten URLs QUALITÄTSREGELN: -- "confirmed" NUR bei mindestens 2 unabhängigen Quellen mit ECHTER URL +- "confirmed" NUR bei mindestens 2 unabhängigen Medienhäusern mit ECHTER URL - KEINE Halluzinationen — nur tatsächlich per WebSearch gefundene Quellen - Bestehende Evidenz BEIBEHALTEN, nur neue ergänzen - Jeder Claim MUSS eine konkrete Quellen-URL als Beleg enthalten @@ -254,8 +263,9 @@ Antworte AUSSCHLIESSLICH als JSON-Array: {{{{ "id": 123, "claim": "Die Faktenaussage...", - "status": "confirmed|unconfirmed|contradicted|developing", + "status": "confirmed|unconfirmed|contradicted|false|developing", "sources_count": 3, + "evidence_type": "primary|secondary|agency", "evidence": "Bestätigt durch: Quelle1 (URL)\\nQuelle2 (URL)", "is_notification": false }}}} @@ -283,14 +293,15 @@ ANWEISUNGEN: Für JEDE Faktenaussage (bestehend UND neu): 1. Suche per WebSearch nach unabhängigen Bestätigungen oder Widerlegungen 2. Bewerte den Status: - - "established": 3+ unabhängige Quellen mit verifizierter URL + - "established": 3+ unabhängige Medienhäuser mit verifizierter URL - "unverified": Nicht unabhängig verifizierbar - - "disputed": Verschiedene Positionen dokumentiert + - "disputed": Die Belege widersprechen einander, verschiedene Positionen dokumentiert + - "false": Ein Beleg widerlegt die Behauptung nachweislich - "developing": Faktenlage im Fluss 3. Dokumentiere die Evidenz mit konkreten, verifizierten URLs QUALITÄTSREGELN: -- "established" NUR bei mindestens 3 unabhängigen Quellen mit ECHTER URL +- "established" NUR bei mindestens 3 unabhängigen Medienhäusern mit ECHTER URL - KEINE Halluzinationen — nur tatsächlich per WebSearch gefundene Quellen - Bestehende Evidenz BEIBEHALTEN, nur neue ergänzen @@ -299,8 +310,9 @@ Antworte AUSSCHLIESSLICH als JSON-Array: {{{{ "id": 123, "claim": "Die Faktenaussage...", - "status": "established|unverified|disputed|developing", + "status": "established|unverified|disputed|false|developing", "sources_count": 3, + "evidence_type": "primary|secondary|agency", "evidence": "Bestätigt durch: Quelle1 (URL)\\nQuelle2 (URL)", "is_notification": false }}}} @@ -321,11 +333,62 @@ _STOPWORDS = frozenset({ STATUS_PRIORITY = { "confirmed": 5, "established": 5, - "contradicted": 4, "disputed": 4, + "contradicted": 4, "disputed": 4, "false": 4, "unconfirmed": 3, "unverified": 3, "developing": 1, } +# Status, die eine belegte Bestaetigung behaupten und deshalb an der Zahl +# unabhaengiger Medienhaeuser gemessen werden. +BESTAETIGT_STATUS = {"confirmed": 2, "established": 3} + +# Ein Primaerbeleg (Originalzitat im Interview, amtliche Mitteilung, Urteil, +# Pressemitteilung der handelnden Stelle) traegt allein. Mehrfache +# Berichterstattung ueber denselben O-Ton ist keine zusaetzliche Verifikation, +# und ihr Fehlen ist kein Mangel. Ohne diese Ausnahme wurde im Vergleich vom +# 01.08.2026 ein ZDF-Interview als "unbestaetigt" gefuehrt, obwohl der +# Betroffene die Aussage dort selbst getroffen hat. +PRIMAERBELEG_MINDESTZAHL = 1 + +_BEWERTUNGSREGELN = """ + +BEWERTUNGSREGELN (verbindlich, sie gehen allen Angaben oben vor): +- Unabhängig heißt: verschiedene Medienhäuser. Mehrere Artikel derselben + Redaktion, Videofassungen derselben Meldung und wortgleiche Übernahmen + derselben Agenturmeldung zählen zusammen als EINE Quelle. +- Weiterleitungen über Sammelportale (news.google.com und ähnliche) sind kein + eigenständiger Beleg. Zähle das Zielmedium, wenn es erkennbar ist. +- "sources_count" ist die Zahl unabhängiger MEDIENHÄUSER, nicht die Zahl der + Fundstellen oder Adressen. +- "evidence_type" beschreibt den besten vorliegenden Beleg: + "primary" = die handelnde oder sprechende Stelle selbst (Originalzitat im + Interview, amtliche Mitteilung, Urteil, Pressemitteilung), + "secondary" = unabhängige eigene Berichterstattung eines Mediums, + "agency" = Agenturmeldung und ihre Übernahmen. + Bei "primary" genügt diese eine Quelle für eine Bestätigung. +- Unterschiedliche Zahlenangaben verschiedener Erhebungsstellen sind KEIN + Widerspruch. Nenne die Spanne mit Zuordnung ("50.000 laut Innenministerium, + 60.000 laut Regionalregierung") und bewerte die Aussage nach ihrer + Belegtiefe. Ein Widerspruchs-Status ist nur richtig, wenn die Belege + einander in der Sache widersprechen. +- Der Status für eine nachweisliche Widerlegung ist ausschließlich "false" und + setzt einen Beleg voraus, der die Behauptung entkräftet. Eine fehlende + Bestätigung ist keine Widerlegung. +- Prüfe nur Tatsachenbehauptungen. Aussagen über den Kenntnisstand ("Der + Auslöser ist unklar", "Eine unabhängige Bestätigung steht aus", "Die Lage + bleibt unübersichtlich") sind nicht prüfbar und gehören NICHT in den + Faktencheck. Formuliere stattdessen die prüfbare Tatsache dahinter oder lass + den Punkt weg.""" + + +def bewertungsregeln() -> str: + """Verbindlicher Regelblock, wird an jeden Faktencheck-Auftrag angehaengt. + + Bewusst am Ende des Auftrags und an einer einzigen Stelle gepflegt, damit + die sechs Vorlagen nicht auseinanderlaufen. + """ + return _BEWERTUNGSREGELN + # Zwei-Phasen-Faktencheck: Konfiguration 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 @@ -363,6 +426,66 @@ 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..]"}}]""" +_URL_RE = re.compile(r'https?://[^\s\)\]\},;"\'<>]+') + + +def zaehle_medienhaeuser(urls: list[str]) -> int: + """Zaehlt unabhaengige Medienhaeuser statt Fundstellen. + + Sechs France24-Treffer, davon vier Videofassungen derselben Meldung, sind + ein Haus und kein sechsfacher Beleg. Gezaehlt wird deshalb nach + registrierbarer Domain. Weiterleitungsportale bekommen keine gemeinsame + Identitaet, weil hinter news.google.com beliebige Zeitungen stecken + koennen: dort zaehlt jede Adresse einzeln. + """ + from services import media_registry + + haeuser: set[str] = set() + for url in urls or []: + sauber = (url or "").rstrip(".,;:)]}\"'") + if not sauber: + continue + domain = media_registry.registrable_domain(sauber) + if not domain: + continue + haeuser.add(sauber.lower() if media_registry.ist_aggregator(sauber) else domain) + return len(haeuser) + + +def pruefe_belegtiefe(fact: dict) -> dict: + """Stuft einen Fakt ab, wenn sein Status mehr Beleg behauptet als vorliegt. + + Ohne diese Nachbedingung stand im Bericht vom 01.08.2026 eine Behauptung + als BESTAETIGT mit einer einzigen Quelle, deren Evidenz woertlich mit "Nur + durch Al Jazeera berichtet" begann. Ursache war, dass die Nachhak-Runde den + Status hochsetzte, ohne die Belegzahl neu zu bestimmen. + + Ein Primaerbeleg (evidence_type 'primary') traegt allein, siehe + PRIMAERBELEG_MINDESTZAHL. + """ + status = (fact.get("status") or "").strip().lower() + noetig = BESTAETIGT_STATUS.get(status) + if not noetig: + return fact + + if (fact.get("evidence_type") or "").strip().lower() == "primary": + noetig = PRIMAERBELEG_MINDESTZAHL + + vorhanden = fact.get("sources_count") + if not isinstance(vorhanden, int): + vorhanden = zaehle_medienhaeuser(_URL_RE.findall(fact.get("evidence") or "")) + fact["sources_count"] = vorhanden + + if vorhanden < noetig: + neuer = "unconfirmed" if status == "confirmed" else "unverified" + logger.warning( + "Fakt herabgestuft (%s -> %s): %d unabhaengige Quelle(n), noetig waeren %d: '%s...'", + status, neuer, vorhanden, noetig, (fact.get("claim") or "")[:60], + ) + fact["status"] = neuer + return fact + + def normalize_claim(claim: str) -> str: """Normalisiert einen Claim fuer Aehnlichkeitsvergleich.""" c = claim.lower().strip() @@ -614,15 +737,15 @@ class FactCheckerAgent: logger.info("EU-Nachhak-Runde: keine zusaetzlichen Belege gefunden, Fakten bleiben unveraendert") return facts, gesamt - status_werte = ("established|unverified|disputed|developing" + status_werte = ("established|unverified|disputed|false|developing" if incident_type == "research" - else "confirmed|unconfirmed|contradicted|developing") + else "confirmed|unconfirmed|contradicted|false|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, - ) + ) + bewertungsregeln() try: result, usage = await call_claude(build_eu_prompt(prompt, block), tools=None) @@ -656,11 +779,36 @@ class FactCheckerAgent: alt = treffer.get("status", "developing") if STATUS_PRIORITY.get(neuer_status, 0) <= STATUS_PRIORITY.get(alt, 0): continue + # "developing" heisst: der Sachverhalt selbst ist noch offen. Das + # ist keine schwache Beleglage, die sich nachrecherchieren laesst, + # und wird deshalb nicht hochgestuft. Ohne diese Sperre wurde am + # 01.08.2026 die Behauptung "Der Ausloeser ist weiterhin unklar" + # auf BESTAETIGT gesetzt, weil mehrere Quellen die Unklarheit + # beschrieben. Eine Unklarheit laesst sich nicht bestaetigen. + if alt == "developing" and neuer_status not in ("contradicted", "disputed", "false"): + logger.info( + "EU-Nachhak-Runde: '%s...' bleibt developing, %s nicht zulaessig", + claim[:60], neuer_status, + ) + 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 + # Die Belegzahl folgt der neuen Evidenz, nicht dem alten Stand. + treffer["sources_count"] = zaehle_medienhaeuser( + _URL_RE.findall(treffer.get("evidence") or "") + ) + if eintrag.get("evidence_type"): + treffer["evidence_type"] = eintrag.get("evidence_type") + pruefe_belegtiefe(treffer) + if treffer["status"] != neuer_status: + logger.info( + "EU-Nachhak-Runde: '%s...' Hochstufung auf %s nicht gedeckt, bleibt %s", + claim[:60], neuer_status, treffer["status"], + ) + continue hochgestuft += 1 logger.info("EU-Nachhak-Runde: '%s...' %s -> %s", claim[:60], alt, neuer_status) @@ -682,7 +830,7 @@ class FactCheckerAgent: title=title, articles_text=articles_text, output_language=output_language, - ) + ) + bewertungsregeln() try: # EU-Modus: gezielte staan-Belegsuche plus Nachhak-Runde statt WebSearch. @@ -733,7 +881,7 @@ class FactCheckerAgent: articles_text=articles_text, existing_facts_text=existing_facts_text, output_language=output_language, - ) + ) + bewertungsregeln() try: # EU-Modus: gezielte Belegsuche zu offenen Fakten plus Nachhak-Runde. @@ -854,7 +1002,7 @@ class FactCheckerAgent: facts_text=facts_text, new_claims_text=new_claims_text, articles_text=articles_text, - ) + ) + bewertungsregeln() try: # EU-Modus: gezielte Belegsuche je Faktengruppe plus Nachhak-Runde. @@ -965,9 +1113,11 @@ class FactCheckerAgent: """Validiert Fakten und ordnet Quellen-URLs aus den Artikeln zu. Stellt sicher, dass jeder confirmed/established Fakt URLs in der - evidence hat, damit das Frontend die Quellen korrekt anzeigen kann. + evidence hat, damit das Frontend die Quellen korrekt anzeigen kann, + zaehlt die Belege nach unabhaengigen Medienhaeusern und stuft Fakten + ab, deren Status mehr Belegtiefe behauptet als vorhanden ist. """ - url_pattern = re.compile(r'https?://') + url_pattern = _URL_RE # Verfuegbare Artikel-URLs sammeln (dedupliziert nach URL) article_sources = [] seen_urls = set() @@ -1025,8 +1175,9 @@ class FactCheckerAgent: ) # sources_count aktualisieren - all_urls = url_pattern.findall(fact["evidence"]) - fact["sources_count"] = len(set(all_urls)) + fact["sources_count"] = zaehle_medienhaeuser( + url_pattern.findall(fact["evidence"]) + ) logger.info( f"Fakt '{fact.get('claim', '')[:50]}...' ergaenzt mit " @@ -1040,6 +1191,15 @@ class FactCheckerAgent: f"Fakt herabgestuft ({old_status} -> {fact['status']}): " f"keine Quellen zuordnebar: '{fact.get('claim', '')[:60]}...'" ) + else: + # Nur Belege aus der Antwort des Modells: die Zahl trotzdem + # selbst bestimmen, statt der Selbstauskunft zu vertrauen. + fact["sources_count"] = zaehle_medienhaeuser( + url_pattern.findall(fact["evidence"] or "") + ) + + for fact in facts: + pruefe_belegtiefe(fact) return facts def _parse_response(self, response: str, articles: list[dict] = None) -> list[dict]: diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index 735190a..ab2cb75 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -2073,7 +2073,7 @@ class AgentOrchestrator: # Status-Statistik sammeln if new_status in ("confirmed", "established"): confirmed_count += 1 - elif new_status in ("contradicted", "disputed"): + elif new_status in ("contradicted", "disputed", "false"): contradicted_count += 1 await db.commit() @@ -2161,7 +2161,7 @@ class AgentOrchestrator: "type": "status_change", "title": title, "text": f"{sc['claim']}: {sc['old_status']} \u2192 {sc['new_status']}", - "icon": "error" if sc["new_status"] in ("contradicted", "disputed") else "success", + "icon": "error" if sc["new_status"] in ("contradicted", "disputed", "false") else "success", }) if created_by: diff --git a/src/report_generator.py b/src/report_generator.py index 7ea86a5..d604ed6 100644 --- a/src/report_generator.py +++ b/src/report_generator.py @@ -31,23 +31,27 @@ FC_STATUS_LABELS_DE = { # 1:1 vom Monitor-Frontend (components.js) — konsistent zum UI. "confirmed": "Bestätigt", "unconfirmed": "Unbestätigt", - "contradicted": "Widerlegt", + # "Widerlegt" war hier falsch: Der Status meint laut Auftrag Belege, die + # einander widersprechen. Im Bericht vom 01.08.2026 stand deshalb eine + # sachlich zutreffende Zahlenspanne als widerlegt. Die echte Widerlegung + # hat jetzt den eigenen Status "false". + "contradicted": "Widersprüchlich", "developing": "Unklar", "established": "Gesichert", "disputed": "Umstritten", "unverified": "Ungeprüft", - "false": "Falsch", + "false": "Widerlegt", } FC_STATUS_LABELS_EN = { "confirmed": "Confirmed", "unconfirmed": "Unconfirmed", - "contradicted": "Contradicted", + "contradicted": "Conflicting", "developing": "Developing", "established": "Established", "disputed": "Disputed", "unverified": "Unverified", - "false": "False", + "false": "Disproved", } diff --git a/src/services/fact_consolidation.py b/src/services/fact_consolidation.py index 297ec7d..76cd055 100644 --- a/src/services/fact_consolidation.py +++ b/src/services/fact_consolidation.py @@ -16,7 +16,7 @@ logger = logging.getLogger("osint.fact_consolidation") STATUS_PRIORITY = { "confirmed": 5, "established": 5, - "contradicted": 4, "disputed": 4, + "contradicted": 4, "disputed": 4, "false": 4, "unconfirmed": 3, "unverified": 3, "developing": 1, } diff --git a/src/services/post_refresh_qc.py b/src/services/post_refresh_qc.py index e1c30f7..a366693 100644 --- a/src/services/post_refresh_qc.py +++ b/src/services/post_refresh_qc.py @@ -20,7 +20,7 @@ logger = logging.getLogger("osint.post_refresh_qc") STATUS_PRIORITY = { "confirmed": 5, "established": 5, - "contradicted": 4, "disputed": 4, + "contradicted": 4, "disputed": 4, "false": 4, "unconfirmed": 3, "unverified": 3, "developing": 1, } diff --git a/src/static/css/style.css b/src/static/css/style.css index ec58c97..d2b1413 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -1199,6 +1199,12 @@ a.dev-source-pill:hover { } .factcheck-icon.contradicted { + background: var(--tint-warning); + color: var(--warning); +} + +/* Widerlegt: eigener Status seit der Trennung von Widerspruch und Widerlegung */ +.factcheck-icon.false { background: var(--tint-error); color: var(--error); } @@ -1914,6 +1920,11 @@ a.dev-source-pill:hover { } .badge-contradicted { + background: var(--tint-warning); + color: var(--warning); +} + +.badge-false { background: var(--tint-error); color: var(--error); } diff --git a/src/static/i18n/de.json b/src/static/i18n/de.json index 47eec8d..d5a42aa 100644 --- a/src/static/i18n/de.json +++ b/src/static/i18n/de.json @@ -111,7 +111,8 @@ "card.sources_overview": "Quellenübersicht", "fc.label.confirmed": "Bestätigt durch mehrere Quellen", "fc.label.unconfirmed": "Nicht unabhängig bestätigt", - "fc.label.contradicted": "Widerlegt", + "fc.label.contradicted": "Quellen widersprechen sich", + "fc.label.false": "Widerlegt", "fc.label.developing": "Faktenlage noch im Fluss", "fc.label.established": "Gesicherter Fakt (3+ Quellen)", "fc.label.disputed": "Umstrittener Sachverhalt", @@ -122,10 +123,12 @@ "fc.tooltip.unconfirmed": "Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.", "fc.tooltip.unverified": "Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.", "fc.tooltip.disputed": "Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.", - "fc.tooltip.contradicted": "Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.", + "fc.tooltip.contradicted": "Widersprüchlich: Die Belege widersprechen einander, keine Seite ist entschieden. Unterschiedliche Zahlen verschiedener Erhebungsstellen gelten nicht als Widerspruch.", + "fc.tooltip.false": "Widerlegt: Ein Beleg entkräftet diese Aussage nachweislich.", "fc.chip.confirmed": "Bestätigt", "fc.chip.unconfirmed": "Unbestätigt", - "fc.chip.contradicted": "Widerlegt", + "fc.chip.contradicted": "Widersprüchlich", + "fc.chip.false": "Widerlegt", "fc.chip.developing": "Unklar", "fc.chip.established": "Gesichert", "fc.chip.disputed": "Umstritten", @@ -133,7 +136,7 @@ "refresh.no_developments": "Keine neuen Entwicklungen", "refresh.new_articles_suffix": "neue Artikel", "refresh.confirmed_suffix": "Fakten bestätigt", - "refresh.contradicted_suffix": "widerlegt", + "refresh.contradicted_suffix": "widersprüchlich", "progress.status.queued": "In Warteschlange", "progress.status.researching": "Recherchiert...", "progress.status.deep_researching": "Tiefenrecherche...", diff --git a/src/static/i18n/en.json b/src/static/i18n/en.json index 5391e2a..26af76a 100644 --- a/src/static/i18n/en.json +++ b/src/static/i18n/en.json @@ -111,7 +111,8 @@ "card.sources_overview": "Sources overview", "fc.label.confirmed": "Confirmed by multiple sources", "fc.label.unconfirmed": "Not independently confirmed", - "fc.label.contradicted": "Contradicted", + "fc.label.contradicted": "Sources conflict", + "fc.label.false": "Disproved", "fc.label.developing": "Facts still developing", "fc.label.established": "Established fact (3+ sources)", "fc.label.disputed": "Disputed matter", @@ -122,10 +123,12 @@ "fc.tooltip.unconfirmed": "Unconfirmed: known from only one source so far. Independent confirmation is pending.", "fc.tooltip.unverified": "Unverified: the claim could not yet be checked against available sources.", "fc.tooltip.disputed": "Disputed: sources disagree. There is both supporting and contradicting evidence.", - "fc.tooltip.contradicted": "Contradicted: reliable sources contradict this claim. Likely false.", + "fc.tooltip.contradicted": "Conflicting: the evidence contradicts itself and no side is settled. Differing figures from different authorities do not count as a conflict.", + "fc.tooltip.false": "Disproved: evidence refutes this claim.", "fc.chip.confirmed": "Confirmed", "fc.chip.unconfirmed": "Unconfirmed", - "fc.chip.contradicted": "Contradicted", + "fc.chip.contradicted": "Conflicting", + "fc.chip.false": "Disproved", "fc.chip.developing": "Developing", "fc.chip.established": "Established", "fc.chip.disputed": "Disputed", @@ -133,7 +136,7 @@ "refresh.no_developments": "No new developments", "refresh.new_articles_suffix": "new articles", "refresh.confirmed_suffix": "facts confirmed", - "refresh.contradicted_suffix": "contradicted", + "refresh.contradicted_suffix": "conflicting", "progress.status.queued": "Queued", "progress.status.researching": "Researching...", "progress.status.deep_researching": "Deep research...", diff --git a/src/static/js/app.js b/src/static/js/app.js index 6ab7fcf..e690306 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -2385,7 +2385,8 @@ async handleRefresh() { confirmed: 'Bestätigt', established: 'Gesichert', unconfirmed: 'Unbestätigt', - contradicted: 'Widersprochen', + contradicted: 'Widersprüchlich', + false: 'Widerlegt', disputed: 'Umstritten', developing: 'In Entwicklung', unverified: 'Ungeprüft', diff --git a/src/static/js/components.js b/src/static/js/components.js index 2442676..5912edc 100644 --- a/src/static/js/components.js +++ b/src/static/js/components.js @@ -82,7 +82,8 @@ const UI = { _fcLabelDefaultsDE: { confirmed: 'Bestätigt durch mehrere Quellen', unconfirmed: 'Nicht unabhängig bestätigt', - contradicted: 'Widerlegt', + contradicted: 'Quellen widersprechen sich', + false: 'Widerlegt', developing: 'Faktenlage noch im Fluss', established: 'Gesicherter Fakt (3+ Quellen)', disputed: 'Umstrittener Sachverhalt', @@ -95,12 +96,14 @@ const UI = { unconfirmed: 'Unbestätigt: Bisher nur aus einer Quelle bekannt. Eine unabhängige Bestätigung steht aus.', unverified: 'Ungeprüft: Die Aussage konnte bisher nicht anhand verfügbarer Quellen überprüft werden.', disputed: 'Umstritten: Quellen widersprechen sich. Es gibt sowohl stützende als auch widersprechende Belege.', - contradicted: 'Widerlegt: Zuverlässige Quellen widersprechen dieser Aussage. Wahrscheinlich falsch.', + contradicted: 'Widersprüchlich: Die Belege widersprechen einander, keine Seite ist entschieden. Unterschiedliche Zahlen verschiedener Erhebungsstellen gelten nicht als Widerspruch.', + false: 'Widerlegt: Ein Beleg entkräftet diese Aussage nachweislich.', }, _fcChipDefaultsDE: { confirmed: 'Bestätigt', unconfirmed: 'Unbestätigt', - contradicted: 'Widerlegt', + contradicted: 'Widersprüchlich', + false: 'Widerlegt', developing: 'Unklar', established: 'Gesichert', disputed: 'Umstritten', @@ -138,7 +141,8 @@ const UI = { factCheckIcons: { confirmed: '✓', unconfirmed: '?', - contradicted: '✗', + contradicted: '⇄', + false: '✗', developing: '↻', established: '✓', disputed: '⚠', @@ -154,7 +158,7 @@ const UI = { factchecks.forEach(fc => { statusCounts[fc.status] = (statusCounts[fc.status] || 0) + 1; }); - const statusOrder = ['confirmed', 'established', 'developing', 'unconfirmed', 'unverified', 'disputed', 'contradicted']; + const statusOrder = ['confirmed', 'established', 'developing', 'unconfirmed', 'unverified', 'disputed', 'contradicted', 'false']; const usedStatuses = statusOrder.filter(s => statusCounts[s]); if (usedStatuses.length <= 1) return ''; @@ -558,7 +562,7 @@ const UI = { const parts = []; if (data.new_articles > 0) parts.push(data.new_articles + ' neue Artikel'); if (data.confirmed_count > 0) parts.push(data.confirmed_count + ' Fakten best\u00e4tigt'); - if (data.contradicted_count > 0) parts.push(data.contradicted_count + ' widerlegt'); + if (data.contradicted_count > 0) parts.push(data.contradicted_count + ' widersprüchlich'); const summaryText = parts.length > 0 ? parts.join(', ') : 'Keine neuen Entwicklungen'; const summaryEl = document.getElementById('progress-complete-summary'); diff --git a/tests/test_eu_factcheck.py b/tests/test_eu_factcheck.py index c3e9b13..21ee072 100644 --- a/tests/test_eu_factcheck.py +++ b/tests/test_eu_factcheck.py @@ -5,6 +5,11 @@ sind durch Testdoubles ersetzt. Aufruf aus dem Projektstamm: venv/bin/python tests/test_eu_factcheck.py + +Seit der Belegtiefe-Pruefung (Block 2) traegt eine einzelne Quelle keine +Bestaetigung mehr. Die Faelle H und J liefern deshalb Belege aus mehreren +unabhaengigen Haeusern, sonst pruefen sie nicht mehr die Zuordnung, sondern +nur noch die Mindestzahl. """ import asyncio import os @@ -219,7 +224,8 @@ antworten.append(json_fakten([ (F_TOTE, "unconfirmed", "unklar"), (F_SANCHEZ, "unconfirmed", "unklar"), ])) -antworten.append(json_fakten([(F_TOTE, "confirmed", "[S1] https://example.org/a belegt")])) +antworten.append(json_fakten([(F_TOTE, "confirmed", + "[S1] https://example.org/a und [S2] https://beispiel.de/b belegen")])) facts_h, _ = asyncio.run(fc._eu_pruefen( "Auftrag", title="Ceuta (Opfer)", articles=None, output_language="Deutsch", search_items=[F_TOTE, F_SANCHEZ], incident_type="adhoc", @@ -254,7 +260,8 @@ neu() # Suchquelle. Vor der Korrektur wurde so ein Fakt herabgestuft. NUR_SUCHE = "Die Innenministerkonferenz beschliesst einen Fahrplan" antworten.append(json_fakten([ - (NUR_SUCHE, "established", "belegt durch die Suche"), + (NUR_SUCHE, "established", + "belegt durch die Suche: https://example.org/a, https://beispiel.de/b, https://muster.at/c"), (F_GRENZE, "confirmed", "belegt"), ])) facts_j, _ = asyncio.run(fc.check("Ceuta", ARTS, "research", "Deutsch")) diff --git a/tests/test_faktencheck_belegtiefe.py b/tests/test_faktencheck_belegtiefe.py new file mode 100644 index 0000000..3c7e6b4 --- /dev/null +++ b/tests/test_faktencheck_belegtiefe.py @@ -0,0 +1,250 @@ +"""Testet die Belegtiefe des Faktenchecks. + +Grundlage sind die drei Fehler, die der Vergleich der Ceuta-Berichte vom +01.08.2026 in der EU-Fassung zeigte: + +1. Eine sachlich zutreffende Zahlenspanne stand als WIDERLEGT, weil der Status + "contradicted" im Auftrag Quellendivergenz meint, in der Anzeige aber als + Widerlegung ausgegeben wurde. +2. "Der Ausloeser ist weiterhin unklar" wurde von der Nachhak-Runde auf + BESTAETIGT hochgestuft, weil "developing" die niedrigste Prioritaet hat. +3. Eine Behauptung stand als BESTAETIGT mit einer Quelle, deren Evidenz + woertlich mit "Nur durch Al Jazeera berichtet" begann. + +Dazu kommt die Zaehlung: sechs France24-Treffer sind ein Medienhaus, nicht +sechs Belege. + +Laeuft ohne Netzzugriff, ohne Kosten und ohne Datenbank. + +Aufruf aus dem Projektstamm: + venv/bin/python tests/test_faktencheck_belegtiefe.py +""" +import asyncio +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "src")) + +import agents.factchecker as fc # noqa: E402 +from agents.claude_client import ClaudeUsage # noqa: E402 +import report_generator as rg # noqa: E402 + +ok = 0 +fail = 0 + + +def pruefe(name, bedingung, extra=""): + global ok, fail + if bedingung: + ok += 1 + print(" OK " + name) + else: + fail += 1 + print(" FEHL " + name + " " + str(extra)) + + +# --------------------------------------------------------------------------- +# A. Zaehlung nach Medienhaus +# --------------------------------------------------------------------------- +print("\nA. Belege zaehlen") +france24 = [ + "https://www.france24.com/en/ceuta-spain-slams-selfish-eu-partners", + "https://www.france24.com/en/video/20260801-ceuta-enclave-almost-back-to-normal-1", + "https://www.france24.com/en/video/20260801-ceuta-most-migrants-have-returned-1", + "https://www.france24.com/en/ceuta-2026-dwarfs-2021-migration-crisis", +] +pruefe("A1 vier France24-Fundstellen sind ein Haus", + fc.zaehle_medienhaeuser(france24) == 1, fc.zaehle_medienhaeuser(france24)) +gemischt = france24 + [ + "https://www.tagesschau.de/ausland/europa/ceuta-100.html", + "https://www.theguardian.com/world/2026/aug/01/spain-ceuta", + "https://www.bbc.co.uk/news/articles/abc", +] +pruefe("A2 drei weitere Haeuser ergeben vier", + fc.zaehle_medienhaeuser(gemischt) == 4, fc.zaehle_medienhaeuser(gemischt)) +redirects = [ + "https://news.google.com/rss/articles/AAA?oc=5", + "https://news.google.com/rss/articles/BBB?oc=5", +] +pruefe("A3 zwei Weiterleitungen bleiben zwei, sie koennen zwei Zeitungen sein", + fc.zaehle_medienhaeuser(redirects) == 2, fc.zaehle_medienhaeuser(redirects)) +pruefe("A4 Satzzeichen am Adressende stoeren nicht", + fc.zaehle_medienhaeuser(["https://www.zeit.de/a.html)."]) == 1) +pruefe("A5 leere Liste ergibt null", fc.zaehle_medienhaeuser([]) == 0) + +# Regressionsschutz: das alte Muster zaehlte nur das Schema und lieferte +# deshalb fuer jede Evidenz 1 oder 2, egal wie viele Quellen darin standen. +evidenz = ("Bestätigt durch: tagesschau (https://www.tagesschau.de/a.html), " + "Guardian (https://www.theguardian.com/b), BBC (https://www.bbc.co.uk/c)") +pruefe("A6 Adressen werden vollstaendig erkannt, nicht nur das Schema", + fc.zaehle_medienhaeuser(fc._URL_RE.findall(evidenz)) == 3, + fc._URL_RE.findall(evidenz)) + + +# --------------------------------------------------------------------------- +# B. Nachbedingung: Status gegen Beleglage +# --------------------------------------------------------------------------- +print("\nB. Belegtiefe pruefen") +einzelquelle = { + "claim": "Spaniens Außenminister versicherte, die Integrität sei garantiert", + "status": "confirmed", + "sources_count": 1, + "evidence": "Nur durch Al Jazeera berichtet (https://www.aljazeera.com/news/x)", +} +fc.pruefe_belegtiefe(einzelquelle) +pruefe("B1 bestaetigt mit einer Quelle wird herabgestuft", + einzelquelle["status"] == "unconfirmed", einzelquelle["status"]) + +zwei = {"claim": "x", "status": "confirmed", "sources_count": 2, "evidence": ""} +fc.pruefe_belegtiefe(zwei) +pruefe("B2 zwei Haeuser tragen eine Bestaetigung", zwei["status"] == "confirmed") + +research_knapp = {"claim": "x", "status": "established", "sources_count": 2, "evidence": ""} +fc.pruefe_belegtiefe(research_knapp) +pruefe("B3 gesichert verlangt drei Haeuser", research_knapp["status"] == "unverified", + research_knapp["status"]) + +primaer = { + "claim": "Weber kritisiert Spanien", + "status": "confirmed", + "sources_count": 1, + "evidence_type": "primary", + "evidence": "O-Ton im ZDF-Interview (https://www.zdfheute.de/politik/x.html)", +} +fc.pruefe_belegtiefe(primaer) +pruefe("B4 Primaerbeleg traegt allein", primaer["status"] == "confirmed", primaer["status"]) + +ohne_zahl = { + "claim": "x", "status": "confirmed", + "evidence": "a (https://www.zeit.de/1), b (https://www.faz.net/2)", +} +fc.pruefe_belegtiefe(ohne_zahl) +pruefe("B5 fehlende Belegzahl wird aus der Evidenz bestimmt", + ohne_zahl["sources_count"] == 2 and ohne_zahl["status"] == "confirmed", ohne_zahl) + +widerspruch = {"claim": "x", "status": "contradicted", "sources_count": 1, "evidence": ""} +fc.pruefe_belegtiefe(widerspruch) +pruefe("B6 Widerspruchs-Status wird nicht angetastet", widerspruch["status"] == "contradicted") + + +# --------------------------------------------------------------------------- +# C. Anzeige: Widerspruch ist keine Widerlegung +# --------------------------------------------------------------------------- +print("\nC. Statusbezeichnungen") +labels = rg._fc_labels("de") +pruefe("C1 contradicted heisst nicht mehr Widerlegt", + labels["contradicted"] == "Widersprüchlich", labels["contradicted"]) +pruefe("C2 Widerlegt ist dem eigenen Status vorbehalten", + labels["false"] == "Widerlegt", labels.get("false")) +pruefe("C3 englische Fassung getrennt", + rg._fc_labels("en")["contradicted"] == "Conflicting", rg._fc_labels("en")["contradicted"]) +pruefe("C4 false hat eine Prioritaet wie andere belegte Befunde", + fc.STATUS_PRIORITY.get("false") == 4, fc.STATUS_PRIORITY.get("false")) + +aufbereitet = rg._prepare_fact_checks([ + {"claim": "a", "status": "contradicted"}, {"claim": "b", "status": "false"}, +], "de") +pruefe("C5 Bericht beschriftet beide Status getrennt", + [f["status_label"] for f in aufbereitet] == ["Widersprüchlich", "Widerlegt"], + [f["status_label"] for f in aufbereitet]) + + +# --------------------------------------------------------------------------- +# D. Bewertungsregeln liegen jedem Auftrag bei +# --------------------------------------------------------------------------- +print("\nD. Auftragsregeln") +regeln = fc.bewertungsregeln() +for stichwort, name in ( + ("Medienhäuser", "D1 Unabhaengigkeit ueber Medienhaeuser"), + ("news.google.com", "D2 Weiterleitungen ausgeschlossen"), + ("Erhebungsstellen", "D3 Zahlenspannen sind kein Widerspruch"), + ("primary", "D4 Primaerbeleg definiert"), + ("Kenntnisstand", "D5 keine Aussagen ueber den Kenntnisstand"), +): + pruefe(name, stichwort in regeln) +umschreibungen = ["fuer", "muessen", "koennen", "haeuser", "zaehlt", "Widerspruechlich", + "Auftraege", "gehoeren", "unabhaengig", "genuegt"] +gefunden = [u for u in umschreibungen if u.lower() in regeln.lower()] +pruefe("D6 Regeln nutzen echte Umlaute statt Umschreibungen", not gefunden, gefunden) + + +# --------------------------------------------------------------------------- +# E. Nachhak-Runde +# --------------------------------------------------------------------------- +print("\nE. Nachhak-Runde") + + +class FakeSuche: + def __init__(self): + self.block = "\n[S1] Beleg | zeit.de | https://www.zeit.de/a" + self.usage = ClaudeUsage() + self.queries = ["q"] + self.quellen = [] + + +async def fake_plan(**kwargs): + return FakeSuche() + + +antwort = {"text": "[]"} + + +async def fake_call(prompt, **kwargs): + return antwort["text"], ClaudeUsage() + + +import agents.eu_researcher as eur # noqa: E402 +import agents.claude_client as cc # noqa: E402 +eur.plan_verification_searches = fake_plan +cc.call_claude = fake_call + +agent = fc.FactCheckerAgent() + +# Fall 1: Die Nachrunde will eine Unklarheit bestaetigen. +unklar = { + "claim": "Der Auslöser des massenhaften Grenzübertritts ist weiterhin unklar", + "status": "developing", "sources_count": 4, + "evidence": "Mehrere Quellen berichten über Unklarheit (https://www.bbc.co.uk/a)", +} +antwort["text"] = ('[{"claim": "Der Auslöser des massenhaften Grenzübertritts ist weiterhin unklar",' + ' "status": "confirmed", "evidence": "Vier Quellen nennen die Unklarheit"}]') +facts, _ = asyncio.run(agent._eu_nachhaken( + facts=[unklar], offene=[unklar], title="Ceuta", output_language="Deutsch", + incident_type="adhoc", avoid_queries=[], +)) +pruefe("E1 eine Unklarheit wird nicht bestaetigt", facts[0]["status"] == "developing", facts[0]["status"]) + +# Fall 2: echte Hochstufung, aber nur eine Quelle im Beleg +duenn = { + "claim": "Spaniens Außenminister versicherte, die Integrität sei garantiert", + "status": "unconfirmed", "sources_count": 1, + "evidence": "Nur durch Al Jazeera berichtet (https://www.aljazeera.com/a)", +} +antwort["text"] = ('[{"claim": "Spaniens Außenminister versicherte, die Integrität sei garantiert",' + ' "status": "confirmed", "evidence": "Auch hier (https://www.aljazeera.com/b)"}]') +facts, _ = asyncio.run(agent._eu_nachhaken( + facts=[duenn], offene=[duenn], title="Ceuta", output_language="Deutsch", + incident_type="adhoc", avoid_queries=[], +)) +pruefe("E2 zweiter Treffer desselben Hauses traegt keine Bestaetigung", + facts[0]["status"] == "unconfirmed", facts[0]["status"]) + +# Fall 3: gedeckte Hochstufung durch ein zweites Haus +tragfaehig = { + "claim": "Italien führt Grenzkontrollen für Flüge und Schiffe aus Spanien ein", + "status": "unconfirmed", "sources_count": 1, + "evidence": "Kurier (https://www.kurier.at/a)", +} +antwort["text"] = ('[{"claim": "Italien führt Grenzkontrollen für Flüge und Schiffe aus Spanien ein",' + ' "status": "confirmed", "evidence": "Bestätigt (https://www.ansa.it/b)"}]') +facts, _ = asyncio.run(agent._eu_nachhaken( + facts=[tragfaehig], offene=[tragfaehig], title="Ceuta", output_language="Deutsch", + incident_type="adhoc", avoid_queries=[], +)) +pruefe("E3 zweites unabhaengiges Haus stuft hoch", + facts[0]["status"] == "confirmed", facts[0]["status"]) +pruefe("E4 Belegzahl folgt der neuen Evidenz", + facts[0]["sources_count"] == 2, facts[0]["sources_count"]) + +print("\nErgebnis: " + str(ok) + " bestanden, " + str(fail) + " fehlgeschlagen") +sys.exit(1 if fail else 0)