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:
2026-05-22 00:12:56 +02:00
Ursprung 75038939b4
Commit 379d14518c
7 geänderte Dateien mit 226 neuen und 20 gelöschten Zeilen

Datei anzeigen

@@ -562,14 +562,27 @@ class ResearcherAgent:
logger.warning(f"Keyword-Extraktion fehlgeschlagen: {e}")
return None, None
async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True, user_id: int = None, existing_articles: list[dict] = None, preferred_sources: list[dict] = None, output_language: str = "Deutsch", output_language_iso: str = "de") -> tuple[list[dict], ClaudeUsage | None, bool]:
async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True, user_id: int = None, existing_articles: list[dict] = None, preferred_sources: list[dict] = None, output_language: str = "Deutsch", output_language_iso: str = "de", research_language_iso: str | None = None) -> tuple[list[dict], ClaudeUsage | None, bool]:
"""Sucht nach Informationen zu einem Vorfall.
Args:
output_language / output_language_iso: Ausgabesprache (Lagebild-Sprache).
research_language_iso: optionaler Override fuer die Sprache, in der gesucht
werden soll. Default = output_language_iso. Bei jp_demo z.B. 'ja',
waehrend output_language_iso 'de' bleibt (Lagebild deutsch, Recherche japanisch).
Returns:
(artikel, usage, parse_failed) — parse_failed ist True, wenn Claude geantwortet hat,
das JSON aber nicht extrahierbar war. So kann der Orchestrator zwischen
"echt keine Treffer" und "kaputte Antwort" unterscheiden.
"""
# research_language defaultet auf output_language. Wenn das aber abweicht
# (z.B. jp_demo: research='ja', output='de'), ueberschreiben wir die
# Sprach-Anweisung im Prompt mit einer eigenen, dual-sprachigen Variante.
research_language_iso = (research_language_iso or output_language_iso or "de").lower()
# Display-Name der Recherche-Sprache fuer Prompts ("Japanese", "Russian", ...)
from services.org_settings import language_display as _lang_display
research_language_display = _lang_display(research_language_iso)
# Bevorzugte Web-Quellen als Prompt-Block (optional)
preferred_sources_block = ""
if preferred_sources:
@@ -589,8 +602,31 @@ class ResearcherAgent:
"aber nicht deine sonstige Recherche.\n"
)
# Asymmetrische Sprach-Auswahl: research_language weicht von output_language ab
# -> eigene Anweisung "primaer in research-language, englische Quellen aus der
# Region auch erlaubt". Sonst die bisherige Logik (primary_only vs international).
asymmetric_lang = research_language_iso != output_language_iso
def _build_lang_instruction(deep: bool) -> str:
if asymmetric_lang:
# jp_demo & Co.: Recherche in Quellsprache + lokale Englisch-Outlets.
return (
f"- Fokus liegt auf {research_language_display}-sprachigen Quellen "
f"(Behoerden, Qualitaetszeitungen, oeffentlich-rechtliche Medien dieser Sprache).\n"
f"- Englischsprachige Outlets mit Fokus auf demselben Sprachraum/Region sind "
f"ebenfalls willkommen (z.B. Japan Times, Nikkei Asia, Kyodo English fuer Japan; "
f"Moscow Times English fuer Russland).\n"
f"- Quellen ausserhalb des Sprachraums NUR, wenn sie exklusive Informationen "
f"ueber die Region liefern (z.B. Reuters/AFP/AP-Berichte aus der Region).\n"
f"- Antworte in der Ausgabesprache {output_language} (das Lagebild wird in "
f"{output_language} angezeigt), aber zitiere die Original-Headlines/Quellen unveraendert."
)
if deep:
return lang_deep_international(output_language) if international else lang_deep_primary_only(output_language)
return lang_international(output_language) if international else lang_primary_only(output_language)
if incident_type == "research":
lang_instruction = lang_deep_international(output_language) if international else lang_deep_primary_only(output_language)
lang_instruction = _build_lang_instruction(deep=True)
# Bestehende Artikel als Kontext für den Prompt aufbereiten
existing_context = ""
if existing_articles:
@@ -611,7 +647,7 @@ class ResearcherAgent:
preferred_sources_block=preferred_sources_block,
)
else:
lang_instruction = lang_international(output_language) if international else lang_primary_only(output_language)
lang_instruction = _build_lang_instruction(deep=False)
# Bestehende Artikel als Kontext: bei Folge-Refreshes findet Claude andere Quellen
existing_context = ""
if existing_articles: