docs(adr): 0010 Stimmverhalten×GWÖ-Aggregat + 0011 Aktuelle-Themen+PM
ADR 0010 dokumentiert die JOIN-Aggregat-Entscheidung (Option B) mit 4 Aggregat-Funktionen, Heuchelei- und Konsistenz-Markern im Detail, Sample-Size-Ehrlichkeit und Cache-Strategie. ADR 0011 dokumentiert Aktuelle-Themen mit Persona-Prompt-Strategie, Versionierung statt Force-Override, AI-Bann-Quellen-Filter und json_object_mode-Recovery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c158cd5fb8
commit
1ba9d8e5d9
127
docs/adr/0010-stimmverhalten-gwoe-aggregat.md
Normal file
127
docs/adr/0010-stimmverhalten-gwoe-aggregat.md
Normal file
@ -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.
|
||||
144
docs/adr/0011-aktuelle-themen-pm-generator.md
Normal file
144
docs/adr/0011-aktuelle-themen-pm-generator.md
Normal file
@ -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 `<strong>` 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.
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user