Fix: NRW-Titel + Regierungsfraktionen-Pflicht im LLM-Prompt

Bug 1 — NRW-Titel "Drucksache XX/YYYYY":
NRW's get_document machte nur HEAD-Request auf die PDF-URL und gab
title="Drucksache 18/18085" zurück — keinen echten Titel. Fix: nutzt
jetzt search(drucksache) um den echten Eintrag von OPAL zu holen.
Fallback: leerer Titel statt generischer, damit der LLM-Titel nicht
überschrieben wird. Plus _pick_best_title Helper: doc.title nur
übernehmen wenn es ein echter Titel ist (nicht "Drucksache XX").

Bug 2 — Nur Antragsteller im Passungsprofil, keine Regierungsfraktionen:
Der LLM ignorierte die "UND Regierungsfraktionen"-Anweisung im Prompt.
Fix: explizite PFLICHT-FRAKTIONEN-Zeile im User-Prompt:
"Du MUSST folgende Fraktionen in wahlprogrammScores bewerten: SPD, CDU, GRÜNE"
(dedupliziert aus fraktionen + regierungsfraktionen).

Tests: 194/194 grün.
Batch-Re-Analyse muss nochmal laufen mit den Fixes (21 bereits fertig,
15 noch offen — werden alle erneut benötigt weil die Titel/Fraktionen
in den neuen Assessments falsch sind).
This commit is contained in:
Dotty Dotter 2026-04-10 16:05:57 +02:00
parent 303b30f6dd
commit 038ebd6447
3 changed files with 43 additions and 10 deletions

View File

@ -282,10 +282,13 @@ async def analyze_antrag(text: str, bundesland: str = "NRW", model: str = "qwen-
{text}
</antrag>
**PFLICHT-FRAKTIONEN:** Du MUSST folgende Fraktionen in `wahlprogrammScores` bewerten:
{', '.join(dict.fromkeys(fraktionen + BUNDESLAENDER[bundesland].regierungsfraktionen))}
Bewerte nach GWÖ-Matrix 2.0 für Gemeinden:
1. GWÖ-Treue (0-10) mit Matrix-Zuordnung und Symbolen (++/+///)
2. Wahlprogrammtreue der einreichenden Fraktion(en) UND Regierungsfraktionen (0-10)
3. Parteiprogrammtreue der einreichenden Fraktion(en) UND Regierungsfraktionen (0-10)
2. Wahlprogrammtreue JEDER der oben genannten Pflicht-Fraktionen (0-10)
3. Parteiprogrammtreue JEDER der oben genannten Pflicht-Fraktionen (0-10)
4. Bis zu 3 Verbesserungsvorschläge in Redline-Syntax
5. Themen-Tags für Kategorisierung

View File

@ -40,6 +40,25 @@ from .parlamente import get_adapter, ADAPTERS
from .bundeslaender import alle_bundeslaender
from .analyzer import analyze_antrag
from .auth import get_current_user, require_auth, keycloak_login_url, _is_auth_enabled
def _pick_best_title(llm_title: str, doc_title: Optional[str], drucksache: str) -> str:
"""Wähle den besten Titel aus LLM-Output und Adapter-Metadata.
Priorität:
1. doc_title, wenn ein echter Titel (nicht "Drucksache XX")
2. llm_title, wenn nicht leer und nicht generisch
3. Generischer Fallback "Drucksache XX"
"""
generic_prefix = f"Drucksache {drucksache.split('/')[0]}"
# doc_title gut? (nicht generisch, nicht leer)
if doc_title and not doc_title.startswith("Drucksache ") and len(doc_title) > 5:
return doc_title
# LLM-Titel gut? (nicht generisch)
if llm_title and not llm_title.startswith("Drucksache ") and len(llm_title) > 5:
return llm_title
# doc_title als Fallback (auch wenn generisch)
return doc_title or llm_title or f"Drucksache {drucksache}"
from .report import generate_html_report, generate_pdf_report
from .embeddings import (
init_embeddings_db, get_programme_info, get_indexing_status,
@ -536,7 +555,11 @@ async def run_drucksache_analysis(
# Prepare data for DB
assessment_data = {
"drucksache": drucksache,
"title": assessment.title or (doc.title if doc else f"Drucksache {drucksache}"),
# Titel-Priorität: LLM-generierter Titel > doc.title,
# ABER nur wenn doc.title ein echter Titel ist (nicht "Drucksache XX",
# wie NRW's get_document zurückgibt). Sonst überschreibt der
# generische doc.title den echten LLM-Titel.
"title": _pick_best_title(assessment.title, doc.title if doc else None, drucksache),
"fraktionen": assessment.fraktionen,
"datum": assessment.datum or (doc.datum if doc else ""),
"link": doc.link if doc else "",

View File

@ -255,23 +255,31 @@ class NRWAdapter(ParlamentAdapter):
return results
async def get_document(self, drucksache: str) -> Optional[Drucksache]:
"""Get document metadata by drucksache ID (e.g. '18/8125')."""
# Parse legislatur and number
"""Get document metadata by drucksache ID (e.g. '18/8125').
Nutzt ``search(drucksache)`` um den echten Titel und die
Fraktionen von OPAL zu bekommen. Fallback auf generischen
PDF-Link wenn die Suche nichts findet.
"""
# Versuch 1: über die Suche den echten Eintrag finden
results = await self.search(drucksache, limit=10)
for doc in results:
if doc.drucksache == drucksache:
return doc
# Fallback: PDF-Link konstruieren ohne Titel/Fraktionen
match = re.match(r"(\d+)/(\d+)", drucksache)
if not match:
return None
legislatur, nummer = match.groups()
pdf_url = f"https://www.landtag.nrw.de/portal/WWW/dokumentenarchiv/Dokument/MMD{legislatur}-{nummer}.pdf"
# Try to fetch and extract basic info
async with httpx.AsyncClient(timeout=30, follow_redirects=True) as client:
try:
resp = await client.head(pdf_url)
if resp.status_code == 200:
return Drucksache(
drucksache=drucksache,
title=f"Drucksache {drucksache}",
title="", # Leer statt generisch, damit LLM-Titel nicht überschrieben wird
fraktionen=[],
datum="",
link=pdf_url,
@ -279,7 +287,6 @@ class NRWAdapter(ParlamentAdapter):
)
except:
pass
return None
async def download_text(self, drucksache: str) -> Optional[str]: