diff --git a/docs/adr/0010-stimmverhalten-gwoe-aggregat.md b/docs/adr/0010-stimmverhalten-gwoe-aggregat.md new file mode 100644 index 0000000..d887f7e --- /dev/null +++ b/docs/adr/0010-stimmverhalten-gwoe-aggregat.md @@ -0,0 +1,127 @@ +# 0010 — Stimmverhalten × GWÖ-Bewertung als JOIN-Aggregat + +| | | +|---|---| +| **Status** | accepted | +| **Datum** | 2026-05-06 | +| **Refs** | #165, #166, #167, #168, #169, #171, #162, MEMORY/project_konsistenz_marker.md | + +## Kontext + +Vor dieser Entscheidung lagen zwei Erkenntnisquellen unverbunden in der DB: + +- `assessments` (~96 produktiv, dev) — pro Drucksache eine GWÖ-Bewertung + mit `gwoe_score` (0–10), `gwoe_matrix` (5×5 Felder, Rating −5..+5), + `wahlprogramm_scores` (pro Fraktion: WP-Treue + PP-Treue + Zitate). +- `plenum_vote_results` (~7281 produktiv, dev) — pro Drucksache wer wie + abgestimmt hat (`fraktionen_ja`/`_nein`/`_enthaltung` als JSON-Arrays, + pro Fraktion normalisiert via `normalize_partei`). + +Die User-Frage „Welche Parteien stimmen häufiger gemeinwohlorientierten +Anträgen zu?" — und die diametrale „Wer stimmt gegen das eigene +Wahlprogramm?" — verlangt einen JOIN auf `(bundesland, drucksache)`. +Heute schneiden sich nur ~19 von 7281 Votes mit Assessments. Die +Aussagekraft ist also methodisch absichtlich konservativ ausgelegt +(`min_n=5`, „ausreichend"-Flag, Sample-Size-Banner sichtbar). + +## Optionen + +### Option A — Materialisiertes Aggregat als eigene Tabelle (`stimm_index`) + +Bei jedem Assessment-Insert oder Vote-Insert ein Trigger, der ein +fertiges Aggregat in eine eigene Tabelle schreibt. + +- **Pro:** Lese-Latenz minimal. +- **Kontra:** Konsistenz-Logik in zwei Codepfaden (Insert-Trigger + + Bulk-Reindex), Schema-Migration nötig, schwer testbar. + +### Option B — On-the-fly-JOIN mit TTL-Cache + +Aggregat-Funktionen lesen synchron aus den Quelltabellen, JOIN per +SQL, Aggregation in Python, Ergebnis 60s im Process-Memory cachen. + +- **Pro:** Keine Schema-Migration, ein Codepfad, testbar wie reine + Funktionen, 60s-TTL deckt Burst-Traffic ab. +- **Kontra:** Erste Anfrage nach Cache-Miss ~3 s, danach ~0,17 s. + +### Option C — Computed-View + +SQLite-View mit dem JOIN. Aggregation im SQL. + +- **Pro:** Eine Query. +- **Kontra:** SQLite-Views sind nicht persistent indexiert, + Performance schlechter als B; aggregations-Logik mit + GROUP_CONCAT-JSON-Tricks unleserlich. + +## Entscheidung + +**Option B** umgesetzt, ergänzt um zwei Marker im Antrag-Detail: + +1. **Vier Aggregat-Funktionen** in `app/auswertungen.py`: + - `aggregate_stimm_index(filter_bl, filter_wp, exclude_antragsteller, min_n)` + → pro Fraktion `Ø(gwoe_score|JA) − Ø(gwoe_score|NEIN)`, Domain `[−10, +10]`. + - `aggregate_heuchelei(...)` → Anteil der Anträge mit + `wahlprogramm.score(F)≥7 ∧ F∈NEIN`. + - `aggregate_stimm_index_pro_wert(...)` → Heatmap Fraktion×{Würde, + Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie}. + - `aggregate_stimm_index_cross_bl(...)` → gleiche Fraktion in + mehreren Ländern nebeneinander. + +2. **Helper `_load_assessments_with_votes`** mit dem zentralen JOIN: + ```sql + SELECT a.…, p.fraktionen_ja, p.fraktionen_nein, p.fraktionen_enthaltung + FROM assessments a + INNER JOIN plenum_vote_results p + ON a.bundesland = p.bundesland AND a.drucksache = p.drucksache + WHERE a.gwoe_score IS NOT NULL + ``` + +3. **Antrag-Detail-Marker** (`templates/v2/screens/antrag_detail.html`): + - **Heuchelei-⚠** neben jeder NEIN-Fraktion mit eigenem + `wahlprogramm.score≥7`. Tooltip nennt den Score. + - **Konsistenz-Block** über dem Vote-Block: rot bei Konflikt + („unterstützen" + abgelehnt; oder „ablehnen" + angenommen), + grün bei Übereinstimmung. Bei mehreren Votes (Überweisung → + Endabstimmung) wird das erste *definitive* Outcome + (`angenommen|abgelehnt|bestätigt`) genommen. + +4. **Sample-Size-Ehrlichkeit:** `min_n=5` als Default, „ausreichend"- + Flag pro Fraktion, Caveat-Banner über jedem Chart, Nutzer kann + Antragsteller-Ausschluss togglen. + +5. **CSV-Export** `/api/auswertungen/stimmverhalten.csv` als + Long-Format für externe Auswertung. + +## Konsequenzen + +### Positiv + +- Eine Domain-Funktion pro Aussage, leicht testbar. +- Cache-Invalidation trivial: nach Assessment-Insert oder Vote-Ingest + `cache_clear()` aufrufen (siehe `news_aggregator.run_aggregator`). +- Fraktionsnamen sind in beiden Quellen vornormalisiert (`SPD`, + `GRÜNE`, …) — kein zusätzlicher Mapping-Layer im JOIN. +- Heuchelei- und Konsistenz-Marker im Detail sind die punktgenauen + Belege für die aggregierten Aussagen im `/auswertungen`-Tab — der + Nutzer kann von Aggregat zu Antrag durchklicken. + +### Negativ + +- Daten-Lücke zwischen Votes und Bewertungen ist sichtbar: 7281 vs. + ~96 Bewertungen, nur 19 in beiden Mengen. Die Aggregate sagen + „ausreichend=False" für viele Fraktionen. Das ist methodisch + ehrlich, aber bedeutet: das Feature wird erst nach **Phase 3 + (Vote-Orphans-Auto-Bewertung)** seine volle Aussagekraft + entfalten. +- Cache-Lebensdauer 60 s: bei `--reload`-Dev-Server ist der Cache + nach jedem Auto-Reload weg. Akzeptabel für dev, in prod ein + Worker-Prozess. + +### Folgen für andere ADRs + +- **0009 Protokoll-Parser-Registry**: pro neuem BL-Parser wachsen + die Daten in `plenum_vote_results`, was den JOIN-Recall direkt + hebt. Phase 5 hardcases verstärken Aussagen aus Phase 1. +- **0011 Aktuelle-Themen × PM-Generator** (folgend): der + Konsistenz-Block ist Vorlage für Empfehlungs-vs-Vote-Konflikte + als PM-Anlass. diff --git a/docs/adr/0011-aktuelle-themen-pm-generator.md b/docs/adr/0011-aktuelle-themen-pm-generator.md new file mode 100644 index 0000000..d2217ab --- /dev/null +++ b/docs/adr/0011-aktuelle-themen-pm-generator.md @@ -0,0 +1,144 @@ +# 0011 — Aktuelle-Themen-Dashboard mit PM-Generator + +| | | +|---|---| +| **Status** | accepted | +| **Datum** | 2026-05-06 | +| **Refs** | #170, MEMORY/project_aktuelle_themen.md | + +## Kontext + +Vor diesem ADR existierte kein Bezug zwischen aktuellen Nachrichten +(Tagespresse, Bundestagsmeldungen) und den GWÖ-bewerteten +Parlamentsanträgen. Die User-Anforderung war: + +> „Pressemitteilungen zu aktuellen Themen × Anträgen — Matching +> warum es ein Thema ist, plus Knopf für PM-Generierung." + +Initial-Vorschlag war ein RND-Abo (`rnd.de`), aber dort verbietet +`robots.txt` per `User-agent: ClaudeBot/GPTBot Disallow: /` die +KI-Verarbeitung. Damit waren nur Quellen mit AI-erlaubender Lizenz +zulässig: öffentlich-rechtlich (Tagesschau-API) und parlamentarisch +(Bundestag-RSS). + +Zweitens stellte sich die Frage: *Wie* generiert man aus einem +politischen Antrag plus einer News-Lage einen brauchbaren PM-Text? +Erste qwen-max-Versuche produzierten den GWÖ-Score als Zahl, listeten +GWÖ-Werte auf und nutzten Matrix-Codes wie „D2 Würde×Mitarbeitende". +Für eine Pressemitteilung an Bürger:innen unbrauchbar. + +## Optionen + +### Option A — Auto-PM auf jeden News-Match + +Beim Aggregator-Lauf für jeden News-Antrag-Match einen LLM-Call. + +- **Pro:** Maximale Abdeckung. +- **Kontra:** LLM-Kosten unkontrollierbar, viele PMs sind irrelevant + oder qualitativ schwach. Falsches Outcome-Gewicht. + +### Option B — Manueller „Generieren"-Knopf pro Match + +User entscheidet, der Match liefert nur das Match. PM-Erzeugung ist +ein expliziter LLM-Call mit qwen-max. + +- **Pro:** Kostenkontrolle, Qualitätsprüfung pro Stück. +- **Kontra:** Volume gering — aber das ist OK, PMs sind editorial. + +### Option C — Pre-aggregiertes Cluster, ein PM pro Cluster + +Cluster die News-Matches und schlage pro Cluster eine PM vor. + +- **Pro:** Konsolidiert ähnliche News. +- **Kontra:** Cluster-Qualität auf den dünnen News-Volumen heute + nicht trennscharf genug. + +## Entscheidung + +**Option B** ist Hauptmodus, mit Option-C-Cluster-Anzeige als +Übersicht (kein automatischer Cluster-PM). + +### Bausteine + +1. **News-Aggregator** (`app/news_aggregator.py`): + - Cron-Wrapper `scripts/auto-fetch-news.sh` läuft alle 30 min. + - Quellen: Tagesschau-API (`/api2u/news?ressort=…`), Bundestag-RSS. + Quellen mit AI-Bann (RND, Spiegel, etc.) **strikt ausgeschlossen** + siehe MEMORY/project_aktuelle_themen.md. + - Ergebnis in `news_articles` (additiv: titel, url, source, ressort, + summary, embedded_at, embedding). + - Nach Insert: `themen_matching.cache_clear()`. + +2. **Match Engine** (`app/themen_matching.py`): + - Cosine-Similarity zwischen News-Embedding (v4) und + Assessment-Embedding (v4) — beide in gleicher Vector-Space. + - `compute_relevance(matches)` aggregiert pro News auf + `high|mid|low|none`. + - `aggregate_top_themen(only_relevant, single_date, …)` mit + TTL-60s-Cache. + - `aggregate_news_cluster(days)` als Greedy-Embedding-Cluster. + - `aggregate_top_antraege_with_news` als Reverse-View. + +3. **PM-Generator** (`app/presse_generator.py`): + - Persona-Prompt „Pressereferent:in einer GWÖ-Initiative" mit + **harter Verbotsliste** (keine GWÖ-Werte-Listen, keine Scores, + keine Matrix-Codes, keine Floskeln) und **Few-Shot-Beispiel** + (Schlecht/Gut). + - Konkrete Bürger:innen-Lebenslagen pro Absatz (Pflegebedürftige, + Mieter:innen, Auszubildende, Pendler:innen, …) — der Antrag + muss als „was ändert sich konkret" beschrieben werden. + - 320–380 Worte, 6 Absätze, getrennt durch `\n\n`. + - **`json_object_mode=True`** + `_recover_unescaped_newlines` + Fallback (qwen-max produziert gelegentlich rohe `\n`-Bytes + im JSON-String, was den Standard-Parser brechen würde). + - Sparsame **fett**-Hervorhebungen (max. 1 pro Absatz, nur Zahlen + oder zentrale Effekte) — wird vom Mini-Markdown-Renderer im + Frontend (`renderPmBody`) als `` gerendert. + +4. **Versionierung statt Force-Override**: + - Neuer Draft pro `force=True`-Aufruf — alle Versionen bleiben in + `presse_drafts`. + - Frontend zeigt Dropdown mit allen Versionen für + `(drucksache, news_url)`. + - `_find_existing_draft()` für Idempotenz-Schutz im Default-Pfad. + +5. **Frontend** (`templates/v2/screens/aktuelle-themen.html`): + - 5 Tabs: News×Anträge, Themen-Cluster, GWÖ-Top-Anträge, + News-Volumen-Zeitreihe, PM-Entwürfe. + - Pre-Filter `only_relevant` (Default an). + - GWÖ-Relevanz-Pills rot/orange/grün. + - Chart-Click filtert auf Datum. + - PM-Modal mit Versions-Dropdown, „Mail"-Direkt-Link + (`mailto:` mit prefilled Subject+Body, Längen-Check), Clipboard, + `renderPmBody` als Mini-Markdown-Renderer. + +## Konsequenzen + +### Positiv + +- LLM-Kosten kontrollierbar (manueller Trigger). +- Keine Daten-Lecks zu Quellen mit AI-Bann. +- Versionierung erlaubt Iteration ohne Verlust früherer Entwürfe. +- Persona-Prompt + Few-Shot-Trick stabilisiert die Sprache; Smoketest + zeigt qwen-max nutzt **fett** dezent (1 Hervorhebung pro PM). + +### Negativ + +- Modell-Lock-in auf qwen-max für PMs. Bei Modell-Wechsel müssen + die Pattern-Recovery-Heuristiken (Newline-Recovery, Post-Process + für `."Großbuchstabe`-Patterns) neu kalibriert werden. +- `json_object_mode` ist DashScope-spezifisch — falls künftig auf + Anthropic/OpenAI ge-portet, braucht es eine eigene Adapter-Schicht + (heute via `LlmRequest.json_object_mode`-Flag in + `qwen_bewerter._post_to_dashscope` realisiert). + +### Folgen für andere ADRs + +- **0010 Stimmverhalten-Aggregat**: Der Konsistenz-Block im + Antrag-Detail („Mehrheit kontra GWÖ-Empfehlung") ist Anlass für + PM-Generierung — kann in einer Phase-4-Iteration als + PM-Auto-Vorschlag genutzt werden. +- **0001 LLM-Citation-Binding**: PM-Generator zitiert *nicht* aus + Wahlprogrammen, daher ist Citation-Binding hier nicht + einschlägig. Für News-Zitate gilt: News-URL als Quelle ist + authoritativ, PM-Body darf paraphrasieren ohne wörtliches Zitat. diff --git a/docs/adr/index.md b/docs/adr/index.md index 5ee5c9b..43ff4fb 100644 --- a/docs/adr/index.md +++ b/docs/adr/index.md @@ -26,6 +26,8 @@ und Konsequenzen. Format inspiriert von [Michael Nygard](https://cognitect.com/b | [0007](0007-test-taxonomy.md) | Test-Taxonomie (Unit / Integration / E2E / Property / Smoke) | accepted | 2026-04-28 | | [0008](0008-ddd-lightweight-migration.md) | DDD-Lightweight-Migration (Repository, LLM-Port, Domain-Verhalten) | accepted | 2026-04-20 | | [0009](0009-protokoll-parser-registry.md) | Plenarprotokoll-Parser-Registry pro Bundesland | accepted | 2026-04-28 | +| [0010](0010-stimmverhalten-gwoe-aggregat.md) | Stimmverhalten × GWÖ-Bewertung als JOIN-Aggregat (Heuchelei + Konsistenz) | accepted | 2026-05-06 | +| [0011](0011-aktuelle-themen-pm-generator.md) | Aktuelle-Themen-Dashboard mit PM-Generator (Persona-Prompt + Versionierung) | accepted | 2026-05-06 | ## Wann ADR, wann nicht