Neueste Entwicklungen aus Lagebild statt aus Artikel-Strom

Bisher extrahierte der Generator Bullets direkt aus den neu eingesammelten
Artikeln und mergte sie mit den bestehenden Developments. Das fuehrte zu
zwei wiederkehrenden Problemen:

1. Off-topic Artikel, die den Keyword-Prefilter aber nicht den Topic-Filter
   passiert hatten, konnten als Bullet landen (die Kachel bildete dann
   Nebenschauplaetze des Weltgeschehens ab statt der Lage).
2. Alte Bullets blieben stehen, auch wenn sie laengst nicht mehr die
   'neuesten' Entwicklungen waren — nur sehr ueberholte Eintraege fielen
   durch das 8-Bullet-Cap raus.

Neue Logik: Der Generator nimmt das frisch erzeugte Lagebild als autoritative
inhaltliche Grundlage und waehlt daraus Bullets aus, die durch eine aktuelle
belegende Meldung (<~7 Tage) gestuetzt sind. Dadurch:

- Thematisch sauber: Lagebild enthaelt bereits nur relevante Inhalte.
- Echt 'neueste': Alte Hintergrund-Erwaehnungen im Lagebild fallen raus,
  weil kein aktueller Artikel sie belegt.
- Klar datiert: Zeitstempel zwingend aus article.published_at der
  belegenden Meldung.
- Kompakt: 4-6 Bullets (vorher 8), nach Zeitstempel absteigend.

Kein Merge mit previous_developments mehr — bei jedem Refresh neu generiert
(behebt das Drift-Problem). previous_developments bleibt nur als Fallback,
falls der Generator im Einzelfall 0 Bullets parst.
Dieser Commit ist enthalten in:
claude-dev
2026-04-21 14:23:18 +00:00
Ursprung efae707fa9
Commit a579e2c275
2 geänderte Dateien mit 83 neuen und 46 gelöschten Zeilen

Datei anzeigen

