2026-03-28 22:30:24 +01:00
""" LLM-based analysis of parliamentary motions against GWÖ matrix. """
Phase A: Audit-Restbefunde #57.3/4/7 (Roadmap #59)
Drei verbleibende Audit-Befunde aus #57 in einem Patch:
- **#57.3 MEDIUM** Drucksache-Regex-Validation: neue
app/validators.py mit validate_drucksache() als gemeinsamer
Validation-Funnel. Pattern ^\d{1,3}/\d{1,7}([-(].{1,20})?$ deckt
alle 10 aktiven Bundesländer (8/6390, 18/12345, 8/6390(neu),
23/3700-A) ab und blockt Path-Traversal (../, /etc/passwd) plus
Standard-Injection (;, <, &). Drei Endpoints durchgeschleust:
/api/assessment, /api/assessment/pdf, /api/analyze-drucksache.
- **#57.4 MEDIUM** print() → logging.getLogger(__name__): main.py
und analyzer.py auf strukturiertes Logging umgestellt. LLM-Inhalte
werden NICHT mehr als Volltext geloggt — neue Helper
_content_fingerprint() liefert nur "len=N sha1=XXXX", reicht zur
Forensik ohne Antrag-Inhalte ins Container-Log zu leaken.
basicConfig() mit ISO-Format setzt strukturiertes Logging früh,
damit logger.exception() auch beim Boot greift.
- **#57.7 LOW-MED** Search-Query-Limit: validate_search_query() mit
MAX_SEARCH_QUERY_LEN=200 schützt /api/search und /api/search-landtag
vor 10-MB-Query-DoS. database._parse_search_query() loggt jetzt
shlex.ValueError-Fallback statt ihn zu verschlucken (deckt Memory-
Regel "stille excepts in Adaptern" ab).
Tests: neue tests/test_main_validators.py mit 22 Cases — Drucksache-
Whitelist-Roundtrip + Path-Traversal-Reject, Search-Query Längen-
Edge-Cases. 107 Unit-Tests grün (85 alt + 22 neu).
Validators in eigenem Modul (app/validators.py), damit Tests sie ohne
slowapi-Dependency direkt importieren können.
Refs: #57, #59 (Phase A)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:15:16 +02:00
import hashlib
2026-03-28 22:30:24 +01:00
import json
Phase A: Audit-Restbefunde #57.3/4/7 (Roadmap #59)
Drei verbleibende Audit-Befunde aus #57 in einem Patch:
- **#57.3 MEDIUM** Drucksache-Regex-Validation: neue
app/validators.py mit validate_drucksache() als gemeinsamer
Validation-Funnel. Pattern ^\d{1,3}/\d{1,7}([-(].{1,20})?$ deckt
alle 10 aktiven Bundesländer (8/6390, 18/12345, 8/6390(neu),
23/3700-A) ab und blockt Path-Traversal (../, /etc/passwd) plus
Standard-Injection (;, <, &). Drei Endpoints durchgeschleust:
/api/assessment, /api/assessment/pdf, /api/analyze-drucksache.
- **#57.4 MEDIUM** print() → logging.getLogger(__name__): main.py
und analyzer.py auf strukturiertes Logging umgestellt. LLM-Inhalte
werden NICHT mehr als Volltext geloggt — neue Helper
_content_fingerprint() liefert nur "len=N sha1=XXXX", reicht zur
Forensik ohne Antrag-Inhalte ins Container-Log zu leaken.
basicConfig() mit ISO-Format setzt strukturiertes Logging früh,
damit logger.exception() auch beim Boot greift.
- **#57.7 LOW-MED** Search-Query-Limit: validate_search_query() mit
MAX_SEARCH_QUERY_LEN=200 schützt /api/search und /api/search-landtag
vor 10-MB-Query-DoS. database._parse_search_query() loggt jetzt
shlex.ValueError-Fallback statt ihn zu verschlucken (deckt Memory-
Regel "stille excepts in Adaptern" ab).
Tests: neue tests/test_main_validators.py mit 22 Cases — Drucksache-
Whitelist-Roundtrip + Path-Traversal-Reject, Search-Query Längen-
Edge-Cases. 107 Unit-Tests grün (85 alt + 22 neu).
Validators in eigenem Modul (app/validators.py), damit Tests sie ohne
slowapi-Dependency direkt importieren können.
Refs: #57, #59 (Phase A)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:15:16 +02:00
import logging
2026-03-28 22:30:24 +01:00
import re
from pathlib import Path
from openai import AsyncOpenAI
from . config import settings
from . models import Assessment
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
from . bundeslaender import BUNDESLAENDER
from . wahlprogramme import (
find_relevant_quotes ,
format_quote_for_prompt ,
WAHLPROGRAMM_KONTEXT_FILES ,
)
2026-03-28 22:30:24 +01:00
from . embeddings import get_relevant_quotes_for_antrag , format_quotes_for_prompt , EMBEDDINGS_DB
Phase A: Audit-Restbefunde #57.3/4/7 (Roadmap #59)
Drei verbleibende Audit-Befunde aus #57 in einem Patch:
- **#57.3 MEDIUM** Drucksache-Regex-Validation: neue
app/validators.py mit validate_drucksache() als gemeinsamer
Validation-Funnel. Pattern ^\d{1,3}/\d{1,7}([-(].{1,20})?$ deckt
alle 10 aktiven Bundesländer (8/6390, 18/12345, 8/6390(neu),
23/3700-A) ab und blockt Path-Traversal (../, /etc/passwd) plus
Standard-Injection (;, <, &). Drei Endpoints durchgeschleust:
/api/assessment, /api/assessment/pdf, /api/analyze-drucksache.
- **#57.4 MEDIUM** print() → logging.getLogger(__name__): main.py
und analyzer.py auf strukturiertes Logging umgestellt. LLM-Inhalte
werden NICHT mehr als Volltext geloggt — neue Helper
_content_fingerprint() liefert nur "len=N sha1=XXXX", reicht zur
Forensik ohne Antrag-Inhalte ins Container-Log zu leaken.
basicConfig() mit ISO-Format setzt strukturiertes Logging früh,
damit logger.exception() auch beim Boot greift.
- **#57.7 LOW-MED** Search-Query-Limit: validate_search_query() mit
MAX_SEARCH_QUERY_LEN=200 schützt /api/search und /api/search-landtag
vor 10-MB-Query-DoS. database._parse_search_query() loggt jetzt
shlex.ValueError-Fallback statt ihn zu verschlucken (deckt Memory-
Regel "stille excepts in Adaptern" ab).
Tests: neue tests/test_main_validators.py mit 22 Cases — Drucksache-
Whitelist-Roundtrip + Path-Traversal-Reject, Search-Query Längen-
Edge-Cases. 107 Unit-Tests grün (85 alt + 22 neu).
Validators in eigenem Modul (app/validators.py), damit Tests sie ohne
slowapi-Dependency direkt importieren können.
Refs: #57, #59 (Phase A)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:15:16 +02:00
logger = logging . getLogger ( __name__ )
def _content_fingerprint ( content : str ) - > str :
""" Cheap, log-safe identifier for an LLM response: length + first 8 chars
of SHA - 1. Lets us correlate retries without ever leaking the LLM ' s
actual output ( which may contain sensitive Antrags - Inhalte ) . Issue
#57 Befund #4."""
if not content :
return " len=0 "
h = hashlib . sha1 ( content . encode ( " utf-8 " , errors = " replace " ) ) . hexdigest ( ) [ : 8 ]
return f " len= { len ( content ) } sha1= { h } "
2026-03-28 22:30:24 +01:00
# Load context files
KONTEXT_DIR = Path ( __file__ ) . parent / " kontext "
def load_context_file ( name : str ) - > str :
""" Load a context file from the kontext directory. """
path = KONTEXT_DIR / name
if path . exists ( ) :
return path . read_text ( )
return " "
def get_system_prompt ( ) - > str :
""" Build the system prompt with GWÖ matrix context. """
return """ Du bist ein Experte für Gemeinwohl-Ökonomie (GWÖ) und parlamentarische Analyse. Du bewertest Anträge aus Landesparlamenten systematisch nach drei Dimensionen:
1. * * GWÖ - Treue * * ( 0 - 10 ) : Übereinstimmung mit der GWÖ - Matrix 2.0 für Gemeinden
2. * * Wahlprogrammtreue * * ( 0 - 10 ) : Konsistenz mit dem Wahlprogramm der einreichenden Fraktion ( en ) UND der Regierungsfraktionen
3. * * Parteiprogrammtreue * * ( 0 - 10 ) : Konsistenz mit dem Grundsatzprogramm der einreichenden Fraktion ( en ) UND der Regierungsfraktionen
## GWÖ-Matrix 2.0 für Gemeinden
Die Matrix besteht aus 5 Berührungsgruppen × 5 Werte = 25 Themenfelder .
### Die fünf Werte (Spalten) mit Staatsprinzipien
| Nr | Wert | Staatsprinzip | Kernfragen |
| - - - - | - - - - - - | - - - - - - - - - - - - - - - | - - - - - - - - - - - - |
| 1 | * * Menschenwürde * * | Rechtsstaatsprinzip | Werden Grundrechte geschützt ? Rechtliche Gleichstellung ? |
| 2 | * * Solidarität * * | Gemeinnutz | Wird das Gemeinwohl gefördert ? Mehrwert für die Gemeinschaft ? |
| 3 | * * Ökologische Nachhaltigkeit * * | Umwelt - Verantwortung | Klimaschutz ? Ressourcenschonung ? Biodiversität ? |
| 4 | * * Soziale Gerechtigkeit * * | Sozialstaatsprinzip | Gerechte Verteilung ? Daseinsvorsorge ? Soziale Absicherung ? |
| 5 | * * Transparenz & Mitbestimmung * * | Demokratie | Bürgerbeteiligung ? Offenlegung ? Demokratische Prozesse ? |
### Die fünf Berührungsgruppen (Zeilen)
| Code | Gruppe | Beschreibung |
| - - - - - - | - - - - - - - - | - - - - - - - - - - - - - |
| * * A * * | Ausgelagerte Betriebe , Lieferant : innen , Dienstleister : innen | Externe Beschaffung , Lieferketten |
| * * B * * | Finanzpartner : innen , Geldgeber : innen , Steuerzahler : innen | Umgang mit öffentlichen Mitteln , Haushalt |
| * * C * * | Politische Führung , Verwaltung , Ehrenamtliche | Mandatsträger : innen , Mitarbeitende |
| * * D * * | Bürger : innen und Wirtschaft | Wirkung innerhalb der Grenzen , Daseinsvorsorge |
| * * E * * | Staat , Gesellschaft und Natur | Wirkung über die Grenzen hinaus , Zukunft |
### Matrix-Feldwertung (Skala -5 bis +5)
| Symbol | Rating | Bedeutung |
| - - - - - - - - | - - - - - - - - | - - - - - - - - - - - |
| ` + + ` | + 4 bis + 5 | Stark fördernd , vorbildlich |
| ` + ` | + 1 bis + 3 | Fördernd |
| ` ○ ` | 0 | Neutral / nicht berührt |
| ` − ` | - 1 bis - 3 | Widersprechend |
| ` − − ` | - 4 bis - 5 | Stark widersprechend , fundamentaler Widerspruch |
* * Skala - Logik : * *
- * * 0 * * = Antrag berührt dieses Feld nicht
- * * + 1 bis + 5 * * = Stärke der Übereinstimmung mit GWÖ - Werten
- * * - 1 bis - 5 * * = Stärke des Widerspruchs zu GWÖ - Werten
### Empfehlungs-Kategorien
| Empfehlung | Kriterium |
| - - - - - - - - - - - - | - - - - - - - - - - - |
| * * Uneingeschränkt unterstützen * * | GWÖ 8 - 10 , keine gravierenden Schwächen |
| * * Unterstützen mit Änderungen * * | GWÖ 5 - 7 , Verbesserungspotenzial vorhanden |
| * * Überarbeiten * * | GWÖ 3 - 4 , grundlegende Probleme |
| * * Ablehnen * * | GWÖ 0 - 2 , fundamentaler Widerspruch zu GWÖ - Werten |
## Ausgabeformat
Antworte NUR mit einem JSON - Objekt im folgenden Format ( keine Markdown - Codeblöcke ) :
{
" drucksache " : " Drucksachennummer falls bekannt, sonst ' unbekannt ' " ,
" title " : " Titel des Antrags " ,
" fraktionen " : [ " Fraktion1 " ] ,
" datum " : " YYYY-MM-DD oder unbekannt " ,
" link " : null ,
" gwoeScore " : 0 - 10 ,
" gwoeBegründung " : " 3-4 Sätze mit Bezug zu konkreten Themenfeldern " ,
" gwoeMatrix " : [
{ " field " : " D4 " , " label " : " Soziale öffentliche Leistung " , " aspect " : " Konkreter Bezug " , " rating " : 2 , " symbol " : " + " }
] ,
" gwoeSchwerpunkt " : [ " D4 " , " D1 " ] ,
" wahlprogrammScores " : [
{
" fraktion " : " SPD " ,
" istAntragsteller " : true ,
" wahlprogramm " : {
" score " : 9 ,
" begründung " : " ... " ,
" zitate " : [
{
" text " : " Exaktes Zitat aus Wahlprogramm " ,
" quelle " : " SPD NRW Wahlprogramm 2022, S. 47 " ,
" url " : " /static/referenzen/spd-nrw-2022.pdf#page=47 "
}
]
} ,
" parteiprogramm " : { " score " : 8 , " begründung " : " ... " }
}
] ,
" verbesserungen " : [
{
" original " : " Originaltext aus dem Antrag " ,
" vorschlag " : " Verbesserter Text mit **Ergänzungen** und ~~Streichungen~~ " ,
" begruendung " : " Bezug zu GWÖ-Themenfeld "
}
] ,
" stärken " : [ " Punkt 1 " , " Punkt 2 " ] ,
" schwächen " : [ " Punkt 1 " ] ,
" empfehlung " : " Ablehnen | Überarbeiten | Unterstützen mit Änderungen | Uneingeschränkt unterstützen " ,
" empfehlungSymbol " : " [X] | [!] | [+] | [++] " ,
" verbesserungspotenzial " : " gering | mittel | hoch | fundamental " ,
" themen " : [ " Bildung " , " Soziales " ] ,
" antragZusammenfassung " : " 1-2 Sätze Kernaussage " ,
" antragKernpunkte " : [ " Punkt 1 " , " Punkt 2 " , " Punkt 3 " ]
}
## Wichtige Regeln
- * * Verbesserungsvorschläge * * : Maximal 3 ! Fokussiere auf die wirkungsvollsten Änderungen , die den GWÖ - Score am meisten verbessern würden .
#60 Fix A+C: ENUM-basiertes Zitieren + top_k 2→5
Strukturelle Lösung für die LLM-Halluzinations-Cases aus #60:
A — ENUM-Anker
- format_quotes_for_prompt nummeriert jeden retrievten Chunk als [Q1], [Q2], …
- Neue ZITATEREGEL im Prompt erzwingt vier Bedingungen:
1. Jedes Zitat MUSS auf genau einen [Qn]-Chunk verweisen
2. Der text-String MUSS eine wörtliche, zusammenhängende Passage von
min. 5 Wörtern aus genau diesem Chunk sein
3. Die quelle MUSS exakt das Source-Label des gewählten Chunks sein
4. Wenn kein Chunk passt: leeres zitate-Array — lieber 0 als erfunden
- analyzer.py:get_system_prompt: Wichtige-Regeln-Block zieht den selben
Mechanismus nach, damit das LLM den [Qn]-Anker auch im System-Prompt
sieht und nicht nur im User-Prompt.
C — Recall-Boost
- analyzer.py:run_analysis: top_k_per_partei 2 → 5. In den drei Cases
aus #60 lagen die "richtigen" Seiten (S.36, S.37) bisher außerhalb
des Top-3-Windows; mit Top-5 erhöht sich die Wahrscheinlichkeit, dass
sie überhaupt im Kontext landen.
Hintergrund — die Halluzinationen waren KEIN Embedding-Bug:
Die retrievten Chunks für Case 1 enthielten S.58 (richtige Seite, falscher
Snippet) — das LLM hat den Snippet aus seinem Trainingswissen über
GRÜNE-Wahlprogramme rekonstruiert statt aus dem retrievten Chunk-Text zu
zitieren. Cases 2/3 hatten die zitierten Seiten gar nicht im Top-3-Window —
das LLM hat sowohl Seite als auch Snippet halluziniert. ENUM-Anker
verhindert beides strukturell, weil ein nicht-existenter [Qn] sofort
als Cheating sichtbar wäre.
Tests:
- test_chunks_get_enum_ids
- test_zitateregel_mentions_enum_anchor
- 179/179 grün
Refs: #60, #54 (Sub-D), #50 (Umbrella E2E)
2026-04-09 22:21:39 +02:00
- * * Zitate * * : Jedes Zitat MUSS auf einen ` [ Qn ] ` - Chunk aus dem mitgelieferten Kontext verweisen und den ` text ` - String * * wörtlich * * ( mind . 5 zusammenhängende Wörter ) aus genau diesem Chunk übernehmen . Kein Paraphrasieren , kein Cross - Referencing aus dem Trainingswissen . Wenn kein Chunk passt : lass ` zitate ` leer — lieber 0 Zitate als ein erfundenes . Die ausführliche ZITATEREGEL steht im wahlprogramm_zitate - Block .
2026-03-28 22:30:24 +01:00
- * * Matrix - Bewertung * * : Bewerte nur Felder , die der Antrag tatsächlich berührt . Nicht jeder Antrag betrifft alle 25 Felder .
- * * Gesamtscore - Berechnung * * : Der gwoeScore ( 0 - 10 ) berücksichtigt die Matrix - Bewertungen :
- Wenn EIN Feld - 4 oder - 5 hat → Gesamtscore maximal 3 / 10
- Wenn EIN Feld - 3 hat → Gesamtscore maximal 4 / 10
- Bei " Ablehnen " → Score 0 - 2 / 10
- Bei " Uneingeschränkt unterstützen " → Score 8 - 10 / 10
- * * Matrix - Felder * * : Bewertung - 5 bis + 5 ( Symbole : − − / − / ○ / + / + + ) """
def get_bundesland_context ( bundesland : str ) - > str :
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
""" Build the LLM context block for a specific state.
Liest Regierungsfraktionen und Parlamentsname aus ` ` BUNDESLAENDER ` ` und
die optionale Wahlprogramm - Übersichtsdatei aus ` ` WAHLPROGRAMM_KONTEXT_FILES ` ` .
Federal - level Grundsatzprogramme ( parteiprogramme . md ) sind bundesländer -
übergreifend .
Raises :
ValueError : bei unbekanntem oder inaktivem Bundesland . Pre - #5
existierte hier ein silent fallback auf NRW — bewusst entfernt ,
damit Konfigurationslücken früh sichtbar werden .
"""
bl = BUNDESLAENDER . get ( bundesland )
if bl is None :
raise ValueError ( f " Unbekanntes Bundesland: { bundesland } " )
if not bl . aktiv :
raise ValueError (
f " Bundesland { bundesland } ist nicht aktiv (siehe bundeslaender.py) "
)
wahlprogramm_kontext_file = WAHLPROGRAMM_KONTEXT_FILES . get ( bundesland )
wahlprogramme_text = (
load_context_file ( wahlprogramm_kontext_file ) if wahlprogramm_kontext_file else " "
)
parteiprogramme_text = load_context_file ( " parteiprogramme.md " )
2026-03-28 22:30:24 +01:00
return f """
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
## Parlament
{ bl . parlament_name } ( Wahlperiode { bl . wahlperiode } , seit { bl . wahlperiode_start } )
2026-03-28 22:30:24 +01:00
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
## Wahlprogramme {bl.name}
{ wahlprogramme_text or ' (keine Übersichtsdatei hinterlegt) ' }
2026-03-28 22:30:24 +01:00
## Grundsatzprogramme der Parteien
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
{ parteiprogramme_text }
## Regierungsfraktionen in {bl.name}
2026-03-28 22:30:24 +01:00
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
{ ' , ' . join ( bl . regierungsfraktionen ) }
2026-03-28 22:30:24 +01:00
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
## Im Landtag vertretene Fraktionen
{ ' , ' . join ( bl . landtagsfraktionen ) }
2026-03-28 22:30:24 +01:00
Bei Oppositionsanträgen : Bewerte zusätzlich , ob die Regierungsfraktionen zustimmen würden .
"""
async def analyze_antrag ( text : str , bundesland : str = " NRW " , model : str = " qwen-plus " ) - > Assessment :
""" Analyze a parliamentary motion using the LLM. """
client = AsyncOpenAI (
api_key = settings . dashscope_api_key ,
base_url = settings . dashscope_base_url ,
)
system_prompt = get_system_prompt ( )
bundesland_context = get_bundesland_context ( bundesland )
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
# Extrahiere Fraktionen aus Text (einfache Heuristik): Welche der im
# Landtag vertretenen Parteien werden im Antrag genannt? Quelle ist
# BUNDESLAENDER.landtagsfraktionen — nicht WAHLPROGRAMME, weil wir
# auch Fraktionen erkennen wollen, für die wir (noch) kein Wahlprogramm
# hinterlegt haben.
landtagsfraktionen = BUNDESLAENDER [ bundesland ] . landtagsfraktionen
text_lower = text . lower ( )
fraktionen = [
partei for partei in landtagsfraktionen
if partei in text or partei . lower ( ) in text_lower
]
2026-03-28 22:30:24 +01:00
# Suche relevante Zitate via semantische Suche (Embeddings)
quotes_context = " "
if EMBEDDINGS_DB . exists ( ) :
try :
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
semantic_quotes = get_relevant_quotes_for_antrag (
#60 Fix A+C: ENUM-basiertes Zitieren + top_k 2→5
Strukturelle Lösung für die LLM-Halluzinations-Cases aus #60:
A — ENUM-Anker
- format_quotes_for_prompt nummeriert jeden retrievten Chunk als [Q1], [Q2], …
- Neue ZITATEREGEL im Prompt erzwingt vier Bedingungen:
1. Jedes Zitat MUSS auf genau einen [Qn]-Chunk verweisen
2. Der text-String MUSS eine wörtliche, zusammenhängende Passage von
min. 5 Wörtern aus genau diesem Chunk sein
3. Die quelle MUSS exakt das Source-Label des gewählten Chunks sein
4. Wenn kein Chunk passt: leeres zitate-Array — lieber 0 als erfunden
- analyzer.py:get_system_prompt: Wichtige-Regeln-Block zieht den selben
Mechanismus nach, damit das LLM den [Qn]-Anker auch im System-Prompt
sieht und nicht nur im User-Prompt.
C — Recall-Boost
- analyzer.py:run_analysis: top_k_per_partei 2 → 5. In den drei Cases
aus #60 lagen die "richtigen" Seiten (S.36, S.37) bisher außerhalb
des Top-3-Windows; mit Top-5 erhöht sich die Wahrscheinlichkeit, dass
sie überhaupt im Kontext landen.
Hintergrund — die Halluzinationen waren KEIN Embedding-Bug:
Die retrievten Chunks für Case 1 enthielten S.58 (richtige Seite, falscher
Snippet) — das LLM hat den Snippet aus seinem Trainingswissen über
GRÜNE-Wahlprogramme rekonstruiert statt aus dem retrievten Chunk-Text zu
zitieren. Cases 2/3 hatten die zitierten Seiten gar nicht im Top-3-Window —
das LLM hat sowohl Seite als auch Snippet halluziniert. ENUM-Anker
verhindert beides strukturell, weil ein nicht-existenter [Qn] sofort
als Cheating sichtbar wäre.
Tests:
- test_chunks_get_enum_ids
- test_zitateregel_mentions_enum_anchor
- 179/179 grün
Refs: #60, #54 (Sub-D), #50 (Umbrella E2E)
2026-04-09 22:21:39 +02:00
text , fraktionen , bundesland = bundesland , top_k_per_partei = 5 ,
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
)
2026-03-28 22:30:24 +01:00
quotes_context = format_quotes_for_prompt ( semantic_quotes )
2026-04-09 21:57:56 +02:00
except ( NameError , AttributeError , TypeError , KeyError ) :
# Programmierfehler (z.B. der partei_upper-Refactor-Rest aus
# #55/eb045d0, der zu Issue #60 führte) sollen hart fehlschlagen
# statt still auf den schwächeren Keyword-Pfad zurückzufallen.
raise
Phase A: Audit-Restbefunde #57.3/4/7 (Roadmap #59)
Drei verbleibende Audit-Befunde aus #57 in einem Patch:
- **#57.3 MEDIUM** Drucksache-Regex-Validation: neue
app/validators.py mit validate_drucksache() als gemeinsamer
Validation-Funnel. Pattern ^\d{1,3}/\d{1,7}([-(].{1,20})?$ deckt
alle 10 aktiven Bundesländer (8/6390, 18/12345, 8/6390(neu),
23/3700-A) ab und blockt Path-Traversal (../, /etc/passwd) plus
Standard-Injection (;, <, &). Drei Endpoints durchgeschleust:
/api/assessment, /api/assessment/pdf, /api/analyze-drucksache.
- **#57.4 MEDIUM** print() → logging.getLogger(__name__): main.py
und analyzer.py auf strukturiertes Logging umgestellt. LLM-Inhalte
werden NICHT mehr als Volltext geloggt — neue Helper
_content_fingerprint() liefert nur "len=N sha1=XXXX", reicht zur
Forensik ohne Antrag-Inhalte ins Container-Log zu leaken.
basicConfig() mit ISO-Format setzt strukturiertes Logging früh,
damit logger.exception() auch beim Boot greift.
- **#57.7 LOW-MED** Search-Query-Limit: validate_search_query() mit
MAX_SEARCH_QUERY_LEN=200 schützt /api/search und /api/search-landtag
vor 10-MB-Query-DoS. database._parse_search_query() loggt jetzt
shlex.ValueError-Fallback statt ihn zu verschlucken (deckt Memory-
Regel "stille excepts in Adaptern" ab).
Tests: neue tests/test_main_validators.py mit 22 Cases — Drucksache-
Whitelist-Roundtrip + Path-Traversal-Reject, Search-Query Längen-
Edge-Cases. 107 Unit-Tests grün (85 alt + 22 neu).
Validators in eigenem Modul (app/validators.py), damit Tests sie ohne
slowapi-Dependency direkt importieren können.
Refs: #57, #59 (Phase A)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:15:16 +02:00
except Exception :
logger . exception ( " Semantic search failed, falling back to keyword search " )
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
quotes = find_relevant_quotes ( text , fraktionen , bundesland = bundesland )
2026-03-28 22:30:24 +01:00
quotes_context = format_quote_for_prompt ( quotes )
else :
# Fallback to keyword search
Refactor wahlprogramme/embeddings/analyzer for multi-state (#5)
Atomic refactor of the three modules that previously hardcoded NRW
behaviour. After this commit, every analysis path consults the central
BUNDESLAENDER registry for governing fractions, parliament name, and
state metadata.
wahlprogramme.py
- WAHLPROGRAMME is now nested {bundesland: {partei: meta}}; NRW data
hoisted unchanged under the "NRW" key.
- New WAHLPROGRAMM_KONTEXT_FILES dict maps a state to its overview
markdown file (currently only NRW).
- find_relevant_quotes(text, fraktionen, bundesland) — bundesland is
now a required positional. Governing fractions for the requested
state are merged with the submitting fractions before lookup.
- Helpers get_wahlprogramm() and parteien_mit_wahlprogramm() expose
the new shape to other modules.
- ValueError on unknown bundesland (no silent fallback).
embeddings.py
- Schema migration in init_embeddings_db: adds a `bundesland` column
to the chunks table when missing, plus an index, and backfills
existing rows from the PROGRAMME registry. Grundsatzprogramme
(federal level) keep bundesland NULL by design.
- find_relevant_chunks accepts a bundesland filter that matches state
rows OR NULL — so federal Grundsatzprogramme remain visible to every
analysis.
- get_relevant_quotes_for_antrag(text, fraktionen, bundesland, …) —
bundesland required, governing fractions read from BUNDESLAENDER
instead of hardcoded ["CDU","GRÜNE"]. Order-preserving dedup
replaces the previous set-based merge.
- index_programm now writes the bundesland column on insert.
- Dropped the hardcoded "Wahlprogramm NRW 2022" label in
format_quotes_for_prompt — bundesland context is implicit in the
surrounding prompt block.
analyzer.py
- get_bundesland_context reads parlament_name, regierungsfraktionen,
landtagsfraktionen and the optional WAHLPROGRAMM_KONTEXT_FILES entry
from the central registry. Throws ValueError on unknown OR inactive
bundesland — kills the silent NRW fallback that previously masked
configuration gaps.
- The Antragsteller-detection heuristic now iterates
BUNDESLAENDER[bundesland].landtagsfraktionen instead of
WAHLPROGRAMME.keys(), so we recognise parties for which we don't
yet have a Wahlprogramm PDF.
- Both quote lookups (semantic + keyword fallback) now receive the
bundesland.
Resolves issue #5. Foundation for #2 (LSA), #3 (Berlin), #4 (MV).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 18:48:11 +02:00
quotes = find_relevant_quotes ( text , fraktionen , bundesland = bundesland )
2026-03-28 22:30:24 +01:00
quotes_context = format_quote_for_prompt ( quotes )
user_prompt = f """ Analysiere den folgenden Antrag:
< kontext >
{ bundesland_context }
< / kontext >
< wahlprogramm_zitate >
{ quotes_context if quotes_context else " Keine relevanten Zitate gefunden. " }
< / wahlprogramm_zitate >
< antrag >
{ text }
< / antrag >
Bewerte nach GWÖ - Matrix 2.0 für Gemeinden :
1. GWÖ - Treue ( 0 - 10 ) mit Matrix - Zuordnung und Symbolen ( + + / + / ○ / − / − − )
2. Wahlprogrammtreue der einreichenden Fraktion ( en ) UND Regierungsfraktionen ( 0 - 10 )
3. Parteiprogrammtreue der einreichenden Fraktion ( en ) UND Regierungsfraktionen ( 0 - 10 )
4. Bis zu 3 Verbesserungsvorschläge in Redline - Syntax
5. Themen - Tags für Kategorisierung
2026-04-08 11:31:21 +02:00
* * ZITATEREGEL — STRIKT : * * In jedem ` ` wahlprogrammScores [ ] . wahlprogramm . zitate [ ] . quelle ` `
und ` ` parteiprogrammScores [ ] . parteiprogramm . zitate [ ] . quelle ` ` musst du * * wortgleich * *
einen der oben in ` ` < wahlprogramm_zitate > ` ` aufgelisteten Quellen - Labels ( Programm - Name +
Seite ) übernehmen — z . B . ` ` " CDU Mecklenburg-Vorpommern Wahlprogramm 2021, S. 33 " ` ` .
Erfinde keine Quellen aus deinem Trainingswissen . Nimm keine Quelle aus einem anderen
Bundesland ( z . B . NRW 2022 ) als die hier aufgelisteten — selbst wenn dir die dortigen
Programme bekannter sind . Findest du oben für eine Partei keinen passenden Chunk , lass
` ` zitate ` ` leer ( ` ` [ ] ` ` ) und vermerke das in der ` ` begruendung ` ` .
2026-03-28 22:30:24 +01:00
Ausgabe als reines JSON ohne Markdown - Codeblöcke . """
# Retry loop for JSON parsing errors
max_retries = 3
last_error = None
for attempt in range ( max_retries ) :
response = await client . chat . completions . create (
model = model ,
messages = [
{ " role " : " system " , " content " : system_prompt } ,
{ " role " : " user " , " content " : user_prompt } ,
] ,
temperature = 0.3 + ( attempt * 0.1 ) , # Slightly increase temp on retry
max_tokens = 4000 ,
)
content = response . choices [ 0 ] . message . content . strip ( )
# Remove markdown code blocks if present
if content . startswith ( " ``` " ) :
content = content . split ( " \n " , 1 ) [ 1 ]
if content . endswith ( " ``` " ) :
content = content . rsplit ( " ``` " , 1 ) [ 0 ]
if content . startswith ( " ```json " ) :
content = content [ 7 : ]
content = content . strip ( )
try :
# Parse JSON
data = json . loads ( content )
# Convert to Assessment model
return Assessment . model_validate ( data )
except json . JSONDecodeError as e :
last_error = e
Phase A: Audit-Restbefunde #57.3/4/7 (Roadmap #59)
Drei verbleibende Audit-Befunde aus #57 in einem Patch:
- **#57.3 MEDIUM** Drucksache-Regex-Validation: neue
app/validators.py mit validate_drucksache() als gemeinsamer
Validation-Funnel. Pattern ^\d{1,3}/\d{1,7}([-(].{1,20})?$ deckt
alle 10 aktiven Bundesländer (8/6390, 18/12345, 8/6390(neu),
23/3700-A) ab und blockt Path-Traversal (../, /etc/passwd) plus
Standard-Injection (;, <, &). Drei Endpoints durchgeschleust:
/api/assessment, /api/assessment/pdf, /api/analyze-drucksache.
- **#57.4 MEDIUM** print() → logging.getLogger(__name__): main.py
und analyzer.py auf strukturiertes Logging umgestellt. LLM-Inhalte
werden NICHT mehr als Volltext geloggt — neue Helper
_content_fingerprint() liefert nur "len=N sha1=XXXX", reicht zur
Forensik ohne Antrag-Inhalte ins Container-Log zu leaken.
basicConfig() mit ISO-Format setzt strukturiertes Logging früh,
damit logger.exception() auch beim Boot greift.
- **#57.7 LOW-MED** Search-Query-Limit: validate_search_query() mit
MAX_SEARCH_QUERY_LEN=200 schützt /api/search und /api/search-landtag
vor 10-MB-Query-DoS. database._parse_search_query() loggt jetzt
shlex.ValueError-Fallback statt ihn zu verschlucken (deckt Memory-
Regel "stille excepts in Adaptern" ab).
Tests: neue tests/test_main_validators.py mit 22 Cases — Drucksache-
Whitelist-Roundtrip + Path-Traversal-Reject, Search-Query Längen-
Edge-Cases. 107 Unit-Tests grün (85 alt + 22 neu).
Validators in eigenem Modul (app/validators.py), damit Tests sie ohne
slowapi-Dependency direkt importieren können.
Refs: #57, #59 (Phase A)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:15:16 +02:00
logger . warning (
" LLM JSON parse error attempt %d / %d ( %s ) — content %s " ,
attempt + 1 , max_retries , e , _content_fingerprint ( content ) ,
)
2026-03-28 22:30:24 +01:00
if attempt < max_retries - 1 :
continue
else :
Phase A: Audit-Restbefunde #57.3/4/7 (Roadmap #59)
Drei verbleibende Audit-Befunde aus #57 in einem Patch:
- **#57.3 MEDIUM** Drucksache-Regex-Validation: neue
app/validators.py mit validate_drucksache() als gemeinsamer
Validation-Funnel. Pattern ^\d{1,3}/\d{1,7}([-(].{1,20})?$ deckt
alle 10 aktiven Bundesländer (8/6390, 18/12345, 8/6390(neu),
23/3700-A) ab und blockt Path-Traversal (../, /etc/passwd) plus
Standard-Injection (;, <, &). Drei Endpoints durchgeschleust:
/api/assessment, /api/assessment/pdf, /api/analyze-drucksache.
- **#57.4 MEDIUM** print() → logging.getLogger(__name__): main.py
und analyzer.py auf strukturiertes Logging umgestellt. LLM-Inhalte
werden NICHT mehr als Volltext geloggt — neue Helper
_content_fingerprint() liefert nur "len=N sha1=XXXX", reicht zur
Forensik ohne Antrag-Inhalte ins Container-Log zu leaken.
basicConfig() mit ISO-Format setzt strukturiertes Logging früh,
damit logger.exception() auch beim Boot greift.
- **#57.7 LOW-MED** Search-Query-Limit: validate_search_query() mit
MAX_SEARCH_QUERY_LEN=200 schützt /api/search und /api/search-landtag
vor 10-MB-Query-DoS. database._parse_search_query() loggt jetzt
shlex.ValueError-Fallback statt ihn zu verschlucken (deckt Memory-
Regel "stille excepts in Adaptern" ab).
Tests: neue tests/test_main_validators.py mit 22 Cases — Drucksache-
Whitelist-Roundtrip + Path-Traversal-Reject, Search-Query Längen-
Edge-Cases. 107 Unit-Tests grün (85 alt + 22 neu).
Validators in eigenem Modul (app/validators.py), damit Tests sie ohne
slowapi-Dependency direkt importieren können.
Refs: #57, #59 (Phase A)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:15:16 +02:00
# Letzter Fehlversuch — Fingerprint reicht zur Forensik;
# Volltext darf nicht ins Log, weil er Antrag-Inhalte enthält
logger . error (
" LLM JSON parsing exhausted retries, content %s " ,
_content_fingerprint ( content ) ,
)
2026-03-28 22:30:24 +01:00
raise