feat(public-mood): Stimmungs-Kachel aus Foren-Quellen
Eigene Pipeline-Stufe nach factcheck, vor summary, die Foren-Artikel (media_type='forum') zu einer Themen-Zusammenfassung verarbeitet. Wird als separate Dashboard-Kachel "Öffentliche Stimmung" angezeigt — getrennt von Lagebild und Faktencheck, damit anonyme Forenposts nicht mit belegter Faktenlage verwechselt werden. - DB-Migration: incidents.public_mood (TEXT) + public_mood_updated_at (TS). - pipeline_tracker: neuer Pipeline-Step "public_mood" (DE/EN-Labels). - analyzer.generate_public_mood: Haiku-Call der Foren-Beitraege pro Quelle gruppiert und 3-6 thematische Bullets erzeugt, mit expliziter Quellen- Herkunft pro Bullet. Bei zu duennem Material gibt's keinen Output. - orchestrator: neuer Schritt zwischen Factcheck und Summary. Laedt alle Foren-Artikel der Lage (via JOIN auf sources), uebergibt sie an den Stimmungs-Agent, speichert den Markdown-Text in incidents.public_mood. - Topic-Filter (analyzer.filter_relevant_articles) markiert Foren-Quellen mit [FORUM]-Tag und bekommt im Prompt die Regel, Foren-Artikel weicher zu bewerten (Lage-Keyword im Titel reicht). Sie sollen in der Stimmungs- Kachel landen, nicht voreilig verworfen werden. - IncidentResponse-Modell: public_mood/public_mood_updated_at ergaenzt. - Frontend: neuer Tab "Öffentliche Stimmung" (nur sichtbar wenn Inhalt da), eigene Kachel mit Warn-Hinweis "keine Faktenlage". UI.renderPublicMood als einfacher Bullet-Renderer. - dashboard.html Cache-Buster fuer components.js + app.js gebumpt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -1492,6 +1492,71 @@ class AgentOrchestrator:
|
||||
logger.warning("build_fact_context_block fehlgeschlagen: %s", ctx_err, exc_info=True)
|
||||
fact_context_block = ""
|
||||
|
||||
# Pipeline-Schritt 6b: Öffentliche Stimmung aus Foren-Quellen
|
||||
# (nur Artikel mit media_type='forum'). Eigene Kachel, kein Faktencheck.
|
||||
# Wird vor dem Lagebild-Schritt ausgefuehrt, damit das Lagebild bei
|
||||
# Bedarf darauf verweisen kann (z.B. Demo-Lagen mit Bezug zur Stimmung).
|
||||
try:
|
||||
# Bestand aller Foren-Artikel der Lage laden (inkl. media_type via JOIN)
|
||||
cursor_fm = await db.execute(
|
||||
"SELECT a.*, s.media_type AS media_type FROM articles a "
|
||||
"LEFT JOIN sources s ON s.name = a.source "
|
||||
"WHERE a.incident_id = ?",
|
||||
(incident_id,),
|
||||
)
|
||||
all_articles_with_mt = [dict(r) for r in await cursor_fm.fetchall()]
|
||||
forum_articles_in_db = [
|
||||
a for a in all_articles_with_mt
|
||||
if (a.get("media_type") or "").lower() == "forum"
|
||||
]
|
||||
# Aus dem aktuellen Refresh-Lauf zusaetzliche Foren-Artikel ergaenzen
|
||||
# (haben media_type aus feed_config, sind aber evtl. noch nicht in DB,
|
||||
# wenn die Persistierung anders laeuft — Robustheit).
|
||||
for art in new_articles_for_analysis:
|
||||
if (art.get("media_type") or "").lower() != "forum":
|
||||
continue
|
||||
# Duplikate vermeiden ueber source_url
|
||||
if any(a.get("source_url") == art.get("source_url") for a in forum_articles_in_db):
|
||||
continue
|
||||
forum_articles_in_db.append(art)
|
||||
|
||||
if forum_articles_in_db:
|
||||
await _pipe_start("public_mood")
|
||||
try:
|
||||
mood_agent = AnalyzerAgent()
|
||||
mood_text, mood_usage = await mood_agent.generate_public_mood(
|
||||
title, description, forum_articles_in_db,
|
||||
output_language=output_language,
|
||||
)
|
||||
if mood_usage:
|
||||
usage_acc.add(mood_usage)
|
||||
if mood_text:
|
||||
await db.execute(
|
||||
"UPDATE incidents SET public_mood = ?, public_mood_updated_at = ? WHERE id = ?",
|
||||
(mood_text, now, incident_id),
|
||||
)
|
||||
await db.commit()
|
||||
logger.info(
|
||||
"Public-Mood gespeichert fuer Incident %d (%d Foren-Artikel)",
|
||||
incident_id, len(forum_articles_in_db),
|
||||
)
|
||||
await _pipe_done(
|
||||
"public_mood",
|
||||
count_value=len(forum_articles_in_db),
|
||||
count_secondary=(1 if mood_text else 0),
|
||||
)
|
||||
except Exception as mood_err:
|
||||
logger.warning("Public-Mood fehlgeschlagen: %s", mood_err, exc_info=True)
|
||||
await _pipe_done("public_mood", count_value=0, count_secondary=0)
|
||||
else:
|
||||
await _pipe_skip("public_mood")
|
||||
except Exception as mood_outer_err:
|
||||
logger.warning("Public-Mood-Block uebersprungen: %s", mood_outer_err)
|
||||
try:
|
||||
await _pipe_skip("public_mood")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Pipeline-Schritt 7: Lagebild verfassen (jetzt mit Faktenkontext)
|
||||
await _pipe_start("summary")
|
||||
logger.info(
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren