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) ? ` +