Live-Monitoring: Quellen-IDs deterministisch aufloesen, Bias-Markierung raus
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.
Dieser Commit ist enthalten in:
@@ -226,9 +226,11 @@ Extrahiere aus den NEUEN Meldungen konkrete Ereignisse und aktualisiere die List
|
||||
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 den Quellen-Namen in geschweiften Klammern, kommasepariert. Beispiel: "{{Reuters, Rybar}}". Verwende EXAKT die Schreibweise aus der "Quelle:"-Zeile der Meldung (z.B. "Der Tagesspiegel", nicht "Tagesspiegel"). Keine Abkuerzungen, keine Umformulierungen.
|
||||
- Wenn mehrere Meldungen dasselbe Ereignis betreffen: EIN Bullet, Zeitstempel = frühester Zeitpunkt, ALLE belegenden Quellen-Namen in den Klammern.
|
||||
- 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), ergaenze sie anhand der URSPRUENGLICHEN Beleg-Meldung so gut moeglich; ansonsten "{{Unbekannt}}".
|
||||
- Jedes Bullet ENDET mit einer Quellen-Klammer — ZWINGEND. Bullets ohne Klammer werden verworfen.
|
||||
- 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}}.
|
||||
- UEBERNOMMENE Bullets aus BISHERIGE ENTWICKLUNGEN: behalten ihre bestehende {{Name1, Name2}}-Klammer UNVERAENDERT. NICHT in M-IDs umwandeln.
|
||||
- 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.
|
||||
@@ -237,8 +239,9 @@ REGELN:
|
||||
- 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. {{Quellenname1}}
|
||||
- [DD.MM. HH:MM] Zweiter Ereignistext. {{Quellenname1, Quellenname2}}
|
||||
- [DD.MM. HH:MM] Ereignistext neu. {{M<ID>}}
|
||||
- [DD.MM. HH:MM] Ereignistext neu mit mehreren Belegen. {{M<ID1>, M<ID2>}}
|
||||
- [DD.MM. HH:MM] Ereignistext aus BISHERIGE ENTWICKLUNGEN. {{Quellenname1, Quellenname2}}
|
||||
..."""
|
||||
|
||||
|
||||
@@ -412,7 +415,7 @@ class AnalyzerAgent:
|
||||
logger.error(f"Latest-Developments-Fehler: {e}")
|
||||
return (prev or None), None
|
||||
|
||||
bullets = self._parse_latest_developments(result)
|
||||
bullets = self._parse_latest_developments(result, new_articles)
|
||||
if not bullets:
|
||||
logger.info("Latest-Developments: keine Bullets geparst, behalte bisherigen Stand")
|
||||
return (prev or None), usage
|
||||
@@ -423,21 +426,67 @@ class AnalyzerAgent:
|
||||
return output, usage
|
||||
|
||||
@staticmethod
|
||||
def _parse_latest_developments(text: str) -> list[str]:
|
||||
"""Extrahiert '- [DD.MM. HH:MM] ...'-Zeilen aus der Claude-Antwort."""
|
||||
def _parse_latest_developments(text: str, new_articles: list[dict] | None = None) -> list[str]:
|
||||
"""Extrahiert '- [DD.MM. HH:MM] ...'-Zeilen aus der Claude-Antwort.
|
||||
|
||||
Jeder Bullet MUSS mit einer Quellen-Klammer enden (geschweifte Klammern).
|
||||
- Items im Format M<ID> werden gegen new_articles aufgeloest zu Quellen-Namen.
|
||||
- Items ohne ID-Pattern werden als bereits-aufgeloeste Namen uebernommen.
|
||||
- Bullets ohne Klammer oder mit leerer Klammer werden verworfen.
|
||||
"""
|
||||
if not text:
|
||||
return []
|
||||
|
||||
articles_by_id: dict[str, str] = {}
|
||||
if new_articles:
|
||||
for a in new_articles:
|
||||
aid = a.get("id")
|
||||
if aid is not None:
|
||||
name = (a.get("source") or "").strip()
|
||||
if name:
|
||||
articles_by_id[str(aid)] = name
|
||||
|
||||
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*$")
|
||||
trailing_braces = re.compile(r"\{([^{}]+)\}\s*\.?\s*$")
|
||||
id_item = re.compile(r"^[M#]\s*(\d+)$", re.IGNORECASE)
|
||||
junk_item = re.compile(r"^(unbekannt|unknown|n/?a|keine|keine quelle|tba)$", re.IGNORECASE)
|
||||
|
||||
for raw_line in text.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
m = bullet_re.match(line)
|
||||
if m:
|
||||
if not m:
|
||||
continue
|
||||
ts = m.group(1)
|
||||
body = m.group(2).rstrip()
|
||||
bullets.append(f"- [{ts}] {body}")
|
||||
|
||||
brace_match = trailing_braces.search(body)
|
||||
if not brace_match:
|
||||
logger.debug(f"Bullet ohne Quellen-Klammer verworfen: {line[:80]}")
|
||||
continue
|
||||
|
||||
raw_items = [it.strip() for it in brace_match.group(1).split(",") if it.strip()]
|
||||
resolved: list[str] = []
|
||||
for it in raw_items:
|
||||
if junk_item.match(it):
|
||||
continue
|
||||
mid = id_item.match(it)
|
||||
if mid:
|
||||
name = articles_by_id.get(mid.group(1))
|
||||
if name and name not in resolved:
|
||||
resolved.append(name)
|
||||
else:
|
||||
if it not in resolved:
|
||||
resolved.append(it)
|
||||
|
||||
if not resolved:
|
||||
logger.debug(f"Bullet mit leerer/unaufloesbarer Quellen-Klammer verworfen: {line[:80]}")
|
||||
continue
|
||||
|
||||
body_clean = body[: brace_match.start()].rstrip()
|
||||
bullets.append(f"- [{ts}] {body_clean} {{{', '.join(resolved)}}}")
|
||||
return bullets
|
||||
|
||||
def _sanitize_sources(self, analysis: dict) -> dict:
|
||||
|
||||
@@ -1096,12 +1096,6 @@ a:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dev-sources-empty {
|
||||
color: var(--text-disabled);
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dev-source-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1127,26 +1121,6 @@ a.dev-source-pill:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dev-bias {
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dev-bias-pro-ru {
|
||||
background: var(--tint-error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.dev-bias-staatsnah {
|
||||
background: var(--tint-warning);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.dev-time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 11px;
|
||||
|
||||
@@ -788,12 +788,10 @@ const UI = {
|
||||
const buildPill = (src, fallbackName) => {
|
||||
const displayName = src ? (src.name || fallbackName) : fallbackName;
|
||||
const esc = this.escape(displayName);
|
||||
const biasClass = this._classifyBias(displayName);
|
||||
const biasHtml = biasClass ? `<span class="dev-bias dev-bias-${biasClass}">${this._biasLabel(biasClass)}</span>` : '';
|
||||
if (src && src.url) {
|
||||
return `<a href="${this.escape(src.url)}" target="_blank" rel="noopener" class="dev-source-pill" title="${esc}">${esc}${biasHtml}</a>`;
|
||||
return `<a href="${this.escape(src.url)}" target="_blank" rel="noopener" class="dev-source-pill" title="${esc}">${esc}</a>`;
|
||||
}
|
||||
return `<span class="dev-source-pill" title="${esc}">${esc}${biasHtml}</span>`;
|
||||
return `<span class="dev-source-pill" title="${esc}">${esc}</span>`;
|
||||
};
|
||||
|
||||
const cards = bulletLines.map(line => {
|
||||
@@ -842,9 +840,7 @@ const UI = {
|
||||
}
|
||||
|
||||
const cleanBody = this.escape(rawBody.trim());
|
||||
const sourcesHtml = pillsHtml
|
||||
? `<span class="dev-sources">${pillsHtml}</span>`
|
||||
: `<span class="dev-sources dev-sources-empty">Keine Quelle</span>`;
|
||||
const sourcesHtml = pillsHtml ? `<span class="dev-sources">${pillsHtml}</span>` : '<span class="dev-sources"></span>';
|
||||
const timeHtml = `<span class="dev-time" title="${this.escape(date + ' ' + time)}">${this.escape(time)} \u00b7 ${this.escape(date)}</span>`;
|
||||
|
||||
return `<div class="dev-bullet"><div class="dev-bullet-head">${sourcesHtml}${timeHtml}</div><div class="dev-body">${cleanBody}</div></div>`;
|
||||
@@ -853,21 +849,6 @@ const UI = {
|
||||
return `<div class="dev-list">${cards.join('')}</div>`;
|
||||
},
|
||||
|
||||
_classifyBias(name) {
|
||||
if (!name) return null;
|
||||
const n = name.toLowerCase();
|
||||
const proRu = ['rybar', 'sputnik', 'tass', 'ria novosti', 'ria.ru', 'tsargrad', 'readovka', 'pravda', 'russia today', 'rt.com', 'rt deutsch'];
|
||||
const staatsnah = ['cctv', 'global times', 'xinhua', "people's daily", 'press tv', 'irna', 'kcna'];
|
||||
if (proRu.some(k => n.includes(k))) return 'pro-ru';
|
||||
if (staatsnah.some(k => n.includes(k))) return 'staatsnah';
|
||||
return null;
|
||||
},
|
||||
|
||||
_biasLabel(cls) {
|
||||
if (cls === 'pro-ru') return 'pro-RU';
|
||||
if (cls === 'staatsnah') return 'staatsnah';
|
||||
return cls;
|
||||
},
|
||||
|
||||
renderSummary(summary, sourcesJson, incidentType) {
|
||||
if (!summary) return '<span style="color:var(--text-tertiary);">Noch keine Zusammenfassung.</span>';
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren