527 lines
19 KiB
Python
527 lines
19 KiB
Python
"""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-
|
||
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
|
||
|
||
- 320–380 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
|
||
|
||
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
|
||
|
||
## Paragraphen-Formatierung
|
||
|
||
WICHTIG: trenne die 6 Abschnitte mit **doppeltem Newline** (`\\n\\n`)
|
||
im JSON-String. NIEMALS Anführungszeichen oder andere Sonderzeichen
|
||
als Paragraph-Trenner verwenden. Beispiel:
|
||
|
||
```json
|
||
{"body": "Lead-Satz.\\n\\nWirkung 1.\\n\\nWirkung 2.\\n\\nWirkung 3.\\n\\nWas fehlt.\\n\\nForderung."}
|
||
```
|
||
|
||
Im JSON: `\\n` als Escape-Sequenz (zwei Zeichen: Backslash + n).
|
||
NICHT: rohe Newline-Bytes im String, NICHT: `"`-Zeichen als Trenner.
|
||
|
||
## Hervorhebungen (sparsam)
|
||
|
||
Du darfst pro Absatz **maximal eine** Schlüssel-Zahl oder den
|
||
zentralen Effekt mit Markdown-`**fett**` markieren — z.B. die
|
||
Abbrecherquote, eine Mehrkostensumme, eine Anzahl betroffener
|
||
Personen. Mehr als eine Markierung pro Absatz wirkt unruhig.
|
||
Niemals ganze Sätze fett, niemals Zwischenüberschriften.
|
||
|
||
## 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:
|
||
{
|
||
"titel": "<thesenstark, max 100 Zeichen, NENNT die Bürger:innengruppe oder den konkreten Effekt — nicht den GWÖ-Score>",
|
||
"body": "<320–380 Worte. Mindestens 3 Lebenslagen mit konkretem Effekt. Keine GWÖ-Werte-Aufzählung. Kein Score.>"
|
||
}"""
|
||
|
||
|
||
SYSTEM_PROMPT_THREAD = """Du bist Social-Media-Redakteur:in einer
|
||
Gemeinwohl-Ökonomie-Initiative. Erzeuge einen knappen Thread (3–5 Posts)
|
||
für Mastodon/Twitter, der einen GWÖ-bewerteten Antrag im Kontext einer
|
||
aktuellen Nachricht erklärt — für Bürger:innen, nicht für Fachpublikum.
|
||
|
||
## ABSOLUT VERBOTEN
|
||
|
||
- Numerische GWÖ-Scores oder Bewertungs-Zahlen.
|
||
- GWÖ-Werte-Listen ("Würde, Solidarität, …") als Schlagwortkette.
|
||
- Matrix-Codes (D2, A1, …) und GWÖ-Berührungsgruppen-Sprache.
|
||
- Lobbyfloskeln ("zukunftsweisend", "innovativ", "richtungsweisend").
|
||
- Reine Schlagworte ohne konkrete Folge im Alltag.
|
||
|
||
## Stil
|
||
|
||
- Pro Post **maximal 280 Zeichen** (inkl. Hashtags). Kein Post länger.
|
||
- Erster Post ist der **Hook**: konkrete Bürger:innengruppe + sichtbare
|
||
Folge. Drucksache nennen.
|
||
- Mittlere Posts: je eine konkrete Lebenslage + Effekt in Zahlen oder
|
||
Personen. Aktive Verben.
|
||
- Letzter Post: was wir fordern, klar, ohne Floskel.
|
||
- 1–2 thematische Hashtags am Schluss (z.B. #GWO #Pflege).
|
||
|
||
## Hervorhebungen
|
||
|
||
Keine Markdown-Formatierung im Body — Mastodon/Twitter rendert das nicht.
|
||
Ausnahme: Zahlen können dezent in Klammern ergänzt werden („30 %", „800
|
||
Pflegekräfte"). Kein **fett**, kein _kursiv_, KEINE eckigen Klammern
|
||
`[…]`, KEINE Backslashes vor Klammern, KEINE Markdown-Links.
|
||
|
||
## Output-Format
|
||
|
||
Antworte NUR mit gültigem JSON:
|
||
{
|
||
"titel": "<Hook-Satz, max 100 Zeichen>",
|
||
"body": "<3-5 Posts in EINEM String, jeder Post in eigenem Absatz>"
|
||
}
|
||
|
||
**TRENNUNG ZWISCHEN POSTS:** im JSON-`body` als `\\n\\n` (Backslash + n
|
||
+ Backslash + n — exakt zwei Escape-Sequenzen, nicht roh, nicht
|
||
einfach Newline). Jeder Post ist eine Mini-Einheit für sich,
|
||
**maximal 280 Zeichen lang**. Mehr als 280 Zeichen pro Post sind ein
|
||
Verstoß gegen Mastodon/Twitter-Limits. Beispiel:
|
||
|
||
```json
|
||
{"body": "Post 1 mit Hook und Drucksache.\\n\\nPost 2 mit erster Lebenslage.\\n\\nPost 3 mit zweiter.\\n\\nWir fordern: ... #GWO"}
|
||
```"""
|
||
|
||
|
||
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.
|
||
"""
|
||
|
||
|
||
def _find_existing_draft(
|
||
drucksache: str, news_url: str, db_path: Path,
|
||
style: str = "pm",
|
||
) -> Optional[dict]:
|
||
"""Sucht einen bereits generierten Draft fuer (drucksache, news_url, style).
|
||
|
||
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, style
|
||
FROM presse_drafts
|
||
WHERE drucksache=? AND news_url=? AND style=?
|
||
ORDER BY id DESC LIMIT 1""",
|
||
(drucksache, news_url, style),
|
||
).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], "style": row[9],
|
||
}
|
||
|
||
|
||
async def generate_draft(
|
||
drucksache: str,
|
||
news_url: str,
|
||
db_path: Optional[Path] = None,
|
||
bewerter=None,
|
||
force: bool = False,
|
||
style: str = "pm",
|
||
) -> 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.
|
||
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.
|
||
|
||
Returns:
|
||
``{"id": int, "drucksache": ..., "bundesland": ...,
|
||
"news_url": ..., "news_titel": ...,
|
||
"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.
|
||
|
||
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
|
||
if style not in ("pm", "thread"):
|
||
raise ValueError(f"unbekannter style: {style}")
|
||
|
||
# Idempotenz-Check: hat es schon einen Draft fuer das (Paar, style)?
|
||
if not force:
|
||
existing = _find_existing_draft(drucksache, news_url, path, style=style)
|
||
if existing:
|
||
existing["_was_existing"] = True
|
||
return existing
|
||
|
||
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()
|
||
|
||
# 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
|
||
|
||
system_prompt_active = SYSTEM_PROMPT_THREAD if style == "thread" else SYSTEM_PROMPT
|
||
req = LlmRequest(
|
||
system_prompt=system_prompt_active,
|
||
user_prompt=user_prompt,
|
||
model=model,
|
||
base_temperature=0.3,
|
||
max_tokens=1500,
|
||
max_retries=2,
|
||
json_object_mode=True,
|
||
)
|
||
result = await bewerter.bewerte(req)
|
||
|
||
titel = (result.get("titel") or "").strip()[:200]
|
||
body = (result.get("body") or "").strip()
|
||
# Post-Process Step 1: literal-escapte Sequenzen → echte Whitespaces.
|
||
# qwen-max liefert manchmal '\\n' als 2 chars statt echtem Newline.
|
||
body = body.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t")
|
||
# Post-Process Step 2: einsame Anführungszeichen mitten im Text als
|
||
# Paragraph-Trenner — qwen tut das gelegentlich trotz Prompt-Anweisung.
|
||
# Heuristik: ein " zwischen "Punkt-Whitespace" und "Großbuchstabe" ist
|
||
# wahrscheinlich ein Trenn-Klumpen, kein semantischer Anfuehrer.
|
||
import re as _re
|
||
body = _re.sub(r'([.!?])"([A-ZÄÖÜ])', r'\1\n\n\2', body)
|
||
if not titel or not body:
|
||
raise ValueError("LLM-Response unvollständig (titel oder body leer)")
|
||
|
||
# Persist (style additiv im Insert)
|
||
conn = sqlite3.connect(str(path))
|
||
try:
|
||
cur = conn.execute(
|
||
"""INSERT INTO presse_drafts
|
||
(drucksache, bundesland, news_url, news_titel, titel, body, model, style)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
||
(drucksache, antrag[0], news_url, news[0], titel, body, model, style),
|
||
)
|
||
draft_id = cur.lastrowid
|
||
row = conn.execute(
|
||
"""SELECT id, drucksache, bundesland, news_url, news_titel,
|
||
titel, body, model, created_at, style
|
||
FROM presse_drafts WHERE id=?""",
|
||
(draft_id,),
|
||
).fetchone()
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
return {
|
||
"_was_existing": False,
|
||
"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], "style": row[9],
|
||
}
|
||
|
||
|
||
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, style
|
||
FROM presse_drafts
|
||
ORDER BY id DESC LIMIT ?""",
|
||
(limit,),
|
||
).fetchall()
|
||
finally:
|
||
conn.close()
|
||
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], "style": r[9] if len(r) > 9 else "pm",
|
||
}
|
||
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, style
|
||
FROM presse_drafts
|
||
WHERE drucksache=? AND news_url=?
|
||
ORDER BY id DESC""",
|
||
(drucksache, news_url),
|
||
).fetchall()
|
||
finally:
|
||
conn.close()
|
||
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], "style": r[9] if len(r) > 9 else "pm",
|
||
}
|
||
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, style
|
||
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], "style": row[9] if len(row) > 9 else "pm",
|
||
}
|