Translator-Agent: dedizierter Haiku-Pass fuer fehlende DE-Uebersetzungen

Bisher haben translations als Teil der Analyzer-JSON-Antwort gelebt
("translations": [...]). Bei vielen Artikeln pro Refresh hat das LLM die
Translations regelmaessig weggelassen (Output-Token-Druck), insbesondere
content_de (lange Texte werden zuerst gestrichen). Folge: viele englische
Artikel ohne deutsche Headline/Inhalt im Frontend.

Aenderungen:
- Neuer Agent src/agents/translator.py:
  * translate_articles_batch / translate_articles
  * Nutzt CLAUDE_MODEL_FAST (Haiku) - billig
  * Batch-Size 5 (mit Reserve gegen Output-Truncate)
  * Robustes JSON-Parsing: Markdown-Codefence, Truncate-Fallback,
    extrahiert auch unvollstaendige Antworten
  * Idempotent: Caller filtert auf fehlende headline_de/content_de
- analyzer.py: translations aus 4 Prompt-Templates entfernt (adhoc/research
  x analyze/enhance) und Fallback-Return-Dict bereinigt -> Analyzer-Output
  wird kompakter und zuverlaessiger
- orchestrator.py:
  * Alter Translation-INSERT-Block entfernt (analysis.translations wird
    nicht mehr genutzt)
  * Nach Analyse + db.commit + cancel-check neuer Translator-Call:
    SELECT WHERE language!=de AND (headline_de OR content_de fehlt),
    translate_articles, normalize_german_umlauts, COALESCE-UPDATE
  * Vor post_refresh_qc -> normalize_umlaut_articles greift auch frische
    Uebersetzungen
  * Failure-tolerant: Translator-Fehler bricht Refresh nicht ab

Backfill: migrations/migrate_translations_2026-05-03.py im Verwaltungs-Repo.
Dieser Commit ist enthalten in:
Claude Code
2026-05-03 00:04:59 +00:00
Ursprung 98c9da64b0
Commit a6f36be9c6
3 geänderte Dateien mit 282 neuen und 23 gelöschten Zeilen

Datei anzeigen

@@ -1410,30 +1410,64 @@ class AgentOrchestrator:
snap_articles, snap_fcs, log_id, now, tenant_id),
)
# Übersetzungen aktualisieren (nur für gültige DB-IDs)
# LLM-Drift abfangen: trotz Prompt-Anweisung kommen manchmal
# ASCII-Umlaute ("Gespraeche" statt "Gespräche") in der Übersetzung.
# Dictionary-basierte Korrektur schreibt nur deutsche Woerter um.
from services.post_refresh_qc import normalize_german_umlauts as _norm_de
for translation in analysis.get("translations", []):
article_id = translation.get("article_id")
if isinstance(article_id, int):
hd = translation.get("headline_de")
cd = translation.get("content_de")
if hd:
hd, _ = _norm_de(hd)
if cd:
cd, _ = _norm_de(cd)
await db.execute(
"UPDATE articles SET headline_de = ?, content_de = ? WHERE id = ? AND incident_id = ?",
(hd, cd, article_id, incident_id),
)
# Translations werden vom dedizierten Translator-Agent unten
# erzeugt (frueher inline im Analyzer-Output, das war token-
# instabil und schaetzte regelmaessig content_de aus).
await db.commit()
# Cancel-Check nach paralleler Verarbeitung
self._check_cancelled(incident_id)
# --- Translator (Haiku) fuer fremdsprachige Artikel ohne DE-Texte ---
# Idempotent: nur Artikel ohne headline_de/content_de werden geholt.
# Lauft nach der Analyse (Lagebild ist schon committed) und vor QC
# (damit normalize_umlaut_articles auch die frischen DE-Texte fasst).
try:
tr_cursor = await db.execute(
"""SELECT id, headline, content_original, language
FROM articles
WHERE incident_id = ?
AND language IS NOT NULL AND LOWER(language) != 'de'
AND (headline_de IS NULL OR headline_de = ''
OR content_de IS NULL OR content_de = '')""",
(incident_id,),
)
pending_translations = [dict(r) for r in await tr_cursor.fetchall()]
if pending_translations:
logger.info(
"Translator fuer Incident %d: %d Artikel ohne DE-Uebersetzung",
incident_id, len(pending_translations),
)
from agents.translator import translate_articles
from services.post_refresh_qc import normalize_german_umlauts as _norm_de2
translations = await translate_articles(
pending_translations,
output_lang="de",
usage_accumulator=usage_acc,
)
for t in translations:
hd = t.get("headline_de")
cd = t.get("content_de")
if hd:
hd, _ = _norm_de2(hd)
if cd:
cd, _ = _norm_de2(cd)
if hd or cd:
await db.execute(
"UPDATE articles SET headline_de = COALESCE(?, headline_de), "
"content_de = COALESCE(?, content_de) WHERE id = ? AND incident_id = ?",
(hd, cd, t["id"], incident_id),
)
await db.commit()
logger.info(
"Translator fuer Incident %d: %d/%d Artikel uebersetzt",
incident_id, len(translations), len(pending_translations),
)
except Exception as e:
logger.error("Translator-Fehler fuer Incident %d: %s", incident_id, e, exc_info=True)
# Refresh trotz Translator-Fehler weiterlaufen lassen
# --- Neueste Entwicklungen (nur Live-Monitoring / adhoc) ---
# Basis ist jetzt das frisch generierte Lagebild (autoritativ, thematisch sauber).
# Zeitstempel und Quellen kommen aus den jüngsten belegenden Artikeln.