diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 6af9d78..6c00f7b 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -206,7 +206,7 @@ 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. +LATEST_DEVELOPMENTS_PROMPT_TEMPLATE = """Du erzeugst die 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). @@ -214,39 +214,34 @@ WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschre LAGE: {title} KONTEXT: {description} -BISHERIGE ENTWICKLUNGEN (chronologisch absteigend, neueste oben): -{previous_developments} +AKTUELLES LAGEBILD (autoritative inhaltliche Grundlage): +{summary} -NEUE MELDUNGEN SEIT DEM LETZTEN UPDATE: -{new_articles_text} +BELEGENDE MELDUNGEN (chronologisch absteigend, neueste zuerst — nur hieraus dürfen Zeitstempel und Quellen-Klammern stammen): +{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). +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: -- 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]". -- Jedes Bullet ENDET mit einer Quellen-Klammer — ZWINGEND. Bullets ohne Klammer werden verworfen. - - NEUE Bullets (aus den NEUEN MELDUNGEN): {{M, M}} mit den ganzzahligen IDs aus der "ID:"-Zeile der belegenden Meldung(en). Beispiele: {{M42}} oder {{M42, M17}}. - - 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. -- 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. -- 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. +REGELN zur Auswahl der Bullets: +- 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. +- "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. +- Wenn das Lagebild ein Ereignis erwähnt, aber KEINE aktuelle belegende Meldung dafür existiert: Bullet verwerfen. Lieber weglassen als fabulieren. +- Bevorzuge Ereignisse mit hohem Neuigkeitswert und konkretem Vorfall/Aussage gegenüber allgemeinen Hintergrundkonstatierungen. + +REGELN zur Formulierung: +- Jedes Bullet = EIN konkretes Ereignis oder eine konkrete Aussage, 1-2 Sätze, präzise und neutral. +- Beginne JEDES Bullet mit dem Zeitstempel der frühesten belegenden Meldung im Format "[DD.MM. HH:MM]". +- 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. +- 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. - 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 "- "): -- [DD.MM. HH:MM] Ereignistext neu. {{M}} -- [DD.MM. HH:MM] Ereignistext neu mit mehreren Belegen. {{M, M}} -- [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. +OUTPUT-FORMAT (ausschliesslich, kein Code-Fence, JEDE Zeile beginnt mit "- "): +- [DD.MM. HH:MM] Ereignistext. {{Quellenname1|URL1}} +- [DD.MM. HH:MM] Ereignistext mit mehreren Belegen. {{Quellenname1|URL1, Quellenname2|URL2}} ...""" @@ -477,28 +472,57 @@ class AnalyzerAgent: self, title: str, description: str, - new_articles: list[dict], - previous_developments: str | None, + summary: str, + recent_articles: list[dict], + previous_developments: str | None = 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). - Wenn keine neuen Artikel vorliegen, werden die bisherigen Bullets unverändert zurückgegeben. + Der LLM extrahiert aus dem Summary die jüngsten Ereignisse und bindet sie an + 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() - if not new_articles: - return (prev or None), None + prev = (previous_developments or "").strip() or None + if not summary or not summary.strip(): + return prev, None + if not recent_articles: + return prev, 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)" + + # 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( title=title, description=description or "Keine weiteren Details", - previous_developments=prev_block, - new_articles_text=new_articles_text, + summary=summary.strip(), + articles_text=articles_text, today=today, 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) except Exception as 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: 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) - logger.info(f"Latest-Developments: {len(bullets)} Bullets generiert") + logger.info(f"Latest-Developments: {len(bullets)} Bullets aus Lagebild generiert") return output, usage @staticmethod diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index 255e5c5..66244c5 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -1293,11 +1293,24 @@ class AgentOrchestrator: self._check_cancelled(incident_id) # --- 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: + # 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_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: usage_acc.add(dev_usage)