diff --git a/CLAUDE.md b/CLAUDE.md index 9e75b32..b3ec33c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,6 +162,7 @@ tests/: hinweis: "Laufen ohne Netzzugriff und ohne Kosten, Modell, Suche und Datenbank sind durch Testdoubles ersetzt. Aufruf aus dem Projektstamm, zum Beispiel venv/bin/python tests/test_eu_factcheck.py" test_eu_factcheck.py: "EU-Faktencheck, Nachhak-Runde, Quellenzuordnung, plus Regressionsschutz fuer den Anthropic-Weg" test_eu_belegsuche.py: "Gezielte Belegsuche ueber staan, Planung, Grenzen, Entdopplung, Ausfallverhalten" + test_faktenspanne.py: "Richtwert fuer die Zahl der Faktenaussagen, waechst mit der Menge der Meldungen" 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" diff --git a/src/agents/factchecker.py b/src/agents/factchecker.py index 0d05b0f..8835bb3 100644 --- a/src/agents/factchecker.py +++ b/src/agents/factchecker.py @@ -29,7 +29,10 @@ STRENGE REGELN - KEINE HALLUZINATIONEN: - Lieber "unconfirmed" als falsch bestätigt AUFTRAG: -1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Meldungen +1. Identifiziere die wichtigsten Faktenaussagen aus den Meldungen. Als Richtwert + sind das etwa {fakten_min} bis {fakten_max} Aussagen. Das ist ein Richtwert und keine + Zielvorgabe. Nenne weniger, wenn das Material nicht mehr hergibt, und mehr, wenn + sonst wichtige Punkte der Lage fehlen wuerden. 2. Prüfe jeden Claim aktiv per WebSearch gegen mindestens eine weitere unabhängige Quelle 3. Kategorisiere jede Aussage: - "confirmed": Mindestens 2 unabhängige Medienhäuser mit überprüfbarer URL bestätigen die Aussage @@ -71,7 +74,10 @@ STRENGE REGELN - KEINE HALLUZINATIONEN: AUFTRAG: Fokus: "Was sind die gesicherten Fakten zu diesem Thema?" -1. Identifiziere die 5-10 wichtigsten Faktenaussagen aus den Quellen +1. Identifiziere die wichtigsten Faktenaussagen aus den Quellen. Als Richtwert + sind das etwa {fakten_min} bis {fakten_max} Aussagen. Das ist ein Richtwert und keine + Zielvorgabe. Nenne weniger, wenn das Material nicht mehr hergibt, und mehr, wenn + sonst wichtige Punkte der Lage fehlen wuerden. 2. Prüfe jeden Claim aktiv per WebSearch gegen weitere unabhängige Quellen 3. Kategorisiere jede Aussage: - "established": Breit dokumentierter Fakt, 3+ unabhängige Medienhäuser mit URL @@ -115,7 +121,8 @@ STRENGE REGELN - KEINE HALLUZINATIONEN: AUFTRAG: 1. Prüfe ob die neuen Meldungen bereits geprüfte Fakten BESTÄTIGEN, WIDERLEGEN oder ERGÄNZEN 2. Aktualisiere den Status bestehender Fakten wenn nötig (z.B. "unconfirmed" → "confirmed") -3. Identifiziere 3-5 NEUE Faktenaussagen aus den neuen Meldungen +3. Identifiziere NEUE Faktenaussagen aus den neuen Meldungen, als Richtwert etwa + {fakten_min} bis {fakten_max}. Auch das ist ein Richtwert, keine Zielvorgabe. 4. Prüfe neue Claims per WebSearch gegen unabhängige Quellen 5. Markiere wichtige Statusänderungen und neue Entwicklungen mit is_notification: true @@ -159,7 +166,8 @@ STRENGE REGELN - KEINE HALLUZINATIONEN: AUFTRAG: 1. Prüfe ob die neuen Quellen bereits geprüfte Fakten bestätigen, widerlegen oder ergänzen 2. Aktualisiere den Status bestehender Fakten wenn nötig -3. Identifiziere 3-5 NEUE Faktenaussagen aus den neuen Quellen +3. Identifiziere NEUE Faktenaussagen aus den neuen Quellen, als Richtwert etwa + {fakten_min} bis {fakten_max}. Auch das ist ein Richtwert, keine Zielvorgabe. 4. Prüfe neue Claims per WebSearch Status-Kategorien: @@ -198,7 +206,8 @@ Analysiere die neuen Artikel und identifiziere: betroffen sein? (neue Bestätigung, Widerlegung, neue Evidenz, Status-Update nötig) 2. NEUE FAKTENAUSSAGEN: Welche neuen prüfbaren Faktenaussagen enthalten die Artikel, - die noch nicht in den bestehenden Fakten erfasst sind? (3-5 neue Claims) + die noch nicht in den bestehenden Fakten erfasst sind? Als Richtwert etwa + {fakten_min} bis {fakten_max} neue Claims, je nachdem was die Artikel hergeben. 3. GRUPPIERUNG: Gruppiere verwandte Fakten thematisch für die parallele Batch-Verarbeitung. Verwandte Fakten MÜSSEN in derselben Gruppe sein. Max {max_per_group} Fakten pro Gruppe. @@ -398,6 +407,34 @@ BEWERTUNGSREGELN (verbindlich, sie gehen allen Angaben oben vor): keine Pflicht zur Erfindung.""" +def faktenspanne(anzahl_meldungen: int, neu: bool = False) -> tuple[int, int]: + """Richtwert fuer die Zahl der Faktenaussagen, gemessen am Material. + + Frueher stand in allen Auftraegen fest "5-10 wichtigste Faktenaussagen". + Das fuehrte dazu, dass aus 16 Meldungen genauso viele Fakten wurden wie aus + 91, obwohl die groessere Lage sichtbar mehr pruefbare Punkte hergab. Das + Modell auf dem EU-Weg traf die Zehn in neun von siebzehn Laeufen exakt, es + las die Spanne als Zielvorgabe. Der Weg ueber die Anthropic-CLI behandelte + sie als Richtwert und lag zwischen neun und einundzwanzig. + + neu=True gilt fuer die Aktualisierung, dort zaehlen nur die neu + hinzugekommenen Meldungen und der Korridor ist enger. + """ + from config import ( + FAKTEN_JE_ARTIKEL, FAKTEN_MAX, FAKTEN_MIN, + FAKTEN_NEU_JE_ARTIKEL, FAKTEN_NEU_MAX, FAKTEN_NEU_MIN, + ) + + je_artikel = FAKTEN_NEU_JE_ARTIKEL if neu else FAKTEN_JE_ARTIKEL + kleinstwert = FAKTEN_NEU_MIN if neu else FAKTEN_MIN + groesstwert = FAKTEN_NEU_MAX if neu else FAKTEN_MAX + + aus_material = round(max(0, anzahl_meldungen) / max(1, je_artikel)) + obergrenze = max(kleinstwert * 2, min(groesstwert, aus_material)) + untergrenze = max(kleinstwert, obergrenze // 2) + return untergrenze, obergrenze + + def bewertungsregeln() -> str: """Verbindlicher Regelblock, wird an jeden Faktencheck-Auftrag angehaengt. @@ -1055,10 +1092,16 @@ class FactCheckerAgent: articles_text = self._format_articles_text(articles) template = RESEARCH_FACTCHECK_PROMPT_TEMPLATE if incident_type == "research" else FACTCHECK_PROMPT_TEMPLATE + fakten_min, fakten_max = faktenspanne(len(articles)) + logger.info( + "Faktencheck: %d Meldungen, Richtwert %d bis %d Faktenaussagen", + len(articles), fakten_min, fakten_max) prompt = template.format( title=title, articles_text=articles_text, output_language=output_language, + fakten_min=fakten_min, + fakten_max=fakten_max, ) + bewertungsregeln() try: @@ -1105,11 +1148,17 @@ class FactCheckerAgent: else: template = INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE + fakten_min, fakten_max = faktenspanne(len(new_articles), neu=True) + logger.info( + "Inkrementeller Faktencheck: %d neue Meldungen, Richtwert %d bis %d neue Aussagen", + len(new_articles), fakten_min, fakten_max) prompt = template.format( title=title, articles_text=articles_text, existing_facts_text=existing_facts_text, output_language=output_language, + fakten_min=fakten_min, + fakten_max=fakten_max, ) + bewertungsregeln() try: @@ -1164,6 +1213,7 @@ class FactCheckerAgent: articles_text = self._format_articles_text(new_articles, max_articles=15) from config import CLAUDE_MODEL_FAST + triage_min, triage_max = faktenspanne(len(new_articles), neu=True) triage_prompt = TRIAGE_PROMPT_TEMPLATE.format( output_language=output_language, fact_count=len(existing_facts), @@ -1171,6 +1221,8 @@ class FactCheckerAgent: article_count=len(new_articles), articles_text=articles_text, max_per_group=MAX_FACTS_PER_VERIFY_GROUP, + fakten_min=triage_min, + fakten_max=triage_max, ) try: diff --git a/src/config.py b/src/config.py index 95f642a..7f4632f 100644 --- a/src/config.py +++ b/src/config.py @@ -140,6 +140,19 @@ EU_VERIFY_COUNTER_SHARE = float(os.environ.get("EU_VERIFY_COUNTER_SHARE", "0.3") EU_VERIFY_FULLTEXT_QUERIES = int(os.environ.get("EU_VERIFY_FULLTEXT_QUERIES", "3")) EU_VERIFY_FULLTEXT_CHARS = int(os.environ.get("EU_VERIFY_FULLTEXT_CHARS", "1800")) +# Wie viele Faktenaussagen ein Faktencheck aufstellen soll. Der Richtwert waechst +# mit der Menge des Materials. Eine feste Obergrenze von zehn fuehrte dazu, dass +# aus 16 Meldungen genauso viele Fakten wurden wie aus 91, obwohl die Lage im +# zweiten Fall deutlich mehr pruefbare Punkte hergibt. +FAKTEN_JE_ARTIKEL = int(os.environ.get("FAKTEN_JE_ARTIKEL", "5")) +FAKTEN_MIN = int(os.environ.get("FAKTEN_MIN", "5")) +FAKTEN_MAX = int(os.environ.get("FAKTEN_MAX", "20")) +# Dasselbe fuer die Aktualisierung, dort zaehlen nur die neu hinzugekommenen +# Meldungen. +FAKTEN_NEU_JE_ARTIKEL = int(os.environ.get("FAKTEN_NEU_JE_ARTIKEL", "5")) +FAKTEN_NEU_MIN = int(os.environ.get("FAKTEN_NEU_MIN", "3")) +FAKTEN_NEU_MAX = int(os.environ.get("FAKTEN_NEU_MAX", "10")) + # --- Verbrauchssaetze (Credits je Aktion) ------------------------------------- # Beschlossen 2026-07-23, der Verbrauchsrechner im Verwaltungsportal nutzt # dieselben Werte: Live-Lauf 45, Recherche je Durchlauf 40 (das Anlegen faehrt diff --git a/tests/test_faktenspanne.py b/tests/test_faktenspanne.py new file mode 100644 index 0000000..1e42113 --- /dev/null +++ b/tests/test_faktenspanne.py @@ -0,0 +1,125 @@ +"""Testet, dass die Zahl der Faktenaussagen mit dem Material waechst. + +Laeuft ohne Netzzugriff und ohne Kosten, der Modellaufruf ist ersetzt. + +Aufruf aus dem Projektstamm: + venv/bin/python tests/test_faktenspanne.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 fcmod # noqa: E402 +import agents.claude_client as cc # noqa: E402 +from agents.factchecker import ( # noqa: E402 + FactCheckerAgent, faktenspanne, FACTCHECK_PROMPT_TEMPLATE, + RESEARCH_FACTCHECK_PROMPT_TEMPLATE, INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE, + INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE, TRIAGE_PROMPT_TEMPLATE, +) +from agents.claude_client import _ai_backend_var, ClaudeUsage # noqa: E402 +from config import FAKTEN_MAX, FAKTEN_MIN, FAKTEN_NEU_MAX, FAKTEN_NEU_MIN # 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)) + + +print("\nA) Die Spanne waechst mit der Zahl der Meldungen") +klein = faktenspanne(16) +mittel = faktenspanne(53) +gross = faktenspanne(88) +sehr_gross = faktenspanne(300) +pruefe("A1 wenige Meldungen ergeben die alte Spanne", klein == (5, 10), klein) +pruefe("A2 mehr Meldungen ergeben mehr Fakten", gross[1] > klein[1], (klein, gross)) +pruefe("A3 die Reihenfolge stimmt", + klein[1] <= mittel[1] <= gross[1] <= sehr_gross[1], + (klein, mittel, gross, sehr_gross)) +pruefe("A4 Obergrenze wird eingehalten", sehr_gross[1] == FAKTEN_MAX, sehr_gross) +pruefe("A5 Untergrenze wird eingehalten", faktenspanne(0)[0] == FAKTEN_MIN, faktenspanne(0)) +pruefe("A6 Untergrenze liegt nie ueber der Obergrenze", + all(faktenspanne(n)[0] <= faktenspanne(n)[1] for n in range(0, 400, 7))) +pruefe("A7 negative Eingabe bricht nicht", faktenspanne(-5) == (5, 10), faktenspanne(-5)) + +print("\nB) Fuer die Aktualisierung gilt ein engerer Korridor") +neu_klein = faktenspanne(8, neu=True) +neu_gross = faktenspanne(200, neu=True) +pruefe("B1 wenige neue Meldungen ergeben die alte Spanne", + neu_klein == (FAKTEN_NEU_MIN, FAKTEN_NEU_MIN * 2), neu_klein) +pruefe("B2 Obergrenze der Aktualisierung greift", neu_gross[1] == FAKTEN_NEU_MAX, neu_gross) +pruefe("B3 Aktualisierung bleibt unter der Erstpruefung", + faktenspanne(200, neu=True)[1] < faktenspanne(200)[1]) + +print("\nC) Die Auftraege enthalten Platzhalter statt fester Zahlen") +for name, vorlage in ( + ("Erstpruefung adhoc", FACTCHECK_PROMPT_TEMPLATE), + ("Erstpruefung Recherche", RESEARCH_FACTCHECK_PROMPT_TEMPLATE), + ("Aktualisierung adhoc", INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE), + ("Aktualisierung Recherche", INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE), + ("Triage", TRIAGE_PROMPT_TEMPLATE)): + pruefe(f"C {name} nutzt den Richtwert", + "{fakten_min}" in vorlage and "{fakten_max}" in vorlage) + pruefe(f"C {name} ohne feste Vorgabe", + "5-10" not in vorlage and "3-5 " not in vorlage and "(3-5" not in vorlage) + +pruefe("C Richtwert ist ausdruecklich keine Zielvorgabe", + "Richtwert und keine" in FACTCHECK_PROMPT_TEMPLATE + and "Zielvorgabe" in FACTCHECK_PROMPT_TEMPLATE) +pruefe("C Abweichen nach oben und unten ist erlaubt", + "weniger" in FACTCHECK_PROMPT_TEMPLATE and "mehr" in FACTCHECK_PROMPT_TEMPLATE) + +print("\nD) Der berechnete Wert landet im Auftrag") +letzter = {"prompt": ""} + + +async def fake_call_claude(prompt, tools="WebSearch,WebFetch", model=None, + raw_text=False, timeout=None): + letzter["prompt"] = prompt + return "[]", ClaudeUsage(input_tokens=10, output_tokens=5, cost_usd=0.01) + + +fcmod.call_claude = fake_call_claude +cc.call_claude = fake_call_claude +_ai_backend_var.set("cli") +fc = FactCheckerAgent() + + +def meldungen(n): + return [{"headline": f"Meldung {i}", "source": "tagesschau", + "source_url": f"https://tagesschau.de/{i}", "content_original": "t"} + for i in range(n)] + + +asyncio.run(fc.check("Lage", meldungen(16), "adhoc", "Deutsch")) +u, o = faktenspanne(16) +pruefe("D1 kleine Lage traegt ihren Richtwert", + f"etwa {u} bis {o} Aussagen" in letzter["prompt"], + [z for z in letzter["prompt"].splitlines() if "Richtwert" in z][:1]) + +asyncio.run(fc.check("Lage", meldungen(88), "adhoc", "Deutsch")) +u2, o2 = faktenspanne(88) +pruefe("D2 grosse Lage traegt einen groesseren Richtwert", + f"etwa {u2} bis {o2} Aussagen" in letzter["prompt"] and o2 > o, + [z for z in letzter["prompt"].splitlines() if "Richtwert" in z][:1]) + +asyncio.run(fc.check_incremental( + "Lage", meldungen(30), + [{"claim": "Ein bestehender Punkt", "status": "confirmed", "evidence": "e"}], + "adhoc", "Deutsch")) +u3, o3 = faktenspanne(30, neu=True) +pruefe("D3 Aktualisierung nutzt den engeren Korridor", + f"etwa\n {u3} bis {o3}" in letzter["prompt"] or f"{u3} bis {o3}" in letzter["prompt"], + [z for z in letzter["prompt"].splitlines() if "Richtwert" in z][:1]) + +print("\nErgebnis: " + str(ok) + " bestanden, " + str(fail) + " fehlgeschlagen") +sys.exit(1 if fail else 0)