From 89ab15820288deeeb4259a13e1dba50cd904306b Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 18 Apr 2026 20:50:46 +0000 Subject: [PATCH] Live-Monitoring: Quellen-IDs deterministisch aufloesen, Bias-Markierung raus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aenderung am Grund-Mechanismus: LLM liefert pro Bullet die Meldungs-IDs im Format {M, M}, 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. --- src/agents/analyzer.py | 73 +++++++++++++++++++++++++++++++------ src/static/css/style.css | 26 ------------- src/static/js/components.js | 25 ++----------- 3 files changed, 64 insertions(+), 60 deletions(-) diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index d74d0e9..06bb46c 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -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, 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 {{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}} +- [DD.MM. HH:MM] Ereignistext neu mit mehreren Belegen. {{M, M}} +- [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 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: - ts = m.group(1) - body = m.group(2).rstrip() - bullets.append(f"- [{ts}] {body}") + if not m: + continue + ts = m.group(1) + body = m.group(2).rstrip() + + 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: diff --git a/src/static/css/style.css b/src/static/css/style.css index 40b1028..149ab91 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -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; diff --git a/src/static/js/components.js b/src/static/js/components.js index 0aaab5d..54d1c43 100644 --- a/src/static/js/components.js +++ b/src/static/js/components.js @@ -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 ? `${this._biasLabel(biasClass)}` : ''; if (src && src.url) { - return `${esc}${biasHtml}`; + return `${esc}`; } - return `${esc}${biasHtml}`; + return `${esc}`; }; const cards = bulletLines.map(line => { @@ -842,9 +840,7 @@ const UI = { } const cleanBody = this.escape(rawBody.trim()); - const sourcesHtml = pillsHtml - ? `${pillsHtml}` - : `Keine Quelle`; + const sourcesHtml = pillsHtml ? `${pillsHtml}` : ''; const timeHtml = `${this.escape(time)} \u00b7 ${this.escape(date)}`; return `
${sourcesHtml}${timeHtml}
${cleanBody}
`; @@ -853,21 +849,6 @@ const UI = { return `
${cards.join('')}
`; }, - _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 'Noch keine Zusammenfassung.';