From 92dcd25f73272ddfd8e4b4457271f642e22c9b0f Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Fri, 10 Apr 2026 09:32:31 +0200 Subject: [PATCH] #63 B+C: Force-Honesty + UI-Warning bei Score ohne Zitate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: BUND 21/3660 zeigt Score 10/10 für Linke und Grüne, aber null Zitate — der Report sieht aus als sei die Bewertung fundiert, obwohl das LLM mangels indexierter Quellen (linke-grundsatz fehlt) aus Trainingswissen geraten hat. User-Feedback: "Da muss stehen warum." Fix C — Force-Honesty im Prompt: - format_quotes_for_prompt akzeptiert neuen Parameter searched_parties. Parteien, für die kein Chunk retrievt wurde, werden explizit als "KEINE QUELLEN VORHANDEN" markiert, mit der Anweisung "score: 0, zitate: [], Begründung: keine Quellen im Index". - Neue ZITATEREGEL Punkt 5: "Wenn KEINE QUELLEN VORHANDEN → score 0." Das ist die strukturelle Lösung — das LLM darf nicht mehr raten. - analyzer.py: fraktionen-Liste wird an format_quotes_for_prompt als searched_parties durchgereicht. Fix B — UI-Transparenz: - index.html: gelbe Warn-Box (amber, border-left #ffc107) wenn wp.wahlprogramm.score > 0 aber wp.wahlprogramm.zitate.length === 0: "Keine belegbaren Quellen im Index gefunden — Score basiert auf LLM-Einschätzung, nicht auf verifizierten Programm-Stellen." - Wird für bestehende Assessments sofort sichtbar (JS-seitig berechnet), keine DB-Migration nötig. Neue Assessments nach Force-Honesty sollten idealerweise Score=0 haben, aber die Warning ist ein Fallback für den Fall dass das LLM die Prompt-Regel nicht immer 100% befolgt. Fix A (Linke/AfD-Grundsatzprogramme) folgt als separater Commit — sind öffentlich downloadbar, brauchen manuellen Sichtbarkeitscheck. Tests: 194/194 grün (keine Schema-Änderung, nur Prompt + Template). Refs: #63, ADR 0001 --- app/analyzer.py | 4 +++- app/embeddings.py | 33 +++++++++++++++++++++++++++++++-- app/templates/index.html | 19 +++++++++++++++++-- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/app/analyzer.py b/app/analyzer.py index e45da65..ba6b2d2 100644 --- a/app/analyzer.py +++ b/app/analyzer.py @@ -251,7 +251,9 @@ async def analyze_antrag(text: str, bundesland: str = "NRW", model: str = "qwen- semantic_quotes = get_relevant_quotes_for_antrag( text, fraktionen, bundesland=bundesland, top_k_per_partei=5, ) - quotes_context = format_quotes_for_prompt(semantic_quotes) + quotes_context = format_quotes_for_prompt( + semantic_quotes, searched_parties=fraktionen, + ) except (NameError, AttributeError, TypeError, KeyError): # Programmierfehler (z.B. der partei_upper-Refactor-Rest aus # #55/eb045d0, der zu Issue #60 führte) sollen hart fehlschlagen diff --git a/app/embeddings.py b/app/embeddings.py index 8af6427..0140bd0 100644 --- a/app/embeddings.py +++ b/app/embeddings.py @@ -769,7 +769,10 @@ def reconstruct_zitate(data: dict, semantic_quotes: dict) -> dict: return data -def format_quotes_for_prompt(quotes: dict) -> str: +def format_quotes_for_prompt( + quotes: dict, + searched_parties: Optional[list[str]] = None, +) -> str: """Format quotes for inclusion in LLM prompt. Each chunk gets a stable ENUM-ID ([Q1], [Q2], …) and the prompt @@ -782,8 +785,14 @@ def format_quotes_for_prompt(quotes: dict) -> str: Each quote is annotated with the fully-qualified source (programme name + page) so the LLM cannot fall back on training-set defaults when constructing its citations. + + Issue #63 erweitert: wenn ``searched_parties`` übergeben wird, werden + Parteien, für die **kein** Chunk retrievt wurde, im Prompt explizit + als "keine Quellen im Index" markiert. Das LLM wird angewiesen, für + diese Parteien ``score: null`` zu setzen statt aus dem Trainingswissen + zu raten. """ - if not quotes: + if not quotes and not searched_parties: return "" lines = ["\n## Relevante Passagen aus Wahl- und Parteiprogrammen\n"] @@ -801,6 +810,11 @@ def format_quotes_for_prompt(quotes: dict) -> str: "ausgeschrieben).\n" "4. Wenn kein Chunk wirklich passt: lass das Zitat-Array leer. " "Lieber 0 Zitate als ein erfundenes Zitat.\n" + "5. **Wenn für eine Fraktion unten KEINE QUELLEN VORHANDEN " + "steht**: setze `score: 0` für `wahlprogramm` UND " + "`parteiprogramm` dieser Fraktion und schreibe in die " + "`begründung`: 'Keine Quellen im Index — Bewertung nicht " + "möglich.' Erfinde KEINEN Score aus dem Trainingswissen.\n" ) counter = 0 @@ -821,6 +835,21 @@ def format_quotes_for_prompt(quotes: dict) -> str: text = chunk["text"][:500] + "..." if len(chunk["text"]) > 500 else chunk["text"] lines.append(f'- [Q{counter}] {_chunk_source_label(chunk)}: "{text}"') + # Issue #63: Parteien ohne jegliche retrievte Chunks explizit markieren, + # damit das LLM nicht aus Trainingswissen halluziniert. + if searched_parties: + parties_with_chunks = set(quotes.keys()) + missing = [p for p in searched_parties if p not in parties_with_chunks] + if missing: + lines.append("\n### KEINE QUELLEN VORHANDEN\n") + lines.append( + "Für folgende Fraktionen sind weder Wahl- noch " + "Grundsatzprogramm-Passagen im Index vorhanden. " + "Bewerte sie mit `score: 0` und `zitate: []`:\n" + ) + for p in missing: + lines.append(f"- **{p}**: KEINE QUELLEN — score 0, keine Zitate.") + return "\n".join(lines) diff --git a/app/templates/index.html b/app/templates/index.html index cfd5992..79f2f48 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1508,17 +1508,32 @@ `).join(''); - + + // Issue #63: Transparenz-Warnung bei Score > 0 ohne Zitate. + // Differenziert zwischen "Score 0 = keine Quellen" (LLM hat + // Force-Honesty befolgt) und "Score > 0 aber 0 Zitate" (LLM + // hat trotzdem geratet oder Zitate wurden von reconstruct_zitate + // verworfen). Nur bei Scores > 0 warnen, weil Score 0 schon + // selbsterklärend ist. + const wpScore = wp.wahlprogramm?.score ?? 0; + const wpZitateCount = (wp.wahlprogramm?.zitate || []).length; + const noQuotesWarning = (wpScore > 0 && wpZitateCount === 0) ? ` +
+ ⚠ Keine belegbaren Quellen im Index gefunden — Score basiert auf LLM-Einschätzung, nicht auf verifizierten Programm-Stellen. +
+ ` : ''; + return `
${wp.fraktion} ${wp.istAntragsteller ? ' (Antragsteller)' : ''} ${wp.istRegierung ? ' (Regierung)' : ''}
- Wahlprogramm: ${wp.wahlprogramm?.score || '-'}/10 · + Wahlprogramm: ${wp.wahlprogramm?.score || '-'}/10 · Parteiprogramm: ${wp.parteiprogramm?.score || '-'}/10
${wp.wahlprogramm?.begründung ? `
${wp.wahlprogramm.begründung}
` : ''} + ${noQuotesWarning} ${zitateHtml}
`}).join('');