215 lines
6.9 KiB
Python
215 lines
6.9 KiB
Python
|
|
"""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)
|