#62 Phase 1+3: ADRs + Doku-Struktur in webapp/docs/
Architektur-Entscheidung aus Issue #62: Diátaxis-Framework für Doku- Pflege ohne Drift. Pflege im Repo, ADRs immutable, Stale-Snapshots explizit als Archiv markiert. Phase 1 — Architecture Decision Records: - docs/README.md — Diátaxis-Index, Erklärung was wo dokumentiert wird - docs/adr/README.md — ADR-Workflow + Index - docs/adr/template.md — Vorlage für neue ADRs - docs/adr/0001-llm-citation-binding.md — Issue #60 Doppel-Fix-Story (A=ENUM-Anker, B=server-seitige Rekonstruktion, warum Option C verworfen) - docs/adr/0002-adapter-architecture.md — ParlamentAdapter-Basisklasse + Registry, Klassen vs. Strategy vs. Modul-pro-Adapter - docs/adr/0003-citation-property-tests.md — Sub-D Strategie, warum Property-Test gegen echte PDFs statt Schema-Tests oder Online-Verify - docs/adr/0004-deployment-workflow.md — Docker-Compose + Volumes Standard-Workflow + SN-XML-Sonderpfad + Container-UTC-Gotcha Phase 3 — Stale Doku archiviert: - DOKUMENTATION.md (24.März, Skript-Architektur vor Webapp-Migrate) → docs/archive/DOKUMENTATION-2026-03-24.md - STATUS-2026-03-28.md (Tagesstand-Snapshot) → docs/archive/STATUS-2026-03-28.md - README.md (28.März, listet nur NRW-Adapter, vor 16 weiteren BLs) → docs/archive/README-2026-03-28.md - docs/archive/README.md erklärt warum die Files da sind und warum niemand sie überschreiben oder ersetzen sollte Plus neue Top-Level-README.md im Project-Root (außerhalb git, da project-root kein Repo ist) als Folder-Index für den User. CLAUDE.md ergänzt um Doku-Sektion mit Verweis auf docs/adr/. Phase 2 (mkdocs Setup) folgt separat — braucht eine Docker-Image- Erweiterung, die ich nicht autark einrollen will ohne Decision. Tests: 194/194 grün (keine Code-Änderung). Refs: #62
This commit is contained in:
parent
4ec6190416
commit
45379a2639
48
docs/README.md
Normal file
48
docs/README.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# GWÖ-Antragsprüfer — Dokumentation
|
||||||
|
|
||||||
|
Diese Dokumentation folgt dem [Diátaxis-Framework](https://diataxis.fr) und ist
|
||||||
|
nach Funktion (nicht nach Themen) organisiert. Drift-immun durch klare Trennung:
|
||||||
|
Reference wird aus dem Code generiert, ADRs sind immutable, Tutorials/How-to
|
||||||
|
sind manuell gepflegt aber knapp.
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── README.md ← du bist hier
|
||||||
|
├── adr/ Architecture Decision Records (immutable)
|
||||||
|
│ ├── README.md ADR-Index + Workflow
|
||||||
|
│ ├── template.md Template für neue ADRs
|
||||||
|
│ └── NNNN-titel.md Eine Datei pro Entscheidung
|
||||||
|
└── archive/ Historische Snapshots, nicht autoritativ
|
||||||
|
```
|
||||||
|
|
||||||
|
Geplant für später (siehe Issue #62):
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── reference/ ← mkdocs autodoc-Output (nicht eingecheckt)
|
||||||
|
├── tutorials/ ← Erst-Schritt-Anleitungen
|
||||||
|
├── how-to/ ← Aufgaben-orientiert, "wie deploye ich"
|
||||||
|
└── explanation/ ← Hintergründe, Konzepte
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nicht hier dokumentiert (bewusst)
|
||||||
|
|
||||||
|
| Was | Wo |
|
||||||
|
|---|---|
|
||||||
|
| API-Reference (Endpoints, Models, Schemas) | Auto-generiert aus FastAPI/Pydantic, nicht in docs/ einchecken |
|
||||||
|
| Code-Reference (Klassen, Funktionen) | mkdocstrings aus den Docstrings (geplant Phase 2) |
|
||||||
|
| Aktueller Projekt-Stand / Issues | Gitea Issues — `repo.toppyr.de/tobias/gwoe-antragspruefer/issues` |
|
||||||
|
| Onboarding für KI-assisted Coder | `CLAUDE.md` im Repo-Root |
|
||||||
|
| Live-System-Status | `https://gwoe.toppyr.de/auswertungen` (dynamisches Dashboard) |
|
||||||
|
| Memory der KI-Sessions | `~/.claude/projects/<projekt>/memory/` (privat) |
|
||||||
|
|
||||||
|
## Gegen Drift
|
||||||
|
|
||||||
|
- ADRs sind **immutable**: nie überschreiben, sondern bei Änderung mit einem
|
||||||
|
neuen ADR superseden, der den alten in seinem Header referenziert.
|
||||||
|
- Reference wird aus dem Code generiert, **nie** von Hand gepflegt.
|
||||||
|
- `docs/archive/` enthält historische Status-Files. Werden gelesen aber **nicht
|
||||||
|
aktualisiert**. Wenn etwas davon noch wahr ist, gehört es in einen ADR oder
|
||||||
|
in die generierte Reference, nicht in einen neuen Status-Snapshot.
|
||||||
128
docs/adr/0001-llm-citation-binding.md
Normal file
128
docs/adr/0001-llm-citation-binding.md
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
# 0001 — LLM-Citations server-seitig binden statt prompt-seitig
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Status** | accepted |
|
||||||
|
| **Datum** | 2026-04-10 |
|
||||||
|
| **Refs** | Issue #60, Commits eb045d0/ed64399/db3ada9/6ced7ae, Sub-D Tests |
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Der GWÖ-Antragsprüfer lässt ein LLM (Qwen Plus via DashScope) parlamentarische
|
||||||
|
Anträge bewerten. Pro Antrag werden via Embedding-Retrieval relevante Chunks
|
||||||
|
aus den Wahl- und Grundsatzprogrammen der involvierten Parteien zugespielt.
|
||||||
|
Das LLM gibt im JSON-Output `wahlprogramm_scores[*].wahlprogramm.zitate[*]`
|
||||||
|
mit jeweils `text`, `quelle` (Source-Label, z.B. "GRÜNE NRW Wahlprogramm
|
||||||
|
2022, S. 58") und `url` zurück.
|
||||||
|
|
||||||
|
Beim ersten Live-Lauf des Sub-D Citation-Property-Tests (siehe ADR 0003)
|
||||||
|
gegen die Prod-DB wurden drei Halluzinations-Cases gefunden: das LLM hatte
|
||||||
|
Snippets erfunden und unter realen Source-Labels zitiert. Issue #60 wurde
|
||||||
|
geöffnet.
|
||||||
|
|
||||||
|
## Optionen
|
||||||
|
|
||||||
|
Drei strukturell unterschiedliche Wege, das LLM zu binden:
|
||||||
|
|
||||||
|
### Option A — Schärferer Prompt
|
||||||
|
|
||||||
|
Die ZITATEREGEL im User-Prompt wird verschärft. Konkret:
|
||||||
|
|
||||||
|
1. Jeder retrievte Chunk wird mit einer ENUM-ID `[Q1]`, `[Q2]`, … getaggt.
|
||||||
|
2. Das LLM wird angewiesen, jedes Zitat per `[Qn]`-Tag an einen Chunk zu
|
||||||
|
binden, den `text` wörtlich aus diesem Chunk zu kopieren und das
|
||||||
|
Source-Label exakt zu übernehmen.
|
||||||
|
3. Top-K wird von 2 → 5 erhöht, damit die "richtige" Seite häufiger im
|
||||||
|
Retrieval-Window landet.
|
||||||
|
|
||||||
|
**Vorteile:** prompt-only, kleiner Diff, keine Server-Pipeline-Änderung.
|
||||||
|
|
||||||
|
**Nachteile:** ENUM-Anker im Prompt ist ein **weicher** Hint. Das LLM darf
|
||||||
|
zwar den Snippet aus Chunk Qn nehmen, aber es gibt nichts, was es daran
|
||||||
|
hindert, die Seite aus Chunk Qm in `quelle` zu schreiben. Cross-Mix bleibt
|
||||||
|
möglich.
|
||||||
|
|
||||||
|
### Option B — Server-seitige Quellen-Rekonstruktion
|
||||||
|
|
||||||
|
Nach dem LLM-Call werden die emittierten Zitate Server-seitig nachgearbeitet:
|
||||||
|
|
||||||
|
1. Für jedes Zitat im Output-JSON wird der `text` per Substring oder
|
||||||
|
5-Wort-Anker gegen **alle** retrievten Chunks gematcht.
|
||||||
|
2. Bei Match: `quelle` und `url` werden aus dem matchenden Chunk **konstruiert**
|
||||||
|
und der LLM-Output für diese Felder verworfen.
|
||||||
|
3. Bei kein-Match: das ganze Zitat wird verworfen.
|
||||||
|
|
||||||
|
**Vorteile:** strukturell — der LLM hat keine Möglichkeit mehr, eine falsche
|
||||||
|
Quelle anzugeben. Die einzigen zwei Outcomes sind "korrekt zitiert" oder
|
||||||
|
"verworfen".
|
||||||
|
|
||||||
|
**Nachteile:** zusätzlicher Server-seitiger Schritt nach `json.loads` und
|
||||||
|
vor Pydantic-Validation. Match-Logik muss konsistent mit Sub-D bleiben.
|
||||||
|
|
||||||
|
### Option C — Schema-Änderung mit `quote_id`-Feld
|
||||||
|
|
||||||
|
Statt `quelle` direkt zu emittieren, soll das LLM ein neues Feld `quote_id`
|
||||||
|
(z.B. `"Q3"`) liefern, aus dem der Server `quelle`/`url` rekonstruiert.
|
||||||
|
|
||||||
|
**Vorteile:** explizit schema-modelliert, keine Heuristik.
|
||||||
|
|
||||||
|
**Nachteile:** invasiv (Pydantic-Modell, JSON-Schema, Frontend, DB-Migration);
|
||||||
|
verlässt sich darauf, dass das LLM die ID korrekt setzt — wenn es lügt,
|
||||||
|
ist die Bindung futsch.
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
**A + B kombiniert.** Option A liefert die Vorab-Härtung im Prompt — sie
|
||||||
|
fängt den Großteil der Cases (etwa 80%). Option B ist die strukturelle
|
||||||
|
Backstop, die die restlichen 20% (echter Snippet aus Qn, falsche Seite
|
||||||
|
aus Qm) verlässlich abdeckt. Option C wurde verworfen, weil der Schema-
|
||||||
|
Eingriff den Aufwand nicht rechtfertigt, solange B die LLM-Eingabe ohnehin
|
||||||
|
ignoriert.
|
||||||
|
|
||||||
|
Konkret in dieser Reihenfolge implementiert:
|
||||||
|
|
||||||
|
1. **`db3ada9`** — Option A: ENUM-Anker `[Q1]/[Q2]/…` im Prompt + Top-K 2→5.
|
||||||
|
Live-Verifikation der drei Original-Cases aus #60: 13/13 ok.
|
||||||
|
2. **Sub-D Live-Run gegen Prod-DB**: 45/46 grün, ein neuer Case (BB 8/673)
|
||||||
|
gefunden mit "echter Snippet, falsche Seite". Beweis dass A allein nicht
|
||||||
|
reicht.
|
||||||
|
3. **`6ced7ae`** — Option B: `embeddings.reconstruct_zitate(data, semantic_quotes)`
|
||||||
|
nach `json.loads` und vor Pydantic-Validation. Helpers `find_chunk_for_text`
|
||||||
|
und `_normalize_for_match` mit identischer Logik wie Sub-D. Bei Match wird
|
||||||
|
`_chunk_source_label` und `_chunk_pdf_url` angewendet, bei No-Match wird
|
||||||
|
das Zitat gedroppt.
|
||||||
|
4. Sub-D Re-Run nach B: **52/52 grün** über NRW/LSA/BE/MV/BB/BUND.
|
||||||
|
|
||||||
|
## Konsequenzen
|
||||||
|
|
||||||
|
### Positiv
|
||||||
|
|
||||||
|
- **Strukturelle Garantie**: nach `reconstruct_zitate` kann ein im DB
|
||||||
|
gespeichertes Zitat nur noch eines von zwei Dingen sein: korrekt zitiert
|
||||||
|
aus einem real retrievten Chunk, oder gar nicht da.
|
||||||
|
- **Sub-D als CI-Gate**: das Property-Test-Pattern fängt jede künftige
|
||||||
|
Regression dieser Bug-Klasse.
|
||||||
|
- **Server vertraut LLM nicht mehr** für `quelle`/`url`. Künftige Modell-
|
||||||
|
Wechsel oder Prompt-Drift können die Quellen-Korrektheit nicht mehr brechen.
|
||||||
|
|
||||||
|
### Negativ
|
||||||
|
|
||||||
|
- **Fehlende Match-Heuristik = stilles Verschwinden**: Wenn der LLM einen
|
||||||
|
echten Chunk leicht paraphrasiert (z.B. Komma anders gesetzt), verwirft
|
||||||
|
Option B das Zitat. Die 5-Wort-Anker-Fallback fängt das in den meisten
|
||||||
|
Fällen, aber Edge-Cases bleiben. Mitigation: doctest-style Sub-D-Run pro
|
||||||
|
Deploy + Monitor der `wahlprogramm_scores[*].zitate`-Counts pro Assessment.
|
||||||
|
- **Code-Pfad-Komplexität**: drei Schichten (Prompt-ENUM, Server-Postprocess,
|
||||||
|
Sub-D-Test) statt einer. Dafür ist jede Schicht isoliert testbar.
|
||||||
|
- **`embeddings.py` hat jetzt mehr Verantwortung** als nur Embedding-Retrieval
|
||||||
|
— auch Match-Helpers und Source-Label-Konstruktion. Akzeptabel solange das
|
||||||
|
Modul thematisch geschlossen bleibt; bei weiterem Wachstum als
|
||||||
|
`citations.py` extrahieren.
|
||||||
|
|
||||||
|
### Folgen für andere ADRs
|
||||||
|
|
||||||
|
- **ADR 0003** (Sub-D Citation-Property-Tests) ist die Test-Säule dieser
|
||||||
|
Entscheidung — wenn 0003 jemals supersedet wird, bricht 0001 ohne den
|
||||||
|
Test-Backstop möglicherweise still.
|
||||||
|
- Neue Adapter (zukünftige Bundesländer) müssen sich nicht extra um
|
||||||
|
Citation-Korrektheit kümmern; der Server-Postprocess ist adapter-agnostisch.
|
||||||
119
docs/adr/0002-adapter-architecture.md
Normal file
119
docs/adr/0002-adapter-architecture.md
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
# 0002 — Adapter-Pattern mit ParlamentAdapter-Basisklasse + Registry
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Status** | accepted |
|
||||||
|
| **Datum** | 2026-04-10 |
|
||||||
|
| **Refs** | Issues #2, #3, #4, #19, #21, #23, #24, #26, #56, parlamente.py |
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Der GWÖ-Antragsprüfer soll Anträge aus 16 Bundesländern + dem Bundestag
|
||||||
|
analysieren können. Jedes Parlament hat ein eigenes Doku-System mit
|
||||||
|
unterschiedlichen Endpoints, Schemas, Auth-Anforderungen und Quirks:
|
||||||
|
|
||||||
|
| Familie | Mitglieder | Charakter |
|
||||||
|
|---|---|---|
|
||||||
|
| portala/eUI | LSA, BE, BB, RP | JSON-Tree-Search mit `parsed`-Strings, HTML-Hits in efxRecordRepeater |
|
||||||
|
| StarWeb (eUI v2) | HE | `browse.tt.json` 2-step + Perl-Dump-Comments |
|
||||||
|
| PARiS (Java-Servlet) | HB | älterer StarWeb-Vorgänger, eigenes Hit-Format |
|
||||||
|
| ParlDok 8.x | MV, HH, TH | SPA mit Fulltext/Search + Resultpage Pagination |
|
||||||
|
| StarFinder-CGI | SH | `lissh.lvn.parlanet.de/cgi-bin/starfinder/0` |
|
||||||
|
| OPAL native | NRW | älteres Eigensystem |
|
||||||
|
| PARLIS (eUI-Variante) | BW | polling, JSON-in-HTML-Comments |
|
||||||
|
| Eigensysteme | BY (TYPO3-Solr), SL (Umbraco/.NET), SN (ASP.NET-Webforms), BUND (DIP-API) | jeweils komplett unique |
|
||||||
|
|
||||||
|
## Optionen
|
||||||
|
|
||||||
|
### Option A — Eine Mega-Klasse mit Strategy-Pattern
|
||||||
|
|
||||||
|
Eine `ParlamentAdapter`-Klasse mit Strategy-Object pro Doku-System,
|
||||||
|
gewählt über Konfiguration.
|
||||||
|
|
||||||
|
**Vorteile:** zentral, ein Eintrittspunkt für gemeinsame Logik wie
|
||||||
|
Fraktion-Normalisierung.
|
||||||
|
|
||||||
|
**Nachteile:** kompliziert sich schnell, wenn jede Strategy ihre eigenen
|
||||||
|
Quirks hat (z.B. BB hat Datum-vor-Drucksachennummer im h6, BE hat
|
||||||
|
730d-Window-Hack, HE hat Perl-Hex-Decoder). Die Trennung wäre
|
||||||
|
"Strategy mit Subclasses" — und damit sind wir bei B.
|
||||||
|
|
||||||
|
### Option B — Abstract Base + Eine konkrete Klasse pro Familie
|
||||||
|
|
||||||
|
`ParlamentAdapter(ABC)` mit `search/get_document/download_text`-Methoden.
|
||||||
|
Pro Doku-System eine konkrete Klasse:
|
||||||
|
`PortalaAdapter`, `ParLDokAdapter`, `StarFinderCGIAdapter`,
|
||||||
|
`StarWebHEAdapter`, `PARiSHBAdapter`, `PARLISAdapter`, `BayernAdapter`,
|
||||||
|
`SaarlandAdapter`, `BundestagAdapter`, `SNEdasXmlAdapter`, `NRWAdapter`.
|
||||||
|
|
||||||
|
Wo möglich, wird die Klasse via Konstruktor-Parameter parametrisiert
|
||||||
|
(`PortalaAdapter` nimmt 6 BLs auf, `ParLDokAdapter` 3) statt jede
|
||||||
|
Wiederverwendung als Subklasse.
|
||||||
|
|
||||||
|
Registriert in einem `ADAPTERS: dict[str, ParlamentAdapter]` am Datei-Ende,
|
||||||
|
indexiert per BL-Code (`"NRW"`, `"BUND"`, …).
|
||||||
|
|
||||||
|
**Vorteile:**
|
||||||
|
- Klare Verantwortlichkeit: ein Test-Lauf gegen einen LT trifft genau
|
||||||
|
eine Klasse.
|
||||||
|
- Reverse-Engineering-Findings können als Class-Docstring dokumentiert
|
||||||
|
werden, wo sie hingehören.
|
||||||
|
- Parametrisierung erlaubt Wiederverwendung ohne Vererbungs-Hierarchie.
|
||||||
|
- `get_adapter(bundesland)` ist O(1).
|
||||||
|
|
||||||
|
**Nachteile:**
|
||||||
|
- Cross-Cutting-Concerns (Fraktion-Normalisierung) müssen separat gelöst
|
||||||
|
werden — siehe ADR-Folge zur `parteien`-Zentralisierung in #55.
|
||||||
|
|
||||||
|
### Option C — Separates Modul pro Adapter
|
||||||
|
|
||||||
|
Ein Python-File pro Adapter (z.B. `app/adapters/lsa.py`).
|
||||||
|
|
||||||
|
**Vorteile:** maximale Datei-Granularität.
|
||||||
|
|
||||||
|
**Nachteile:** Import-Overhead, der Registry-Eintrag muss in einer dritten
|
||||||
|
Datei liegen, kein nennenswerter Vorteil gegenüber B solange die
|
||||||
|
Adapter-Klassen unter ~500 Zeilen bleiben (PortalaAdapter ist mit ~520
|
||||||
|
Zeilen der größte).
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
**Option B**. Eine `parlamente.py` mit einer Klasse pro Doku-System,
|
||||||
|
parametrisierbar wo sinnvoll, registriert in `ADAPTERS`. Cross-Cutting-Logik
|
||||||
|
wie Fraktion-Normalisierung wandert in separate Module (`parteien.py`,
|
||||||
|
`bundeslaender.py`) und wird per Import oder Funktions-Argument injiziert.
|
||||||
|
|
||||||
|
## Konsequenzen
|
||||||
|
|
||||||
|
### Positiv
|
||||||
|
|
||||||
|
- **17 Adapter sauber separierbar** in ~3000 Zeilen `parlamente.py`. Jeder
|
||||||
|
Adapter kommt mit seinem Reverse-Engineering-Kommentar als Docstring,
|
||||||
|
was bei Re-Reads nach Wochen die Wiedereinarbeit auf Minuten bringt.
|
||||||
|
- **Tests parametrisieren über `ADAPTERS`-Registry**: ein einzelner
|
||||||
|
Test-Helper iteriert alle aktiven Adapter und prüft die Akzeptanz-
|
||||||
|
Kriterien (search liefert ≥3 Hits, get_document funktioniert,
|
||||||
|
download_text extrahiert Text).
|
||||||
|
- **Neue Bundesländer** kommen als 100-300-Zeilen-Klasse + 1 Eintrag in
|
||||||
|
`ADAPTERS` rein, ohne Berührung anderer Adapter.
|
||||||
|
|
||||||
|
### Negativ
|
||||||
|
|
||||||
|
- **`parlamente.py` ist groß** (~3000 Zeilen, 17 Klassen). Code-Navigation
|
||||||
|
via Grep statt Auto-Imports.
|
||||||
|
- **Reverse-Engineering-Findings sind in Kommentaren**, nicht in einer
|
||||||
|
reference-Sektion. Wenn sich das System ändert, kann der Kommentar
|
||||||
|
veralten — Mitigation: jede Reverse-Engineering-Session legt einen
|
||||||
|
HAR-Trace in `TEMP/` und referenziert ihn im Class-Docstring.
|
||||||
|
- **Adapter-Bugs leak nicht in andere Adapter**, aber **gemeinsame
|
||||||
|
Fraktion-Disambiguation** muss manuell konsistent gehalten werden
|
||||||
|
(siehe #55-Refactor und seine Regression in Issue #60-Side-Befund:
|
||||||
|
ein Refactor-Rest in `embeddings.py:528` wurde durch ein anderes
|
||||||
|
Modul verbreitet).
|
||||||
|
|
||||||
|
### Folgen für andere ADRs
|
||||||
|
|
||||||
|
- ADR 0001 (LLM-Citation-Binding) ist davon unabhängig — der Postprocess
|
||||||
|
läuft adapter-agnostisch nach jedem `analyze_antrag`.
|
||||||
|
- Eine künftige Module-Splitter-Entscheidung (`adapters/` als Folder)
|
||||||
|
würde den ADR superseden, wenn `parlamente.py` über ~5000 Zeilen wächst.
|
||||||
106
docs/adr/0003-citation-property-tests.md
Normal file
106
docs/adr/0003-citation-property-tests.md
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
# 0003 — Sub-D Property-Verification: Zitate als Substring der zitierten PDF-Seite
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Status** | accepted |
|
||||||
|
| **Datum** | 2026-04-10 |
|
||||||
|
| **Refs** | Issues #50, #54, #60; tests/integration/test_citations_substring.py |
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Der LLM-Output enthält pro Assessment N Zitate, jedes mit `text`, `quelle`
|
||||||
|
(z.B. "GRÜNE NRW Wahlprogramm 2022, S. 58") und `url`. Wahrscheinlich
|
||||||
|
korrekt — aber wie verifizieren wir das, ohne jedes einzeln händisch
|
||||||
|
nachzuschlagen?
|
||||||
|
|
||||||
|
Die naheliegenden Test-Optionen sind alle unbefriedigend:
|
||||||
|
|
||||||
|
- **Mock-LLM-Tests**: prüfen das Schema, sagen aber nichts über die
|
||||||
|
inhaltliche Korrektheit.
|
||||||
|
- **Snapshot-Tests** der LLM-Outputs: drift mit jedem Modell-Update.
|
||||||
|
- **Manuelles Stichprobenchecken**: skaliert nicht über mehrere BLs.
|
||||||
|
|
||||||
|
## Optionen
|
||||||
|
|
||||||
|
### Option A — Schema-only Tests (was wir vorher hatten)
|
||||||
|
|
||||||
|
Pydantic validiert dass jedes Zitat die Felder `text`, `quelle`, `url` hat
|
||||||
|
und `url` mit `/static/referenzen/` beginnt. Erkennt syntaktische
|
||||||
|
Korruption, aber keine Halluzinationen.
|
||||||
|
|
||||||
|
### Option B — Property-Test gegen die echten PDFs
|
||||||
|
|
||||||
|
Pro Zitat in der Prod-DB:
|
||||||
|
1. `quelle` per Token-Coverage-Match auf den `PROGRAMME`-Eintrag mappen.
|
||||||
|
2. Seitennummer aus `quelle` extrahieren.
|
||||||
|
3. Per `fitz` die PDF-Seite lesen, Whitespace + Soft-Hyphen normalisieren.
|
||||||
|
4. `text` muss als Substring (oder 5-Wort-Anker) in der Seite vorkommen.
|
||||||
|
5. Bug-Klasse 17 (Cross-Bundesland-Zitat): das aufgelöste Programm muss
|
||||||
|
zum Bundesland des Antrags passen, oder ein Grundsatzprogramm sein.
|
||||||
|
|
||||||
|
**Vorteile:** prüft die einzige Eigenschaft die wirklich zählt — "war
|
||||||
|
das was zitiert wird auch wirklich da". Findet Halluzinationen direkt.
|
||||||
|
|
||||||
|
**Nachteile:** braucht eine lokale Kopie der `gwoe-antraege.db` und der
|
||||||
|
Wahlprogramm-PDFs. Test ist Pydantic-Schema-übergreifend (Integration,
|
||||||
|
nicht Unit). Skipped sauber wenn DB nicht gemounted ist.
|
||||||
|
|
||||||
|
### Option C — Online-Verifikation pro Assessment-Insert
|
||||||
|
|
||||||
|
Im `analyze_antrag`-Flow direkt nach LLM-Call jedes Zitat verifizieren
|
||||||
|
und bei Failure abbrechen oder retry.
|
||||||
|
|
||||||
|
**Vorteile:** kein "stale data in DB"-Risiko.
|
||||||
|
|
||||||
|
**Nachteile:** fügt Latenz und Komplexität in den Hot-Path. Die
|
||||||
|
Verifikation ist O(N×M), wo N=Zitate und M=Wahlprogramm-Pages.
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
**Option B als pytest-Integration-Test** — `tests/integration/test_citations_substring.py`,
|
||||||
|
parametrisiert per `_load_recent_assessments(limit_per_bl=5)` × `_flat_zitate()`.
|
||||||
|
|
||||||
|
**Strict substring** als Default-Match (Whitespace + Soft-Hyphen normalisiert,
|
||||||
|
LLM-Truncation-Marker `...` toleriert), **5-Wort-Anker als Fallback** für
|
||||||
|
geringfügige Wort-Drift wie "LLM hat mittendrin gekürzt". Min-Length-Guard
|
||||||
|
von 20 Zeichen verhindert false-positive Matches auf trivialen Snippets.
|
||||||
|
|
||||||
|
Marker `pytestmark = pytest.mark.integration` — der Test läuft nicht in
|
||||||
|
der Default-Suite, sondern explizit per `pytest -m integration`. Skipped
|
||||||
|
wenn `webapp/data/gwoe-antraege.db` nicht existiert (Dev-Setup ohne
|
||||||
|
DB-Kopie).
|
||||||
|
|
||||||
|
Match-Helpers (`_normalize`, `_is_substring`, `_resolve_quelle_to_programm_id`,
|
||||||
|
`_extract_page_number`) sind eigene Unit-Tests in `TestHelpers` — die Match-
|
||||||
|
Logik selbst ist nicht-trivial und braucht ihre Eigenkontrolle.
|
||||||
|
|
||||||
|
## Konsequenzen
|
||||||
|
|
||||||
|
### Positiv
|
||||||
|
|
||||||
|
- **Findet Halluzinationen direkt**: Issue #60 wurde durch den ersten
|
||||||
|
Live-Lauf dieses Tests entdeckt (3 von 36 Citations failed), ohne
|
||||||
|
dass ein Mensch Wahlprogramm-PDFs aufmachen musste.
|
||||||
|
- **Re-runnable als Regression-Gate**: nach jedem Deploy einmal `pytest -m
|
||||||
|
integration` gegen die DB → 0 Failures = OK.
|
||||||
|
- **Test-Logik = Production-Logik**: ADR 0001 Option B (`reconstruct_zitate`)
|
||||||
|
nutzt **identische** Match-Heuristiken (`find_chunk_for_text`,
|
||||||
|
`_normalize_for_match`). Damit kann der Test nichts fangen, was die
|
||||||
|
Production nicht auch fangen würde, und umgekehrt — kein Test-/Prod-Drift.
|
||||||
|
|
||||||
|
### Negativ
|
||||||
|
|
||||||
|
- **Lokale DB-Kopie nötig**: vor jedem Sub-D-Run muss `data/gwoe-antraege.db`
|
||||||
|
vom Container gepullt werden. CI-Integration steht aus.
|
||||||
|
- **Test ist langsam-ish**: ~50 Citations × ein PDF-Open pro Programm ist
|
||||||
|
bei den ~30 indexierten Programmen ~250ms im Ganzen, nicht trivial aber
|
||||||
|
nicht prohibitiv.
|
||||||
|
- **Token-Coverage-Heuristik für Quelle-zu-Programm-Mapping** kann false-
|
||||||
|
positive bei sehr ähnlichen Programmen werden (z.B. CDU NRW 2022 vs. CDU
|
||||||
|
Niedersachsen 2022 — würde durch Bundesland-Bonus-Check abgefangen).
|
||||||
|
|
||||||
|
### Folgen für andere ADRs
|
||||||
|
|
||||||
|
- **ADR 0001** ist von ADR 0003 abhängig — wenn dieser Test entfernt würde,
|
||||||
|
hätte der LLM-Citation-Postprocess keinen Backstop und neue Halluzinations-
|
||||||
|
Bug-Klassen würden still durchrutschen.
|
||||||
135
docs/adr/0004-deployment-workflow.md
Normal file
135
docs/adr/0004-deployment-workflow.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# 0004 — Docker Compose Deploy mit DB-/Reports-Volume und SN-XML-Sonderpfad
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Status** | accepted |
|
||||||
|
| **Datum** | 2026-04-10 |
|
||||||
|
| **Refs** | CLAUDE.md "Deployment", `docker-compose.yml`, Issue #5, project_sn_xml_export |
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Der GWÖ-Antragsprüfer läuft als Docker-Container `gwoe-antragspruefer` auf
|
||||||
|
einem VServer hinter Traefik (Let's Encrypt SSL, Domain
|
||||||
|
`gwoe.toppyr.de`). Code wird via Git aus `repo.toppyr.de` gezogen.
|
||||||
|
|
||||||
|
Drei Subsysteme haben unterschiedliche Lebenszyklen:
|
||||||
|
|
||||||
|
1. **Code** (`app/`) — wird bei jedem Deploy neu kopiert.
|
||||||
|
2. **SQLite-Daten** (`data/gwoe-antraege.db`, `data/embeddings.db`) —
|
||||||
|
Source of Truth, MUSS persistent über Deploys hinweg.
|
||||||
|
3. **PDF-Reports** (`reports/*.pdf`) — sind generierte Artefakte, könnten
|
||||||
|
theoretisch regeneriert werden, sind aber teuer (LLM-Calls + WeasyPrint),
|
||||||
|
also auch persistent.
|
||||||
|
|
||||||
|
Issue #5 hatte einen frühen Schmerzpunkt: der Container-Build hat die
|
||||||
|
DB überschrieben, weil `data/` nicht aus dem Build-Context exkludiert war.
|
||||||
|
|
||||||
|
## Optionen
|
||||||
|
|
||||||
|
### Option A — Alles im Image, manuelle Backups
|
||||||
|
|
||||||
|
Code, DB, Reports zusammen im Image. Backups via `docker cp` vor jedem
|
||||||
|
Deploy.
|
||||||
|
|
||||||
|
**Nachteile:** Image wird gigantisch, jeder Deploy ist ein Risk-Event,
|
||||||
|
kein automatischer Persistenz-Mechanismus.
|
||||||
|
|
||||||
|
### Option B — Docker-Volumes für DB und Reports
|
||||||
|
|
||||||
|
`docker-compose.yml` mountet `./data` und `./reports` vom Host als Volumes.
|
||||||
|
Build kopiert nur `app/` und `requirements.txt`.
|
||||||
|
|
||||||
|
**Vorteile:** Host-Volumes überleben Container-Restart und Image-Rebuild.
|
||||||
|
Backups via Standard-Linux-Tools auf dem Host.
|
||||||
|
|
||||||
|
**Nachteile:** Build-Context muss `data/`, `reports/`, `.env` exkludieren
|
||||||
|
(siehe `.dockerignore`), sonst werden sie versehentlich ins Image kopiert
|
||||||
|
und überschreiben das gemountete Volume bei Container-Start.
|
||||||
|
|
||||||
|
### Option C — Externe DB (Postgres)
|
||||||
|
|
||||||
|
Postgres als separater Container, gwoe-antragspruefer verbindet per asyncpg.
|
||||||
|
|
||||||
|
**Vorteile:** standard, robust, Backups via pg_dump.
|
||||||
|
|
||||||
|
**Nachteile:** Migration aller existierenden Queries, neue Abhängigkeit,
|
||||||
|
mehr Operational Surface. SQLite reicht für die aktuelle Last (~30 Anträge,
|
||||||
|
selten parallele Writes).
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
**Option B**. Docker-Compose mit Host-Volumes für `data/` und `reports/`.
|
||||||
|
`.dockerignore` exkludiert beide aus dem Build-Context.
|
||||||
|
|
||||||
|
### Standard-Deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh vserver 'cd /opt/gwoe-antragspruefer && git pull && docker compose up -d --build'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manueller Tar-Upload (falls Git-Workflow blockiert ist)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd webapp
|
||||||
|
tar czf /tmp/gwoe-webapp.tar.gz \
|
||||||
|
--exclude='venv' --exclude='__pycache__' \
|
||||||
|
--exclude='data' --exclude='reports' --exclude='.env' .
|
||||||
|
scp /tmp/gwoe-webapp.tar.gz vserver:/tmp/
|
||||||
|
ssh vserver 'cd /opt/gwoe-antragspruefer && tar xzf /tmp/gwoe-webapp.tar.gz && docker compose up -d --build'
|
||||||
|
```
|
||||||
|
|
||||||
|
Beachte: `--exclude='data'` ist **zwingend**, sonst überschreibt der Tar
|
||||||
|
die Live-DB.
|
||||||
|
|
||||||
|
### SN-XML-Sonderpfad
|
||||||
|
|
||||||
|
Sachsen hat keinen scrape-baren Endpoint und liest stattdessen wöchentlich
|
||||||
|
manuell exportierte XML-Dumps aus EDAS. Workflow:
|
||||||
|
|
||||||
|
1. User exportiert XML aus EDAS (manuell, im Browser).
|
||||||
|
2. `cp dokumente_export.xml gwoe-antragspruefer:/app/data/sn-edas/`
|
||||||
|
3. `scp` aus dem Container ins Host-Volume — kein Container-Restart nötig.
|
||||||
|
4. SNEdasXmlAdapter liest die XML beim nächsten Search-Call.
|
||||||
|
|
||||||
|
Details in `~/.claude/projects/<projekt>/memory/project_sn_xml_export.md`.
|
||||||
|
|
||||||
|
### Container-Zeitzone
|
||||||
|
|
||||||
|
Der Container läuft **UTC**, nicht CEST. DB-Timestamps in `assessments.created_at`
|
||||||
|
sind UTC (kein TZ-Suffix, aber UTC). Beim Korrelieren mit Commit-Zeiten
|
||||||
|
(lokal CEST = UTC+2) muss konvertiert werden, sonst fließen falsche
|
||||||
|
Schlussfolgerungen ein. Detailliert in
|
||||||
|
`~/.claude/projects/<projekt>/memory/reference_container_utc.md`.
|
||||||
|
|
||||||
|
## Konsequenzen
|
||||||
|
|
||||||
|
### Positiv
|
||||||
|
|
||||||
|
- **DB überlebt jeden Deploy** — verifiziert seit Issue #5 (Fix in
|
||||||
|
`.dockerignore`-Update).
|
||||||
|
- **Backups sind trivial**: `tar czf gwoe-data-$(date +%F).tar.gz data/`
|
||||||
|
auf dem Host.
|
||||||
|
- **Build ist schnell** (~30s für nicht-cached Layers), weil das Image
|
||||||
|
nur Code + Dependencies enthält.
|
||||||
|
|
||||||
|
### Negativ
|
||||||
|
|
||||||
|
- **`.dockerignore` ist ein Foot-Gun** — wenn jemand vergisst, `data/` neu
|
||||||
|
hinzuzufügen nach einem Refactor, kann es passieren dass der Build die
|
||||||
|
Live-DB überschreibt. Mitigation: ein dedicated Sub-D-style Test, der
|
||||||
|
nach dem Build prüft, dass die DB die erwarteten Tabellen hat — steht
|
||||||
|
noch aus.
|
||||||
|
- **SN-XML braucht manuelle Pflege** wöchentlich. Akzeptiert weil es kein
|
||||||
|
scrape-baren Endpoint gibt.
|
||||||
|
- **SQLite skaliert nicht über parallele Writes** — bei mehr als ~5
|
||||||
|
gleichzeitigen Analyses würde es Lock-Contention geben. Aktuell läuft
|
||||||
|
alles seriell durch den Background-Task-Mechanismus, also kein Problem.
|
||||||
|
Bei Wachstum auf >50 Analyses/Tag wäre ein Postgres-Migration ein
|
||||||
|
separater ADR.
|
||||||
|
|
||||||
|
### Folgen für andere ADRs
|
||||||
|
|
||||||
|
- ADR 0002 (Adapter-Architektur) ist davon unabhängig — Adapter sind reine
|
||||||
|
Code-Klassen ohne State.
|
||||||
|
- Ein zukünftiger Postgres-Migration-ADR würde diesen ADR partial superseden
|
||||||
|
(DB-Persistenz, nicht Reports).
|
||||||
36
docs/adr/README.md
Normal file
36
docs/adr/README.md
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Architecture Decision Records (ADRs)
|
||||||
|
|
||||||
|
ADRs dokumentieren signifikante Architektur-Entscheidungen mit Kontext, Optionen
|
||||||
|
und Konsequenzen. Format inspiriert von [Michael Nygard](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions).
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Neue Entscheidung steht an → Kopie von `template.md` mit nächster freier
|
||||||
|
Nummer (`NNNN-kebap-titel.md`).
|
||||||
|
2. Status `proposed` → diskutiert in Issue/PR → bei Akzeptanz auf `accepted`.
|
||||||
|
3. **Niemals editieren nach `accepted`.** Wenn eine Entscheidung sich ändert,
|
||||||
|
neuer ADR mit `Supersedes: NNNN-…` im Header und der alte ADR bekommt
|
||||||
|
`Superseded by: MMMM-…`.
|
||||||
|
4. Status `deprecated` für Entscheidungen, die ohne Nachfolger auslaufen.
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
| ID | Titel | Status | Datum |
|
||||||
|
|---|---|---|---|
|
||||||
|
| [0001](0001-llm-citation-binding.md) | LLM-Citations server-seitig binden statt prompt-seitig | accepted | 2026-04-10 |
|
||||||
|
| [0002](0002-adapter-architecture.md) | Adapter-Pattern mit ParlamentAdapter-Basisklasse + Registry | accepted | 2026-04-10 |
|
||||||
|
| [0003](0003-citation-property-tests.md) | Sub-D Property-Verification: Zitate als Substring der zitierten PDF-Seite | accepted | 2026-04-10 |
|
||||||
|
| [0004](0004-deployment-workflow.md) | Docker Compose Deploy mit DB-/Reports-Volume und SN-XML-Sonderpfad | accepted | 2026-04-10 |
|
||||||
|
|
||||||
|
## Wann ADR, wann nicht
|
||||||
|
|
||||||
|
| ADR-würdig | nicht ADR-würdig |
|
||||||
|
|---|---|
|
||||||
|
| Wahl zwischen mehreren plausiblen Architekturen mit Trade-offs | Bug-Fix |
|
||||||
|
| Strukturelle Konsequenzen für mehrere Module | Refactoring innerhalb eines Moduls |
|
||||||
|
| Reverse-Engineering-Findings die andere Adapter beeinflussen | Stiländerungen, Linting-Konventionen |
|
||||||
|
| Neue externe Abhängigkeiten oder APIs | Dependency-Bumps ohne API-Änderung |
|
||||||
|
| Workflow-Konventionen die mehrere Sessions überdauern müssen | Tagesgeschäft, Issue-Tracking |
|
||||||
|
|
||||||
|
Faustregel: Wenn ein neuer Kollege (oder eine neue Session) die Entscheidung
|
||||||
|
sonst rückgängig machen würde, gehört sie in einen ADR.
|
||||||
48
docs/adr/template.md
Normal file
48
docs/adr/template.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# NNNN — Titel der Entscheidung
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Status** | proposed / accepted / deprecated / superseded |
|
||||||
|
| **Datum** | YYYY-MM-DD |
|
||||||
|
| **Supersedes** | (optional) link auf vorherigen ADR |
|
||||||
|
| **Superseded by** | (optional) link auf neueren ADR |
|
||||||
|
| **Refs** | Issues, PRs, Commits |
|
||||||
|
|
||||||
|
## Kontext
|
||||||
|
|
||||||
|
Welches Problem hat zu der Entscheidung geführt? Was ist der Stand vorher,
|
||||||
|
welche Constraints sind im Spiel, welche Stakeholder sind betroffen?
|
||||||
|
|
||||||
|
## Optionen
|
||||||
|
|
||||||
|
Welche Alternativen wurden ernsthaft erwogen? Mindestens 2, gerne mehr.
|
||||||
|
|
||||||
|
### Option A — …
|
||||||
|
|
||||||
|
Beschreibung. Vor- und Nachteile.
|
||||||
|
|
||||||
|
### Option B — …
|
||||||
|
|
||||||
|
Beschreibung. Vor- und Nachteile.
|
||||||
|
|
||||||
|
## Entscheidung
|
||||||
|
|
||||||
|
Welche Option wurde gewählt und warum? Konkret und unmissverständlich, sodass
|
||||||
|
ein neuer Kollege ohne Diskussion weitermachen kann.
|
||||||
|
|
||||||
|
## Konsequenzen
|
||||||
|
|
||||||
|
Was wird leichter / schwerer durch diese Entscheidung? Welche Folge-Arbeiten
|
||||||
|
fallen an? Welche Teile des Systems werden berührt?
|
||||||
|
|
||||||
|
### Positiv
|
||||||
|
|
||||||
|
- …
|
||||||
|
|
||||||
|
### Negativ
|
||||||
|
|
||||||
|
- …
|
||||||
|
|
||||||
|
### Folgen für andere ADRs
|
||||||
|
|
||||||
|
- …
|
||||||
381
docs/archive/DOKUMENTATION-2026-03-24.md
Normal file
381
docs/archive/DOKUMENTATION-2026-03-24.md
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
# GWÖ-Antragsprüfer — Dokumentation
|
||||||
|
|
||||||
|
*Stand: 24. März 2026, 22:15*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Datenbank
|
||||||
|
|
||||||
|
Alle Bewertungen werden in einer SQLite-Datenbank gespeichert: `gwoe-antraege.db`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# DB initialisieren (einmalig)
|
||||||
|
sqlite3 gwoe-antraege.db < scripts/init-db.sql
|
||||||
|
|
||||||
|
# Übersicht aller Anträge
|
||||||
|
sqlite3 -header -column gwoe-antraege.db "SELECT * FROM v_antraege_uebersicht;"
|
||||||
|
|
||||||
|
# Statistik nach Fraktion
|
||||||
|
sqlite3 -header -column gwoe-antraege.db "SELECT * FROM v_statistik_fraktionen;"
|
||||||
|
|
||||||
|
# Themen-Ranking
|
||||||
|
sqlite3 -header -column gwoe-antraege.db "SELECT * FROM v_themen_ranking;"
|
||||||
|
```
|
||||||
|
|
||||||
|
Schema: siehe `scripts/init-db.sql` und `prompt-gwoe-antragspruefer-v4.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Batch-Workflow
|
||||||
|
|
||||||
|
### Vollautomatisiert
|
||||||
|
```bash
|
||||||
|
# Einzelner Antrag
|
||||||
|
./scripts/process_single.sh 18-18102
|
||||||
|
|
||||||
|
# Batch aus Datei
|
||||||
|
./scripts/gwoe-batch.sh --batch neue-antraege.txt
|
||||||
|
|
||||||
|
# Alle neu rendern
|
||||||
|
./scripts/gwoe-batch.sh --render-all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manuell (Schritt für Schritt)
|
||||||
|
```bash
|
||||||
|
# 1. PDF herunterladen
|
||||||
|
curl -sL "https://www.landtag.nrw.de/portal/WWW/dokumentenarchiv/Dokument/MMD18-18102.pdf" \
|
||||||
|
-o antraege/18-18102.pdf
|
||||||
|
|
||||||
|
# 2. Text extrahieren
|
||||||
|
pdftotext -layout antraege/18-18102.pdf antraege/18-18102.txt
|
||||||
|
|
||||||
|
# 3. LLM analysieren (manuell oder via API)
|
||||||
|
# → JSON in assessments.json einfügen
|
||||||
|
|
||||||
|
# 4. data.ts aktualisieren
|
||||||
|
cat assessments.json > quarto-report/src/data.ts # vereinfacht
|
||||||
|
|
||||||
|
# 5. Quarto rendern
|
||||||
|
cd quarto-report && npm run generate
|
||||||
|
cd reports && quarto render gwoe-18-18102.qmd --to pdf
|
||||||
|
|
||||||
|
# 6. Mit Original mergen
|
||||||
|
pdftk quarto-report/reports/gwoe-18-18102.pdf antraege/18-18102.pdf \
|
||||||
|
cat output output/gwoe-bericht-18-18102.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
### API-Aufruf (Claude)
|
||||||
|
```bash
|
||||||
|
curl -sS https://api.anthropic.com/v1/messages \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "x-api-key: $ANTHROPIC_API_KEY" \
|
||||||
|
-H "anthropic-version: 2023-06-01" \
|
||||||
|
-d '{
|
||||||
|
"model": "claude-sonnet-4-20250514",
|
||||||
|
"max_tokens": 4096,
|
||||||
|
"system": "<SYSTEM_PROMPT>",
|
||||||
|
"messages": [{"role": "user", "content": "<USER_PROMPT>"}]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Überblick
|
||||||
|
|
||||||
|
Der GWÖ-Antragsprüfer bewertet parlamentarische Anträge (aktuell: NRW Landtag) systematisch nach drei Dimensionen:
|
||||||
|
|
||||||
|
1. **GWÖ-Treue (0-10):** Übereinstimmung mit der Gemeinwohl-Ökonomie Matrix 2.0
|
||||||
|
2. **Wahlprogrammtreue (0-10):** Konsistenz mit dem NRW-Wahlprogramm 2022 der einreichenden Fraktion(en) und der Fraktionen in Regierungsverantwortung
|
||||||
|
3. **Parteiprogrammtreue (0-10):** Konsistenz mit dem Grundsatzprogramm der einreichenden Fraktion(en) und der Fraktionen in Regierungsverantwortung
|
||||||
|
|
||||||
|
Das Tool generiert professionelle PDF-Berichte mit dem offiziellen ECOnGOOD-Branding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Gewählte Matrix: V2.0 für Gemeinden
|
||||||
|
|
||||||
|
### Warum nicht Matrix 5.1?
|
||||||
|
|
||||||
|
Die **GWÖ-Matrix 5.1** ist für Unternehmen konzipiert mit Berührungsgruppen wie "Kund:innen", "Eigentümer:innen", "Mitarbeitende". Diese passen nicht zu parlamentarischen Anträgen.
|
||||||
|
|
||||||
|
### Warum nicht Matrix 2.1.A?
|
||||||
|
|
||||||
|
Die Matrix 2.1.A ist für die "öffentliche Hand" allgemein. Die **Matrix 2.0 für Gemeinden** bietet einen etwas weiteren Blick und ist kompakter formuliert — ideal für parlamentarische Analyse.
|
||||||
|
|
||||||
|
### Die Matrix 2.0 für Gemeinden
|
||||||
|
|
||||||
|
| Aspekt | Beschreibung |
|
||||||
|
|--------|--------------|
|
||||||
|
| **Struktur** | 5×5 (25 Felder) |
|
||||||
|
| **Fokus** | Kommunale Gebietskörperschaften |
|
||||||
|
| **Zielgruppe** | Kommunalpolitik, Landesanträge |
|
||||||
|
| **Staatsprinzipien** | ja (verfassungsrechtlich verankert) |
|
||||||
|
|
||||||
|
### Die 5×5-Struktur
|
||||||
|
|
||||||
|
**Spalten (Werte des Gemeinwohls + Staatsprinzipien):**
|
||||||
|
|
||||||
|
| Nr | Wert | Staatsprinzip |
|
||||||
|
|----|------|---------------|
|
||||||
|
| 1 | Menschenwürde | Rechtsstaatsprinzip |
|
||||||
|
| 2 | Solidarität | Gemeinnutz |
|
||||||
|
| 3 | Ökologische Nachhaltigkeit | Umwelt-Verantwortung |
|
||||||
|
| 4 | Soziale Gerechtigkeit | Sozialstaatsprinzip |
|
||||||
|
| 5 | Transparenz & Mitbestimmung | Demokratie |
|
||||||
|
|
||||||
|
**Zeilen (Berührungsgruppen):**
|
||||||
|
|
||||||
|
| Code | Gruppe | Beschreibung |
|
||||||
|
|------|--------|--------------|
|
||||||
|
| A | Ausgelagerte Betriebe, Lieferant:innen, Dienstleister:innen | Externe Beschaffung, Lieferketten |
|
||||||
|
| B | Finanzpartner:innen, Geldgeber:innen, Steuerzahler:innen | Haushalt, Finanzpolitik |
|
||||||
|
| C | Politische Führung, Verwaltung, Ehrenamtliche | Mandatsträger:innen, Mitarbeitende |
|
||||||
|
| D | Bürger:innen und Wirtschaft | Interne Wirkung, Daseinsvorsorge |
|
||||||
|
| E | Staat, Gesellschaft und Natur | Überregionale/langfristige Wirkung |
|
||||||
|
|
||||||
|
### Relevanz für Landesanträge
|
||||||
|
|
||||||
|
Die meisten Parlamentsanträge betreffen:
|
||||||
|
- **D-Zeile:** Wirkung auf Bürger:innen und Wirtschaft im Land
|
||||||
|
- **E-Zeile:** Überregionale oder langfristige Auswirkungen
|
||||||
|
|
||||||
|
**Prinzip:** D (intern) hat Vorrang vor E (extern).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Programmtreue-Bewertung
|
||||||
|
|
||||||
|
### Zwei Dimensionen
|
||||||
|
|
||||||
|
Anträge werden sowohl gegen das **Wahlprogramm** als auch gegen das **Grundsatzprogramm** der Fraktionen geprüft:
|
||||||
|
|
||||||
|
| Partei | Wahlprogramm | Grundsatzprogramm |
|
||||||
|
|--------|--------------|-------------------|
|
||||||
|
| CDU | NRW 2022 | "In Freiheit leben" (2024) |
|
||||||
|
| SPD | NRW 2022 | Hamburger Programm (2007) |
|
||||||
|
| GRÜNE | NRW 2022 | "...zu achten und zu schützen..." (2020) |
|
||||||
|
| FDP | NRW 2022 | "Verantwortung für die Freiheit" (2012) |
|
||||||
|
| AfD | NRW 2022 | "Programm für Deutschland" (2016) |
|
||||||
|
|
||||||
|
### Bewertungsskala
|
||||||
|
|
||||||
|
| Score | Bedeutung |
|
||||||
|
|-------|-----------|
|
||||||
|
| 9-10 | Vollständige Übereinstimmung |
|
||||||
|
| 7-8 | Hohe Übereinstimmung |
|
||||||
|
| 5-6 | Partielle Übereinstimmung |
|
||||||
|
| 3-4 | Geringe Übereinstimmung |
|
||||||
|
| 1-2 | Widerspricht Teilaspekten |
|
||||||
|
| 0 | Vollständiger Widerspruch |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Bewertungsskala GWÖ
|
||||||
|
|
||||||
|
| Punkte | Stufe | Beschreibung |
|
||||||
|
|--------|-------|--------------|
|
||||||
|
| 7-10 | **Vorbildlich** | Innovative Maßnahmen, weitreichende Verbesserungen |
|
||||||
|
| 4-6 | **Erfahren** | Erkennbare Verbesserungen, gute Ergebnisse |
|
||||||
|
| 2-3 | **Fortgeschritten** | Erste Maßnahmen, erste Erfolge |
|
||||||
|
| 1 | **Erste Schritte** | Erstes Engagement |
|
||||||
|
| 0 | **Basislinie** | Nur gesetzliche Anforderungen |
|
||||||
|
| negativ | **Widerspruch** | Aktiver Widerspruch zu GWÖ-Werten |
|
||||||
|
|
||||||
|
### Matrix-Feldwertung
|
||||||
|
|
||||||
|
Für jedes berührte Feld im Antrag:
|
||||||
|
- `++` (+2/+3): Stark fördernd
|
||||||
|
- `+` (+1): Fördernd
|
||||||
|
- `○` (0): Neutral
|
||||||
|
- `−` (-1): Widersprechend
|
||||||
|
- `−−` (-2/-3): Stark widersprechend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. ECOnGOOD Corporate Design
|
||||||
|
|
||||||
|
### Branding (aus CD Manual 2024)
|
||||||
|
|
||||||
|
Das Layout folgt strikt dem offiziellen ECOnGOOD Corporate Design Manual (Juni 2024).
|
||||||
|
|
||||||
|
**Farbpalette (Primärfarben aus Logo):**
|
||||||
|
|
||||||
|
| Farbe | Hex | CMYK | Pantone | Verwendung |
|
||||||
|
|-------|-----|------|---------|------------|
|
||||||
|
| **Dunkelgrau** | `#5a5a5a` | 0-0-0-80 | 425 U | Fließtext, H1-Überschriften |
|
||||||
|
| **Grün** | `#889e33` | 40-10-90-20 | 583 U | Positive Werte, H3, Akzente |
|
||||||
|
| **Blau** | `#009da5` | 100-10-40-00 | 320 U | Hauptfarbe, H2, Links, Linien |
|
||||||
|
| **Hellgrau** | `#bfbfbf` | 0-0-0-30 | 421 U | Hintergründe, Fußzeilen |
|
||||||
|
|
||||||
|
**Typografie:**
|
||||||
|
- **Primär:** Avenir (light, roman, black)
|
||||||
|
- **Fallback:** Arial
|
||||||
|
- **Web:** Avenir
|
||||||
|
- **Überschriften:**
|
||||||
|
- H1: Avenir black, 22pt, Dunkelgrau
|
||||||
|
- H2: Avenir black, 16pt, Blau oder Grün
|
||||||
|
- H3: Avenir black, 12pt, Blau oder Grün
|
||||||
|
- **Fließtext:** Avenir roman, 10pt, Dunkelgrau
|
||||||
|
- **Hervorhebungen:** Avenir black
|
||||||
|
|
||||||
|
**Logo:** ECOnGOOD-Logo (`econgood-logo.png`) — zwei stilisierte Pusteblumen (Blau + Grün)
|
||||||
|
|
||||||
|
**Boxen (CD-konform):**
|
||||||
|
- Flächig farbig (Grün oder Blau) mit weißem Text
|
||||||
|
- Oder mit farbiger Outline und farbigem Text
|
||||||
|
- Keine abgerundeten Ecken (arc=0pt)
|
||||||
|
|
||||||
|
**Tabellen:**
|
||||||
|
- Titelleiste in Markenfarbe (Blau)
|
||||||
|
- Weißer Text in Titelleiste, fett, Großbuchstaben
|
||||||
|
- Innere horizontale Linien max. 1pt
|
||||||
|
- 3mm Abstand zwischen Text und Linie
|
||||||
|
|
||||||
|
**Layout:**
|
||||||
|
- Offenes, leichtes Design
|
||||||
|
- Reinweißer Hintergrund
|
||||||
|
- Kräftige Farben (aktiv)
|
||||||
|
- Genug Weißraum zwischen Elementen
|
||||||
|
|
||||||
|
### Empfehlungs-Symbole
|
||||||
|
|
||||||
|
| Empfehlung | Symbol | Farbe | Hex |
|
||||||
|
|------------|--------|-------|-----|
|
||||||
|
| Uneingeschränkt unterstützen | `[++]` | Grün | `#889e33` |
|
||||||
|
| Unterstützen mit Änderungen | `[+]` | Blau | `#009da5` |
|
||||||
|
| Überarbeiten | `[!]` | Orange | `#F7941D` |
|
||||||
|
| Ablehnen | `[X]` | Rot | `#d00000` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Technische Umsetzung
|
||||||
|
|
||||||
|
### Stack
|
||||||
|
|
||||||
|
- **TypeScript** — Datentypen, Generator
|
||||||
|
- **Quarto** — Markdown → PDF (via LuaLaTeX)
|
||||||
|
- **TikZ** — Grafische Elemente (Score-Balken)
|
||||||
|
- **pdftk** — PDF-Merge (Bewertung + Original-Antrag)
|
||||||
|
|
||||||
|
### Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
quarto-report/
|
||||||
|
├── src/
|
||||||
|
│ ├── types.ts # Datentypen, Matrix-Labels
|
||||||
|
│ ├── data.ts # Testbewertungen
|
||||||
|
│ └── generate-qmd.ts # Quarto-Generator
|
||||||
|
├── reports/ # Generierte .qmd + .pdf
|
||||||
|
├── econgood-logo.png # Logo (aus CD Manual)
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
|
||||||
|
1. LLM analysiert Antrag → JSON-Ausgabe
|
||||||
|
2. JSON → TypeScript Assessment-Objekt
|
||||||
|
3. Generator erzeugt `.qmd` (Quarto Markdown)
|
||||||
|
4. Quarto rendert → PDF
|
||||||
|
5. pdftk merged Bewertung + Original-Antrag
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Kontext-Dateien
|
||||||
|
|
||||||
|
Im Ordner `kontext/`:
|
||||||
|
|
||||||
|
| Datei | Beschreibung |
|
||||||
|
|-------|--------------|
|
||||||
|
| `gwoe-matrix-2.0.md` | Matrix 2.0 für Gemeinden — vollständige Dokumentation |
|
||||||
|
| `gwoe-matrix-2.0-gemeinden.pdf` | Offizielles Matrix-PDF |
|
||||||
|
| `gwoe-arbeitsbuch-gemeinden-2.0.pdf` | Arbeitsbuch zur Matrix |
|
||||||
|
| `parteiprogramme.md` | Kurzreferenz Wahlprogramme + Grundsatzprogramme |
|
||||||
|
| `cdu-grundsatzprogramm-2024.pdf` | CDU "In Freiheit leben" |
|
||||||
|
| `spd-hamburger-programm-2007.pdf` | SPD Hamburger Programm |
|
||||||
|
| `gruene-grundsatzprogramm-2020.pdf` | Grüne Grundsatzprogramm |
|
||||||
|
| `fdp-grundsatzprogramm-2012.pdf` | FDP "Verantwortung für die Freiheit" |
|
||||||
|
| `afd-grundsatzprogramm-2016.pdf` | AfD "Programm für Deutschland" |
|
||||||
|
| `wahlprogramme-nrw-2022.md` | NRW Wahlprogramme 2022 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Getestete Anträge
|
||||||
|
|
||||||
|
| Drucksache | Titel | GWÖ | Empfehlung |
|
||||||
|
|------------|-------|-----|------------|
|
||||||
|
| 18/18081 | Schlechte-Kita-Gesetz stoppen (SPD) | 8/10 | Uneingeschränkt unterstützen |
|
||||||
|
| 18/18088 | Iran-Solidarität (alle) | 9/10 | Uneingeschränkt unterstützen |
|
||||||
|
| 18/18094 | Versorgungssicherheit/Fracking (AfD) | 1/10 | Ablehnen |
|
||||||
|
| 18/18099 | Kultur-Schecks (CDU/Grüne) | 6/10 | Unterstützen mit Änderungen |
|
||||||
|
| 18/18104 | KI-Korrektur (FDP) | 6/10 | Unterstützen mit Änderungen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Gendering
|
||||||
|
|
||||||
|
**Standard:** `:` (Doppelpunkt) statt `*` (Sternchen)
|
||||||
|
|
||||||
|
Beispiele:
|
||||||
|
- Bürger:innen
|
||||||
|
- Lieferant:innen
|
||||||
|
- Mitarbeiter:innen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Quellen
|
||||||
|
|
||||||
|
- **GWÖ-Matrix 2.0:** https://germany.econgood.org/wp-content/uploads/sites/8/2024/04/Matrix-Gemeinwohl-Bilanzierung-Gemeinden-V2.0.pdf
|
||||||
|
- **Arbeitsbuch 2.0:** https://austria.econgood.org/wp-content/uploads/sites/7/2022/01/Arbeitsbuch-Gemeinden_2.pdf
|
||||||
|
- **ECOnGOOD CD Manual:** Corporate Design/2024_ECOnGOOD_Manual_DE_Juni_02.pdf
|
||||||
|
- **GWÖ Deutschland:** https://germany.econgood.org
|
||||||
|
- **Kontakt:** Gemeinde@ecogood.org
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Changelog
|
||||||
|
|
||||||
|
| Datum | Version | Änderung |
|
||||||
|
|-------|---------|----------|
|
||||||
|
| 24.03.2026 | v4 | Matrix 2.0 statt 2.1.A, Parteiprogrammtreue ergänzt, `:` beim Gendern, ECOnGOOD-Logo |
|
||||||
|
| 23.03.2026 | v3 | Umstellung auf Matrix 2.1.A (5×5), GWÖ-Branding |
|
||||||
|
| 23.03.2026 | v2 | Vollständiger Kontext (Matrix + Wahlprogramme) |
|
||||||
|
| 23.03.2026 | v1 | Initiale Version mit Matrix 5.1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Projektordner:* `~/Nextcloud/dotty/projekte/2026-03-23 GWÖ-Antragsprüfer _WIP_/`
|
||||||
|
*Telegram:* https://t.me/c/3823618505/4247
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session 28.03.2026 (Nachmittag/Abend)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- CSP, X-Frame-Options, X-XSS-Protection etc. als Middleware
|
||||||
|
- /docs deaktiviert
|
||||||
|
|
||||||
|
### v5-Prompt mit Verbesserungsvorschlägen
|
||||||
|
Neues Format:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"original": "Zitat aus Antrag",
|
||||||
|
"vorschlag": "Verbesserter Text mit **Markierungen**",
|
||||||
|
"begruendung": "GWÖ-Begründung"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI-Erweiterungen
|
||||||
|
- Partei-Filter (Dropdown)
|
||||||
|
- Tag-Wolke mit Multi-Select
|
||||||
|
- Partei-Durchschnitte in Stats-Bar
|
||||||
|
- Kombinierte Filter (Score + Partei)
|
||||||
|
|
||||||
|
### Infrastruktur-Fixes
|
||||||
|
- DB-Persistenz: Dockerfile kopiert data/ nicht mehr
|
||||||
|
- JSON-Import deaktiviert
|
||||||
|
- Retry-Logik für LLM (3 Versuche)
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
- https://repo.toppyr.de/tobias/gwoe-antragspruefer
|
||||||
|
- MIT License
|
||||||
|
- Vollständige Dokumentation
|
||||||
293
docs/archive/README-2026-03-28.md
Normal file
293
docs/archive/README-2026-03-28.md
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
# GWÖ-Antragsprüfer — Projektdokumentation
|
||||||
|
|
||||||
|
**Stand:** 28.03.2026, 23:58 Uhr
|
||||||
|
**Telegram-Topic:** [🌱 GWÖ-Antragsprüfer](https://t.me/c/3823618505/4247) (thread_id 4247)
|
||||||
|
|
||||||
|
## 🎯 Projektziel
|
||||||
|
|
||||||
|
Automatische Bewertung von Parlamentsanträgen nach der **Gemeinwohl-Ökonomie (GWÖ) Matrix 2.0 für Gemeinden**. Das Tool analysiert Anträge aus Landesparlamenten und bewertet sie nach GWÖ-Kriterien, vergleicht mit Wahl- und Parteiprogrammen und schlägt konkrete Textverbesserungen vor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Live-System
|
||||||
|
|
||||||
|
| Was | URL |
|
||||||
|
|-----|-----|
|
||||||
|
| **Webapp** | https://gwoe.toppyr.de |
|
||||||
|
| **Git-Repository** | https://repo.toppyr.de/tobias/gwoe-antragspruefer |
|
||||||
|
| **Server** | VServer 152.53.119.77 |
|
||||||
|
| **Container** | `/opt/gwoe-antragspruefer/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architektur
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Frontend │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ Durchsuchen │ │ 🏷️ Tags │ │ Prüfen │ │
|
||||||
|
│ │ (Liste) │ │ (Wolke) │ │ (Upload/Analyse) │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ FastAPI Backend │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
|
||||||
|
│ │ /api/ │ │ /api/ │ │ /api/ │ │ /api/ │ │
|
||||||
|
│ │assessments│ │search- │ │analyze- │ │assessment/ │ │
|
||||||
|
│ │ │ │landtag │ │drucksache│ │pdf │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ └─────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐
|
||||||
|
│ SQLite │ │ OPAL API │ │ DashScope (Qwen) │
|
||||||
|
│ gwoe-antraege│ │ (Landtag │ │ qwen-plus-latest │
|
||||||
|
│ .db │ │ NRW) │ │ (LLM Analyse) │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
webapp/
|
||||||
|
├── app/
|
||||||
|
│ ├── main.py # FastAPI-Endpoints + Security Middleware
|
||||||
|
│ ├── analyzer.py # LLM-Analyse mit v5-Prompt + Retry-Logik
|
||||||
|
│ ├── database.py # SQLite (aiosqlite) Persistenz
|
||||||
|
│ ├── models.py # Pydantic Assessment-Model
|
||||||
|
│ ├── parlamente.py # OPAL-Adapter für NRW Landtag
|
||||||
|
│ ├── report.py # PDF-Generierung (WeasyPrint)
|
||||||
|
│ ├── config.py # Settings + Environment
|
||||||
|
│ ├── embeddings.py # Wahlprogramm-Embeddings (optional)
|
||||||
|
│ ├── kontext/
|
||||||
|
│ │ ├── gwoe-matrix-2.0.md # GWÖ-Matrix Referenz
|
||||||
|
│ │ ├── wahlprogramme-nrw-2022.md # Zusammenfassungen
|
||||||
|
│ │ ├── parteiprogramme.md # Grundsatzprogramme
|
||||||
|
│ │ └── *-nrw-2022-paged.txt # Volltext (paginiert)
|
||||||
|
│ ├── templates/
|
||||||
|
│ │ ├── index.html # Haupt-UI (Jinja2)
|
||||||
|
│ │ └── quellen.html # Quellenübersicht
|
||||||
|
│ └── static/
|
||||||
|
│ └── referenzen/ # Original-PDFs der Programme
|
||||||
|
├── data/ # SQLite-DBs (Docker Volume)
|
||||||
|
│ └── gwoe-antraege.db # Haupt-Datenbank
|
||||||
|
├── reports/ # Generierte PDFs (Docker Volume)
|
||||||
|
├── docker-compose.yml # Traefik + Let's Encrypt
|
||||||
|
├── Dockerfile # Python 3.12 + WeasyPrint
|
||||||
|
├── requirements.txt
|
||||||
|
├── .env.example
|
||||||
|
├── .gitignore
|
||||||
|
├── .dockerignore
|
||||||
|
├── LICENSE (MIT)
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features (Stand 28.03.2026)
|
||||||
|
|
||||||
|
### Analyse
|
||||||
|
- [x] GWÖ-Score (0-10) mit Matrix-Zuordnung
|
||||||
|
- [x] Bewertungssymbole (++/+/○/−/−−) pro Matrix-Feld
|
||||||
|
- [x] Wahlprogrammtreue (0-10) pro Fraktion
|
||||||
|
- [x] Parteiprogrammtreue (0-10) pro Fraktion
|
||||||
|
- [x] **Verbesserungsvorschläge im Redline-Format**
|
||||||
|
- Original-Zitat aus dem Antrag
|
||||||
|
- Konkreter Verbesserungsvorschlag
|
||||||
|
- GWÖ-Begründung
|
||||||
|
- [x] Themen-Tags für Kategorisierung
|
||||||
|
- [x] Retry-Logik (3 Versuche) bei JSON-Parse-Fehlern
|
||||||
|
|
||||||
|
### UI
|
||||||
|
- [x] Score-Filter (Alle / 8-10 / 5-7 / 0-4)
|
||||||
|
- [x] **Partei-Filter** (Dropdown)
|
||||||
|
- [x] **Durchschnittswerte pro Partei** (kompakte Stats-Bar)
|
||||||
|
- [x] **Tag-Wolke** mit Multi-Select (Schnittmenge)
|
||||||
|
- [x] Landtag-Suche via OPAL-API
|
||||||
|
- [x] "Jetzt prüfen"-Button für neue Anträge
|
||||||
|
- [x] Detail-Ansicht mit allen Bewertungen
|
||||||
|
- [x] PDF-Download pro Antrag
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- [x] Content-Security-Policy
|
||||||
|
- [x] X-Frame-Options: DENY
|
||||||
|
- [x] X-Content-Type-Options: nosniff
|
||||||
|
- [x] X-XSS-Protection
|
||||||
|
- [x] Referrer-Policy
|
||||||
|
- [x] Permissions-Policy
|
||||||
|
- [x] /docs, /redoc, /openapi.json deaktiviert
|
||||||
|
|
||||||
|
### Infrastruktur
|
||||||
|
- [x] Docker Compose mit Traefik Reverse Proxy
|
||||||
|
- [x] Let's Encrypt SSL
|
||||||
|
- [x] **Persistente SQLite-DB** (Volume, überlebt Container-Neustarts)
|
||||||
|
- [x] Git-Repository auf repo.toppyr.de
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Konfiguration
|
||||||
|
|
||||||
|
### Environment-Variablen (.env)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DASHSCOPE_API_KEY=sk-... # Alibaba DashScope API
|
||||||
|
KEYCLOAK_URL=https://sso.toppyr.de # Optional: SSO
|
||||||
|
KEYCLOAK_REALM=collaboration
|
||||||
|
KEYCLOAK_CLIENT_ID=gwoe-antragspruefer
|
||||||
|
```
|
||||||
|
|
||||||
|
### LLM-Modelle
|
||||||
|
|
||||||
|
| Modell | Verwendung | Kosten |
|
||||||
|
|--------|------------|--------|
|
||||||
|
| `qwen-plus-latest` | Standard (Free Tier) | Kostenlos |
|
||||||
|
| `qwen-plus` | Fallback (Paid) | ~$0.80/MTok |
|
||||||
|
| `qwen-max` | Premium | ~$2.40/MTok |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 API-Endpoints
|
||||||
|
|
||||||
|
| Methode | Pfad | Beschreibung |
|
||||||
|
|---------|------|--------------|
|
||||||
|
| GET | `/` | Web-UI |
|
||||||
|
| GET | `/health` | Health Check |
|
||||||
|
| GET | `/quellen` | Quellenübersicht |
|
||||||
|
| GET | `/api/assessments` | Alle Bewertungen (JSON) |
|
||||||
|
| GET | `/api/assessment?drucksache=18/12345` | Einzelne Bewertung |
|
||||||
|
| GET | `/api/search?q=Klima` | Interne DB-Suche |
|
||||||
|
| GET | `/api/search-landtag?q=Klima` | Landtag OPAL-Suche |
|
||||||
|
| POST | `/api/analyze-drucksache` | Neue Analyse starten |
|
||||||
|
| GET | `/api/assessment/pdf?drucksache=18/12345` | PDF-Download |
|
||||||
|
| GET | `/api/bundeslaender` | Verfügbare Bundesländer |
|
||||||
|
| GET | `/status/{job_id}` | Job-Status abfragen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Prompt v5 — Kern-Features
|
||||||
|
|
||||||
|
Der Analyse-Prompt fordert:
|
||||||
|
|
||||||
|
1. **GWÖ-Treue (0-10)** mit Matrix-Zuordnung:
|
||||||
|
- Matrix-Feld (z.B. "D1 Menschenwürde")
|
||||||
|
- Symbol (++/+/○/−/−−)
|
||||||
|
- Kurzbegründung
|
||||||
|
|
||||||
|
2. **Programmtreue** für Antragsteller UND Regierungsfraktionen:
|
||||||
|
- Wahlprogramm-Score (0-10)
|
||||||
|
- Parteiprogramm-Score (0-10)
|
||||||
|
- Begründungen
|
||||||
|
|
||||||
|
3. **Verbesserungsvorschläge** (max. 3):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"original": "Zitat aus dem Antrag",
|
||||||
|
"vorschlag": "Konkret **verbesserter** Text",
|
||||||
|
"begruendung": "Stärkt GWÖ-Wert X durch Y"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Themen-Tags** für Kategorisierung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Deployment
|
||||||
|
|
||||||
|
### Erstinstallation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh vserver
|
||||||
|
cd /opt
|
||||||
|
git clone https://repo.toppyr.de/tobias/gwoe-antragspruefer.git
|
||||||
|
cd gwoe-antragspruefer
|
||||||
|
|
||||||
|
# .env erstellen
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env # DASHSCOPE_API_KEY eintragen
|
||||||
|
|
||||||
|
# Starten
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Lokal
|
||||||
|
cd ~/Nextcloud/dotty/projekte/2026-03-23\ GWÖ-Antragsprüfer\ _WIP_/webapp
|
||||||
|
git add . && git commit -m "Update" && git push
|
||||||
|
|
||||||
|
# Auf Server
|
||||||
|
ssh vserver 'cd /opt/gwoe-antragspruefer && git pull && docker compose up -d --build'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manuelles Deploy (ohne Git auf Server)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Nextcloud/dotty/projekte/2026-03-23\ GWÖ-Antragsprüfer\ _WIP_/webapp
|
||||||
|
tar czf /tmp/gwoe-webapp.tar.gz --exclude='venv' --exclude='__pycache__' --exclude='data' --exclude='reports' --exclude='.env' .
|
||||||
|
scp /tmp/gwoe-webapp.tar.gz vserver:/tmp/
|
||||||
|
ssh vserver 'cd /opt/gwoe-antragspruefer && tar xzf /tmp/gwoe-webapp.tar.gz && docker compose up -d --build'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Statistiken (28.03.2026)
|
||||||
|
|
||||||
|
| Metrik | Wert |
|
||||||
|
|--------|------|
|
||||||
|
| Analysierte Anträge | 20 |
|
||||||
|
| Ø GWÖ-Score gesamt | 4.6 |
|
||||||
|
| Ø SPD | 7.7 |
|
||||||
|
| Ø GRÜNE | 6.0 |
|
||||||
|
| Ø CDU | 6.0 |
|
||||||
|
| Ø FDP | 4.8 |
|
||||||
|
| Ø AfD | 1.5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Bekannte Issues / TODOs
|
||||||
|
|
||||||
|
### Offen
|
||||||
|
- [ ] Keycloak SSO-Integration aktivieren
|
||||||
|
- [ ] Weitere Bundesländer (BY, BW) anbinden
|
||||||
|
- [ ] Batch-Analyse für viele Anträge
|
||||||
|
- [ ] Wahlprogramm-Zitate mit Seitenzahlen
|
||||||
|
- [ ] Export als CSV/Excel
|
||||||
|
|
||||||
|
### Gelöst (28.03.2026)
|
||||||
|
- [x] JSON-Parse-Fehler bei LLM-Output → Retry-Logik
|
||||||
|
- [x] DB nicht persistent → Dockerfile gefixt, data/ als Volume
|
||||||
|
- [x] Alte Assessments überschreiben neue → JSON-Import deaktiviert
|
||||||
|
- [x] Partei-Filter zeigt "Suchfehler" → JS-Bug gefixt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Changelog
|
||||||
|
|
||||||
|
### v1.0.0 (28.03.2026)
|
||||||
|
- Initial Release
|
||||||
|
- GWÖ-Matrix 2.0 Analyse für NRW
|
||||||
|
- Verbesserungsvorschläge im Redline-Format
|
||||||
|
- Tag-Wolke mit Multi-Select
|
||||||
|
- Partei-Filter + Durchschnittswerte
|
||||||
|
- Security Headers
|
||||||
|
- Docker Deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Quellen
|
||||||
|
|
||||||
|
- [GWÖ-Matrix 2.0 für Gemeinden (Arbeitsbuch)](https://econgood.org)
|
||||||
|
- [ECOnGOOD Corporate Design Manual 2024](https://econgood.org)
|
||||||
|
- [NRW OPAL Parlamentsdokumentation](https://www.landtag.nrw.de)
|
||||||
|
- Wahlprogramme NRW 2022 (CDU, GRÜNE, SPD, FDP, AfD)
|
||||||
|
- Grundsatzprogramme der Parteien
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Entwickelt von Tobias Rödel mit Unterstützung von Dotty** 👻
|
||||||
26
docs/archive/README.md
Normal file
26
docs/archive/README.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Archiv
|
||||||
|
|
||||||
|
Hier liegen historische Doku-Snapshots, die **nicht mehr autoritativ** sind.
|
||||||
|
Sie beschreiben jeweils einen Zustand zu einem bestimmten Zeitpunkt; vieles
|
||||||
|
davon ist heute überholt.
|
||||||
|
|
||||||
|
## Warum nicht löschen?
|
||||||
|
|
||||||
|
- Sie sind als historische Quelle nützlich, wenn man verstehen will, wie
|
||||||
|
das Projekt zu einem früheren Zeitpunkt strukturiert war.
|
||||||
|
- Manche Findings sind in die ADRs (`../adr/`) übernommen, andere bewusst
|
||||||
|
nicht — weil sie tagesaktueller Stand waren und nie ADR-würdig.
|
||||||
|
|
||||||
|
## Inhalt
|
||||||
|
|
||||||
|
| Datei | Stand | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `DOKUMENTATION-2026-03-24.md` | 2026-03-24 | Frühe Skript-basierte Architektur, vor dem Webapp-Migrate. Beschreibt `scripts/process_single.sh`-Workflow, der heute durch FastAPI-Backend ersetzt ist. |
|
||||||
|
| `STATUS-2026-03-28.md` | 2026-03-28 | Tagesstand-Snapshot. Beschreibt v5-Prompt, Persistenz-Volume, Security-Headers — alle drei sind heute Standard. |
|
||||||
|
| `README-2026-03-28.md` | 2026-03-28 | Erste Repo-README. Listet nur den NRW-Adapter (vor #2/#3/#4 und 13 weiteren BLs). Architektur-Diagramm ist veraltet. |
|
||||||
|
|
||||||
|
## Wenn etwas davon noch wahr ist…
|
||||||
|
|
||||||
|
…gehört es in einen ADR (`../adr/`) oder die generierte Reference (geplant
|
||||||
|
für #62 Phase 2). Bitte **nicht** in einen neuen Status-Snapshot ziehen.
|
||||||
|
Status-Snapshots sind die Drift-Falle, die wir hier vermeiden wollen.
|
||||||
182
docs/archive/STATUS-2026-03-28.md
Normal file
182
docs/archive/STATUS-2026-03-28.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# GWÖ-Antragsprüfer — Status 28.03.2026 (Final)
|
||||||
|
|
||||||
|
## ✅ Heute erledigt (28.03.2026)
|
||||||
|
|
||||||
|
### Vormittag
|
||||||
|
- [x] "Jetzt prüfen" aus Landtag-Suche implementiert
|
||||||
|
- [x] Zitat-System mit Wahlprogramm-Seitenreferenzen
|
||||||
|
- [x] 5 Original-Wahlprogramme integriert (CDU, SPD, Grüne, FDP, AfD)
|
||||||
|
|
||||||
|
### Nachmittag/Abend
|
||||||
|
- [x] **Security Headers** eingebaut (CSP, X-Frame-Options, etc.)
|
||||||
|
- [x] /docs, /redoc, /openapi.json deaktiviert
|
||||||
|
- [x] **v5-Prompt** mit Verbesserungsvorschlägen im Redline-Format
|
||||||
|
- [x] **Retry-Logik** (3 Versuche) bei JSON-Parse-Fehlern
|
||||||
|
- [x] **Partei-Filter** (Dropdown)
|
||||||
|
- [x] **Tag-Wolke** mit Multi-Select (Schnittmenge-Filter)
|
||||||
|
- [x] **Partei-Durchschnittswerte** in kompakter Stats-Bar
|
||||||
|
- [x] **Persistente DB** — Dockerfile gefixt, data/ als Volume
|
||||||
|
- [x] JSON-Import deaktiviert (DB ist Source of Truth)
|
||||||
|
- [x] 20 Test-Anträge neu analysiert mit v5-Prompt
|
||||||
|
- [x] **Git-Repository** auf repo.toppyr.de gepusht
|
||||||
|
- [x] **Dokumentation** komplett
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Live-System
|
||||||
|
|
||||||
|
| Was | URL |
|
||||||
|
|-----|-----|
|
||||||
|
| **Webapp** | https://gwoe.toppyr.de |
|
||||||
|
| **Repository** | https://repo.toppyr.de/tobias/gwoe-antragspruefer |
|
||||||
|
| **Server** | VServer 152.53.119.77 (`/opt/gwoe-antragspruefer/`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Aktueller Stand
|
||||||
|
|
||||||
|
| Metrik | Wert |
|
||||||
|
|--------|------|
|
||||||
|
| Analysierte Anträge | 20 |
|
||||||
|
| Alle mit Verbesserungsvorschlägen | ✅ |
|
||||||
|
| Ø GWÖ-Score gesamt | 4.6 |
|
||||||
|
| Ø SPD | 7.7 |
|
||||||
|
| Ø CDU/GRÜNE | 6.0 |
|
||||||
|
| Ø FDP | 4.8 |
|
||||||
|
| Ø AfD | 1.5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Technische Änderungen heute
|
||||||
|
|
||||||
|
### Security (main.py)
|
||||||
|
```python
|
||||||
|
# Middleware hinzugefügt:
|
||||||
|
- X-Content-Type-Options: nosniff
|
||||||
|
- X-Frame-Options: DENY
|
||||||
|
- X-XSS-Protection: 1; mode=block
|
||||||
|
- Content-Security-Policy
|
||||||
|
- Referrer-Policy: strict-origin-when-cross-origin
|
||||||
|
- Permissions-Policy: geolocation=(), microphone=(), camera=()
|
||||||
|
|
||||||
|
# FastAPI Docs deaktiviert:
|
||||||
|
docs_url=None, redoc_url=None, openapi_url=None
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retry-Logik (analyzer.py)
|
||||||
|
```python
|
||||||
|
# 3 Versuche bei JSON-Parse-Fehlern
|
||||||
|
# Temperatur steigt pro Versuch (0.3 → 0.4 → 0.5)
|
||||||
|
max_retries = 3
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
# ... LLM Call ...
|
||||||
|
try:
|
||||||
|
data = json.loads(content)
|
||||||
|
return Assessment.model_validate(data)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue # Retry
|
||||||
|
```
|
||||||
|
|
||||||
|
### Persistente DB (Dockerfile)
|
||||||
|
```dockerfile
|
||||||
|
# VORHER (kaputt):
|
||||||
|
COPY data/ ./data/
|
||||||
|
COPY reports/ ./reports/
|
||||||
|
|
||||||
|
# NACHHER (korrekt):
|
||||||
|
# data/ und reports/ werden als Volumes gemountet
|
||||||
|
RUN mkdir -p /app/data /app/reports
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy-Workflow
|
||||||
|
```bash
|
||||||
|
# .tarignore und --exclude verhindern Überschreiben der Server-DB
|
||||||
|
tar czf ... --exclude='data' --exclude='reports' ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Repository-Struktur
|
||||||
|
|
||||||
|
```
|
||||||
|
gwoe-antragspruefer/
|
||||||
|
├── app/
|
||||||
|
│ ├── main.py # FastAPI + Security
|
||||||
|
│ ├── analyzer.py # LLM + Retry
|
||||||
|
│ ├── database.py # SQLite
|
||||||
|
│ ├── models.py # Pydantic
|
||||||
|
│ ├── parlamente.py # OPAL-Adapter
|
||||||
|
│ ├── report.py # PDF
|
||||||
|
│ ├── kontext/ # GWÖ-Matrix, Programme
|
||||||
|
│ ├── templates/ # Jinja2 UI
|
||||||
|
│ └── static/referenzen/ # Original-PDFs
|
||||||
|
├── docker-compose.yml
|
||||||
|
├── Dockerfile
|
||||||
|
├── requirements.txt
|
||||||
|
├── .env.example
|
||||||
|
├── .gitignore
|
||||||
|
├── LICENSE (MIT)
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Offene TODOs
|
||||||
|
|
||||||
|
### Prio 1
|
||||||
|
- [ ] Keycloak SSO aktivieren
|
||||||
|
- [ ] Batch-Analyse für viele Anträge optimieren
|
||||||
|
|
||||||
|
### Prio 2
|
||||||
|
- [ ] Weitere Bundesländer (BY, BW)
|
||||||
|
- [ ] CSV/Excel-Export
|
||||||
|
- [ ] Zitat-Highlighting in PDFs
|
||||||
|
|
||||||
|
### Nice to have
|
||||||
|
- [ ] Historische Trend-Analyse
|
||||||
|
- [ ] Newsletter-Integration
|
||||||
|
- [ ] API-Rate-Limiting für öffentliche Nutzung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Deployment-Befehle
|
||||||
|
|
||||||
|
### Standard-Update
|
||||||
|
```bash
|
||||||
|
# Lokal committen
|
||||||
|
cd ~/Nextcloud/dotty/projekte/2026-03-23\ GWÖ-Antragsprüfer\ _WIP_/webapp
|
||||||
|
git add . && git commit -m "..." && git push
|
||||||
|
|
||||||
|
# Server aktualisieren
|
||||||
|
ssh vserver 'cd /opt/gwoe-antragspruefer && git pull && docker compose up -d --build'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manuell (ohne Server-Git)
|
||||||
|
```bash
|
||||||
|
cd ~/Nextcloud/dotty/projekte/2026-03-23\ GWÖ-Antragsprüfer\ _WIP_/webapp
|
||||||
|
tar czf /tmp/gwoe-webapp.tar.gz --exclude='venv' --exclude='__pycache__' --exclude='data' --exclude='reports' --exclude='.env' .
|
||||||
|
scp /tmp/gwoe-webapp.tar.gz vserver:/tmp/
|
||||||
|
ssh vserver 'cd /opt/gwoe-antragspruefer && tar xzf /tmp/gwoe-webapp.tar.gz && docker compose up -d --build'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logs prüfen
|
||||||
|
```bash
|
||||||
|
ssh vserver 'docker logs gwoe-antragspruefer --tail 50'
|
||||||
|
```
|
||||||
|
|
||||||
|
### DB-Status
|
||||||
|
```bash
|
||||||
|
ssh vserver 'docker exec gwoe-antragspruefer python -c "
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect(\"/app/data/gwoe-antraege.db\")
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(\"SELECT COUNT(*) FROM assessments\")
|
||||||
|
print(f\"Assessments: {cur.fetchone()[0]}\")
|
||||||
|
"'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Projekt-Status: FUNKTIONSFÄHIG** ✅
|
||||||
|
|
||||||
|
*Dokumentiert von Dotty, 28.03.2026, 23:58 Uhr*
|
||||||
Loading…
Reference in New Issue
Block a user