E2E Sub-D: Citation Property-Verification (Substring im PDF) #54

Closed
opened 2026-04-09 09:02:11 +02:00 by tobias · 1 comment
Owner

Sub-Issue von #50.

Zweck

Sub-Issue D des E2E-Test-Umbrellas. Die kritischste Test-Klasse — verifiziert pro realer Antrag-Analyse in der prod-DB, ob die LLM-Zitate als Substring auf der angegebenen PDF-Seite auffindbar sind.

Adressiert Bug-Klasse 7 (LLM halluziniert "FDP NRW 2022"-Quellen für MV-Anträge) und alle künftigen Prompt-Drifts.

Datei

webapp/tests/integration/test_citations_substring.py

Match-Strategie (vom User bestätigt: strict substring)

Whitespace-normalisiertes lowercased exact substring matching. Der vom LLM zitierte text muss als Substring auf der PDF-Seite vorhanden sein, die in quelle referenziert ist. Toleranz nur für:

  • Whitespace-Normalisierung (mehrere Spaces / Newlines / Tabs → ein Space)
  • Lowercased
  • LLM-Truncation-Marker (... am Anfang/Ende werden vor dem Match entfernt)

Keine Fuzzy-Matches, kein Jaccard, kein 80%-Overlap. Striktest mögliche Variante, fängt jede Halluzination und jeden Page-Off-by-One-Fehler.

Workflow

  1. Sample: Die neuesten 5 Assessments pro aktivem BL aus gwoe-antraege.db
  2. Pro Assessment: parse wahlprogramm_scores (JSON), iteriere über zitate jeder Fraktion
  3. Pro Zitat:
    • quelle parsen → programm_id via _resolve_quelle_to_programm_id()
    • Wenn kein Match in embeddings.PROGRAMME: Test fail mit "halluzinierte Quelle"
    • Seitennummer aus quelle extrahieren via _extract_page_number()
    • PDF laden via _pdf_page_text(programm_id, seite) (echtes fitz)
    • _normalized_substring(zitat["text"], page_text) prüfen
    • Bei Fail: Fehlermeldung mit zitiertem Text + tatsächlichem Seiteninhalt (Diff)

Helper-Funktionen (neu)

def _resolve_quelle_to_programm_id(quelle: str) -> Optional[str]:
    """Volltext-Match gegen PROGRAMME[*].name. Tolerant gegenüber
    Tipp-Varianten und Year-Suffixes."""

def _extract_page_number(quelle: str) -> Optional[int]:
    """Regex auf S\.\s*(\d+) oder Seite\s+(\d+)"""

def _pdf_page_text(programm_id: str, seite: int) -> str:
    """fitz.open(...) [seite-1].get_text() mit Whitespace-Normalisierung"""

def _normalized_substring(needle: str, haystack: str) -> bool:
    """Beide whitespace-normalisiert + lowercased; needle leading/trailing
    '...' wird entfernt."""

Stichproben-Größe

5 neueste Assessments pro aktivem BL × 10 aktive BL × ~3 Fraktionen × ~2 Zitate ≈ 300 Asserts pro Run. Bei mehr Assessments im prod kann das später skaliert werden.

Negativ-Test (Verifikation dass die Test-Suite überhaupt fängt)

Ein bekanntes Halluzinations-Sample (das alte 8/6390-Assessment vor Commit bc7f4a6 mit der "FDP NRW Wahlprogramm 2022, S. 75"-Halluzination) als Fixture im Repo. Dieses Sample MUSS explizit fehlschlagen, sonst weiß man nicht ob der Test überhaupt funktioniert.

Bug-Klassen

Deckt 7 (LLM-Halluzination, alle Varianten), 10 (Source-Erfindung), 17 (Cross-Bundesland-Zitat — _resolve_quelle_to_programm_id() prüft auch ob bundesland zur erwarteten BL passt).

Akzeptanzkriterien

  • Helper-Funktionen mit ihren eigenen kleinen Unit Tests
  • pytest -m integration tests/integration/test_citations_substring.py -v mit min. 50 Asserts
  • Negativ-Test (bekannte Halluzination) schlägt explizit fehl
  • Bei einem real fehlerhaften Assessment in der prod-DB sieht man pro Zitat Diff zwischen erwartetem Snippet und tatsächlicher PDF-Seite
  • Performance: Test-Lauf ≤ 60s (PDF-Caching pro programm_id)
