feat(multitenancy): Sprach-Whitelist + Translator-Override + Forum-Quellenklasse
Vorbereitung fuer jp_demo-Organisation: drei separate Sprach-Settings statt einer einzigen output_language. org_settings.py: - get_source_language_whitelist: Liste erlaubter Quellsprachen als JSON-Array (z.B. ["ja"] beschraenkt RSS/Telegram auf japanische Quellen). - get_research_language: Sprache fuer WebSearch-Prompts (Default: output_language). - get_translator_enabled: Pro-Org-Override des globalen TRANSLATOR_ENABLED-Flags. - LANGUAGE_DISPLAY_NAMES um ja/zh/ko/ru/ar/fa/he/fr/es erweitert. source_rules.py: - get_feeds_with_metadata filtert nach source_language_whitelist, wenn gesetzt. - Feeds ohne primary_language fallen bei aktiver Whitelist raus (gewollt). - SELECT um media_type erweitert, damit es im Feed-Dict ankommt. orchestrator.py: - Laedt research_language, source_language_whitelist, translator_enabled aus den Org-Settings. - Wenn Whitelist gesetzt: international_sources-Flag wird ignoriert. - research_language_iso wird an researcher.search() weitergegeben. - translate_articles bekommt enabled-Parameter aus Org-Setting. - Geoparsing ueberspringt media_type='forum' Artikel. - SELECT * FROM articles wird zu JOIN sources, damit media_type beim Reload am Article-Dict haengt. researcher.py: - search() akzeptiert research_language_iso. Asymmetrische Sprach-Auswahl (Recherche != Output) erzeugt eigene Prompt-Anweisung "primaer in Quell- sprache, englische Region-Outlets erlaubt". translator.py: - translate_articles akzeptiert enabled-Parameter. Ueberschreibt die globale TRANSLATOR_ENABLED-Konstante pro Aufruf. factchecker.py: - _format_articles_text filtert Artikel mit media_type='forum' aus. Anonyme Foren-Posts gelten nicht als Faktenbeleg. rss_parser.py: - _fetch_feed traegt media_type aus feed_config ins Article-Dict ein, damit downstream Pipeline-Schritte Foren-Quellen erkennen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -744,14 +744,42 @@ class AgentOrchestrator:
|
||||
description = incident["description"] or ""
|
||||
incident_type = incident["type"] or "adhoc"
|
||||
international = bool(incident["international_sources"]) if "international_sources" in incident.keys() else True
|
||||
# Wenn die Org eine Sprach-Whitelist gesetzt hat, ist 'international' bedeutungslos —
|
||||
# die Whitelist gewinnt. Wir setzen 'international' auf True, damit der nachgelagerte
|
||||
# Code alle (durch Whitelist gefilterten) Feeds in Betracht zieht. Tatsaechliche
|
||||
# Einschraenkung passiert in get_feeds_with_metadata.
|
||||
# Hinweis: source_lang_whitelist wird weiter unten geladen.
|
||||
include_telegram = bool(incident["include_telegram"]) if "include_telegram" in incident.keys() else False
|
||||
visibility = incident["visibility"] if "visibility" in incident.keys() else "public"
|
||||
created_by = incident["created_by"] if "created_by" in incident.keys() else None
|
||||
tenant_id = incident["tenant_id"] if "tenant_id" in incident.keys() else None
|
||||
# Org-Sprache fuer alle KI-Agenten (Lagebild, Faktencheck, Recherche)
|
||||
from services.org_settings import get_org_language, language_display
|
||||
from services.org_settings import (
|
||||
get_org_language, language_display, get_research_language,
|
||||
get_source_language_whitelist, get_translator_enabled,
|
||||
)
|
||||
output_language_iso = await get_org_language(db, tenant_id) if tenant_id else "de"
|
||||
output_language = language_display(output_language_iso)
|
||||
# research_language steuert nur den WebSearch-Prompt ("suche in Sprache X").
|
||||
# Default = output_language_iso. Bei jp_demo wird das auf 'ja' gesetzt, waehrend
|
||||
# output_language_iso 'de' bleibt (Lagebild auf Deutsch, Recherche auf Japanisch).
|
||||
research_language_iso = await get_research_language(db, tenant_id) if tenant_id else output_language_iso
|
||||
# source_language_whitelist schraenkt RSS-/Telegram-Quellenpool ein (z.B. ['ja']).
|
||||
# Wenn gesetzt, wird das incident-level Flag international_sources ignoriert
|
||||
# (Whitelist ist explizit, das Flag ist Default-Verhalten).
|
||||
source_lang_whitelist = await get_source_language_whitelist(db, tenant_id) if tenant_id else None
|
||||
# Pro-Org-Override des globalen TRANSLATOR_ENABLED-Flags.
|
||||
translator_enabled = await get_translator_enabled(db, tenant_id)
|
||||
# Whitelist gewinnt ueber das incident-Flag international_sources:
|
||||
# wenn die Org eine Sprach-Whitelist hat, sind alle gewaehlten Feeds
|
||||
# ohnehin "Wunsch-Sprache" — kein Splitting in primary/international noetig.
|
||||
if source_lang_whitelist:
|
||||
international = True
|
||||
logger.info(
|
||||
"Org %s hat source_language_whitelist=%s gesetzt; "
|
||||
"incident.international_sources wird ignoriert",
|
||||
tenant_id, source_lang_whitelist,
|
||||
)
|
||||
previous_summary = incident["summary"] or ""
|
||||
previous_sources_json = incident["sources_json"] if "sources_json" in incident.keys() else None
|
||||
previous_developments = incident["latest_developments"] if "latest_developments" in incident.keys() else None
|
||||
@@ -936,6 +964,7 @@ class AgentOrchestrator:
|
||||
preferred_sources=preferred_sources,
|
||||
output_language=output_language,
|
||||
output_language_iso=output_language_iso,
|
||||
research_language_iso=research_language_iso,
|
||||
)
|
||||
logger.info(
|
||||
f"Claude-Recherche: {len(results)} Ergebnisse"
|
||||
@@ -1209,14 +1238,25 @@ class AgentOrchestrator:
|
||||
await db.commit()
|
||||
|
||||
# Geoparsing: Orte aus neuen Artikeln extrahieren und speichern
|
||||
if new_articles_for_analysis:
|
||||
# Foren-Quellen (media_type='forum') ausschliessen: 5ch/Hatena/Note-Posts haben
|
||||
# keinen eigenen, fuer das Lagebild interessanten geographischen Bezug; spart Haiku-Calls.
|
||||
articles_for_geoparsing = [
|
||||
a for a in new_articles_for_analysis
|
||||
if (a.get("media_type") or "").lower() != "forum"
|
||||
]
|
||||
if new_articles_for_analysis and not articles_for_geoparsing:
|
||||
logger.info(
|
||||
"Geoparsing uebersprungen: alle %d neuen Artikel sind Forum-Quellen",
|
||||
len(new_articles_for_analysis),
|
||||
)
|
||||
if articles_for_geoparsing:
|
||||
# Pipeline-Schritt 5: Orte erkennen (Start)
|
||||
await _pipe_start("geoparsing")
|
||||
try:
|
||||
from agents.geoparsing import geoparse_articles
|
||||
incident_context = f"{title} - {description}"
|
||||
logger.info(f"Geoparsing fuer {len(new_articles_for_analysis)} neue Artikel...")
|
||||
geo_results, category_labels = await geoparse_articles(new_articles_for_analysis, incident_context)
|
||||
logger.info(f"Geoparsing fuer {len(articles_for_geoparsing)} neue Artikel (Foren ausgeschlossen)...")
|
||||
geo_results, category_labels = await geoparse_articles(articles_for_geoparsing, incident_context)
|
||||
geo_count = 0
|
||||
for art_id, locations in geo_results.items():
|
||||
for loc in locations:
|
||||
@@ -1294,7 +1334,12 @@ class AgentOrchestrator:
|
||||
all_articles_preloaded = None
|
||||
if not previous_summary or new_count == 0 or not existing_facts:
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM articles WHERE incident_id = ? ORDER BY collected_at DESC",
|
||||
# JOIN auf sources, damit media_type pro Artikel verfuegbar ist
|
||||
# (Faktencheck schliesst Foren-Quellen aus, das Stimmungs-Modul nimmt
|
||||
# nur diese). Bei Quellen ohne Match in sources bleibt media_type NULL.
|
||||
"SELECT a.*, s.media_type AS media_type FROM articles a "
|
||||
"LEFT JOIN sources s ON s.name = a.source "
|
||||
"WHERE a.incident_id = ? ORDER BY a.collected_at DESC",
|
||||
(incident_id,),
|
||||
)
|
||||
all_articles_preloaded = [dict(row) for row in await cursor.fetchall()]
|
||||
@@ -1582,8 +1627,9 @@ class AgentOrchestrator:
|
||||
from services.post_refresh_qc import normalize_german_umlauts as _norm_de2
|
||||
translations = await translate_articles(
|
||||
pending_translations,
|
||||
output_lang="de",
|
||||
output_lang=output_language_iso,
|
||||
usage_accumulator=usage_acc,
|
||||
enabled=translator_enabled,
|
||||
)
|
||||
for t in translations:
|
||||
hd = t.get("headline_de")
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren