Zum Inhalt springen

Feature: Ratsdokumente & Beschlüsse

Auswertung der öffentlichen Dokumente des Oldenburger Ratsinformationssystems: Sitzungsprotokolle (→ Beschlüsse, Abstimmungen, Anwesenheit), Vorlagen (→ Sachverhalt & Begründung) und Anlagen (→ Original-Anträge der Fraktionen mit Antragsteller-Erkennung) — durchsuchbar im Web, verknüpft mit den Themen der Nutzer:innen.

Diagramm: Welche Dokumente Ratslotse auswertet — Sitzungskalender, Protokolle, Vorlagen und Anlagen — und welche Features daraus entstehen

  • Vergangene Sitzungen tragen ein „Protokoll (öffentlich)“-PDF (getfile.php?id=…), erkennbar am Label. Veröffentlicht mit Verzögerung (Tage–Wochen).
  • Inhalt pro Protokoll: Metadaten (Protokoll-Nr., Datum, Beginn/Ende), Teilnehmer mit Fraktion, und pro TOP Beschlusstext + Abstimmungsergebnis (einstimmig / mehrheitlich bei N Gegenstimmen, welche Fraktion, angenommen/abgelehnt/vertagt).
  • Bestand: lückenlos seit Januar 2018 (~100 Sitzungen/Jahr, ~820 Protokolle, über 7.700 Beschlüsse). Kalendereinträge ohne veröffentlichtes Protokoll (Absagen) sind normal und werden bei jedem Lauf erneut geprüft.
  • Vorlagen (~5.000) tragen ihren Inhalt nur im PDF — die vo0050-Seite liefert bloß Metadaten (Betreff, Nr., Art). Es gibt keine Vorlagen-Art „Antrag“: Die Original-Anträge der Fraktionen hängen als Anlagen-PDFs an den Vorlagen.
  • Kosten: LLM braucht nur die Protokoll-Extraktion ≈ $0,007/Protokoll (1 Call/Protokoll) und die Klassifikation. Vorlagen und Anlagen werden ohne LLM ausgewertet (pypdf + Wortlisten) — ihr Backfill kostet $0.

Die Protokoll-Auswertung lebt in drei Tabellen. Sie sind bewusst so geschnitten, dass spätere Features andocken können (siehe „Forward-looking“).

council_protocols -- eine Zeile je verarbeitetem Protokoll
ksinr PK, document_id, document_url, protocol_nr,
session_start, session_end, raw_text, n_pages,
model, extracted_at, status -- ok | failed
council_decisions -- eine Zeile je TOP/Beschluss
id PK, ksinr, position, item_number, title,
kind, parent_item, -- decision | subvote (Änderungsantrag zu parent_item)
beschluss, outcome, -- angenommen|abgelehnt|vertagt|zur_kenntnis|kein_beschluss
vote, -- einstimmig|mehrheitlich|null
gegenstimmen, enthaltungen, -- nullable INTEGER
factions, -- JSON-Array (z.B. ["SPD","CDU","FDP"])
vorlage_nr, kvonr, -- Link zur Vorlage
raw_result, -- Roh-String der Abstimmung
policy_field, policy_tags, summary, -- Themenfeld-Klassifikation (council/topics.py)
amount_eur, -- erkannter €-Betrag (council/money.py)
simple_summary, -- „Lotti erklärt's einfach" (council/simple_summary.py)
importance, -- angezeigter Wichtigkeits-Wert 0–100 (Heuristik + Tragweite, 50/50)
impact, impact_reason, -- Tragweite 0–100 + 1-Satz-Begründung (council/impact.py)
interest, interest_reason -- Gesprächswert 0–100 + Begründung (council/interest.py)
council_attendance -- eine Zeile je Person je Sitzung
id PK, ksinr, name, party, role, note -- role: vorsitz|mitglied|verwaltung|protokoll|gast

raw_text wird gespeichert, damit wir Protokolle ohne erneuten Download mit besseren Prompts neu auswerten können. Beschlüsse tragen vorlage_nr/kvonr, damit sie später mit Vorlagen verknüpfbar sind.

Seither dazugekommen — beide Dokumentklassen werden ohne LLM ausgewertet:

council_vorlagen -- eine Zeile je Vorlage (kvonr = SessionNet-Dokument-ID)
kvonr PK, vorlage_nr, title, art, -- art: Beschlussvorlage | Berichtsvorlage | …
document_id, document_url,
raw_text, n_pages, -- pypdf-Volltext (Sachverhalt & Begründung)
fetched_at, status, -- ok | empty | no_pdf | failed
anlagen_scanned
council_anlagen -- eine Zeile je Anlage einer Vorlage
document_id PK, kvonr, label, url,
is_antrag, -- Label-Muster: Antrag/Änderungsantrag/Anfrage
antragsteller, -- JSON, erkannte Fraktionen (Wortlisten, parties_in_text)
raw_text, n_pages, -- Volltext NUR für Anträge
fetched_at, status -- listed | ok | empty | failed
  • Beschluss ↔ Vorlage läuft über vorlage_nr (mit Basis-Fallback: 22/0348/122/0348) — Protokolle liefern nie ein kvonr.
  • Antragsteller-Erkennung: Label zuerst („Antrag der SPD-Fraktion …“), sonst erste PDF-Seite; Mehrparteien-Labels („Antrag SPD CDU Grüne FDP”) zählen für alle. Wortgrenzen verhindern Fehltreffer („Begrünung“ ≠ Grüne).
  • Täglicher Rescan der letzten 45 Tage: Änderungsanträge landen oft erst kurz vor der Sitzung auf der Vorlagen-Seite; bereits eingelesene Dokumente werden nicht erneut geladen.
  • Daraus entstehen: „Aus der Vorlage“ + Anlagen-Dossier auf der Beschluss-Seite, die Erfolgsquoten je Fraktion in der Analyse, Vorlagen-/Antragstext im FTS-Index und im Kontext der KI-Frage.