@@ -206,7 +206,7 @@ Antworte AUSSCHLIESSLICH als JSON-Objekt mit diesen Feldern:
Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung.""" 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. LATEST_DEVELOPMENTS_PROMPT_TEMPLATE = """Du erzeugst die Kachel "Neueste Entwicklungen" für eine Live-Monitoring-Lage.
HEUTIGES DATUM: {today} HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
@@ -214,39 +214,34 @@ WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschre
LAGE: {title} LAGE: {title}
KONTEXT: {description} KONTEXT: {description}
BISHERIGE ENTWICKLUNGEN (chronologisch absteigend, neueste oben): AKTUELLES LAGEBILD (autoritative inhaltliche Grundlage):
{previous_developments} {summary}
NEUE MELDUNGEN SEIT DEM LETZTEN UPDATE: BELEGENDE MELDUNGEN (chronologisch absteigend, neueste zuerst — nur hieraus dürfen Zeitstempel und Quellen-Klammern stammen):
{new_articles_text} {articles_text}
AUFTRAG: 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). Extrahiere aus dem LAGEBILD die wichtigsten jüngsten Ereignisse und stelle sie als chronologisch absteigende Bullet-Liste dar. Für jedes Bullet wählst du eine oder mehrere belegende Meldungen aus der obigen Liste und übernimmst deren Publikationsdatum als Zeitstempel.
REGELN: REGELN zur Auswahl der Bullets:
- Jedes Bullet = EIN konkretes Ereignis (1-2 Sätze, faktenbasiert). Keine Themen-Zusammenfassungen. - Ziel: 4 bis 6 Bullets. Wenn das Lagebild weniger tatsächlich AKTUELLE Ereignisse hergibt, dann lieber 3 ehrliche Bullets als 6 mit veralteten. Kein Auffüllen.
- Jedes Bullet beginnt mit dem Zeitstempel der frühesten belegenden Quelle im Format "[DD.MM. HH:MM]". - "AKTUELL" bedeutet: belegende Meldung ist spätestens ~7 Tage alt (relativ zu HEUTIGES DATUM). Ältere Ereignisse — auch wenn sie im Lagebild stehen — gehören NICHT rein. Sie sind Hintergrund, keine Neuesten Entwicklungen.
- Jedes Bullet ENDET mit einer Quellen-Klammer — ZWINGEND. Bullets ohne Klammer werden verworfen. - Wenn das Lagebild ein Ereignis erwähnt, aber KEINE aktuelle belegende Meldung dafür existiert: Bullet verwerfen. Lieber weglassen als fabulieren.
- NEUE Bullets (aus den NEUEN MELDUNGEN): {{M<ID1>, M<ID2>}} mit den ganzzahligen IDs aus der "ID:"-Zeile der belegenden Meldung(en). Beispiele: {{M42}} oder {{M42, M17}}. - Bevorzuge Ereignisse mit hohem Neuigkeitswert und konkretem Vorfall/Aussage gegenüber allgemeinen Hintergrundkonstatierungen.
- UEBERNOMMENE Bullets aus BISHERIGE ENTWICKLUNGEN: behalten ihre bestehende Klammer KOMPLETT UND UNVERAENDERT, inklusive des Pipe-Zeichens und der URL. Beispiel: {{Reuters|https://reuters.com/article, Rybar|https://t.me/rybar/123}}. NICHT in M-IDs umwandeln, NICHT die URL entfernen, NICHT umformatieren.
- Wenn mehrere Meldungen dasselbe Ereignis belegen: EIN Bullet, Zeitstempel = frühester Zeitpunkt, ALLE IDs in der Klammer. REGELN zur Formulierung:
- 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 einem uebernommenen Bullet die Quellen-Klammer fehlt (Altformat): Bullet VERWERFEN und nicht in die neue Liste uebernehmen. - Jedes Bullet = EIN konkretes Ereignis oder eine konkrete Aussage, 1-2 Sätze, präzise und neutral.
- 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..."). - Beginne JEDES Bullet mit dem Zeitstempel der frühesten belegenden Meldung im Format "[DD.MM. HH:MM]".
- Neutral und sachlich — keine Wertungen oder Spekulationen. - Ende JEDES Bullet mit einer Quellen-Klammer mit Pipe-getrennten Paaren "Name|URL", kommagetrennt bei mehreren Belegen: {{Reuters|https://reuters.com/..., Rybar|https://t.me/rybar/123}}. Maximal 3 Quellen pro Bullet. Bullets ohne Klammer werden verworfen.
- KEINE Gedankenstriche (—, –) — stattdessen Kommas, Doppelpunkte oder neue Sätze. - Sortiere die Bullets nach Zeitstempel absteigend — neueste zuerst.
- Wenn eine Quelle eine erkennbare politische Ausrichtung hat (pro-russisch, staatsnah, rechtsextrem etc.), im Bullet-Text erwähnen ("laut pro-russischem Telegram-Kanal Rybar...").
- KEINE Gedankenstriche (—, –). Stattdessen Kommas, Doppelpunkte, neue Sätze.
- Bei widersprüchlichen Angaben beide Seiten knapp nennen. - Bei widersprüchlichen Angaben beide Seiten knapp nennen.
- KEINE Einleitung, KEINE Überschrift, KEINE Nachbemerkungen. - 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, JEDE Zeile beginnt mit "- "): OUTPUT-FORMAT (ausschliesslich, kein Code-Fence, JEDE Zeile beginnt mit "- "):
- [DD.MM. HH:MM] Ereignistext neu. {{M<ID>}} - [DD.MM. HH:MM] Ereignistext. {{Quellenname1|URL1}}
- [DD.MM. HH:MM] Ereignistext neu mit mehreren Belegen. {{M<ID1>, M<ID2>}} - [DD.MM. HH:MM] Ereignistext mit mehreren Belegen. {{Quellenname1|URL1, Quellenname2|URL2}}
- [DD.MM. HH:MM] Ereignistext aus BISHERIGE ENTWICKLUNGEN. {{Quellenname1|URL1, Quellenname2|URL2}}
RELEVANZ-GATE (vor der Bullet-Erzeugung anwenden):
- Jedes Bullet MUSS einen klaren inhaltlichen Bezug zum SPEZIFISCHEN Kernthema der Lage (siehe LAGE + KONTEXT) haben. Keyword-Überschneidungen reichen NICHT.
- Beispiel: Lautet das Thema "Russische Militärblogger bewerten die Lage der russischen Armee", ist ein allgemeiner Iran-Konflikt- oder Nahost-Bericht NICHT relevant, auch wenn er in den neuen Meldungen auftaucht. Nur Einschätzungen/Aussagen russischer Militärblogger zur russischen Armee gehören rein.
- Im Zweifel: Meldung ignorieren, kein Bullet erzeugen. Lieber weniger Bullets als off-topic.
...""" ..."""
@@ -477,28 +472,57 @@ class AnalyzerAgent:
self, self,
title: str, title: str,
description: str, description: str,
new_articles: list[dict], summary: str,
previous_developments: str | None, recent_articles: list[dict],
previous_developments: str | None = None,
) -> tuple[str | None, ClaudeUsage | None]: ) -> tuple[str | None, ClaudeUsage | None]:
"""Pflegt die Kachel 'Neueste Entwicklungen' für Live-Monitoring-Lagen. """Generiert die Kachel 'Neueste Entwicklungen' aus dem Lagebild.
Gibt Markdown-Bullets mit Zeitstempel zurück (max 8, neueste oben). Der LLM extrahiert aus dem Summary die jüngsten Ereignisse und bindet sie an
Wenn keine neuen Artikel vorliegen, werden die bisherigen Bullets unverändert zurückgegeben. das Publikationsdatum der belegenden Meldungen (recent_articles). Damit bleiben
die Einträge zwingend aktuell und thematisch an das Lagebild gekoppelt. Alte
Hintergrund-Erwähnungen im Lagebild erzeugen keine Bullets, weil keine aktuelle
Meldung sie belegen würde.
Gibt 4–6 Bullets (absteigend nach Zeitstempel) zurück. Bei Fehler/Parsing-Leer:
Fallback auf previous_developments (falls vorhanden), sonst None.
""" """
prev = (previous_developments or "").strip() prev = (previous_developments or "").strip() or None
if not new_articles: if not summary or not summary.strip():
return (prev or None), None return prev, None
if not recent_articles:
return prev, None
from config import OUTPUT_LANGUAGE, CLAUDE_MODEL_FAST from config import OUTPUT_LANGUAGE, CLAUDE_MODEL_FAST
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y") 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)" # Kompakter Artikel-Block: nur die für Zeitstempel/Quellen nötigen Felder.
# Sortiert nach published_at absteigend — damit der LLM die jüngsten sofort sieht.
def _pub_sort_key(a: dict) -> str:
return a.get("published_at") or ""
sorted_articles = sorted(recent_articles, key=_pub_sort_key, reverse=True)
lines: list[str] = []
for a in sorted_articles[:60]:
headline = a.get("headline_de") or a.get("headline", "")
source = a.get("source", "Unbekannt")
url = a.get("source_url", "")
published = a.get("published_at") or "unbekannt"
bias = a.get("source_bias") or ""
line = f"- [{published}] {source}"
if bias:
line += f" ({bias})"
line += f" | {headline}"
if url:
line += f" | {url}"
lines.append(line)
articles_text = "\n".join(lines) if lines else "(keine belegenden Meldungen verfügbar)"
prompt = LATEST_DEVELOPMENTS_PROMPT_TEMPLATE.format( prompt = LATEST_DEVELOPMENTS_PROMPT_TEMPLATE.format(
title=title, title=title,
description=description or "Keine weiteren Details", description=description or "Keine weiteren Details",
previous_developments=prev_block, summary=summary.strip(),
new_articles_text=new_articles_text, articles_text=articles_text,
today=today, today=today,
output_language=OUTPUT_LANGUAGE, output_language=OUTPUT_LANGUAGE,
) )
@@ -507,16 +531,16 @@ class AnalyzerAgent:
result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST, raw_text=True) result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST, raw_text=True)
except Exception as e: except Exception as e:
logger.error(f"Latest-Developments-Fehler: {e}") logger.error(f"Latest-Developments-Fehler: {e}")
return (prev or None), None return prev, None
bullets = self._parse_latest_developments(result, new_articles) bullets = self._parse_latest_developments(result, recent_articles)
if not bullets: if not bullets:
logger.info("Latest-Developments: keine Bullets geparst, behalte bisherigen Stand") logger.info("Latest-Developments: keine Bullets geparst, behalte bisherigen Stand")
return (prev or None), usage return prev, usage
bullets = bullets[:8] bullets = bullets[:6]
output = "\n".join(bullets) output = "\n".join(bullets)
logger.info(f"Latest-Developments: {len(bullets)} Bullets generiert") logger.info(f"Latest-Developments: {len(bullets)} Bullets aus Lagebild generiert")
return output, usage return output, usage
@staticmethod @staticmethod

