#!/usr/bin/env python3 """ GWÖ-Wahlprüfsteine Analyzer Bewertet Antworten mit Qwen LLM nach GWÖ-Kriterien. """ import sqlite3 import json import os import time from pathlib import Path from dataclasses import dataclass from openai import OpenAI # GWÖ-Matrix 2.0 Kontext (gekürzt für Prompt) GWOE_MATRIX_CONTEXT = """ ## GWÖ-Matrix 2.0 für Gemeinden ### Die 5 Werte (Spalten) 1. **Menschenwürde** — Rechtsstaatsprinzip, Grundrechte 2. **Solidarität** — Gemeinnutz, Mehrwert für Gemeinschaft 3. **Ökologische Nachhaltigkeit** — Klimaschutz, Ressourcenschonung 4. **Soziale Gerechtigkeit** — Daseinsvorsorge, gerechte Verteilung 5. **Transparenz & Mitbestimmung** — Bürgerbeteiligung, Demokratie ### Die 5 Berührungsgruppen (Zeilen) - **A**: Lieferant:innen, Dienstleister:innen (Beschaffung, Lieferketten) - **B**: Finanzpartner:innen, Steuerzahler:innen (Haushalt, Finanzpolitik) - **C**: Politische Führung, Verwaltung (Mandatsträger:innen, Mitarbeitende) - **D**: Bürger:innen und Wirtschaft (Daseinsvorsorge, lokale Wirkung) - **E**: Staat, Gesellschaft, Natur (überregional, Zukunft) ### Relevante Felder für Kommunalpolitik - **D5**: Demokratische Einbindung der Bürger:innen - **D2**: Gesamtwohl der Gemeinde - **C2**: Gemeinwohlorientierte Zielvereinbarung - **A3/A4**: Nachhaltige/soziale Beschaffung - **B5**: Partizipation in Finanzpolitik ### Bewertungsskala - 7-10: Vorbildlich (innovative Maßnahmen) - 4-6: Erfahren (erkennbare Verbesserungen) - 2-3: Fortgeschritten (erste Maßnahmen) - 1: Erste Schritte - 0: Basislinie """ SYSTEM_PROMPT = """Du bist ein Experte für Gemeinwohl-Ökonomie (GWÖ) und analysierst Antworten auf Wahlprüfsteine. {matrix_context} ## Deine Aufgabe Bewerte die Antwort eines Bürgermeister-Kandidaten auf eine Wahlprüfstein-Frage. ## Ausgabeformat (JSON) {{ "substanz_score": 0-3, "substanz_begruendung": "Kurze Begründung", "umfang": "keine|kurz|mittel|ausführlich", "gwoe_score": 0.0-10.0, "gwoe_begruendung": "2-3 Sätze mit Matrix-Bezug", "matrix_felder": ["D5", "C2"], "staerken": ["Punkt 1", "Punkt 2"], "schwaechen": ["Punkt 1"] }} ### Substanz-Score - 0: Keine Antwort oder nur Ja/Nein ohne Erläuterung - 1: Ausweichend, Floskeln, keine konkreten Maßnahmen - 2: Substanziell, erkennbare Haltung, erste Ideen - 3: Umfassend mit konkreten Maßnahmen und Zeitrahmen ### Umfang - keine: Leer oder nur "/" - kurz: 1-2 Sätze - mittel: 3-5 Sätze - ausführlich: >5 Sätze mit Details ### GWÖ-Score Bewerte nach Matrix 2.0. Berücksichtige: - Konkrete GWÖ-Bezüge (Gemeinwohl-Bilanz, Matrix) - Allgemeine Gemeinwohl-Orientierung - Partizipation und Transparenz - Ökologische und soziale Aspekte - Umsetzbarkeit der genannten Maßnahmen Antworte NUR mit validem JSON, keine Erklärungen davor oder danach.""".format(matrix_context=GWOE_MATRIX_CONTEXT) @dataclass class AntwortZuBewerten: antwort_id: int kandidat: str kommune: str partei: str frage_nr: int frage_text: str antwort_kurz: str | None antwort_erlaeuterung: str | None def get_unbewertete_antworten(conn: sqlite3.Connection, limit: int = None) -> list[AntwortZuBewerten]: """Holt alle noch nicht bewerteten Antworten.""" query = """ SELECT ar.id as antwort_id, k.vorname || ' ' || k.nachname as kandidat, k.kommune, k.partei_normalisiert as partei, f.nummer as frage_nr, f.volltext as frage_text, ar.antwort_kurz, ar.antwort_erlaeuterung FROM antworten_raw ar JOIN kandidaten k ON ar.kandidat_id = k.id JOIN fragen f ON ar.frage_id = f.id LEFT JOIN bewertungen b ON ar.id = b.antwort_id WHERE b.id IS NULL ORDER BY k.id, f.nummer """ if limit: query += f" LIMIT {limit}" cursor = conn.execute(query) return [AntwortZuBewerten(**dict(row)) for row in cursor.fetchall()] def build_user_prompt(antwort: AntwortZuBewerten) -> str: """Baut den User-Prompt für eine Antwort.""" erlaeuterung = antwort.antwort_erlaeuterung or "(keine Erläuterung)" kurz = antwort.antwort_kurz or "(keine Angabe)" return f"""## Wahlprüfstein-Frage {antwort.frage_nr} **Frage:** {antwort.frage_text} ## Antwort von {antwort.kandidat} ({antwort.partei}), Kandidat:in für {antwort.kommune} **Grundsätzlich:** {kurz} **Erläuterung:** {erlaeuterung} --- Bewerte diese Antwort nach den GWÖ-Kriterien.""" def analyze_with_llm(client: OpenAI, antwort: AntwortZuBewerten, model: str = "qwen-plus") -> dict: """Analysiert eine Antwort mit dem LLM.""" user_prompt = build_user_prompt(antwort) response = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_prompt} ], temperature=0.3, response_format={"type": "json_object"} ) result = json.loads(response.choices[0].message.content) # Wortanzahl berechnen text = (antwort.antwort_erlaeuterung or "") result['wortanzahl'] = len(text.split()) if text else 0 return result def save_bewertung(conn: sqlite3.Connection, antwort_id: int, bewertung: dict, model: str): """Speichert eine Bewertung in der Datenbank.""" conn.execute(""" INSERT OR REPLACE INTO bewertungen (antwort_id, substanz_score, umfang, wortanzahl, gwoe_score, gwoe_begruendung, matrix_felder, staerken, schwaechen, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( antwort_id, bewertung.get('substanz_score'), bewertung.get('umfang'), bewertung.get('wortanzahl'), bewertung.get('gwoe_score'), bewertung.get('gwoe_begruendung'), json.dumps(bewertung.get('matrix_felder', []), ensure_ascii=False), json.dumps(bewertung.get('staerken', []), ensure_ascii=False), json.dumps(bewertung.get('schwaechen', []), ensure_ascii=False), model )) conn.commit() def main(): import argparse parser = argparse.ArgumentParser(description='GWÖ-Wahlprüfsteine Analyzer') parser.add_argument('--db', type=Path, default=Path(__file__).parent / 'wahlpruefsteine.db', help='Pfad zur SQLite-Datenbank') parser.add_argument('--model', default='qwen-plus', choices=['qwen-plus', 'qwen-max', 'qwen-turbo'], help='Qwen-Modell') parser.add_argument('--limit', type=int, help='Maximal zu bewertende Antworten') parser.add_argument('--delay', type=float, default=0.5, help='Pause zwischen API-Calls (Sekunden)') parser.add_argument('--dry-run', action='store_true', help='Nur anzeigen, nicht speichern') parser.add_argument('--verbose', '-v', action='store_true', help='Ausführliche Ausgabe') args = parser.parse_args() # API-Key aus Keychain api_key = os.popen("security find-generic-password -s qwen-api -w 2>/dev/null").read().strip() if not api_key: api_key = os.environ.get('DASHSCOPE_API_KEY') if not api_key: print("FEHLER: Kein API-Key gefunden (Keychain 'qwen-api' oder DASHSCOPE_API_KEY)") return 1 # DashScope Client client = OpenAI( api_key=api_key, base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1" ) # Datenbank öffnen conn = sqlite3.connect(args.db) conn.row_factory = sqlite3.Row # Unbewertete Antworten holen antworten = get_unbewertete_antworten(conn, args.limit) print(f"Zu bewerten: {len(antworten)} Antworten") if not antworten: print("Alle Antworten bereits bewertet.") return 0 # Bewerten erfolg = 0 fehler = 0 for i, antwort in enumerate(antworten, 1): print(f"[{i}/{len(antworten)}] {antwort.kandidat} - Frage {antwort.frage_nr}...", end=" ", flush=True) try: bewertung = analyze_with_llm(client, antwort, args.model) if args.verbose: print(f"\n GWÖ: {bewertung.get('gwoe_score')}, Substanz: {bewertung.get('substanz_score')}") print(f" Begründung: {bewertung.get('gwoe_begruendung', '')[:100]}...") if not args.dry_run: save_bewertung(conn, antwort.antwort_id, bewertung, args.model) print(f"OK (GWÖ: {bewertung.get('gwoe_score', '?')})") erfolg += 1 except Exception as e: print(f"FEHLER: {e}") fehler += 1 if i < len(antworten): time.sleep(args.delay) conn.close() print(f"\nFertig: {erfolg} erfolgreich, {fehler} Fehler") if args.dry_run: print("(Dry-Run — nichts gespeichert)") return 0 if fehler == 0 else 1 if __name__ == '__main__': exit(main())