"""Wahlprogramm-Referenzsystem mit Zitaten und Seitenreferenzen. Bundesland-bewusst seit Issue #5: ``WAHLPROGRAMME[bundesland][partei]`` statt flach. Konsumiert ``BUNDESLAENDER`` aus ``bundeslaender.py`` für die Regierungsfraktionen-Lookup und für Plausibilitätsprüfungen. Verantwortlich für die schlüsselwortbasierte Fallback-Suche in den paged-Textversionen der Wahlprogramme. Die semantische Suche lebt in ``embeddings.py``. """ import re from pathlib import Path from typing import Optional from .bundeslaender import BUNDESLAENDER # WAHLPROGRAMME[bundesland][partei] -> Metadaten # Beim Hinzufügen eines neuen Bundeslands: Eintrag hier UND parallel # in WAHLPROGRAMM_KONTEXT_FILES. WAHLPROGRAMME: dict[str, dict[str, dict]] = { "NRW": { "CDU": { "file": "cdu-nrw-2022.pdf", "titel": "Machen, worauf es ankommt", "partei": "CDU NRW", "jahr": 2022, "seiten": 109, }, "SPD": { "file": "spd-nrw-2022.pdf", "titel": "Unser Land von morgen", "partei": "SPD NRW", "jahr": 2022, "seiten": 116, }, "GRÜNE": { "file": "gruene-nrw-2022.pdf", "titel": "Von hier an Zukunft", "partei": "BÜNDNIS 90/DIE GRÜNEN NRW", "jahr": 2022, "seiten": 100, }, "FDP": { "file": "fdp-nrw-2022.pdf", "titel": "Nie gab es mehr zu tun", "partei": "FDP NRW", "jahr": 2022, "seiten": 96, }, "AfD": { "file": "afd-nrw-2022.pdf", "titel": "Wer sonst.", "partei": "AfD NRW", "jahr": 2022, "seiten": 68, }, }, # Sachsen-Anhalt — Wahlprogramme zur LTW 06.06.2021. Die aktuelle 8. WP # (seit 07/2021) wird mit diesen Programmen analysiert. "LSA": { "CDU": { "file": "cdu-lsa-2021.pdf", "titel": "Unsere Heimat. Unsere Verantwortung.", "partei": "CDU Sachsen-Anhalt", "jahr": 2021, "seiten": 82, }, "SPD": { "file": "spd-lsa-2021.pdf", "titel": "Zusammenhalt und neue Chancen. Politik fürs ganze Land", "partei": "SPD Sachsen-Anhalt", "jahr": 2021, "seiten": 77, }, "GRÜNE": { "file": "gruene-lsa-2021.pdf", "titel": "Verlässlich für Sachsen-Anhalt", "partei": "BÜNDNIS 90/DIE GRÜNEN Sachsen-Anhalt", "jahr": 2021, "seiten": 164, }, "FDP": { "file": "fdp-lsa-2021.pdf", "titel": "Wahlprogramm der FDP Sachsen-Anhalt zur Landtagswahl 2021", "partei": "FDP Sachsen-Anhalt", "jahr": 2021, "seiten": 76, }, "AfD": { "file": "afd-lsa-2021.pdf", "titel": "Alles für unsere Heimat! Programm der AfD Sachsen-Anhalt zur Landtagswahl 2021", "partei": "AfD Sachsen-Anhalt", "jahr": 2021, "seiten": 64, }, "LINKE": { "file": "linke-lsa-2021.pdf", "titel": "Wahlprogramm zur Landtagswahl 2021", "partei": "DIE LINKE Sachsen-Anhalt", "jahr": 2021, "seiten": 88, }, }, # Mecklenburg-Vorpommern — Wahlprogramme zur LTW 26.09.2021. Die # aktuelle 8. WP (seit 26.10.2021) wird mit diesen Programmen # analysiert. Issue #4. "MV": { "CDU": { "file": "cdu-mv-2021.pdf", "titel": "Zusammen. Den Blick nach vorn. Gemeinsam die Zukunft meistern", "partei": "CDU Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 56, }, "SPD": { "file": "spd-mv-2021.pdf", "titel": "Verantwortung für heute und morgen — Regierungsprogramm 2021–2026", "partei": "SPD Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 95, }, "GRÜNE": { "file": "gruene-mv-2021.pdf", "titel": "Für Klima, Land und ein besseres Miteinander", "partei": "BÜNDNIS 90/DIE GRÜNEN Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 88, }, "FDP": { "file": "fdp-mv-2021.pdf", "titel": "Wahlprogramm der Freien Demokraten Mecklenburg-Vorpommern zur Landtagswahl 2021", "partei": "FDP Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 120, }, "AfD": { "file": "afd-mv-2021.pdf", "titel": "Landeswahlprogramm der AfD Mecklenburg-Vorpommern 2021", "partei": "AfD Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 84, }, "LINKE": { "file": "linke-mv-2021.pdf", "titel": "Das ist links! — Zukunftsprogramm für Mecklenburg-Vorpommern", "partei": "DIE LINKE Mecklenburg-Vorpommern", "jahr": 2021, "seiten": 82, }, }, # Berlin — Wahlprogramme zur Abgeordnetenhauswahl 2021 (am 26.09.2021, # wiederholt am 12.02.2023). Die laufende 19. WP (seit 27.04.2023) wird # mit den 2021er Programmen analysiert, weil die Parteien zur # Wiederholungswahl mit denselben Programmen angetreten sind. Issue #10. "BE": { "CDU": { "file": "cdu-be-2023.pdf", "titel": "Unser Berlin. Mehr geht nur gemeinsam. — Berlin-Plan der CDU Berlin 2021–2026", "partei": "CDU Berlin", "jahr": 2021, "seiten": 135, }, "SPD": { "file": "spd-be-2023.pdf", "titel": "Ganz sicher Berlin — Wahlprogramm der SPD Berlin zur Abgeordnetenhauswahl 2021", "partei": "SPD Berlin", "jahr": 2021, "seiten": 86, }, "GRÜNE": { "file": "gruene-be-2023.pdf", "titel": "Unser Plan für Berlin — Landeswahlprogramm BÜNDNIS 90/DIE GRÜNEN Berlin 2021", "partei": "BÜNDNIS 90/DIE GRÜNEN Berlin", "jahr": 2021, "seiten": 280, }, "LINKE": { "file": "linke-be-2023.pdf", "titel": "rot. radikal. realistisch. — Unser Programm für die soziale Stadt", "partei": "DIE LINKE Berlin", "jahr": 2021, "seiten": 130, }, "AfD": { "file": "afd-be-2023.pdf", "titel": "Wahlprogramm der AfD Berlin für die Wahl des Abgeordnetenhauses am 26. September 2021", "partei": "AfD Berlin", "jahr": 2021, "seiten": 166, }, }, } # Pro Bundesland: Markdown-Übersichtsdatei mit Wahlprogramm-Zusammenfassungen, # wird als Kontext in den LLM-Prompt geladen (nicht für die Suche). WAHLPROGRAMM_KONTEXT_FILES: dict[str, str] = { "NRW": "wahlprogramme-nrw-2022.md", } REFERENZEN_PATH = Path(__file__).parent / "static" / "referenzen" KONTEXT_PATH = Path(__file__).parent / "kontext" def get_wahlprogramm(bundesland: str, partei: str) -> Optional[dict]: """Liefert die Wahlprogramm-Metadaten oder None, wenn keins vorliegt.""" return WAHLPROGRAMME.get(bundesland, {}).get(partei) def parteien_mit_wahlprogramm(bundesland: str) -> list[str]: """Liste der Parteien, für die im gegebenen Bundesland ein Wahlprogramm vorliegt.""" return list(WAHLPROGRAMME.get(bundesland, {}).keys()) def load_wahlprogramm_text(bundesland: str, partei: str) -> dict[int, str]: """Lädt Wahlprogramm-Text mit Seitenzuordnung. Returns: Dict mit Seitennummer -> Text. Leer, wenn kein Wahlprogramm hinterlegt oder die paged-Textdatei fehlt. """ info = get_wahlprogramm(bundesland, partei) if not info: return {} # Versuche paged-Textdatei zu laden paged_file = KONTEXT_PATH / info['file'].replace('.pdf', '-paged.txt') if not paged_file.exists(): # Fallback: Normale Textdatei txt_file = KONTEXT_PATH / info['file'].replace('.pdf', '.txt') if txt_file.exists(): return {1: txt_file.read_text()} return {} text = paged_file.read_text() pages = {} current_page = 1 current_text = [] for line in text.split('\n'): if line.startswith('--- PAGE '): if current_text: pages[current_page] = '\n'.join(current_text) match = re.search(r'PAGE (\d+)', line) if match: current_page = int(match.group(1)) current_text = [] else: current_text.append(line) if current_text: pages[current_page] = '\n'.join(current_text) return pages def search_wahlprogramm( bundesland: str, partei: str, keywords: list[str], max_results: int = 3, ) -> list[dict]: """Sucht relevante Passagen in einem Wahlprogramm. Args: bundesland: Bundesland-Code (NRW, LSA, …) partei: Partei-Kürzel (CDU, SPD, GRÜNE, FDP, AfD, …) keywords: Suchbegriffe max_results: Maximale Anzahl Ergebnisse Returns: Liste von {bundesland, partei, seite, text, score, url, quelle} """ info = get_wahlprogramm(bundesland, partei) if not info: return [] pages = load_wahlprogramm_text(bundesland, partei) if not pages: return [] results = [] keywords_lower = [k.lower() for k in keywords] for page_num, text in pages.items(): text_lower = text.lower() score = sum(1 for kw in keywords_lower if kw in text_lower) if score > 0: paragraphs = text.split('\n\n') relevant_paragraphs = [] for para in paragraphs: para_clean = para.strip() if len(para_clean) < 50: continue para_lower = para_clean.lower() if any(kw in para_lower for kw in keywords_lower): relevant_paragraphs.append(para_clean) if relevant_paragraphs: best_para = max( relevant_paragraphs, key=lambda p: sum(1 for kw in keywords_lower if kw in p.lower()), ) if len(best_para) > 300: best_para = best_para[:297] + "..." results.append({ "partei": partei, "bundesland": bundesland, "seite": page_num, "text": best_para, "score": score, "url": f"/static/referenzen/{info['file']}#page={page_num}", "quelle": f"{info['partei']} Wahlprogramm {info['jahr']}, S. {page_num}", }) results.sort(key=lambda x: x['score'], reverse=True) return results[:max_results] def find_relevant_quotes( antrag_text: str, fraktionen: list[str], bundesland: str, ) -> dict[str, list[dict]]: """Findet relevante Zitate aus Wahlprogrammen für einen Antrag. Args: antrag_text: Volltext des Antrags fraktionen: Liste der einreichenden Fraktionen bundesland: Bundesland-Code (Pflichtparameter; bestimmt, welche Wahlprogramme durchsucht werden und welche Regierungsfraktionen zusätzlich einbezogen werden). Returns: Dict mit Partei -> Liste von Zitaten """ if bundesland not in BUNDESLAENDER: raise ValueError(f"Unbekanntes Bundesland: {bundesland}") # Extrahiere Keywords aus Antrag (einfache Heuristik) stopwords = { 'der', 'die', 'das', 'und', 'oder', 'für', 'mit', 'von', 'zu', 'auf', 'ist', 'sind', 'wird', 'werden', 'hat', 'haben', 'ein', 'eine', 'einer', 'den', 'dem', 'des', 'im', 'in', 'an', 'bei', 'nach', 'über', 'unter', 'durch', 'als', 'auch', 'nur', 'noch', 'aber', 'wenn', 'dass', 'sich', 'nicht', 'wie', 'so', 'aus', 'zum', 'zur', 'vom', 'beim', 'seit', 'bis', } words = re.findall(r'\b[A-Za-zäöüÄÖÜß]{4,}\b', antrag_text) keywords = [w for w in words if w.lower() not in stopwords] word_freq: dict[str, int] = {} for w in keywords: w_lower = w.lower() word_freq[w_lower] = word_freq.get(w_lower, 0) + 1 top_keywords = sorted(word_freq.keys(), key=lambda x: word_freq[x], reverse=True)[:15] # Antragsteller + Regierungsfraktionen des Bundeslands regierungsfraktionen = BUNDESLAENDER[bundesland].regierungsfraktionen parteien_to_search = set(fraktionen) | set(regierungsfraktionen) quotes: dict[str, list[dict]] = {} for partei in parteien_to_search: if get_wahlprogramm(bundesland, partei): found = search_wahlprogramm(bundesland, partei, top_keywords, max_results=2) if found: quotes[partei] = found return quotes def format_quote_for_prompt(quotes: dict[str, list[dict]]) -> str: """Formatiert Zitate für den LLM-Prompt.""" if not quotes: return "" lines = ["\n## Relevante Passagen aus Wahlprogrammen\n"] lines.append("Nutze diese Originalzitate als Belege in deiner Bewertung:\n") for partei, zitate in quotes.items(): for z in zitate: lines.append(f"### {z['quelle']}") lines.append(f'> "{z["text"]}"') lines.append("") return "\n".join(lines)