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.
Neue Artikel passieren jetzt vor DB-Speicherung einen Haiku-Relevanzfilter
(AnalyzerAgent.filter_relevant_articles), der Artikel verwirft, die nur
auf generische Keywords matchen, aber das Kernthema der Lage nicht
inhaltlich behandeln. Bei Parsing-/API-Fehler oder 100%-Rejection: Fallback
auf unveraenderte Kandidatenliste.
Orchestrator trennt DB-Dedup und INSERT, damit der Filter nur auf neue
Kandidaten laeuft (Kostenoptimierung). LATEST_DEVELOPMENTS-Prompt erhaelt
zusaetzliche Relevanz-Gate-Regel als zweite Sicherung.
Hintergrund: Incident 'Russische Militaerblogger' sammelte bisher Iran-,
Nahost- und allgemeine Ukraine-Artikel ein, weil Keyword-Match ab 2 von 8
Begriffen ('iran', 'russland', 'drohne', ...) genuegt. Der semantische
Filter verwirft solche Zufallstreffer.
Problem: Pill-Link verwies auf falschen Post, weil sources_json fuer
Telegram-Kanaele viele Eintraege mit gleichem Namen aber unterschiedlichen
Post-URLs hat. Der Name-Match traf den ersten Eintrag (falschen Post).
Fix: Bullet-Format von {Name, Name} auf {Name|URL, Name|URL} erweitert.
Backend-Parser loest {M<ID>} nun zu Name|URL auf, URL kommt direkt vom
articles.source_url des belegenden Artikels. Kein sources_json-Lookup
noetig, keine Name-Kollision mehr moeglich.
Backend (analyzer.py):
- _parse_latest_developments: articles_by_id speichert (name, url) Tuple,
Items werden als Name|URL gespeichert. Uebernommene Klammer-Items mit
Pipe werden akzeptiert. Legacy-Items ohne Pipe bleiben als reiner Name.
- Prompt-Regel und Output-Beispiel auf {Name|URL, Name|URL} erweitert.
Frontend (components.js):
- buildPill-Aufruf vor Pipe-Split: Name und URL getrennt, wenn URL vorhanden
wird Pseudo-src {name, url} uebergeben — eindeutiger Klicklink. Ohne URL
Fallback auf lookupByName in sources_json (fuer Legacy-Bullets).
Claude Haiku 4.5 laesst gelegentlich den fuehrenden Dash oder den zweiten
Datums-Punkt im Bullet-Format weg (z.B. "[18.04 21:49]" statt
"- [18.04. 21:49]"). Der strikte Parser-Regex verwarf dadurch alle Bullets.
- Regex akzeptiert nun Dash als optional und zweiten Datums-Punkt als optional
- Parser normalisiert Datum + Zeit auf kanonisches Format "DD.MM. HH:MM" mit Zero-Padding
- Frontend-Regex analog toleranter (auch fuer Altdaten-Mix)
- OUTPUT-FORMAT-Hinweis im Prompt verschaerft ("JEDE Zeile beginnt mit - ")
Backfill-Skript (scripts/backfill_latest_developments.py): Laedt die N
neuesten Artikel einer Lage aus der DB und ruft generate_latest_developments
mit previous_developments=None auf — nuetzlich nach DB-Cleanups, wenn die
inkrementelle Logik zu wenige Bullets liefert.
Einmaliger Run fuer Lage #66 (Militaerblogger): 8 Bullets vom 18.04. mit
aufgeloesten Quellen (Spiegel, Guardian, Bloomberg, n-tv, Telegram-Kanaele).
Aenderung am Grund-Mechanismus: LLM liefert pro Bullet die Meldungs-IDs
im Format {M<ID>, M<ID>}, das Backend loest die IDs gegen new_articles
zu Quellen-Namen auf und schreibt {Reuters, Rybar} in die DB. Uebernommene
Bullets aus previous_developments behalten ihre bestehende {Name}-Klammer.
Bullets ohne Quellen-Klammer oder mit unaufloesbarer Klammer werden vom
Parser verworfen — dadurch existiert "Keine Quelle" nicht mehr.
Frontend: Bias-Farbcodierung (pro-RU, staatsnah) + zugehoerige Heuristik
_classifyBias/_biasLabel entfernt. Kein Sonderfall-Rendering fuer leere
Pills mehr.
Der LATEST_DEVELOPMENTS-Prompt produzierte Bullets ohne Citations — das
Frontend zeigte daher "Keine Quelle". Prompt ergaenzt: jedes Bullet endet mit
{Quellenname1, Quellenname2} (geschweifte Klammern, exakte Schreibweise aus
Quelle:-Zeile). Frontend-Parser extrahiert diese Klammer, matcht Namen
case-insensitive gegen sources_json und erstellt klickbare Pills.
Fallback fuer Legacy-Bullets: Inline-[N]-Citations werden weiterhin erkannt.
Altbestand-Bullets ohne Marker erhalten beim naechsten Refresh Quellen.
Drei unabhaengige Schutzschichten gegen falsche Umschreibungen
(ae/oe/ue/ss statt ä/ö/ü/ß) im Lagebild:
1. Prompt-Ergaenzung in INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE und
INCREMENTAL_BRIEFING_PROMPT_TEMPLATE (analyzer.py): explizite
Priorisierung, dass die Regel "echte UTF-8-Umlaute" Vorrang vor
"bestehende Formulierungen beibehalten" hat. Adressiert den Fall,
dass Claude beim inkrementellen Update Altlasten weitertraegt.
2. Deterministische Normalisierung in post_refresh_qc.py:
- normalize_german_umlauts(text) - Regex mit Wortgrenzen, case-
preserving, Whitelist-tauglich, ~140 Eintraege im Woerterbuch
abgeleitet aus den 140 Hard-Hits in Lage #6
- normalize_umlaut_fields(db, incident_id) - laedt summary und
latest_developments, normalisiert, schreibt nur bei Aenderungen
zurueck (idempotent)
- Eingehaengt in run_post_refresh_qc() nach dem Location-Check,
Fehler stoppen die Pipeline nicht (identisches Muster wie
bestehende Checks)
3. scripts/bootstrap_umlaut_repair.py - Einmal-Skript zur
Bestandsbereinigung der bereits gespeicherten summary-Felder.
Idempotent. Beim initialen Lauf auf Produktiv-DB: 14 Lagen
aktualisiert, 431 Ersetzungen insgesamt, Lage #6 von 140 auf
15 Rest-Treffer reduziert.
Whitelist (leer): aktuell kein Konflikt zwischen deutschen Ziel-
Woertern und englischen Fremdwoertern. Kann bei Bedarf erweitert
werden ohne Schema-Aenderung.
Verifikation:
- py_compile OK fuer alle drei Dateien
- Service-Restart ohne Errors
- Unit-Tests: positive Faelle ("Oeffnung der Strasse" -> 4 Ersetzungen),
Whitelist ("Boeing liefert Business-Access" -> 0 Ersetzungen),
Komposita ("Wasserstrasse", "Parlamentspraesident") korrekt
- Bootstrap 2x ausgefuehrt (erster Lauf 288 Ersetzungen, zweiter 143
nach Dict-Erweiterung), kumulativ 431
Architektur bleibt dormant ohne Daten-Altlasten: wenn keine Lage
Umschreibungen enthaelt, arbeitet normalize_umlaut_fields in <1ms
und schreibt nichts. Kein Overhead im Refresh-Pfad.
- 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
- Frontend + Backend erkennen jetzt sowohl ## ZUSAMMENFASSUNG als
auch ## ÜBERBLICK als Zusammenfassungs-Sektion
- Inkrementelles Prompt weist Modell an, ÜBERBLICK in
ZUSAMMENFASSUNG umzubenennen und als Bullet-Points zu formatieren
- Bestehende Lagen zeigen Zusammenfassung sofort in der Kachel
Nutzer koennen per Klick auf Chips Anweisungen zur Beschreibung
hinzufuegen: Zusammenfassung, Vergleichstabelle, Zeitverlauf,
Pro/Contra oder eigene Tabellen. Format: [TABELLE: ...] und
[ZUSAMMENFASSUNG]. Mehrere Anweisungen moeglich. Analyzer-Prompts
beachten diese Anweisungen verbindlich. Beschreibung-generieren
bewahrt bestehende Direktiven.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Analyzer-Prompts erlauben jetzt Tabellen wenn Daten sich strukturiert
vergleichen lassen (Produkte, Modelle, Kennzahlen etc.).
Frontend parst Markdown-Tabellensyntax und rendert sie als HTML-Tabellen
mit passendem Styling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The LLM occasionally generates source references with letter suffixes
(e.g. [1383a], [1396b]) despite being instructed not to. This caused
broken links because the sources array only contained integer nr values.
Backend: Add _sanitize_sources() to strip letter suffixes after parsing
and deduplicate, preferring entries with valid URLs.
Frontend: Add fallback in citation renderer - when a suffix reference
like [1383a] has no matching source with URL, fall back to the base
number [1383].
Also cleaned up 99 broken suffix entries and 44 suffix references in
the Irankonflikt incident (ID 6) database records.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Probleme:
- Frontend-Regex matchte nur reine Zahlen, nicht [389a]-Style Refs
- 17 alphanumerische Quellen im Irankonflikt blieben unverlinkt
- Orchestrator-Validierung erkannte diese Refs nicht als fehlend
Fixes:
- Frontend: Regex erweitert auf [\d+a-z?], Vergleich mit String und Number
- Orchestrator: Validierung erkennt jetzt auch alphanumerische Refs
- Analyzer-Prompts: Explizite Anweisung, nur ganze Zahlen als Nr zu verwenden
- 822a und 859a in Irankonflikt sources_json nachgetragen
- Cache-Buster aktualisiert
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parteiische Quellen (pro-russisch, pro-iranisch, rechtsextrem etc.)
werden jetzt im Lagebericht-Fliesstext als solche gekennzeichnet,
damit der Leser die Informationen einordnen kann.
Aenderungen:
- Orchestrator reichert Artikel mit source_bias aus der sources-Tabelle an
- Analyzer zeigt Einordnung im Artikel-Kontext fuer den Claude-Prompt
- Alle 4 Prompt-Templates enthalten neue Regel zur Quellenkennzeichnung
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lagebilder sollen so ausführlich wie nötig erstellt werden.
Alle 4 Templates (Analyse, Briefing, inkrementell) angepasst.
Inkrementelle Analyse behält nun alle Themenabschnitte bei
statt aggressiv zu kürzen.
Claude liefert bei der Analyse nur noch neue Quellen statt alle bisherigen
zu wiederholen. Der Merge mit bestehenden Quellen passiert deterministisch
im Code. Spart Tokens und verhindert das versehentliche Vergessen von Quellen.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Analyse-Antworten von Claude wurden bei langen Outputs nicht korrekt
geparst, wodurch das Lagebild-Briefing eingefroren blieb. Neuer Parser
mit 4-stufigem Fallback: direktes Parsen, Regex-Extraktion,
Reparatur abgeschnittenes JSON, Regex-Fallback fuer Summary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Regel in alle 4 Analyse-Prompt-Templates eingefuegt: Claude soll keine
Gedankenstriche verwenden, sondern Kommas, Doppelpunkte oder neue Saetze.
Bestehende Dashes in DB (Summary + 76 Snapshots) bereinigt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude-Antworten mit ```json ... ``` Wrapping werden korrekt geparst.
Verhindert den Verlust von Analyse-Ergebnissen bei inkrementellen Refreshes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- researcher.py: Neuer dedizierter Haiku-Call extract_dynamic_keywords()
analysiert die letzten 30 Headlines und generiert 5 DE+EN Begriffspaare
- orchestrator.py: Dynamische Keywords vor Feed-Selektion aus DB-Headlines
- rss_parser.py: min_matches auf max 2 gedeckelt (vorher n/2, bei 10 Keywords = 5)
- analyzer.py: Fettdruck-Anweisungen entfernt
Vorher: 0 RSS-Treffer (min_matches=5 unerreichbar)
Nachher: 22 RSS-Treffer (Tagesschau 11, Al Jazeera 5, BBC 4, NYT 2)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>