gwoe-antragspruefer/app/wahlprogramme.py

215 lines
6.9 KiB
Python
Raw Normal View History

"""Wahlprogramm-Referenzsystem mit Zitaten und Seitenreferenzen."""
import json
import re
from pathlib import Path
from typing import Optional
# Wahlprogramm-Metadaten
WAHLPROGRAMME = {
"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,
},
}
# Basis-Pfad für Referenzdokumente
REFERENZEN_PATH = Path(__file__).parent / "static" / "referenzen"
KONTEXT_PATH = Path(__file__).parent / "kontext"
def load_wahlprogramm_text(partei: str) -> dict[int, str]:
"""Lädt Wahlprogramm-Text mit Seitenzuordnung.
Returns:
Dict mit Seitennummer -> Text
"""
if partei not in WAHLPROGRAMME:
return {}
# Versuche paged-Textdatei zu laden
paged_file = KONTEXT_PATH / f"{WAHLPROGRAMME[partei]['file'].replace('.pdf', '-paged.txt')}"
if not paged_file.exists():
# Fallback: Normale Textdatei
txt_file = KONTEXT_PATH / f"{WAHLPROGRAMME[partei]['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 '):
# Speichere vorherige Seite
if current_text:
pages[current_page] = '\n'.join(current_text)
# Extrahiere neue Seitenzahl
match = re.search(r'PAGE (\d+)', line)
if match:
current_page = int(match.group(1))
current_text = []
else:
current_text.append(line)
# Letzte Seite speichern
if current_text:
pages[current_page] = '\n'.join(current_text)
return pages
def search_wahlprogramm(partei: str, keywords: list[str], max_results: int = 3) -> list[dict]:
"""Sucht relevante Passagen in einem Wahlprogramm.
Args:
partei: Partei-Kürzel (CDU, SPD, GRÜNE, FDP, AfD)
keywords: Suchbegriffe
max_results: Maximale Anzahl Ergebnisse
Returns:
Liste von {seite, text, score, url}
"""
pages = load_wahlprogramm_text(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()
# Zähle Keyword-Treffer
score = sum(1 for kw in keywords_lower if kw in text_lower)
if score > 0:
# Finde relevante Absätze (mit Keyword)
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:
# Nimm den relevantesten Absatz (mit meisten Keywords)
best_para = max(relevant_paragraphs,
key=lambda p: sum(1 for kw in keywords_lower if kw in p.lower()))
# Kürze auf ~300 Zeichen
if len(best_para) > 300:
best_para = best_para[:297] + "..."
results.append({
"partei": partei,
"seite": page_num,
"text": best_para,
"score": score,
"url": f"/static/referenzen/{WAHLPROGRAMME[partei]['file']}#page={page_num}",
"quelle": f"{WAHLPROGRAMME[partei]['partei']} Wahlprogramm {WAHLPROGRAMME[partei]['jahr']}, S. {page_num}"
})
# Sortiere nach Score, nimm Top-Ergebnisse
results.sort(key=lambda x: x['score'], reverse=True)
return results[:max_results]
def find_relevant_quotes(antrag_text: str, fraktionen: list[str]) -> dict[str, list[dict]]:
"""Findet relevante Zitate aus Wahlprogrammen für einen Antrag.
Args:
antrag_text: Volltext des Antrags
fraktionen: Liste der Fraktionen (Antragsteller + Regierung)
Returns:
Dict mit Partei -> Liste von Zitaten
"""
# Extrahiere Keywords aus Antrag (einfache Heuristik)
# Entferne Stoppwörter und kurze Wörter
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]
# Zähle Worthäufigkeit
word_freq = {}
for w in keywords:
w_lower = w.lower()
word_freq[w_lower] = word_freq.get(w_lower, 0) + 1
# Top-Keywords (häufigste)
top_keywords = sorted(word_freq.keys(), key=lambda x: word_freq[x], reverse=True)[:15]
# Suche in relevanten Wahlprogrammen
quotes = {}
# Immer Regierungsfraktionen einbeziehen
parteien_to_search = set(fraktionen) | {"CDU", "GRÜNE"}
for partei in parteien_to_search:
if partei in WAHLPROGRAMME:
found = search_wahlprogramm(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)