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>
181 Zeilen
5.7 KiB
Python
181 Zeilen
5.7 KiB
Python
"""Organization-Settings-Helper.
|
|
|
|
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
|
|
|
|
import aiosqlite
|
|
|
|
logger = logging.getLogger("osint.org_settings")
|
|
|
|
_CACHE: dict[tuple[int, str], tuple[float, Optional[str]]] = {}
|
|
_TTL_SECONDS = 60.0
|
|
|
|
|
|
def _cache_get(tenant_id: int, key: str) -> tuple[bool, Optional[str]]:
|
|
"""(hit, value). hit=True heisst Cache traf; value kann auch None sein."""
|
|
entry = _CACHE.get((tenant_id, key))
|
|
if entry is None:
|
|
return (False, None)
|
|
expires_at, value = entry
|
|
if time.monotonic() > expires_at:
|
|
_CACHE.pop((tenant_id, key), None)
|
|
return (False, None)
|
|
return (True, value)
|
|
|
|
|
|
def _cache_put(tenant_id: int, key: str, value: Optional[str]) -> None:
|
|
_CACHE[(tenant_id, key)] = (time.monotonic() + _TTL_SECONDS, value)
|
|
|
|
|
|
def _cache_invalidate(tenant_id: int, key: str) -> None:
|
|
_CACHE.pop((tenant_id, key), None)
|
|
|
|
|
|
async def get_org_setting(
|
|
db: aiosqlite.Connection,
|
|
tenant_id: int,
|
|
key: str,
|
|
default: Optional[str] = None,
|
|
) -> Optional[str]:
|
|
"""Liest ein Org-Setting. Fallback auf default."""
|
|
if tenant_id is None:
|
|
return default
|
|
hit, cached = _cache_get(tenant_id, key)
|
|
if hit:
|
|
return cached if cached is not None else default
|
|
cursor = await db.execute(
|
|
"SELECT value FROM organization_settings WHERE organization_id = ? AND key = ?",
|
|
(tenant_id, key),
|
|
)
|
|
row = await cursor.fetchone()
|
|
value = row["value"] if row else None
|
|
_cache_put(tenant_id, key, value)
|
|
return value if value is not None else default
|
|
|
|
|
|
async def set_org_setting(
|
|
db: aiosqlite.Connection,
|
|
tenant_id: int,
|
|
key: str,
|
|
value: str,
|
|
) -> None:
|
|
"""Setzt ein Org-Setting (upsert)."""
|
|
await db.execute(
|
|
"""INSERT INTO organization_settings (organization_id, key, value, updated_at)
|
|
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
|
ON CONFLICT(organization_id, key) DO UPDATE SET
|
|
value = excluded.value,
|
|
updated_at = CURRENT_TIMESTAMP""",
|
|
(tenant_id, key, value),
|
|
)
|
|
await db.commit()
|
|
_cache_invalidate(tenant_id, key)
|
|
logger.info("Org %s Setting %s='%s' gespeichert", tenant_id, key, value)
|
|
|
|
|
|
# Bekannte Sprachen + Anzeigenamen fuer Prompts
|
|
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",
|
|
}
|
|
|
|
|
|
async def get_org_language(
|
|
db: aiosqlite.Connection,
|
|
tenant_id: int,
|
|
) -> str:
|
|
"""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)
|
|
return "de"
|
|
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)
|