Sub-Issue von #50. ## Zweck Sub-Issue D des E2E-Test-Umbrellas. Die kritischste Test-Klasse — verifiziert pro **realer Antrag-Analyse** in der prod-DB, ob die LLM-Zitate als Substring auf der angegebenen PDF-Seite auffindbar sind. Adressiert Bug-Klasse 7 (LLM halluziniert "FDP NRW 2022"-Quellen für MV-Anträge) und alle künftigen Prompt-Drifts. ## Datei `webapp/tests/integration/test_citations_substring.py` ## Match-Strategie (vom User bestätigt: strict substring) Whitespace-normalisiertes lowercased exact substring matching. Der vom LLM zitierte `text` muss als Substring auf der PDF-Seite vorhanden sein, die in `quelle` referenziert ist. Toleranz nur für: - Whitespace-Normalisierung (mehrere Spaces / Newlines / Tabs → ein Space) - Lowercased - LLM-Truncation-Marker (`...` am Anfang/Ende werden vor dem Match entfernt) Keine Fuzzy-Matches, kein Jaccard, kein 80%-Overlap. Striktest mögliche Variante, fängt jede Halluzination und jeden Page-Off-by-One-Fehler. ## Workflow 1. **Sample**: Die neuesten 5 Assessments pro aktivem BL aus `gwoe-antraege.db` 2. **Pro Assessment**: parse `wahlprogramm_scores` (JSON), iteriere über `zitate` jeder Fraktion 3. **Pro Zitat**: - `quelle` parsen → `programm_id` via `_resolve_quelle_to_programm_id()` - Wenn kein Match in `embeddings.PROGRAMME`: **Test fail** mit "halluzinierte Quelle" - Seitennummer aus `quelle` extrahieren via `_extract_page_number()` - PDF laden via `_pdf_page_text(programm_id, seite)` (echtes fitz) - `_normalized_substring(zitat["text"], page_text)` prüfen - Bei Fail: Fehlermeldung mit zitiertem Text + tatsächlichem Seiteninhalt (Diff) ## Helper-Funktionen (neu) ```python def _resolve_quelle_to_programm_id(quelle: str) -> Optional[str]: """Volltext-Match gegen PROGRAMME[*].name. Tolerant gegenüber Tipp-Varianten und Year-Suffixes.""" def _extract_page_number(quelle: str) -> Optional[int]: """Regex auf S\.\s*(\d+) oder Seite\s+(\d+)""" def _pdf_page_text(programm_id: str, seite: int) -> str: """fitz.open(...) [seite-1].get_text() mit Whitespace-Normalisierung""" def _normalized_substring(needle: str, haystack: str) -> bool: """Beide whitespace-normalisiert + lowercased; needle leading/trailing '...' wird entfernt.""" ``` ## Stichproben-Größe 5 neueste Assessments pro aktivem BL × 10 aktive BL × ~3 Fraktionen × ~2 Zitate ≈ 300 Asserts pro Run. Bei mehr Assessments im prod kann das später skaliert werden. ## Negativ-Test (Verifikation dass die Test-Suite überhaupt fängt) Ein bekanntes Halluzinations-Sample (das alte 8/6390-Assessment vor Commit `bc7f4a6` mit der "FDP NRW Wahlprogramm 2022, S. 75"-Halluzination) als Fixture im Repo. Dieses Sample MUSS explizit fehlschlagen, sonst weiß man nicht ob der Test überhaupt funktioniert. ## Bug-Klassen Deckt 7 (LLM-Halluzination, alle Varianten), 10 (Source-Erfindung), 17 (Cross-Bundesland-Zitat — `_resolve_quelle_to_programm_id()` prüft auch ob `bundesland` zur erwarteten BL passt). ## Akzeptanzkriterien - [ ] Helper-Funktionen mit ihren eigenen kleinen Unit Tests - [ ] `pytest -m integration tests/integration/test_citations_substring.py -v` mit min. 50 Asserts - [ ] Negativ-Test (bekannte Halluzination) schlägt explizit fehl - [ ] Bei einem real fehlerhaften Assessment in der prod-DB sieht man pro Zitat Diff zwischen erwartetem Snippet und tatsächlicher PDF-Seite - [ ] Performance: Test-Lauf ≤ 60s (PDF-Caching pro programm_id)
Author
Owner

Sub-D erledigt — tests/integration/test_citations_substring.py + 13 Helper-Unit-Tests grün. Commit 73a7f76. Property-Verification: jedes LLM-Zitat muss als Substring auf der genannten PDF-Seite vorhanden sein. Cross-BL-Check (Bug-Klasse 17) inklusive.

Sub-D erledigt — tests/integration/test_citations_substring.py + 13 Helper-Unit-Tests grün. Commit 73a7f76. Property-Verification: jedes LLM-Zitat muss als Substring auf der genannten PDF-Seite vorhanden sein. Cross-BL-Check (Bug-Klasse 17) inklusive.
Sign in to join this conversation.
No description provided.