From 3495409377ff3efbbe9ae937450e416fb0d5e3df Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 1 Aug 2026 22:01:30 +0200 Subject: [PATCH] fix(faktencheck): Belegtiefe zaehlt Medienhaeuser, Widerspruch ist keine Widerlegung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Block 2 der Auswertung des Ceuta-Vergleichs vom 01.08.2026. Der Faktencheck der EU-Fassung zeigte dort drei Fehler, die alle dieselbe Wurzel haben: Der Status behauptete eine Belegtiefe, die niemand nachgerechnet hat. 1. Die Zaehlung war kaputt. Das Muster fuer die Quellenzaehlung war r'https?://' ohne den Rest der Adresse, findall lieferte also eine Liste aus "https://"-Bruchstuecken und len(set(...)) ergab immer 1 oder 2. Daher stand im Bericht eine Behauptung als BESTAETIGT mit 1 Quelle, obwohl vier Haeuser in der Evidenz standen, und der gesamte Faktencheck der Anthropic-Fassung wies durchgehend 1 bis 2 Quellen aus. Die Adresse wird jetzt vollstaendig erfasst. 2. Gezaehlt wird nach Medienhaus, nicht nach Fundstelle. Sechs France24-Treffer, davon vier Videofassungen derselben Meldung, sind ein Haus. Grundlage ist die registrierbare Domain aus services/media_registry.py. Weiterleitungsportale bekommen keine gemeinsame Identitaet, hinter news.google.com koennen beliebige Zeitungen stecken. Nachgerechnet an Lage 53 sinken die Angaben von 9 auf 6, von 8 auf 4 und von 7 auf 5 Belege. 3. Neue Nachbedingung. pruefe_belegtiefe stuft jeden Fakt ab, dessen Status mehr behauptet als vorliegt: confirmed braucht zwei, established drei unabhaengige Haeuser. Ein Primaerbeleg (Originalzitat im Interview, amtliche Mitteilung, Urteil) traegt allein, dafuer liefert das Modell jetzt "evidence_type". Damit verschwindet auch die Unterkonfidenz der Gegenprobe, in der ein ZDF-Interview als unbestaetigt galt, obwohl der Kritisierte dort selbst spricht. 4. Die Nachhak-Runde stuft "developing" nicht mehr hoch. Sie hatte die Behauptung "Der Ausloeser ist weiterhin unklar" auf BESTAETIGT gesetzt, weil developing in der Statusordnung die niedrigste Stufe ist. Eine Unklarheit laesst sich nicht bestaetigen. Ausserdem rechnet die Runde die Belegzahl nach dem Hochstufen neu und nimmt die Stufe zurueck, wenn die Belege sie nicht decken. 5. Widerspruch und Widerlegung sind getrennt. "contradicted" meint laut Auftrag Belege, die einander widersprechen, wurde aber als "Widerlegt" angezeigt. Im Bericht stand deshalb die sachlich zutreffende Spanne von 50.000 bis 60.000 Grenzuebertritten als widerlegt, obwohl die Evidenz woertlich sagt "Die Zahlen variieren je nach Quelle". Jetzt: contradicted = "Widersprüchlich", der neue Status "false" = "Widerlegt" und setzt einen entkraeftenden Beleg voraus. Angepasst in Bericht, Oberflaeche, beiden Sprachdateien, Farben und Statusordnung. 6. Ein verbindlicher Regelblock haengt an jedem Faktencheck-Auftrag (bewertungsregeln()): Unabhaengigkeit heisst verschiedene Medienhaeuser, Agenturuebernahmen zaehlen einmal, unterschiedliche Zahlen verschiedener Erhebungsstellen sind kein Widerspruch sondern eine Spanne mit Zuordnung, und Aussagen ueber den Kenntnisstand ("Der Ausloeser ist unklar") sind keine pruefbaren Behauptungen und gehoeren nicht in den Faktencheck. Die Aenderungen wirken auf beide Modellwege. Die Fixtures in test_eu_factcheck liefern jetzt Belege aus mehreren Haeusern, weil eine einzelne Quelle keine Bestaetigung mehr traegt. Nachgerechnet an beiden gespeicherten Laeufen: kein Fakt wird durch die neue Nachbedingung abgestuft, die Belegzahlen werden aber durchgehend ehrlicher. Neu sind 27 Pruefungen, insgesamt laufen 156 ohne Netzzugriff und ohne Kosten. Offen: Die Oberflaechen-Aenderungen (components.js, app.js, style.css, i18n) sind noch nicht in den Lokal-Fork portiert. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 7 +- src/agents/analyzer.py | 4 +- src/agents/factchecker.py | 234 +++++++++++++++++++++---- src/agents/orchestrator.py | 4 +- src/report_generator.py | 12 +- src/services/fact_consolidation.py | 2 +- src/services/post_refresh_qc.py | 2 +- src/static/css/style.css | 11 ++ src/static/i18n/de.json | 11 +- src/static/i18n/en.json | 11 +- src/static/js/app.js | 3 +- src/static/js/components.js | 16 +- tests/test_eu_factcheck.py | 11 +- tests/test_faktencheck_belegtiefe.py | 250 +++++++++++++++++++++++++++ 14 files changed, 512 insertions(+), 66 deletions(-) create mode 100644 tests/test_faktencheck_belegtiefe.py 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)