Neueste Entwicklungen: Kachel fuer adhoc-Lagen

- DB-Migration: Spalte latest_developments (TEXT) in incidents
- Analyzer: neuer Prompt LATEST_DEVELOPMENTS_PROMPT_TEMPLATE und
  Methode generate_latest_developments() liefert chronologische
  Bullet-Liste (max. 8, neueste oben, Zeitstempel DD.MM. HH:MM)
- Orchestrator: nach Analyse+Faktencheck ein Extra-Schritt nur fuer
  incident_type=adhoc, der die neue Kachel fortschreibt
- Analyzer-Prompts (Erst- und inkrementell): erzeugen KEINE
  Zusammenfassung-Sektion mehr im Lagebild (vermeidet Duplikat mit
  der neuen Kachel)
- models.IncidentResponse um latest_developments erweitert
- Frontend: Rendering der Kachel in app.js
Dieser Commit ist enthalten in:
claude-dev
2026-04-18 11:47:10 +00:00
Ursprung acfc74ffe7
Commit d6c541cb95
5 geänderte Dateien mit 161 neuen und 16 gelöschten Zeilen

Datei anzeigen

@@ -30,6 +30,7 @@ STRUKTUR:
- Wenn verschiedene Aspekte oder Themenfelder aufkommen (z.B. Ereignis + Reaktionen + Hintergrund): Gliedere mit kurzen Markdown-Zwischenüberschriften (##)
- Wenn sich Daten strukturiert vergleichen lassen (z.B. Produkte, Unternehmen, Kennzahlen, Modelle), verwende eine Markdown-Tabelle (| Spalte1 | Spalte2 | ... mit Trennzeile |---|---|)
- Die Entscheidung liegt bei dir — Überschriften und Tabellen nur wenn sie dem Leser helfen
- ERZEUGE KEINE Sektion "## ZUSAMMENFASSUNG", "## ÜBERBLICK" oder "## KERNPUNKTE". Die neuesten Entwicklungen werden separat als eigene Kachel aufbereitet und dürfen im Lagebild NICHT dupliziert werden. Steige direkt mit dem Fließtext oder der ersten inhaltlichen Zwischenüberschrift ein.
REGELN:
- Neutral und sachlich - keine Wertungen oder Spekulationen
@@ -43,7 +44,7 @@ REGELN:
- Ältere Quellen zeitlich einordnen (z.B. "laut einem Bericht vom Januar", "Anfang Februar berichtete...")
Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
- "summary": Zusammenfassung auf {output_language} mit Quellenverweisen [1], [2] etc. im Text (Markdown-Überschriften ## erlaubt wenn sinnvoll)
- "summary": Zusammenfassung auf {output_language} mit Quellenverweisen [1], [2] etc. im Text (Markdown-Überschriften ## erlaubt wenn sinnvoll, aber KEINE "## ZUSAMMENFASSUNG"/"## ÜBERBLICK"-Sektion)
- "sources": Array von Quellenobjekten, je: {{"nr": 1, "name": "Quellenname", "url": "https://..."}}
- "key_facts": Array von bestätigten Kernfakten (Strings, in Ausgabesprache)
- "translations": Array von Objekten mit "article_id", "headline_de", "content_de" (nur für fremdsprachige Artikel)
@@ -133,6 +134,7 @@ STRUKTUR:
- Fließtext oder mit Markdown-Zwischenüberschriften (##) — je nach Komplexität
- Wenn sich Daten strukturiert vergleichen lassen (z.B. Produkte, Unternehmen, Kennzahlen, Modelle), verwende eine Markdown-Tabelle (| Spalte1 | Spalte2 | ... mit Trennzeile |---|---|)
- KEIN Fettdruck (**) verwenden
- ERZEUGE KEINE Sektion "## ZUSAMMENFASSUNG", "## ÜBERBLICK" oder "## KERNPUNKTE". Falls das BISHERIGE LAGEBILD eine solche Sektion enthält, ENTFERNE sie vollständig beim Aktualisieren. Die neuesten Entwicklungen werden separat als eigene Kachel gepflegt und dürfen im Lagebild NICHT dupliziert werden.
REGELN:
- Neutral und sachlich - keine Wertungen oder Spekulationen
@@ -202,6 +204,41 @@ Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung."""
LATEST_DEVELOPMENTS_PROMPT_TEMPLATE = """Du pflegst eine Kachel "Neueste Entwicklungen" für eine Live-Monitoring-Lage.
HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
LAGE: {title}
KONTEXT: {description}
BISHERIGE ENTWICKLUNGEN (chronologisch absteigend, neueste oben):
{previous_developments}
NEUE MELDUNGEN SEIT DEM LETZTEN UPDATE:
{new_articles_text}
AUFTRAG:
Extrahiere aus den NEUEN Meldungen konkrete Ereignisse und aktualisiere die Liste. Fasse die bisherigen und neuen Ereignisse zu EINER Liste zusammen (max. 8 Bullets, neueste oben).
REGELN:
- Jedes Bullet = EIN konkretes Ereignis (1-2 Sätze, faktenbasiert). Keine Themen-Zusammenfassungen.
- Jedes Bullet beginnt mit dem Zeitstempel der frühesten belegenden Quelle im Format "[DD.MM. HH:MM]".
- Wenn mehrere Meldungen dasselbe Ereignis betreffen: EIN Bullet, Zeitstempel = frühester Zeitpunkt.
- Bestehende Bullets aus BISHERIGE ENTWICKLUNGEN sinngemäß übernehmen, NICHT umformulieren. Nur entfernen, wenn sie durch neue Meldungen nachweislich überholt sind oder die 8-Bullet-Grenze überschritten wird (dann älteste fallen raus).
- Wenn eine Quelle eine erkennbare politische Ausrichtung hat (z.B. pro-russisch, staatsnah, rechtsextrem), im Bullet-Text erwähnen ("laut pro-russischem Telegram-Kanal Rybar...").
- Neutral und sachlich — keine Wertungen oder Spekulationen.
- KEINE Gedankenstriche (—, –) — stattdessen Kommas, Doppelpunkte oder neue Sätze.
- Bei widersprüchlichen Angaben beide Seiten knapp nennen.
- KEINE Einleitung, KEINE Überschrift, KEINE Nachbemerkungen.
- Wenn aus den neuen Meldungen kein neues Ereignis extrahierbar ist: BISHERIGE ENTWICKLUNGEN unverändert zurückgeben.
OUTPUT-FORMAT (ausschliesslich, keine Anführungszeichen, kein Code-Fence):
- [DD.MM. HH:MM] Erster Ereignistext.
- [DD.MM. HH:MM] Zweiter Ereignistext.
..."""
class AnalyzerAgent:
"""Analysiert und übersetzt Meldungen über Claude CLI."""
@@ -336,6 +373,70 @@ class AnalyzerAgent:
logger.error(f"Inkrementelle Analyse-Fehler: {e}")
return None, None
async def generate_latest_developments(
self,
title: str,
description: str,
new_articles: list[dict],
previous_developments: str | None,
) -> tuple[str | None, ClaudeUsage | None]:
"""Pflegt die Kachel 'Neueste Entwicklungen' für Live-Monitoring-Lagen.
Gibt Markdown-Bullets mit Zeitstempel zurück (max 8, neueste oben).
Wenn keine neuen Artikel vorliegen, werden die bisherigen Bullets unverändert zurückgegeben.
"""
prev = (previous_developments or "").strip()
if not new_articles:
return (prev or None), None
from config import OUTPUT_LANGUAGE, CLAUDE_MODEL_FAST
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
new_articles_text = self._format_articles_text(new_articles, max_articles=25)
prev_block = prev if prev else "(noch keine Einträge)"
prompt = LATEST_DEVELOPMENTS_PROMPT_TEMPLATE.format(
title=title,
description=description or "Keine weiteren Details",
previous_developments=prev_block,
new_articles_text=new_articles_text,
today=today,
output_language=OUTPUT_LANGUAGE,
)
try:
result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST, raw_text=True)
except Exception as e:
logger.error(f"Latest-Developments-Fehler: {e}")
return (prev or None), None
bullets = self._parse_latest_developments(result)
if not bullets:
logger.info("Latest-Developments: keine Bullets geparst, behalte bisherigen Stand")
return (prev or None), usage
bullets = bullets[:8]
output = "\n".join(bullets)
logger.info(f"Latest-Developments: {len(bullets)} Bullets generiert")
return output, usage
@staticmethod
def _parse_latest_developments(text: str) -> list[str]:
"""Extrahiert '- [DD.MM. HH:MM] ...'-Zeilen aus der Claude-Antwort."""
if not text:
return []
bullets: list[str] = []
bullet_re = re.compile(r"^\s*[-*•]\s*\[(\d{1,2}\.\d{1,2}\.(?:\d{2,4})?\s+\d{1,2}:\d{2})\]\s*(.+?)\s*$")
for raw_line in text.splitlines():
line = raw_line.strip()
if not line:
continue
m = bullet_re.match(line)
if m:
ts = m.group(1)
body = m.group(2).rstrip()
bullets.append(f"- [{ts}] {body}")
return bullets
def _sanitize_sources(self, analysis: dict) -> dict:
"""Entfernt Buchstaben-Suffixe aus Quellennummern (z.B. '1383a' -> 1383).

Datei anzeigen

@@ -674,6 +674,7 @@ class AgentOrchestrator:
tenant_id = incident["tenant_id"] if "tenant_id" in incident.keys() else None
previous_summary = incident["summary"] or ""
previous_sources_json = incident["sources_json"] if "sources_json" in incident.keys() else None
previous_developments = incident["latest_developments"] if "latest_developments" in incident.keys() else None
# Bei Retry: vorherigen running-Eintrag als error markieren
if retry_count > 0:
@@ -1233,6 +1234,25 @@ class AgentOrchestrator:
# Cancel-Check nach paralleler Verarbeitung
self._check_cancelled(incident_id)
# --- Neueste Entwicklungen (nur Live-Monitoring / adhoc) ---
if incident_type == "adhoc" and new_articles_for_analysis:
try:
dev_analyzer = AnalyzerAgent()
dev_text, dev_usage = await dev_analyzer.generate_latest_developments(
title, description, new_articles_for_analysis, previous_developments,
)
if dev_usage:
usage_acc.add(dev_usage)
if dev_text is not None:
await db.execute(
"UPDATE incidents SET latest_developments = ? WHERE id = ?",
(dev_text, incident_id),
)
await db.commit()
previous_developments = dev_text
except Exception as e:
logger.warning(f"Latest-Developments-Generator fehlgeschlagen: {e}")
# Cancel-Check nach Analyse+Faktencheck
self._check_cancelled(incident_id)