Commits vergleichen
2 Commits
10606dba95
...
74f50c3b6e
| Autor | SHA1 | Datum | |
|---|---|---|---|
| 74f50c3b6e | |||
| b4898614c4 |
@@ -258,7 +258,8 @@ REGELN:
|
|||||||
- Breit gefasste Lagen (z.B. "Iran-Israel-Krieg", "Ukrainekrieg – aktuelle Lage") akzeptieren alle Meldungen, die einen der direkt beteiligten Akteure oder Kriegsschauplätze behandeln.
|
- Breit gefasste Lagen (z.B. "Iran-Israel-Krieg", "Ukrainekrieg – aktuelle Lage") akzeptieren alle Meldungen, die einen der direkt beteiligten Akteure oder Kriegsschauplätze behandeln.
|
||||||
- Eng gefasste Lagen (z.B. "Russische Militärblogger", "Ausfall bei Cloudflare", "Cybervorfall Stadtwerke X") akzeptieren NUR Meldungen zum Spezifikum. Peripheres, auch wenn im selben Großkontext, wird abgelehnt.
|
- Eng gefasste Lagen (z.B. "Russische Militärblogger", "Ausfall bei Cloudflare", "Cybervorfall Stadtwerke X") akzeptieren NUR Meldungen zum Spezifikum. Peripheres, auch wenn im selben Großkontext, wird abgelehnt.
|
||||||
- Eine Meldung gilt auch dann als relevant, wenn sie das Thema aus einer gegnerischen/kritischen Perspektive behandelt — es geht um thematische Zugehörigkeit, nicht um Ausrichtung.
|
- Eine Meldung gilt auch dann als relevant, wenn sie das Thema aus einer gegnerischen/kritischen Perspektive behandelt — es geht um thematische Zugehörigkeit, nicht um Ausrichtung.
|
||||||
- Im Zweifel: NICHT relevant. Ein zu schmaler Filter ist besser als ein Schwall off-topic-Treffer.
|
- FREMDSPRACHIGE QUELLEN (CJK, Arabisch, Hebräisch, Kyrillisch): Wo verfügbar steht eine "Übersetzung:"-Zeile unter der Originalüberschrift. NUTZE die Übersetzung für deine Bewertung. Verwirf einen fremdsprachigen Artikel NICHT pauschal aus Sicherheit, wenn die Übersetzung das Lagethema sichtbar berührt — wende dieselben Maßstäbe an wie auf englische Artikel.
|
||||||
|
- Im Zweifel bei lateinisch geschriebenen Quellen: NICHT relevant. Im Zweifel bei nicht-lateinischen Quellen mit übersetzter, thematisch passender Überschrift: relevant.
|
||||||
|
|
||||||
Antworte AUSSCHLIESSLICH als JSON-Objekt — KEINE Erklärung, KEINE Einleitung:
|
Antworte AUSSCHLIESSLICH als JSON-Objekt — KEINE Erklärung, KEINE Einleitung:
|
||||||
{{"relevant_ids": [1, 3, 7]}}"""
|
{{"relevant_ids": [1, 3, 7]}}"""
|
||||||
@@ -526,10 +527,17 @@ class AnalyzerAgent:
|
|||||||
headline = article.get("headline_de") or article.get("headline", "")
|
headline = article.get("headline_de") or article.get("headline", "")
|
||||||
source = article.get("source", "Unbekannt")
|
source = article.get("source", "Unbekannt")
|
||||||
content = article.get("content_de") or article.get("content_original") or ""
|
content = article.get("content_de") or article.get("content_original") or ""
|
||||||
|
# Pre-Topic-Translation für fremdsprachige Headlines (gesetzt vom Orchestrator)
|
||||||
|
headline_en = article.get("headline_en_for_topic")
|
||||||
|
content_en = article.get("content_en_for_topic")
|
||||||
lines.append(f"[{i}] Quelle: {source}")
|
lines.append(f"[{i}] Quelle: {source}")
|
||||||
lines.append(f" Überschrift: {headline}")
|
lines.append(f" Überschrift: {headline}")
|
||||||
|
if headline_en and headline_en.strip().lower() != (headline or "").strip().lower():
|
||||||
|
lines.append(f" Übersetzung: {headline_en}")
|
||||||
if content:
|
if content:
|
||||||
lines.append(f" Inhalt: {content[:400]}")
|
lines.append(f" Inhalt: {content[:400]}")
|
||||||
|
if content_en and content_en.strip().lower() != (content or "")[:len(content_en)].strip().lower():
|
||||||
|
lines.append(f" Inhalt (EN): {content_en[:400]}")
|
||||||
articles_text = "\n".join(lines)
|
articles_text = "\n".join(lines)
|
||||||
|
|
||||||
prompt = TOPIC_FILTER_PROMPT_TEMPLATE.format(
|
prompt = TOPIC_FILTER_PROMPT_TEMPLATE.format(
|
||||||
|
|||||||
@@ -1139,6 +1139,25 @@ class AgentOrchestrator:
|
|||||||
await _pipe_start("relevance")
|
await _pipe_start("relevance")
|
||||||
_candidates_before_topic = len(new_candidates)
|
_candidates_before_topic = len(new_candidates)
|
||||||
|
|
||||||
|
# --- Pre-Topic-Übersetzung: fremdsprachige Headlines ins Englische ---
|
||||||
|
# Damit der nachgelagerte Topic-Filter (Haiku) auch CJK/Arabisch/
|
||||||
|
# Hebräisch/Kyrillisch-Headlines fair beurteilen kann statt sie aus
|
||||||
|
# Sicherheit zu verwerfen.
|
||||||
|
if new_candidates:
|
||||||
|
try:
|
||||||
|
from agents.translator import translate_headlines_for_topic_filter
|
||||||
|
_pt_count, _pt_usage = await translate_headlines_for_topic_filter(new_candidates)
|
||||||
|
if _pt_usage:
|
||||||
|
usage_acc.add(_pt_usage)
|
||||||
|
if _pt_count:
|
||||||
|
logger.info(
|
||||||
|
f"Pre-Topic-Translate: {_pt_count} fremdsprachige Headlines übersetzt"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Pre-Topic-Translate fehlgeschlagen (Pipeline laeuft weiter): {e}"
|
||||||
|
)
|
||||||
|
|
||||||
# --- Semantischer Topic-Filter (Haiku) ---
|
# --- Semantischer Topic-Filter (Haiku) ---
|
||||||
# Wirft Artikel raus, die zwar Keyword-Treffer hatten, aber das Kernthema
|
# Wirft Artikel raus, die zwar Keyword-Treffer hatten, aber das Kernthema
|
||||||
# der Lage nicht inhaltlich behandeln. Bei Fehler Fallback auf alle Kandidaten.
|
# der Lage nicht inhaltlich behandeln. Bei Fehler Fallback auf alle Kandidaten.
|
||||||
|
|||||||
@@ -215,6 +215,159 @@ async def translate_articles_batch(
|
|||||||
return valid, usage
|
return valid, usage
|
||||||
|
|
||||||
|
|
||||||
|
# --- Pre-Topic-Filter: schmale Headline-Übersetzung -----------------------------
|
||||||
|
#
|
||||||
|
# Der Topic-Filter (analyzer.filter_relevant_articles) ist ein Haiku-Call, der pro
|
||||||
|
# Artikel beurteilt, ob er thematisch zur Lage passt. Bei fremdsprachigen Headlines
|
||||||
|
# (CJK/Arabisch/Hebräisch/Kyrillisch) bewertet Haiku konservativ und verwirft sie
|
||||||
|
# häufig, weil er sie nur halb versteht. Damit landeten z.B. die japanischen
|
||||||
|
# Ministeriums-Feeds (MOD, NHK, Asahi) in Lagen mit Japan-Bezug nie in der finalen
|
||||||
|
# Auswahl, obwohl der RSS-Match korrekt griff.
|
||||||
|
#
|
||||||
|
# Diese Funktion übersetzt einen einzelnen Batch-Call alle nicht-lateinischen
|
||||||
|
# Headlines + erste Content-Sätze ins Englische und hängt das Ergebnis als
|
||||||
|
# article["headline_en_for_topic"] / article["content_en_for_topic"] an. Der
|
||||||
|
# Topic-Filter zeigt das dem LLM zusätzlich zum Original.
|
||||||
|
#
|
||||||
|
# WICHTIG: Diese Mini-Übersetzung ist UNABHÄNGIG vom TRANSLATOR_ENABLED-Flag —
|
||||||
|
# sie wird auch dann gemacht, wenn der nachgelagerte Volltext-Translator
|
||||||
|
# deaktiviert ist (Pflicht für korrektes Topic-Filtering, sehr kleine Kosten).
|
||||||
|
|
||||||
|
_TOPIC_TRANSLATE_CONTENT_MAX = 240
|
||||||
|
|
||||||
|
|
||||||
|
def _needs_pretopic_translate(article: dict) -> bool:
|
||||||
|
"""Erkennt fremdsprachige Headlines, die für den Topic-Filter übersetzt
|
||||||
|
werden sollten.
|
||||||
|
|
||||||
|
Heuristik: Headline enthält Non-ASCII-Zeichen, die NICHT in den typischen
|
||||||
|
deutsch/franz./span./port./skand. Latin-1-Erweiterungen liegen.
|
||||||
|
Das sind v.a. CJK (Kanji/Kana/Hangul), Arabisch, Hebräisch, Kyrillisch,
|
||||||
|
Thai, Devanagari etc.
|
||||||
|
"""
|
||||||
|
headline = (article.get("headline_de") or article.get("headline") or "").strip()
|
||||||
|
if not headline:
|
||||||
|
return False
|
||||||
|
for ch in headline:
|
||||||
|
cp = ord(ch)
|
||||||
|
# Bereiche ausschließen, die in Latin-Schrift normal sind:
|
||||||
|
# ASCII (0-127), Latin-1 Supplement (128-255), Latin Extended-A/B (256-591)
|
||||||
|
if cp <= 591:
|
||||||
|
continue
|
||||||
|
# Alles darüber sind fremde Schriftsysteme → übersetzen
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def translate_headlines_for_topic_filter(
|
||||||
|
articles: list[dict],
|
||||||
|
target_lang: str = "en",
|
||||||
|
) -> tuple[int, ClaudeUsage]:
|
||||||
|
"""Übersetzt die Headlines fremdsprachiger Artikel ins Englische, damit der
|
||||||
|
nachgelagerte Topic-Filter (Haiku) sie zuverlässig beurteilen kann.
|
||||||
|
|
||||||
|
Setzt direkt auf den Artikel-Dicts:
|
||||||
|
article["headline_en_for_topic"]: str | None
|
||||||
|
article["content_en_for_topic"]: str | None
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(anzahl_übersetzt, ClaudeUsage)
|
||||||
|
"""
|
||||||
|
if not articles:
|
||||||
|
return 0, ClaudeUsage()
|
||||||
|
|
||||||
|
candidates = [a for a in articles if _needs_pretopic_translate(a)]
|
||||||
|
if not candidates:
|
||||||
|
return 0, ClaudeUsage()
|
||||||
|
|
||||||
|
# Eindeutige Indizes (auch wenn article kein "id"-Feld hat, weil noch nicht
|
||||||
|
# in der DB): wir nutzen die Position in der gesamten articles-Liste.
|
||||||
|
idx_by_obj = {id(a): i for i, a in enumerate(articles)}
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for a in candidates:
|
||||||
|
idx = idx_by_obj.get(id(a))
|
||||||
|
if idx is None:
|
||||||
|
continue
|
||||||
|
headline = (a.get("headline_de") or a.get("headline") or "").strip()
|
||||||
|
content_src = (a.get("content_de") or a.get("content_original") or "")
|
||||||
|
items.append({
|
||||||
|
"i": idx,
|
||||||
|
"h": headline[:200],
|
||||||
|
"c": content_src[:_TOPIC_TRANSLATE_CONTENT_MAX],
|
||||||
|
})
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
return 0, ClaudeUsage()
|
||||||
|
|
||||||
|
lang_label = {"en": "English", "de": "German"}.get(target_lang, target_lang)
|
||||||
|
prompt = f"""Translate these news headlines and short content snippets to {lang_label}.
|
||||||
|
Keep proper names (people, organizations, places) untouched. Keep it concise; the goal
|
||||||
|
is to let another model judge topical relevance, not to publish.
|
||||||
|
|
||||||
|
Return ONLY a JSON array. Each item: {{"i": <index>, "h": <headline in {lang_label}>, "c": <content snippet in {lang_label}>}}.
|
||||||
|
Keep the same "i" values. No prose, no markdown fences.
|
||||||
|
|
||||||
|
INPUT:
|
||||||
|
{json.dumps(items, ensure_ascii=False)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_text, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Pre-Topic-Translate Claude-Call fehlgeschlagen: {e}")
|
||||||
|
return 0, ClaudeUsage()
|
||||||
|
|
||||||
|
# Robustes Parsing (Markdown-Codefence + nacktes Array)
|
||||||
|
text = result_text.strip()
|
||||||
|
if text.startswith("```"):
|
||||||
|
text = re.sub(r"^```(?:json)?\s*", "", text)
|
||||||
|
text = re.sub(r"\s*```\s*$", "", text)
|
||||||
|
text = text.strip()
|
||||||
|
try:
|
||||||
|
data = json.loads(text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
m = re.search(r"\[.*\]", text, re.DOTALL)
|
||||||
|
if not m:
|
||||||
|
logger.warning(
|
||||||
|
f"Pre-Topic-Translate: kein JSON-Array in Antwort. Sample: {text[:200]!r}"
|
||||||
|
)
|
||||||
|
return 0, usage
|
||||||
|
try:
|
||||||
|
data = json.loads(m.group(0))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data = _extract_complete_objects(text)
|
||||||
|
|
||||||
|
if not isinstance(data, list):
|
||||||
|
logger.warning(
|
||||||
|
f"Pre-Topic-Translate: Antwort ist kein Array ({type(data).__name__})"
|
||||||
|
)
|
||||||
|
return 0, usage
|
||||||
|
|
||||||
|
applied = 0
|
||||||
|
for entry in data:
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
idx = entry.get("i")
|
||||||
|
if not isinstance(idx, int) or not (0 <= idx < len(articles)):
|
||||||
|
try:
|
||||||
|
idx = int(idx)
|
||||||
|
if not (0 <= idx < len(articles)):
|
||||||
|
continue
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
h = (entry.get("h") or "").strip() or None
|
||||||
|
c = (entry.get("c") or "").strip() or None
|
||||||
|
if h:
|
||||||
|
articles[idx]["headline_en_for_topic"] = h
|
||||||
|
if c:
|
||||||
|
articles[idx]["content_en_for_topic"] = c
|
||||||
|
if h or c:
|
||||||
|
applied += 1
|
||||||
|
|
||||||
|
return applied, usage
|
||||||
|
|
||||||
|
|
||||||
async def translate_articles(
|
async def translate_articles(
|
||||||
articles: list[dict],
|
articles: list[dict],
|
||||||
output_lang: str = "de",
|
output_lang: str = "de",
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren