- SYSTEM_PROMPT mit explizitem 'Mindestens 320 Worte, < 280 ist
Verstoss' + Hinweis 'wenn Substanz ausgeht: Lebenslage vertiefen
statt abbrechen'.
- Output-Format-Beispiel mit MINDESTENS-Hinweis.
- generate_draft prüft nach LLM-Call die Wortzahl. Bei <280 Worten:
ein einzelner Re-Prompt mit höherer Temperatur (0.5) und Hint zur
ersten zu-kurzen Wortzahl. Wenn der zweite Versuch laenger ist,
wird er übernommen — sonst bleibt der erste.
- max_retries=1 fuer den zweiten Call (nicht endlos).
Audit-Hauptbefund war 15/19 PMs unter Soll 320–380 Worten.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _split_into_thread_posts() splittet zu lange Bodies an Satzgrenzen
in mehrere Posts ≤ max_chars (Default 280). Greedy: möglichst viele
Sätze pro Post. Hashtags am Ende bleiben erhalten.
- generate_draft(style='thread') ruft den Splitter auf, wenn das LLM
weniger als 3 Posts oder Posts > 290 chars liefert.
- 7 Unit-Tests fuer den Splitter (test_thread_splitter.py).
- scripts/pm-quality-audit.sh: prueft alle PM-Drafts gegen Verbotsliste
(GWÖ-Score, Matrix-Codes, Floskeln) + Wortzahl + Absatzzahl + Post-Laengen.
Markdown-Report-Output. Audit von 23 Drafts: 4/23 ohne Auffaelligkeit;
Hauptbefund: PMs haeufig zu kurz, Threads splittten ohne Auto-Splitter
nicht zuverlaessig — Splitter behebt das.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Schema additiv: presse_drafts.style TEXT NOT NULL DEFAULT 'pm' via
ALTER TABLE (idempotent in init_db).
- presse_generator.generate_draft(style='pm'|'thread') nutzt eigenen
SYSTEM_PROMPT_THREAD (3-5 Posts à ≤280 Zeichen, Hook + Lebenslagen +
Forderung, Hashtags am Schluss; keine **fett**-Markdown).
- _find_existing_draft, list_drafts, list_drafts_for, get_draft liefern
jetzt auch das style-Feld zurueck.
- Endpoint /api/aktuelle-themen/generate-presse?style=thread baut den
Switch ein. Ohne Param weiterhin 'pm'.
- Frontend: PM-Modal zeigt den style-Tag (📰 PM / 🐦 Thread) im Banner
und bietet einen Knopf "Auch als Thread / Auch als PM" generieren.
Idempotenz pro (drucksache, news_url, style)-Tripel.
Refs: #170, #178
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PM-Prompt erlaubt nun max. eine Markdown-Bold-Markierung pro Absatz
(Schluessel-Zahl/Effekt). Force-Regen-Test bestaetigt: qwen-max liefert
**30 %** wie im Beispiel; renderPmBody im Frontend rendert das als
<strong>. Smoketests gegen die neuen Endpoints (score-histogram x4,
admin/stand x2 Auth-Walls) absichern Regressionen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Beobachtung im Draft #6: qwen-max nutzte einsame Anfuehrungs-
zeichen (") als Paragraph-Trenner statt \\n\\n. Optisch wirkte das
wie inkorrekte JSON-Escapes mitten im Text.
Zwei Mechanismen:
**1. Prompt-Erweiterung:**
Neuer Abschnitt "Paragraphen-Formatierung" mit explizitem Beispiel:
`"body": "Lead.\\n\\nWirkung 1.\\n\\nWirkung 2.\\n\\n..."`. Klar:
keine Anfuehrungszeichen oder Sonderzeichen als Trenner.
**2. Post-Process-Heuristik:**
Regex `([.!?])"([A-ZÄÖÜ])` → `\\1\\n\\n\\2`. Wenn ein " genau zwischen
Punkt+Whitespace und Großbuchstabe steht, ist es wahrscheinlich ein
Trenn-Klumpen, kein semantischer Anfuehrer. Wird durch echten
Paragraph-Break ersetzt.
Konservativ: nur dieses spezifische Pattern wird touched. Echte
Quotes (z.B. "Es ist Zeit, …", sagt X) bleiben unangetastet, weil sie
nicht direkt nach Satzschluss-Punkt stehen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beobachtung beim ersten Pressereferent-Output: qwen-max liefert
manchmal literale Backslash-n Sequenzen (2 chars: \\ + n) statt echter
Newline-Bytes im JSON-Body. Auch mit response_format=json_object aktiv.
Post-Process im PM-Generator: \\n / \\r / \\t Sequenzen durch echte
Newlines / CR / Tab ersetzen. Konservativ (nur diese drei).
Macht das Modal richtig formatiert mit Paragraphen-Breaks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beobachtung beim Force-Regen: alle 2 Retries scheiterten mit
"Invalid control character at: line 3 column 275". qwen-max produziert
JSON mit rohen \n statt \\n im body-String, was json.loads sprengt.
Zwei Fixes parallel:
**1. response_format={"type": "json_object"}** als optionaler Mode im
LlmRequest. PM-Generator setzt das jetzt. DashScope unterstuetzt das
fuer qwen-max + qwen-plus und zwingt valide JSON-Strings.
**2. Newline-Recovery als Fallback** im QwenBewerter:
`_recover_unescaped_newlines` iteriert char-weise mit String-Tracking,
ersetzt unescaped \n/\r/\t in Strings durch \\n/\\r/\\t. Backslash-
Folgen bleiben unangetastet. Wird vor dem Retry-Re-throw versucht.
Bewertungs-Pfad (analyzer.py) bekommt json_object_mode=False als Default,
um die bewaehrte Retry-Semantik nicht zu aendern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
**1. Default min_similarity 0.40 statt 0.50.** Live-Test auf dev:
mit 0.50 zeigt only_relevant=true 0 buckets, weil zu strikt fuer die
aktuelle Sparse-Datenlage (77 Bewertungen × 30 News). Mit 0.40 bleiben
1 high + 2 mid News pro 7-Tage-Fenster — genau die kuratierte Sicht,
die wir wollen.
**2. PM-System-Prompt umgeschrieben** als Pressereferent statt
Redakteur. User-Wunsch: "Bürger:innen anschaulich machen, was sich
durch den Antrag konkret im Leben vor Ort aendert".
Pflicht-Elemente im neuen Prompt:
- Konkrete Alltagswirkung (mindestens 2 Beispiele aus Lebenslagen:
Pflegekraefte, Familien, Mieter:innen, Pendler:innen, ...)
- GWÖ-Verbesserungspotential bei nicht voll ueberzeugenden Antraegen
(was fehlt, wie ginge es besser aus GWÖ-Sicht)
- Bei negativen Antraegen: klar benennen was verschlechtert wird,
konkret quantifiziert wo moeglich
- 220–280 Worte (vorher 200–250)
- Aktive Verben, kurze Saetze, keine Floskeln
- Strukturierter Aufbau: Lead → Beispiele + GWÖ-Bewertung →
Verbesserungspotential → Forderung
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>