gwoe-antragspruefer/app/presse_generator.py

443 lines
15 KiB
Python
Raw Normal View History

feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
"""Pressemitteilungs-Generator fuer #170 Phase 4.
Erzeugt einen LLM-generierten Pressemitteilungs-Vorschlag, der einen
GWÖ-bewerteten Antrag in den Kontext eines aktuellen News-Artikels stellt.
Manueller Trigger via UI-Button kein Auto-Versand. Drafts werden in
``presse_drafts`` persistiert und in der UI als Liste sichtbar.
Tonalitaet:
- GWÖ-Sicht (Gemeinwohl-orientiert, nicht parteipolitisch)
- Faktenbasiert, keine Lobbying-Sprache
- 200-250 Worte, presseaehnlicher Aufbau (Lead-Paragraph + Begruendung)
"""
from __future__ import annotations
import json
import logging
import sqlite3
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
SYSTEM_PROMPT = """Du bist Pressereferent:in einer Gemeinwohl-Ökonomie-
fix(#174): PM-Prompt mit harter Verbotsliste + Few-Shot User-Feedback nach Live-Test: PMs waren kuerzer + nicht anschaulicher. Im Output stand "Score von 4,0/10", "in den Bereichen Buerger:innen, Wirtschaft, Staat, Gesellschaft und Natur" (Matrix-Zeilen D+E), "staerkt Solidaritaet, Wuerde und Demokratie" (GWÖ-Werte-Liste), Floskeln wie "innovative Loesungen" und "faktenbasierter Dialog". Komplett-Refactor: **ABSOLUT VERBOTEN im PM-Text:** - Numerische Scores ("GWÖ-Score 4/10", "X von 10 Punkten") - GWÖ-Wert-Listen als Aufzaehlung - Beruehrungsgruppen-Sprache ("Bereiche Buerger, Wirtschaft, Staat, ...") - Matrix-Codes ("Feld D2", "A1") - GWÖ-Begriffe als Schlagwort (max 1× pro Begriff, nur konkret) - Floskeln (zukunftsweisend, innovativ, faktenbasierter Dialog, ...) **PFLICHT: Mindestens 3 Buerger:innen-Lebenslagen mit konkreter Wirkung:** - Familien mit Kindern (Beträge, KiTa-Plätze) - Pflegebeduerftige + Angehoerige (Wartezeiten, Kosten) - Auszubildende / Studierende (Abbruchrisiko, BAföG) - Pendler:innen (Spritpreis, ÖPNV-Tarif) - Mieter:innen (Mietniveau, Nebenkosten) - Rentner:innen / Geringverdiener:innen (Kaufkraft in Euro) - Selbststaendige / kleine Betriebe (Buerokratie-Stunden, Steuern) Pro Lebenslage: konkreter quantifizierter Effekt ("verlaengert Wartezeit auf Heimplatz von 8 auf 12 Wochen", "spart einer vierkoepfigen Familie etwa 1.800 €/Jahr"). **Few-Shot:** Schlechtes Beispiel + Gutes Beispiel im Prompt. Das gute Beispiel zeigt 30%-Abbrecherquote, 2 Stunden Beratung, 800 zusaetzliche Pflegekraefte in 5 Jahren — konkret quantifizierte Wirkungen aus echten Zahlen. **Laenger:** 320–380 Worte (vorher 220–280) — konkrete Beispiele brauchen Platz. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:22:00 +02:00
Initiative. Du schreibst für **Bürger:innen vor Ort**, nicht für
Fachpublikum. Deine PM erklärt nur eines: **Was ändert sich durch diesen
Antrag konkret im Alltag positiv oder negativ?**
## ABSOLUT VERBOTEN im PM-Text
Diese Begriffe und Konstrukte dürfen im Body NICHT vorkommen:
- **Numerische Scores oder Bewertungen** kein "GWÖ-Score 4/10",
kein "X von 10 Punkten", kein "der Antrag erhält". Du verwendest
die Bewertung nur INTERN als Kompass. Im Text: nur die Wirkung.
- **GWÖ-Wert-Listen als Aufzählung** kein "stärkt Menschenwürde,
Solidarität und Demokratie". Stattdessen die konkrete Wirkung beim
Bürger nennen.
- **GWÖ-Berührungsgruppen-Sprache** kein "in den Bereichen Bürger,
Wirtschaft, Staat, Gesellschaft und Natur", kein "ökologischer
Wirkungshorizont", kein "Lieferant:innen-Dimension".
- **Matrix-Codes** nie "Feld D2", "A1", "Würde×Lieferanten".
- **GWÖ-Begriffe als Werte-Schlagwort** Begriffe wie "Solidarität",
"Würde", "Nachhaltigkeit", "Gerechtigkeit", "Demokratie",
"Gemeinwohl" dürfen jeweils maximal EINMAL vorkommen, und nur dann,
wenn sie eine konkrete Handlung qualifizieren ("solidarisch finanziert
durch eine Mehreinnahme aus Erbschaftsteuer" ✓ ja).
- **Floskeln**: "zukunftsweisend", "innovativ", "richtungsweisend",
"Systemwechsel", "faktenbasierter Dialog", "wir laden zum Dialog ein",
"im Sinne von", "zielgerichtet", "ganzheitlich", "umfassend",
"ausgewogen", "nachhaltige Zukunft sichern".
## PFLICHT im PM-Text
Mindestens DREI dieser Bürger:innen-Lebenslagen müssen mit konkreter,
quantifizierter oder qualitativer Wirkung benannt werden:
- **Familien mit Kindern**: konkrete Beträge, KiTa-Plätze, Schulgeld, Wohnraum
- **Pflegebedürftige + ihre Angehörigen**: Wartezeiten, Eigenanteile,
Heimplatz-Kosten
- **Auszubildende / Studierende**: Ausbildungsabbruch-Risiko, BAföG,
Mietkosten
- **Pendler:innen**: Spritpreis, ÖPNV-Tarif, Anbindung
- **Mieter:innen / Eigentümer:innen**: Mietniveau, Nebenkosten,
Sanierungskosten
- **Rentner:innen / Geringverdiener:innen**: Kaufkraft-Effekt in Euro
- **Selbstständige / kleine Betriebe**: bürokratische Pflicht-Stunden,
Energiekosten, Steuern
Pro Lebenslage: ein konkreter Effekt ("verlängert die Wartezeit auf einen
Heimplatz von 8 auf 12 Wochen", "spart einer vierköpfigen Familie etwa
1.800 pro Jahr", "erhöht die Mietnebenkosten in Bestandsgebäuden um
geschätzt 25 /Monat").
## Wenn die GWÖ-Bewertung KRITISCH ist (intern niedrig)
Drücke das in der PM aus über:
- **Wer verliert** ("Mieter:innen in Großstädten zahlen mehr")
- **Was fehlt** ("Der Antrag adressiert nicht die ökologischen Folgen
des Strassenausbaus, obwohl 40 % der CO2-Emissionen aus Verkehr stammen")
- **Was eine bessere Alternative wäre** ("Statt der Pendlerpauschale
würde ein Mobilitätsgeld unabhängig vom Verkehrsmittel auch
ÖPNV-Nutzer:innen entlasten")
## Wenn die GWÖ-Bewertung POSITIV ist
Drücke das aus über:
- **Wer gewinnt konkret** ("Auszubildende mit Lernschwierigkeiten bekommen
2 Stunden Beratung pro Woche")
- **Was sich messbar verbessert** ("die Abbrecherquote in der Pflege
könnte um geschätzt 15 % sinken")
- **Wo der Antrag stärker werden könnte** (1-2 konkrete Vorschläge,
ohne Floskel)
## Stil
fix(#174): PM-Prompt mit harter Verbotsliste + Few-Shot User-Feedback nach Live-Test: PMs waren kuerzer + nicht anschaulicher. Im Output stand "Score von 4,0/10", "in den Bereichen Buerger:innen, Wirtschaft, Staat, Gesellschaft und Natur" (Matrix-Zeilen D+E), "staerkt Solidaritaet, Wuerde und Demokratie" (GWÖ-Werte-Liste), Floskeln wie "innovative Loesungen" und "faktenbasierter Dialog". Komplett-Refactor: **ABSOLUT VERBOTEN im PM-Text:** - Numerische Scores ("GWÖ-Score 4/10", "X von 10 Punkten") - GWÖ-Wert-Listen als Aufzaehlung - Beruehrungsgruppen-Sprache ("Bereiche Buerger, Wirtschaft, Staat, ...") - Matrix-Codes ("Feld D2", "A1") - GWÖ-Begriffe als Schlagwort (max 1× pro Begriff, nur konkret) - Floskeln (zukunftsweisend, innovativ, faktenbasierter Dialog, ...) **PFLICHT: Mindestens 3 Buerger:innen-Lebenslagen mit konkreter Wirkung:** - Familien mit Kindern (Beträge, KiTa-Plätze) - Pflegebeduerftige + Angehoerige (Wartezeiten, Kosten) - Auszubildende / Studierende (Abbruchrisiko, BAföG) - Pendler:innen (Spritpreis, ÖPNV-Tarif) - Mieter:innen (Mietniveau, Nebenkosten) - Rentner:innen / Geringverdiener:innen (Kaufkraft in Euro) - Selbststaendige / kleine Betriebe (Buerokratie-Stunden, Steuern) Pro Lebenslage: konkreter quantifizierter Effekt ("verlaengert Wartezeit auf Heimplatz von 8 auf 12 Wochen", "spart einer vierkoepfigen Familie etwa 1.800 €/Jahr"). **Few-Shot:** Schlechtes Beispiel + Gutes Beispiel im Prompt. Das gute Beispiel zeigt 30%-Abbrecherquote, 2 Stunden Beratung, 800 zusaetzliche Pflegekraefte in 5 Jahren — konkret quantifizierte Wirkungen aus echten Zahlen. **Laenger:** 320–380 Worte (vorher 220–280) — konkrete Beispiele brauchen Platz. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:22:00 +02:00
- 320380 Worte (länger als bisher konkrete Beispiele brauchen Platz)
- Aktive Verben, kurze Sätze (max 22 Worte)
- Drucksachen-Nummer einmal im Lead nennen ("Drucksache 21/4757")
- Bezug zur News-Lage in 1 Satz, ohne den Medienanbieter zu nennen
- Keine Negativ-Polemik gegen Parteien sachliche Kritik am Inhalt
## Struktur
fix(#174): PM-Prompt mit harter Verbotsliste + Few-Shot User-Feedback nach Live-Test: PMs waren kuerzer + nicht anschaulicher. Im Output stand "Score von 4,0/10", "in den Bereichen Buerger:innen, Wirtschaft, Staat, Gesellschaft und Natur" (Matrix-Zeilen D+E), "staerkt Solidaritaet, Wuerde und Demokratie" (GWÖ-Werte-Liste), Floskeln wie "innovative Loesungen" und "faktenbasierter Dialog". Komplett-Refactor: **ABSOLUT VERBOTEN im PM-Text:** - Numerische Scores ("GWÖ-Score 4/10", "X von 10 Punkten") - GWÖ-Wert-Listen als Aufzaehlung - Beruehrungsgruppen-Sprache ("Bereiche Buerger, Wirtschaft, Staat, ...") - Matrix-Codes ("Feld D2", "A1") - GWÖ-Begriffe als Schlagwort (max 1× pro Begriff, nur konkret) - Floskeln (zukunftsweisend, innovativ, faktenbasierter Dialog, ...) **PFLICHT: Mindestens 3 Buerger:innen-Lebenslagen mit konkreter Wirkung:** - Familien mit Kindern (Beträge, KiTa-Plätze) - Pflegebeduerftige + Angehoerige (Wartezeiten, Kosten) - Auszubildende / Studierende (Abbruchrisiko, BAföG) - Pendler:innen (Spritpreis, ÖPNV-Tarif) - Mieter:innen (Mietniveau, Nebenkosten) - Rentner:innen / Geringverdiener:innen (Kaufkraft in Euro) - Selbststaendige / kleine Betriebe (Buerokratie-Stunden, Steuern) Pro Lebenslage: konkreter quantifizierter Effekt ("verlaengert Wartezeit auf Heimplatz von 8 auf 12 Wochen", "spart einer vierkoepfigen Familie etwa 1.800 €/Jahr"). **Few-Shot:** Schlechtes Beispiel + Gutes Beispiel im Prompt. Das gute Beispiel zeigt 30%-Abbrecherquote, 2 Stunden Beratung, 800 zusaetzliche Pflegekraefte in 5 Jahren — konkret quantifizierte Wirkungen aus echten Zahlen. **Laenger:** 320–380 Worte (vorher 220–280) — konkrete Beispiele brauchen Platz. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:22:00 +02:00
1. **Lead-Paragraph** (2-3 Sätze): Welche Bürger:innengruppe wird wie
betroffen? Drucksache nennen.
2. **Konkrete Wirkung 1** (3-4 Sätze): erste Lebenslage + Effekt
3. **Konkrete Wirkung 2** (3-4 Sätze): zweite Lebenslage + Effekt
4. **Konkrete Wirkung 3** (2-3 Sätze): dritte Lebenslage + Effekt
5. **Was fehlt / was wäre besser** (2-3 Sätze): konkreter Vorschlag
6. **Schluss-Satz**: was wir fordern, ohne Floskel
## BEISPIELE für den Stil
**SCHLECHT** (verboten):
> Der Antrag stärkt Menschenwürde, Solidarität und Demokratie. Er trägt
> zu einer nachhaltigeren Zukunft bei und stärkt das Gemeinwohl in den
> Bereichen Bürger:innen und Staat. GWÖ-Score: 8.0/10.
**GUT** (gewünscht):
> Auszubildende in der Pflege brechen ihre Ausbildung heute zu rund
> 30 % ab meist wegen Überlastung oder fehlender Lernunterstützung.
> Die in Drucksache 8/310 vorgeschlagene sozialpädagogische Begleitung
> würde diese Lücke schließen. Konkret: zwei Stunden Einzelberatung
> pro Auszubildender pro Woche. Für Familien, deren Kinder einen
> Pflegeberuf wählen, sinkt damit das Risiko, dass die teure Ausbildung
> erfolglos endet. Für Krankenhäuser und Altenheime in Brandenburg
> bedeutet das: in fünf Jahren etwa 800 zusätzliche fertig ausgebildete
> Pflegekräfte. Was der Antrag nicht regelt: die Bezahlung in der
> Ausbildungszeit selbst. Solange Auszubildende neben dem Lernen
> arbeiten müssen, um die Miete zu zahlen, hilft auch die beste Beratung
> nur begrenzt. Wir fordern, eine Mindest-Ausbildungsvergütung
> mitzudenken.
## Output-Format
Antworte NUR mit gültigem JSON:
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
{
fix(#174): PM-Prompt mit harter Verbotsliste + Few-Shot User-Feedback nach Live-Test: PMs waren kuerzer + nicht anschaulicher. Im Output stand "Score von 4,0/10", "in den Bereichen Buerger:innen, Wirtschaft, Staat, Gesellschaft und Natur" (Matrix-Zeilen D+E), "staerkt Solidaritaet, Wuerde und Demokratie" (GWÖ-Werte-Liste), Floskeln wie "innovative Loesungen" und "faktenbasierter Dialog". Komplett-Refactor: **ABSOLUT VERBOTEN im PM-Text:** - Numerische Scores ("GWÖ-Score 4/10", "X von 10 Punkten") - GWÖ-Wert-Listen als Aufzaehlung - Beruehrungsgruppen-Sprache ("Bereiche Buerger, Wirtschaft, Staat, ...") - Matrix-Codes ("Feld D2", "A1") - GWÖ-Begriffe als Schlagwort (max 1× pro Begriff, nur konkret) - Floskeln (zukunftsweisend, innovativ, faktenbasierter Dialog, ...) **PFLICHT: Mindestens 3 Buerger:innen-Lebenslagen mit konkreter Wirkung:** - Familien mit Kindern (Beträge, KiTa-Plätze) - Pflegebeduerftige + Angehoerige (Wartezeiten, Kosten) - Auszubildende / Studierende (Abbruchrisiko, BAföG) - Pendler:innen (Spritpreis, ÖPNV-Tarif) - Mieter:innen (Mietniveau, Nebenkosten) - Rentner:innen / Geringverdiener:innen (Kaufkraft in Euro) - Selbststaendige / kleine Betriebe (Buerokratie-Stunden, Steuern) Pro Lebenslage: konkreter quantifizierter Effekt ("verlaengert Wartezeit auf Heimplatz von 8 auf 12 Wochen", "spart einer vierkoepfigen Familie etwa 1.800 €/Jahr"). **Few-Shot:** Schlechtes Beispiel + Gutes Beispiel im Prompt. Das gute Beispiel zeigt 30%-Abbrecherquote, 2 Stunden Beratung, 800 zusaetzliche Pflegekraefte in 5 Jahren — konkret quantifizierte Wirkungen aus echten Zahlen. **Laenger:** 320–380 Worte (vorher 220–280) — konkrete Beispiele brauchen Platz. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:22:00 +02:00
"titel": "<thesenstark, max 100 Zeichen, NENNT die Bürger:innengruppe oder den konkreten Effekt — nicht den GWÖ-Score>",
"body": "<320380 Worte. Mindestens 3 Lebenslagen mit konkretem Effekt. Keine GWÖ-Werte-Aufzählung. Kein Score.>"
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
}"""
def _build_user_prompt(
drucksache: str,
bundesland: str,
antrag_titel: str,
antrag_zusammenfassung: str,
gwoe_score: float,
gwoe_begruendung: str,
empfehlung: str,
news_titel: str,
news_summary: str,
news_url: str,
) -> str:
"""Konstruiert den User-Prompt aus Antrags- und News-Daten."""
return f"""## Aktueller Antrag
Drucksache: {drucksache} ({bundesland})
Titel: {antrag_titel}
Zusammenfassung: {antrag_zusammenfassung or "(keine vorhanden)"}
GWÖ-Score: {gwoe_score}/10
GWÖ-Begründung: {gwoe_begruendung or "(keine vorhanden)"}
Empfehlung: {empfehlung or "(keine)"}
## Aktueller Nachrichten-Kontext
Schlagzeile: {news_titel}
Inhalt: {news_summary or "(keine Zusammenfassung verfügbar)"}
Quelle: {news_url}
## Deine Aufgabe
Schreibe eine Pressemitteilung, die diesen Antrag in den Kontext der
aktuellen Nachrichtenlage stellt. Begründe aus GWÖ-Sicht, warum der
Antrag gerade jetzt relevant ist (oder warum er die aktuelle Debatte
ergänzt/korrigiert). Wenn der GWÖ-Score niedrig ist (< 5), sei dabei
kritisch die PM kann auch eine Ablehnung des Antrags begründen.
"""
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
def _find_existing_draft(
drucksache: str, news_url: str, db_path: Path,
) -> Optional[dict]:
"""Sucht einen bereits generierten Draft fuer (drucksache, news_url).
Bei mehreren Treffern wird der NEUESTE zurueckgegeben. Idempotenz-
Schutz vor doppelter LLM-Generierung (#170 Followup).
"""
if not Path(db_path).exists():
return None
conn = sqlite3.connect(str(db_path))
try:
row = conn.execute(
"""SELECT id, drucksache, bundesland, news_url, news_titel,
titel, body, model, created_at
FROM presse_drafts
WHERE drucksache=? AND news_url=?
ORDER BY id DESC LIMIT 1""",
(drucksache, news_url),
).fetchone()
finally:
conn.close()
if not row:
return None
return {
"id": row[0], "drucksache": row[1], "bundesland": row[2],
"news_url": row[3], "news_titel": row[4],
"titel": row[5], "body": row[6], "model": row[7],
"created_at": row[8],
}
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
async def generate_draft(
drucksache: str,
news_url: str,
db_path: Optional[Path] = None,
bewerter=None,
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
force: bool = False,
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
) -> dict:
"""Erzeugt einen Pressemitteilungs-Draft und persistiert ihn.
Args:
drucksache: ID des Antrags (mit Bundesland-Kontext aus DB).
news_url: URL des News-Artikels (Lookup in news_articles).
db_path: optional override fuer Tests.
bewerter: optional injected QwenBewerter (fuer Tests). Wenn None,
wird der Default mit settings instanziiert.
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
force: Wenn True, wird auch bei vorhandenem Draft fuer das gleiche
(drucksache, news_url)-Paar ein neuer LLM-Call gemacht.
Default False Idempotenz-Schutz vor LLM-Kosten.
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
Returns:
``{"id": int, "drucksache": ..., "bundesland": ...,
"news_url": ..., "news_titel": ...,
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
"titel": str, "body": str, "model": str, "created_at": ISO,
"_was_existing": bool}``
``_was_existing=True`` zeigt an, dass kein neuer LLM-Call gemacht
wurde, sondern ein vorhandener Draft zurueckgegeben wurde.
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
Raises:
ValueError: wenn drucksache oder news_url nicht gefunden.
"""
from .config import settings
from .adapters.qwen_bewerter import LlmRequest
path = db_path or settings.db_path
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
# Idempotenz-Check: hat es schon einen Draft fuer das Paar?
if not force:
existing = _find_existing_draft(drucksache, news_url, path)
if existing:
existing["_was_existing"] = True
return existing
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
conn = sqlite3.connect(str(path))
try:
antrag = conn.execute(
"""SELECT bundesland, title, antrag_zusammenfassung, gwoe_score,
gwoe_begruendung, empfehlung
FROM assessments WHERE drucksache=?""",
(drucksache,),
).fetchone()
news = conn.execute(
"SELECT titel, summary FROM news_articles WHERE url=?",
(news_url,),
).fetchone()
finally:
conn.close()
if not antrag:
raise ValueError(f"Drucksache {drucksache} nicht in assessments")
if not news:
raise ValueError(f"News-URL {news_url} nicht in news_articles")
user_prompt = _build_user_prompt(
drucksache=drucksache,
bundesland=antrag[0],
antrag_titel=antrag[1] or "",
antrag_zusammenfassung=antrag[2] or "",
gwoe_score=antrag[3] or 0.0,
gwoe_begruendung=antrag[4] or "",
empfehlung=antrag[5] or "",
news_titel=news[0],
news_summary=news[1] or "",
news_url=news_url,
)
if bewerter is None:
from .adapters.qwen_bewerter import QwenBewerter
bewerter = QwenBewerter()
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
# Premium-Modell (qwen-max) statt -plus, weil PM-Erzeugung hoehere
# Sprachqualitaet braucht als Antrags-Bewertung. Tradeoff: ~3× teurer
# (~6 Cent statt 2 Cent), ~2× langsamer (~30 s statt 15 s).
model = settings.llm_model_premium
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
req = LlmRequest(
system_prompt=SYSTEM_PROMPT,
user_prompt=user_prompt,
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
model=model,
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
base_temperature=0.3,
max_tokens=1500,
max_retries=2,
json_object_mode=True,
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
)
result = await bewerter.bewerte(req)
titel = (result.get("titel") or "").strip()[:200]
body = (result.get("body") or "").strip()
if not titel or not body:
raise ValueError("LLM-Response unvollständig (titel oder body leer)")
# Persist
conn = sqlite3.connect(str(path))
try:
cur = conn.execute(
"""INSERT INTO presse_drafts
(drucksache, bundesland, news_url, news_titel, titel, body, model)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
(drucksache, antrag[0], news_url, news[0], titel, body, model),
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
)
draft_id = cur.lastrowid
row = conn.execute(
"""SELECT id, drucksache, bundesland, news_url, news_titel,
titel, body, model, created_at
FROM presse_drafts WHERE id=?""",
(draft_id,),
).fetchone()
conn.commit()
finally:
conn.close()
return {
feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen User-Feedback nach Live-Test: **1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert, doppelter Klick erzeugte doppelten Draft + LLM-Kosten. - Neuer Helper `_find_existing_draft(drucksache, news_url)` der den neuesten Draft fuer das Paar zurueckgibt - `generate_draft()` prueft per Default zuerst den Lookup, liefert existing zurueck mit `_was_existing=True` (kein LLM-Call) - `force=True` Parameter fuer bewusste Neu-Generierung - Endpoint nimmt `?force=true` Query-Param entgegen - UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner, mit "Neu generieren"-Button im existing-Banner **2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet ("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max). - Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer (~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in Pressemitteilungs-Diktion - confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s **3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt: - Container-Check (skip wenn down) analog zu run-digest.sh - START/END-Timestamps - Ausfuehrliche cron-install-Doku im Header - Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit), wird embed_pending_articles bis zu 500 weitere nachgeholt Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite 1053 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:10:20 +02:00
"_was_existing": False,
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
"id": row[0], "drucksache": row[1], "bundesland": row[2],
"news_url": row[3], "news_titel": row[4],
"titel": row[5], "body": row[6], "model": row[7],
"created_at": row[8],
}
def list_drafts(
limit: int = 20,
db_path: Optional[Path] = None,
) -> list[dict]:
"""Liste der zuletzt generierten Drafts. Default-Limit 20."""
from .config import settings
path = db_path or settings.db_path
if not Path(path).exists():
return []
conn = sqlite3.connect(str(path))
try:
rows = conn.execute(
"""SELECT id, drucksache, bundesland, news_url, news_titel,
titel, body, model, created_at
FROM presse_drafts
ORDER BY id DESC LIMIT ?""",
(limit,),
).fetchall()
finally:
conn.close()
feat(#170 followup 2): Pre-Filter, Cluster, Antrags-Initiative, PM-Versionierung, Mail-Link User-Feedback: Aktuelle-Themen-Dashboard war "Detective-Modus" — durch viele News scrollen, Match-Stärke selbst interpretieren. Komplett-Refactor zur kuratierten Sicht mit Tabs. **1. Pre-Filter + GWÖ-Relevanz-Score (#134)** `compute_relevance(matches)`: Score = max(antrag.gwoe_score × similarity). Level: high (≥4.0) / mid (≥2.5) / low (>0) / none. Pro News in der UI ein farbiger Pill (gruen/orange/grau) + Reason-Text: "GWÖ-9.0/10-Antrag „Klimaschutzgesetz" (GRÜNE) passt mit Similarity 0.55." Default-Filter "Nur GWÖ-relevant" aktiv (only_relevant=true) — zeigt nur high/mid News, blendet Rauschen aus. Toggle-Checkbox. `/api/aktuelle-themen/top` neuer Param `only_relevant=true|false`. **2. PM-Versionierung im Modal (#135)** `list_drafts_for(drucksache, news_url)`: alle Versionen, neueste oben. Endpoint `/api/aktuelle-themen/drafts-versions`. Modal zeigt Dropdown wenn >1 Version, Switch ohne LLM-Call. Force-Regen bleibt als Button im "bestehender Entwurf"-Banner. **3. News-Cluster-View (#136)** `aggregate_news_cluster(intra_threshold=0.55, min_cluster_size=2)`: Greedy-Embedding-Cluster + zentralster Antrags-Match per Centroid- Vektor. Zweiter Tab "Themen-Cluster": 5 News über "Pflege" → 1 Cluster mit gemeinsamem Antrag-Vorschlag, statt 5 separate Cards. Endpoint: `/api/aktuelle-themen/cluster`. **4. Mail-Direkt-Link + Clipboard (#137)** Im PM-Modal zwei Buttons: - "📧 Per Mail versenden" (mailto: mit subject + body, ~1900 Char Limit) - "📋 In Zwischenablage kopieren" (navigator.clipboard.writeText) - Bei langem PM (>1900 Char): mailto-Link wird ausgegraut, Hinweis "PM zu lang für Mail-Link — Clipboard nutzen" **5. Antrags-Initiative (#138)** `aggregate_top_antraege_with_news(min_gwoe_score=8.0, days=14)`: Reverse-Sicht — pro Antrag mit GWÖ ≥ 8 die News-Resonanz. Antraege ohne Match werden trotzdem angezeigt mit "keine News"-Pill. Dritter Tab "GWÖ-Top-Anträge". Endpoint `.../top-antraege`. **UI-Restrukturierung:** statt einer langen Scroll-Liste jetzt 5 Tabs mit gemeinsamer Filter-Bar: - News × Anträge (Default, kuratiert via Pre-Filter) - Themen-Cluster (Bündel ähnlicher News) - GWÖ-Top-Anträge (Reverse) - News-Volumen (Chart) - PM-Entwürfe (Drafts-Liste) Default min_similarity 0.40 → 0.50 erhoeht (weniger Rauschen). Tests: 14 neue (compute_relevance × 5, only_relevant + sort × 3, cluster × 3, top_antraege × 3). Suite 1067 gruen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:41:31 +02:00
return [
{
"id": r[0], "drucksache": r[1], "bundesland": r[2],
"news_url": r[3], "news_titel": r[4],
"titel": r[5], "body": r[6], "model": r[7],
"created_at": r[8],
}
for r in rows
]
def list_drafts_for(
drucksache: str,
news_url: str,
db_path: Optional[Path] = None,
) -> list[dict]:
"""Alle Versions-Drafts fuer ein (drucksache, news_url)-Paar, neueste oben."""
from .config import settings
path = db_path or settings.db_path
if not Path(path).exists():
return []
conn = sqlite3.connect(str(path))
try:
rows = conn.execute(
"""SELECT id, drucksache, bundesland, news_url, news_titel,
titel, body, model, created_at
FROM presse_drafts
WHERE drucksache=? AND news_url=?
ORDER BY id DESC""",
(drucksache, news_url),
).fetchall()
finally:
conn.close()
feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen Vollständiges 4-Phasen-Feature: **Phase 1 — News-Aggregator** (`app/news_aggregator.py`) - Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen - Bundestag-RSS für aktuellethemen / pressemitteilungen / hib - DB-Tabelle `news_articles` (URL-PK, idempotent) - Embeddings via existierender qwen-v4-Pipeline - Cron-Script `scripts/auto-fetch-news.sh` - Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot, CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich- rechtliche/parlamentarische Quellen - Volltexte werden NICHT persistiert (nur Titel + erster Satz) **Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`) - News-Embedding × Assessment-summary_embedding via Cosine-Similarity - `find_anträge_for_news`: pro News die Top-K passenden Anträge - `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d) - `aggregate_top_themen`: primärer Dashboard-Endpoint - `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source **Phase 3 — Dashboard-View** (`/aktuelle-themen`) - Neuer linker Nav-Eintrag „Aktuelle Themen" - Stacked-Area-Chart News-Volumen pro Quelle (30d) - Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button - Filter: Zeitfenster, Top-N, min_similarity - Auth-protected (require_auth) **Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`) - LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output) - Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py - DB-Tabelle `presse_drafts` (Persistenz) - POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min, auth-only (LLM-Kosten) - GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail - Manueller Trigger via UI-Button, kein Auto-Versand - Modal-Anzeige des generierten Texts **Compliance:** - robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI- erlaubende Quellen verwendet) - UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion - Pressemitteilungen sind explizit Drafts, nicht Auto-Versand - LLM-Calls rate-limited, auth-only **Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching + 8 presse_generator). Suite jetzt 1048 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
return [
{
"id": r[0], "drucksache": r[1], "bundesland": r[2],
"news_url": r[3], "news_titel": r[4],
"titel": r[5], "body": r[6], "model": r[7],
"created_at": r[8],
}
for r in rows
]
def get_draft(
draft_id: int,
db_path: Optional[Path] = None,
) -> Optional[dict]:
"""Einen Draft per ID abrufen."""
from .config import settings
path = db_path or settings.db_path
if not Path(path).exists():
return None
conn = sqlite3.connect(str(path))
try:
row = conn.execute(
"""SELECT id, drucksache, bundesland, news_url, news_titel,
titel, body, model, created_at
FROM presse_drafts WHERE id=?""",
(draft_id,),
).fetchone()
finally:
conn.close()
if not row:
return None
return {
"id": row[0], "drucksache": row[1], "bundesland": row[2],
"news_url": row[3], "news_titel": row[4],
"titel": row[5], "body": row[6], "model": row[7],
"created_at": row[8],
}