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:
@@ -1,12 +1,17 @@
|
||||
"""Organization-Settings-Helper.
|
||||
|
||||
KV-Store pro Organisation. Aktuell genutzt fuer output_language ('de'|'en').
|
||||
Spaeter erweiterbar (Default-Modell, Telegram-Toggle, Theme, ...).
|
||||
KV-Store pro Organisation. Aktuell genutzt fuer:
|
||||
- output_language ('de'|'en'|...) - Anzeige-/Lagebild-Sprache
|
||||
- source_language_whitelist (JSON-Liste, z.B. ["ja"]) - schraenkt RSS/Telegram-Quellen ein
|
||||
- research_language (ISO-Code) - steuert WebSearch-Prompts (default = output_language)
|
||||
- translator_enabled ('true'|'false') - override fuer das globale TRANSLATOR_ENABLED-Flag
|
||||
|
||||
Cache: TTL 60s in-memory pro (tenant_id, key). Wird bei set_org_setting()
|
||||
invalidiert.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
@@ -84,6 +89,15 @@ async def set_org_setting(
|
||||
LANGUAGE_DISPLAY_NAMES = {
|
||||
"de": "Deutsch",
|
||||
"en": "English",
|
||||
"ja": "Japanese",
|
||||
"zh": "Chinese",
|
||||
"ko": "Korean",
|
||||
"ru": "Russian",
|
||||
"ar": "Arabic",
|
||||
"fa": "Persian",
|
||||
"he": "Hebrew",
|
||||
"fr": "French",
|
||||
"es": "Spanish",
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +105,10 @@ async def get_org_language(
|
||||
db: aiosqlite.Connection,
|
||||
tenant_id: int,
|
||||
) -> str:
|
||||
"""Liefert ISO-2-Sprachcode der Org (default 'de')."""
|
||||
"""Liefert ISO-2-Sprachcode der Org (default 'de').
|
||||
|
||||
Steuert die Lagebild-/Anzeige-Sprache.
|
||||
"""
|
||||
value = await get_org_setting(db, tenant_id, "output_language", default="de")
|
||||
if value not in LANGUAGE_DISPLAY_NAMES:
|
||||
logger.warning("Unbekannte output_language '%s' fuer Org %s -- fallback 'de'", value, tenant_id)
|
||||
@@ -99,6 +116,65 @@ async def get_org_language(
|
||||
return value
|
||||
|
||||
|
||||
async def get_source_language_whitelist(
|
||||
db: aiosqlite.Connection,
|
||||
tenant_id: int,
|
||||
) -> Optional[list[str]]:
|
||||
"""Liefert Liste erlaubter Quellsprachen oder None (= keine Einschränkung).
|
||||
|
||||
Gespeichert als JSON-Array unter dem Key 'source_language_whitelist'.
|
||||
Beispiel-Wert: '["ja"]' -> nur japanischsprachige Quellen.
|
||||
"""
|
||||
raw = await get_org_setting(db, tenant_id, "source_language_whitelist", default=None)
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError) as e:
|
||||
logger.warning(
|
||||
"source_language_whitelist fuer Org %s ist kein JSON ('%s'): %s",
|
||||
tenant_id, raw, e,
|
||||
)
|
||||
return None
|
||||
if not isinstance(parsed, list):
|
||||
logger.warning("source_language_whitelist fuer Org %s ist keine Liste: %r", tenant_id, parsed)
|
||||
return None
|
||||
cleaned = [str(x).strip().lower() for x in parsed if str(x).strip()]
|
||||
return cleaned or None
|
||||
|
||||
|
||||
async def get_research_language(
|
||||
db: aiosqlite.Connection,
|
||||
tenant_id: int,
|
||||
) -> str:
|
||||
"""Liefert die Sprache, in der der WebSearch-Researcher primär sucht.
|
||||
|
||||
Default = output_language. Bei jp_demo z.B. 'ja', während output_language='de' bleibt.
|
||||
"""
|
||||
value = await get_org_setting(db, tenant_id, "research_language", default=None)
|
||||
if value and value in LANGUAGE_DISPLAY_NAMES:
|
||||
return value
|
||||
return await get_org_language(db, tenant_id)
|
||||
|
||||
|
||||
async def get_translator_enabled(
|
||||
db: aiosqlite.Connection,
|
||||
tenant_id: Optional[int],
|
||||
) -> bool:
|
||||
"""Liefert true wenn der (volle) Translator-Schritt fuer diese Org laufen soll.
|
||||
|
||||
Hierarchie:
|
||||
1. Org-Setting 'translator_enabled' ('true'/'false') gewinnt, wenn gesetzt.
|
||||
2. Sonst: globales ENV-Flag TRANSLATOR_ENABLED (Default true im config.py).
|
||||
"""
|
||||
if tenant_id is not None:
|
||||
raw = await get_org_setting(db, tenant_id, "translator_enabled", default=None)
|
||||
if raw is not None:
|
||||
return str(raw).strip().lower() in ("true", "1", "yes", "on")
|
||||
env_value = os.environ.get("TRANSLATOR_ENABLED", "true").strip().lower()
|
||||
return env_value in ("true", "1", "yes", "on")
|
||||
|
||||
|
||||
def language_display(lang_iso: str) -> str:
|
||||
"""ISO-Code -> Anzeigename fuer Prompts ('de' -> 'Deutsch')."""
|
||||
return LANGUAGE_DISPLAY_NAMES.get(lang_iso, lang_iso)
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren