From 038ebd6447843d802489dc67cf67fbee5461b7c9 Mon Sep 17 00:00:00 2001 From: Dotty Dotter Date: Fri, 10 Apr 2026 16:05:57 +0200 Subject: [PATCH] Fix: NRW-Titel + Regierungsfraktionen-Pflicht im LLM-Prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- app/analyzer.py | 7 +++++-- app/main.py | 25 ++++++++++++++++++++++++- app/parlamente.py | 21 ++++++++++++++------- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/analyzer.py b/app/analyzer.py index ba6b2d2..a4747a0 100644 --- a/app/analyzer.py +++ b/app/analyzer.py @@ -282,10 +282,13 @@ async def analyze_antrag(text: str, bundesland: str = "NRW", model: str = "qwen- {text} +**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 diff --git a/app/main.py b/app/main.py index b98ad89..ed47418 100644 --- a/app/main.py +++ b/app/main.py @@ -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 "", diff --git a/app/parlamente.py b/app/parlamente.py index f087a19..c9ffae2 100644 --- a/app/parlamente.py +++ b/app/parlamente.py @@ -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]: