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
|
|
|
"""Tests fuer app.presse_generator (#170 Phase 4)."""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import sqlite3
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from app.presse_generator import (
|
|
|
|
|
_build_user_prompt,
|
2026-05-03 13:10:20 +02:00
|
|
|
_find_existing_draft,
|
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
|
|
|
generate_draft,
|
|
|
|
|
get_draft,
|
|
|
|
|
list_drafts,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# Fixture: DB mit Antrag + News
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def db_with_antrag_and_news(tmp_path: Path) -> Path:
|
|
|
|
|
db = tmp_path / "test_presse.db"
|
|
|
|
|
conn = sqlite3.connect(str(db))
|
|
|
|
|
conn.execute("""
|
|
|
|
|
CREATE TABLE assessments (
|
|
|
|
|
drucksache TEXT PRIMARY KEY,
|
|
|
|
|
title TEXT,
|
|
|
|
|
bundesland TEXT,
|
|
|
|
|
antrag_zusammenfassung TEXT,
|
|
|
|
|
gwoe_score REAL,
|
|
|
|
|
gwoe_begruendung TEXT,
|
|
|
|
|
empfehlung TEXT
|
|
|
|
|
)
|
|
|
|
|
""")
|
|
|
|
|
conn.execute("""
|
|
|
|
|
CREATE TABLE news_articles (
|
|
|
|
|
url TEXT PRIMARY KEY,
|
|
|
|
|
titel TEXT NOT NULL,
|
|
|
|
|
summary TEXT
|
|
|
|
|
)
|
|
|
|
|
""")
|
|
|
|
|
conn.execute("""
|
|
|
|
|
CREATE TABLE presse_drafts (
|
|
|
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
|
|
drucksache TEXT NOT NULL,
|
|
|
|
|
bundesland TEXT NOT NULL,
|
|
|
|
|
news_url TEXT NOT NULL,
|
|
|
|
|
news_titel TEXT NOT NULL,
|
|
|
|
|
titel TEXT NOT NULL,
|
|
|
|
|
body TEXT NOT NULL,
|
|
|
|
|
model TEXT NOT NULL,
|
chore: konsolidiere Working-Tree mit dev-Stand nach Nextcloud-Sync-Resolution
Mehrtaegiger Sync-Stillstand hatte ueber 50 conflicted-copy-Dateien im
Working-Tree erzeugt. Die jeweils neuere Version wurde basierend auf
md5-Hash-Vergleich zum laufenden gwoe-antragspruefer-dev-Container
eingespielt.
Konsolidiert (38 modifiziert):
- analyzer.py, auswertungen.py, auth.py, config.py, database.py,
drucksache_typen.py, embeddings.py, main.py, models.py, parlamente.py,
ports/llm_bewerter.py, presse_generator.py, redline_utils.py, report.py,
validators.py, wahlprogramm_fetch.py, wahlprogramm-links.yaml,
wahlprogramm-shas.lock.json
- v2-Templates: base, components/{icon, matrix_mini, queue_widget,
result_row}, screens/{admin_queue, admin_stand, aktuelle-themen,
antrag_detail, auswertungen, cluster, landtag_suche, merkliste,
methodik, tags}, static/v2/v2.css
- Tests: test_embeddings (Strict-Mode-Drop in reconstruct_zitate),
test_endpoints_smoke, test_presse_generator, test_report,
test_wahlprogramme (mit TestRegierungsbildung-Block, +120 LOC)
- docker-compose.dev.yml, docs/adr/index.md, docs/reference/api.md, mkdocs.yml
Neuzugaenge:
- app/marker.py, app/pm_render.py — Konsistenz-Marker, PM-Render-Adapter
- app/templates/v2/screens/scorecard{,_portrait,_werkstatt}.html — Cloud-Design-Scorecard
- app/static/v3/, app/templates/v3/ — v3-Layout-Hierarchie
- docs/adr/0010-stimmverhalten-gwoe-aggregat.md
- docs/adr/0011-aktuelle-themen-pm-generator.md
- docs/adr/0012-debug-auth-token-bypass.md
- scripts/{auto-rate-orphans, pm-quality-audit, pm-sample-bundle, rotate-debug-token}.sh
- tests/e2e/test_smoke_browser.py, tests/test_{auto_rate_runs, icons,
marker, pm_render, presse_generator_style, thread_splitter,
v2_pdf_consistency}.py
Plus inhaltlich uebernommen aus dem Conflict-Stand:
- embeddings.py: fw-by-2023.partei korrigiert von "FW" zu "FREIE WAEHLER"
(war Mismatch zu wahlprogramme.py)
- embeddings.py: detailliertere Naming der BTW-2025-Wahlprogramme
Test-Suite-Stand: 1209 passed, 73 skipped (4 pre-existing failures in
test_presse_generator_style.py + 1 collection error in
integration/test_citations_substring.py — beide nicht durch dieses
Konsolidierungs-Commit verursacht).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:04:21 +02:00
|
|
|
style TEXT NOT NULL DEFAULT 'pm',
|
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
|
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
|
|
|
)
|
|
|
|
|
""")
|
|
|
|
|
conn.execute(
|
|
|
|
|
"""INSERT INTO assessments
|
|
|
|
|
(drucksache, title, bundesland, antrag_zusammenfassung,
|
|
|
|
|
gwoe_score, gwoe_begruendung, empfehlung)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
|
|
|
(
|
|
|
|
|
"18/A", "Wohnungsbau-Reform-Antrag", "NRW",
|
|
|
|
|
"Antrag fuer mehr sozialen Wohnungsbau",
|
|
|
|
|
8.5, "Stark gemeinwohlorientiert",
|
|
|
|
|
"Uneingeschränkt unterstützen",
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
conn.execute(
|
|
|
|
|
"INSERT INTO news_articles (url, titel, summary) VALUES (?, ?, ?)",
|
|
|
|
|
(
|
|
|
|
|
"https://example.com/wohnen",
|
|
|
|
|
"Wohnungsmarkt im Umbruch",
|
|
|
|
|
"Die Mietpreise steigen weiter, der Bundestag berät heute",
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
conn.close()
|
|
|
|
|
return db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# _build_user_prompt
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestBuildUserPrompt:
|
|
|
|
|
def test_includes_drucksache(self):
|
|
|
|
|
prompt = _build_user_prompt(
|
|
|
|
|
drucksache="18/A", bundesland="NRW",
|
|
|
|
|
antrag_titel="Test", antrag_zusammenfassung="Summary",
|
|
|
|
|
gwoe_score=7.5, gwoe_begruendung="ok",
|
|
|
|
|
empfehlung="Unterstützen",
|
|
|
|
|
news_titel="News", news_summary="Lead",
|
|
|
|
|
news_url="https://example.com",
|
|
|
|
|
)
|
|
|
|
|
assert "18/A" in prompt
|
|
|
|
|
assert "NRW" in prompt
|
|
|
|
|
assert "7.5" in prompt
|
|
|
|
|
assert "News" in prompt
|
|
|
|
|
|
|
|
|
|
def test_handles_missing_zusammenfassung(self):
|
|
|
|
|
prompt = _build_user_prompt(
|
|
|
|
|
drucksache="x", bundesland="x", antrag_titel="x",
|
|
|
|
|
antrag_zusammenfassung="", gwoe_score=5.0,
|
|
|
|
|
gwoe_begruendung="", empfehlung="",
|
|
|
|
|
news_titel="x", news_summary="", news_url="",
|
|
|
|
|
)
|
|
|
|
|
assert "(keine vorhanden)" in prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# generate_draft (mocked QwenBewerter)
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class FakeBewerter:
|
|
|
|
|
"""Mock fuer QwenBewerter, gibt fixe LLM-Response zurueck."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, response: dict):
|
|
|
|
|
self._response = response
|
|
|
|
|
self.last_request = None
|
|
|
|
|
|
|
|
|
|
async def bewerte(self, request):
|
|
|
|
|
self.last_request = request
|
|
|
|
|
return self._response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_generate_draft_persists_record(db_with_antrag_and_news, monkeypatch):
|
|
|
|
|
bewerter = FakeBewerter({
|
|
|
|
|
"titel": "Wohnungsbau jetzt",
|
|
|
|
|
"body": "Der vorliegende Antrag der Drucksache 18/A ..."
|
|
|
|
|
* 10, # langer Body
|
|
|
|
|
})
|
|
|
|
|
# Patch settings.dashscope_model fuer den INSERT
|
|
|
|
|
from app.config import settings as real_settings
|
2026-05-03 13:10:20 +02:00
|
|
|
monkeypatch.setattr(real_settings, "llm_model_premium", "qwen-test")
|
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 generate_draft(
|
|
|
|
|
drucksache="18/A",
|
|
|
|
|
news_url="https://example.com/wohnen",
|
|
|
|
|
db_path=db_with_antrag_and_news,
|
|
|
|
|
bewerter=bewerter,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result["id"] == 1
|
|
|
|
|
assert result["drucksache"] == "18/A"
|
|
|
|
|
assert result["bundesland"] == "NRW"
|
|
|
|
|
assert result["news_titel"] == "Wohnungsmarkt im Umbruch"
|
|
|
|
|
assert result["titel"] == "Wohnungsbau jetzt"
|
|
|
|
|
assert "18/A" in result["body"]
|
2026-05-03 13:10:20 +02:00
|
|
|
assert result["_was_existing"] is False
|
|
|
|
|
assert result["model"] == "qwen-test" # premium-Modell wurde verwendet
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_generate_draft_idempotency_returns_existing(
|
|
|
|
|
db_with_antrag_and_news, monkeypatch,
|
|
|
|
|
):
|
|
|
|
|
"""Zweiter Aufruf mit gleicher (drucksache, news_url) liefert den
|
|
|
|
|
existing Draft, ohne neuen LLM-Call (kein call_count increase)."""
|
|
|
|
|
bewerter = FakeBewerter({
|
|
|
|
|
"titel": "PM Erstgenerierung",
|
|
|
|
|
"body": "Body 1 ..." * 30,
|
|
|
|
|
})
|
|
|
|
|
from app.config import settings as real_settings
|
|
|
|
|
monkeypatch.setattr(real_settings, "llm_model_premium", "qwen-test")
|
|
|
|
|
|
|
|
|
|
first = await generate_draft(
|
|
|
|
|
drucksache="18/A",
|
|
|
|
|
news_url="https://example.com/wohnen",
|
|
|
|
|
db_path=db_with_antrag_and_news, bewerter=bewerter,
|
|
|
|
|
)
|
|
|
|
|
assert first["_was_existing"] is False
|
|
|
|
|
|
|
|
|
|
# Zweiter Call mit anderer Bewerter-Antwort, soll aber NICHT verwendet
|
|
|
|
|
# werden — existing Draft wird zurueckgeliefert.
|
|
|
|
|
bewerter2 = FakeBewerter({
|
|
|
|
|
"titel": "Sollte NICHT auftauchen",
|
|
|
|
|
"body": "Sollte NICHT auftauchen ..." * 30,
|
|
|
|
|
})
|
|
|
|
|
second = await generate_draft(
|
|
|
|
|
drucksache="18/A",
|
|
|
|
|
news_url="https://example.com/wohnen",
|
|
|
|
|
db_path=db_with_antrag_and_news, bewerter=bewerter2,
|
|
|
|
|
)
|
|
|
|
|
assert second["_was_existing"] is True
|
|
|
|
|
assert second["id"] == first["id"]
|
|
|
|
|
assert second["titel"] == "PM Erstgenerierung" # nicht der zweite!
|
|
|
|
|
# Bewerter2 wurde NICHT aufgerufen
|
|
|
|
|
assert bewerter2.last_request is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_generate_draft_force_makes_new_call(
|
|
|
|
|
db_with_antrag_and_news, monkeypatch,
|
|
|
|
|
):
|
|
|
|
|
"""Mit force=True wird auch bei vorhandenem Draft neu generiert."""
|
|
|
|
|
bewerter = FakeBewerter({"titel": "Erstgen", "body": "Body 1 " * 30})
|
|
|
|
|
from app.config import settings as real_settings
|
|
|
|
|
monkeypatch.setattr(real_settings, "llm_model_premium", "qwen-test")
|
|
|
|
|
|
|
|
|
|
first = await generate_draft(
|
|
|
|
|
drucksache="18/A",
|
|
|
|
|
news_url="https://example.com/wohnen",
|
|
|
|
|
db_path=db_with_antrag_and_news, bewerter=bewerter,
|
|
|
|
|
)
|
|
|
|
|
bewerter2 = FakeBewerter({"titel": "Force-Regen", "body": "Body 2 " * 30})
|
|
|
|
|
second = await generate_draft(
|
|
|
|
|
drucksache="18/A",
|
|
|
|
|
news_url="https://example.com/wohnen",
|
|
|
|
|
db_path=db_with_antrag_and_news, bewerter=bewerter2,
|
|
|
|
|
force=True,
|
|
|
|
|
)
|
|
|
|
|
assert second["_was_existing"] is False
|
|
|
|
|
assert second["id"] != first["id"]
|
|
|
|
|
assert second["titel"] == "Force-Regen"
|
|
|
|
|
assert bewerter2.last_request is not None
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_generate_draft_unknown_drucksache(db_with_antrag_and_news):
|
|
|
|
|
bewerter = FakeBewerter({"titel": "x", "body": "y"})
|
|
|
|
|
with pytest.raises(ValueError, match="Drucksache"):
|
|
|
|
|
await generate_draft(
|
|
|
|
|
drucksache="99/MISSING",
|
|
|
|
|
news_url="https://example.com/wohnen",
|
|
|
|
|
db_path=db_with_antrag_and_news,
|
|
|
|
|
bewerter=bewerter,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_generate_draft_unknown_news(db_with_antrag_and_news):
|
|
|
|
|
bewerter = FakeBewerter({"titel": "x", "body": "y"})
|
|
|
|
|
with pytest.raises(ValueError, match="News-URL"):
|
|
|
|
|
await generate_draft(
|
|
|
|
|
drucksache="18/A",
|
|
|
|
|
news_url="https://example.com/missing",
|
|
|
|
|
db_path=db_with_antrag_and_news,
|
|
|
|
|
bewerter=bewerter,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_generate_draft_empty_response_raises(db_with_antrag_and_news, monkeypatch):
|
|
|
|
|
bewerter = FakeBewerter({"titel": "", "body": ""})
|
|
|
|
|
from app.config import settings as real_settings
|
2026-05-03 13:10:20 +02:00
|
|
|
monkeypatch.setattr(real_settings, "llm_model_premium", "qwen-test")
|
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
|
|
|
with pytest.raises(ValueError, match="unvollständig"):
|
|
|
|
|
await generate_draft(
|
|
|
|
|
drucksache="18/A",
|
|
|
|
|
news_url="https://example.com/wohnen",
|
|
|
|
|
db_path=db_with_antrag_and_news,
|
|
|
|
|
bewerter=bewerter,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
# list_drafts + get_draft
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
2026-05-03 13:10:20 +02:00
|
|
|
class TestFindExistingDraft:
|
|
|
|
|
def test_returns_none_when_no_match(self, db_with_antrag_and_news):
|
|
|
|
|
assert _find_existing_draft(
|
|
|
|
|
"18/A", "https://x.de/n", db_with_antrag_and_news,
|
|
|
|
|
) is None
|
|
|
|
|
|
|
|
|
|
def test_returns_existing_draft(self, db_with_antrag_and_news):
|
|
|
|
|
conn = sqlite3.connect(str(db_with_antrag_and_news))
|
|
|
|
|
conn.execute(
|
|
|
|
|
"""INSERT INTO presse_drafts
|
|
|
|
|
(drucksache, bundesland, news_url, news_titel, titel, body, model)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
|
|
|
("18/A", "NRW", "https://x.de/n", "News",
|
|
|
|
|
"PM-Titel", "PM-Body", "qwen-test"),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
conn.close()
|
|
|
|
|
d = _find_existing_draft("18/A", "https://x.de/n", db_with_antrag_and_news)
|
|
|
|
|
assert d is not None
|
|
|
|
|
assert d["titel"] == "PM-Titel"
|
|
|
|
|
|
|
|
|
|
def test_returns_newest_when_multiple_exist(self, db_with_antrag_and_news):
|
|
|
|
|
conn = sqlite3.connect(str(db_with_antrag_and_news))
|
|
|
|
|
for titel in ["Alt1", "Alt2", "Neu"]:
|
|
|
|
|
conn.execute(
|
|
|
|
|
"""INSERT INTO presse_drafts
|
|
|
|
|
(drucksache, bundesland, news_url, news_titel, titel, body, model)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
|
|
|
("18/A", "NRW", "https://x.de/n", "News",
|
|
|
|
|
titel, "Body", "qwen-test"),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
conn.close()
|
|
|
|
|
d = _find_existing_draft("18/A", "https://x.de/n", db_with_antrag_and_news)
|
|
|
|
|
# Neuester Draft (höchste id) zurueckgeliefert
|
|
|
|
|
assert d["titel"] == "Neu"
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
class TestListAndGetDrafts:
|
|
|
|
|
def test_empty(self, db_with_antrag_and_news):
|
|
|
|
|
assert list_drafts(db_path=db_with_antrag_and_news) == []
|
|
|
|
|
assert get_draft(99, db_path=db_with_antrag_and_news) is None
|
|
|
|
|
|
|
|
|
|
def test_after_insert(self, db_with_antrag_and_news):
|
|
|
|
|
# Direct DB-Insert (test setup)
|
|
|
|
|
conn = sqlite3.connect(str(db_with_antrag_and_news))
|
|
|
|
|
conn.execute(
|
|
|
|
|
"""INSERT INTO presse_drafts
|
|
|
|
|
(drucksache, bundesland, news_url, news_titel, titel, body, model)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
|
|
|
("18/A", "NRW", "https://x.de/n", "News-Titel",
|
|
|
|
|
"PM-Titel", "PM-Body", "test-model"),
|
|
|
|
|
)
|
|
|
|
|
conn.commit()
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
drafts = list_drafts(db_path=db_with_antrag_and_news)
|
|
|
|
|
assert len(drafts) == 1
|
|
|
|
|
assert drafts[0]["drucksache"] == "18/A"
|
|
|
|
|
assert drafts[0]["titel"] == "PM-Titel"
|
|
|
|
|
|
|
|
|
|
d = get_draft(drafts[0]["id"], db_path=db_with_antrag_and_news)
|
|
|
|
|
assert d is not None
|
|
|
|
|
assert d["body"] == "PM-Body"
|