Datei anzeigen

@@ -1293,11 +1293,24 @@ class AgentOrchestrator:
self._check_cancelled(incident_id) self._check_cancelled(incident_id)
# --- Neueste Entwicklungen (nur Live-Monitoring / adhoc) --- # --- Neueste Entwicklungen (nur Live-Monitoring / adhoc) ---
if incident_type == "adhoc" and new_articles_for_analysis: # Basis ist jetzt das frisch generierte Lagebild (autoritativ, thematisch sauber).
# Zeitstempel und Quellen kommen aus den jüngsten belegenden Artikeln.
dev_summary_source = (locals().get("new_summary") or previous_summary or "").strip()
if incident_type == "adhoc" and dev_summary_source:
try: try:
# Top-60 neueste Artikel mit Publikationsdatum als Beleg-Pool.
dev_cursor = await db.execute(
"""SELECT id, headline, headline_de, source, source_url, published_at
FROM articles
WHERE incident_id = ? AND published_at IS NOT NULL
ORDER BY published_at DESC LIMIT 60""",
(incident_id,),
)
dev_articles = [dict(row) for row in await dev_cursor.fetchall()]
dev_analyzer = AnalyzerAgent() dev_analyzer = AnalyzerAgent()
dev_text, dev_usage = await dev_analyzer.generate_latest_developments( dev_text, dev_usage = await dev_analyzer.generate_latest_developments(
title, description, new_articles_for_analysis, previous_developments, title, description, dev_summary_source, dev_articles, previous_developments,
) )
if dev_usage: if dev_usage:
usage_acc.add(dev_usage) usage_acc.add(dev_usage)