From d6c541cb9503e66888df8e2ef6c6f257cff98f2d Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 18 Apr 2026 11:47:10 +0000 Subject: [PATCH] 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 --- src/agents/analyzer.py | 103 ++++++++++++++++++++++++++++++++++++- src/agents/orchestrator.py | 20 +++++++ src/database.py | 5 ++ src/models.py | 1 + src/static/js/app.js | 48 +++++++++++------ 5 files changed, 161 insertions(+), 16 deletions(-) diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 8cd3172..e961426 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -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). diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index 74e4f8e..3b9183c 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -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) diff --git a/src/database.py b/src/database.py index a22891b..a6937d0 100644 --- a/src/database.py +++ b/src/database.py @@ -369,6 +369,11 @@ async def init_db(): await db.commit() logger.info("Migration: refresh_start_time zu incidents hinzugefuegt (bestehende Auto-Lagen auf 07:00)") + if "latest_developments" not in columns: + await db.execute("ALTER TABLE incidents ADD COLUMN latest_developments TEXT") + await db.commit() + logger.info("Migration: latest_developments zu incidents hinzugefuegt") + # Migration: Token-Spalten fuer refresh_log cursor = await db.execute("PRAGMA table_info(refresh_log)") rl_columns = [row[1] for row in await cursor.fetchall()] diff --git a/src/models.py b/src/models.py index d7bbc58..9ec638b 100644 --- a/src/models.py +++ b/src/models.py @@ -89,6 +89,7 @@ class IncidentResponse(BaseModel): retention_days: int visibility: str = "public" summary: Optional[str] + latest_developments: Optional[str] = None sources_json: Optional[str] = None international_sources: bool = True include_telegram: bool = False diff --git a/src/static/js/app.js b/src/static/js/app.js index 7eece18..ff38356 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -850,25 +850,43 @@ const App = { const zusammenfassungText = document.getElementById('zusammenfassung-text'); const summaryText = document.getElementById('summary-text'); const zusammenfassungCard = document.getElementById('zusammenfassung-card'); + const zusammenfassungTitle = zusammenfassungCard ? zusammenfassungCard.querySelector('.card-title') : null; - if (incident.summary && incident.type === 'research') { - const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary); - if (zusammenfassung) { - if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(zusammenfassung, incident.sources_json); - if (zusammenfassungCard) zusammenfassungCard.style.display = ''; - summaryText.innerHTML = UI.renderSummary(remaining, incident.sources_json, incident.type); + if (incident.type === 'research') { + // Recherche: ZUSAMMENFASSUNG-Sektion aus Briefing extrahieren + if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Zusammenfassung'; + if (zusammenfassungTitle) zusammenfassungTitle.setAttribute('onclick', "openContentModal('Zusammenfassung', 'zusammenfassung-content')"); + if (incident.summary) { + const { zusammenfassung, remaining } = UI.extractZusammenfassung(incident.summary); + if (zusammenfassung) { + if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(zusammenfassung, incident.sources_json); + if (zusammenfassungCard) zusammenfassungCard.style.display = ''; + summaryText.innerHTML = UI.renderSummary(remaining, incident.sources_json, incident.type); + } else { + if (zusammenfassungText) zusammenfassungText.innerHTML = 'Zusammenfassung wird beim n\u00e4chsten Refresh generiert.'; + if (zusammenfassungCard) zusammenfassungCard.style.display = ''; + summaryText.innerHTML = UI.renderSummary(incident.summary, incident.sources_json, incident.type); + } } else { - if (zusammenfassungText) zusammenfassungText.innerHTML = 'Zusammenfassung wird beim n\u00e4chsten Refresh generiert.'; - if (zusammenfassungCard) zusammenfassungCard.style.display = ''; - summaryText.innerHTML = UI.renderSummary(incident.summary, incident.sources_json, incident.type); + if (zusammenfassungCard) zusammenfassungCard.style.display = 'none'; + summaryText.innerHTML = 'Noch keine Zusammenfassung. Klicke auf "Aktualisieren" um die Recherche zu starten.'; } - } else if (incident.summary) { - // Adhoc/Live: Keine Zusammenfassung-Kachel - if (zusammenfassungCard) zusammenfassungCard.style.display = 'none'; - summaryText.innerHTML = UI.renderSummary(incident.summary, incident.sources_json, incident.type); } else { - if (zusammenfassungCard) zusammenfassungCard.style.display = 'none'; - summaryText.innerHTML = 'Noch keine Zusammenfassung. Klicke auf "Aktualisieren" um die Recherche zu starten.'; + // Live-Monitoring (adhoc): Kachel zeigt "Neueste Entwicklungen" (max 8 Bullets mit Zeitstempel) + if (zusammenfassungTitle) zusammenfassungTitle.textContent = 'Neueste Entwicklungen'; + if (zusammenfassungTitle) zusammenfassungTitle.setAttribute('onclick', "openContentModal('Neueste Entwicklungen', 'zusammenfassung-content')"); + if (zusammenfassungCard) zusammenfassungCard.style.display = ''; + const devText = (incident.latest_developments || '').trim(); + if (devText) { + if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(devText, incident.sources_json); + } else if (zusammenfassungText) { + zusammenfassungText.innerHTML = 'Noch keine Entwicklungen erfasst. Wird beim n\u00e4chsten Refresh generiert.'; + } + if (incident.summary) { + summaryText.innerHTML = UI.renderSummary(incident.summary, incident.sources_json, incident.type); + } else { + summaryText.innerHTML = 'Noch kein Lagebild. Klicke auf "Aktualisieren" um die Recherche zu starten.'; + } } // Meta (im Header-Strip) — relative Zeitangabe mit vollem Datum als Tooltip