Beratungsfolge & Personen-Stammdaten (council/stammdaten.py)

Abschnitt betitelt „Beratungsfolge & Personen-Stammdaten (council/stammdaten.py)“

Drei weitere Seitentypen des Ratsinfos, ebenfalls ohne LLM ausgewertet:

council_beratungen -- eine Zeile je Beratungsstation einer Vorlage
kvonr, datum, gremium, top, is_public,
ergebnis, -- NULL = geplant/ohne Ergebnis
ksinr -- Link zur Sitzung
council_persons -- Mandatsträger (kpenr = SessionNet-Personen-ID)
kpenr PK, name, fraktion_aktuell -- Fraktion NUR als aktueller Stand!
council_memberships -- Gremien-Mitgliedschaften je Person
kpenr, kgrnr, gremium, rolle, von, bis -- zurück bis 2001
  • Die Beratungsfolge zeigt den offiziellen Weg jeder Vorlage durch die Gremien — mit Ergebnis je Station und erst geplanten künftigen Beratungen. Sichtbar als „Weg der Vorlage“ auf den Beschluss-Seiten.
  • Wichtig: Das Ratsinfo überschreibt Fraktionszugehörigkeiten rückwirkend mit dem aktuellen Stand (an einem Fraktionswechsler verifiziert). Der Fraktions-Verlauf einer Person wird deshalb aus unseren Anwesenheitsdaten je Sitzung abgeleitet — die Ratsinfo-Fraktion dient nur als aktueller Stand.
  • Kontaktdaten der Personenseiten (Adresse, Telefon, Beruf) werden bewusst nicht übernommen.

Beschlüsse werden gegen die Themen der Nutzer:innen klassifiziert (mit strengem Verify-Pass) und speisen „Beschlüsse zu deinen Themen“ samt Benachrichtigung. Die Treffer liegen in council_topic_matches — und zwar in der Konten-DB (nwz.sqlite), nicht in council.sqlite: Sie hängen an einem Konto, die Ratsdaten selbst bleiben nutzerunabhängig. Welche Treffer schon gesehen wurden, steht in topic_hits_seen (Grundlage des „Neu“-Zählers).

Das Modul council/protocols.py erledigt drei Schritte:

  • das öffentliche Protokoll-PDF erkennen (Dokument-Link mit dem Label „Protokoll … öffentlich“ auf der Sitzungsseite),
  • den Text extrahieren (pypdf — die PDFs tragen seit 2018 eine Textebene, kein OCR nötig),
  • ein LLM-Aufruf je Protokoll → Metadaten, Anwesenheit und alle Beschlüsse als strukturiertes JSON (robust gegen leere Modell-Antworten).

Ein idempotenter Backfill über Datumsbereiche hat den Bestand seit 2018 eingelesen; der tägliche Lauf zieht neu veröffentlichte Protokolle nach und lädt Vorlagen-/Anlagen-Volltexte (inkl. Rescan der letzten Wochen, s. o.).

/council (Ratsinformationssystem) hat zwei Tabs:

  • „Sitzungen & Tagesordnungen“ — die TOP-Suche über kommende und vergangene Sitzungen.
  • „Beschlüsse“ — Volltextsuche mit Filtern (Ausschuss / Datum / Ergebnis / Fraktion / Themenfeld), sortierbar auch nach Wichtigkeit („Wichtigste zuerst“). Bedeutende Beschlüsse tragen ein „Wichtig“-Zeichen.

Die Beschluss-Detailseite zeigt Abstimmung, Anwesenheit, „Aus der Vorlage“, das Anlagen-Dossier und Links zu den Original-PDFs; die Analyse rechnet daraus u. a. Erfolgsquoten je Fraktion.

Der angezeigte Wichtigkeits-Wert (0–100) entsteht aus zwei Hälften: einer regelbasierten Heuristik (council/importance.py — Geldbetrag, Umstrittenheit, Verbindlichkeit & Gremien-Ebene, Länge des Beratungswegs) und der per LLM bewerteten Tragweite (council/impact.py). Beide werden 50/50 gemischt, sobald die Tragweite vorliegt. Fehlt ein Heuristik-Signal (kein €-Betrag, keine Abstimmung), fällt es aus der Gewichtung, statt als 0 zu zählen — die übrigen Signale tragen dann entsprechend mehr. Die Beschluss-Seite schlüsselt beides auf: den Punkte-Beitrag je Signal, die Tragweite mit ihrer Ein-Satz-Begründung („Warum wichtig: …“) und den Mittelwert daraus.

Details, Gewichte und Rechenbeispiele: Bewertungs-Scores.

Daten, Web-UI, Themen-Anbindung (Beschlüsse ↔ Nutzer-Themen mit Verify-Pass und Benachrichtigung), Vorlagen & Anlagen sowie Beratungsfolge und Personen-/Gremien-Stammdaten sind produktiv. Aus dem Abstimmungssatz der Protokolle werden außerdem Teilvoten je Fraktion geparst (council_decision_votes): wo die Niederschrift Gegenstimmen oder Enthaltungen ausdrücklich einer Fraktion/Gruppe zuschreibt („bei Gegenstimmen der Fraktion X“), steht das strukturiert in der Datenbank — konservativ, ohne Zustimmung abzuleiten, Gruppen bleiben Gruppen. Offen: Redebeiträge (stehen nicht im Ratsinfo), namentliche Abstimmungen je Person (existieren dort nicht — mehr als der Abstimmungssatz gibt die Quelle nicht her).