"""Konfiguration für den OSINT Lagemonitor.""" import os from zoneinfo import ZoneInfo # Zeitzone für alle Anwendungs-Timestamps (DB, Logs, UI) TIMEZONE = ZoneInfo("Europe/Berlin") # Pfade BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA_DIR = os.path.join(BASE_DIR, "data") LOG_DIR = os.path.join(BASE_DIR, "logs") STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static") DB_PATH = os.environ.get("DB_PATH") or os.path.join(DATA_DIR, "osint.db") # JWT _JWT_SECRET = os.environ.get("JWT_SECRET", "") def get_jwt_secret() -> str: """Gibt JWT_SECRET zurück. Wirft RuntimeError wenn nicht gesetzt.""" if not _JWT_SECRET: raise RuntimeError("JWT_SECRET Umgebungsvariable muss gesetzt sein") return _JWT_SECRET # Rückwärtskompatibel für direkte Imports JWT_SECRET = _JWT_SECRET JWT_ALGORITHM = "HS256" JWT_EXPIRE_HOURS = 24 # Claude CLI CLAUDE_PATH = os.environ.get("CLAUDE_PATH", "/usr/bin/claude") CLAUDE_TIMEOUT = 1800 # Sekunden (30 Min - Lage-Updates mit vielen Artikeln brauchen mehr Zeit) # Claude Modelle CLAUDE_MODEL_FAST = "claude-haiku-4-5-20251001" # Für einfache Aufgaben (Feed-Selektion) CLAUDE_MODEL_MEDIUM = "claude-sonnet-4-6" # Für qualitätskritische Aufgaben (Netzwerkanalyse) CLAUDE_MODEL_STANDARD = "claude-opus-4-7" # Standard-Opus für Recherche, Analyse, Faktencheck # --- KI-Backend (EU/DSGVO-Umbau, Phase 1) ------------------------------------- # 'cli' = heutiger Weg über das Claude CLI (Anthropic, Abo-Konto). # 'bedrock' = AWS Bedrock über EU-Inferenzprofile (Frankfurt). Verarbeitung # bleibt in Europa. Phase 1 bedient nur werkzeuglose Aufrufe, # WebSearch-Aufrufe laufen weiter übers CLI, bis die # staan-Recherche-Schleife (Phase 2) fertig ist. # Auflösung je Refresh in agents/orchestrator.py. Lage (incidents.ai_backend) # gewinnt vor Org-Setting 'ai_backend', das vor diesem globalen Default. AI_BACKEND = os.environ.get("AI_BACKEND", "cli").strip().lower() BEDROCK_REGION = os.environ.get("BEDROCK_REGION") or os.environ.get("AWS_REGION", "eu-central-1") # Obergrenze je Bedrock-Antwort. In Phase 1 laufen nur JSON-Aufgaben über # Bedrock (Feed-Selektion, Topic-Filter, Geoparsing, Übersetzung, QC), # dafür reichen 16k gut aus. Große Lagebilder bleiben in Phase 1 beim CLI. BEDROCK_MAX_TOKENS = int(os.environ.get("BEDROCK_MAX_TOKENS", "16000")) # Abbildung der CLI-Modellnamen auf EU-Inferenzprofile. NUR eu.-Profile # eintragen, agents/bedrock_client.py verweigert alles andere. BEDROCK_MODEL_MAP = { CLAUDE_MODEL_FAST: "eu.anthropic.claude-haiku-4-5-20251001-v1:0", CLAUDE_MODEL_MEDIUM: "eu.anthropic.claude-sonnet-4-6", CLAUDE_MODEL_STANDARD: "eu.anthropic.claude-opus-4-7", } # Preise in USD je 1 Mio Token (Stand 29.07.2026, Anthropic-Listenpreise, # Bedrock rechnet für Anthropic-Modelle zu denselben Sätzen ab). Nach der # ersten AWS-Rechnung gegen die tatsächlichen Beträge verifizieren. BEDROCK_PRICING = { CLAUDE_MODEL_FAST: {"input": 1.00, "output": 5.00}, CLAUDE_MODEL_MEDIUM: {"input": 3.00, "output": 15.00}, CLAUDE_MODEL_STANDARD: {"input": 5.00, "output": 25.00}, } BEDROCK_CACHE_WRITE_FACTOR = 1.25 BEDROCK_CACHE_READ_FACTOR = 0.10 # --- staan.ai Such-API (EU/DSGVO-Umbau, Phase 2) ------------------------------- # Europäischer Suchindex (European Search Perspective). Ersetzt im EU-Modellweg # die eingebaute WebSearch/WebFetch des Claude CLI. Client in # services/staan_client.py, Recherche-Schleife in agents/eu_researcher.py. STAAN_API_KEY = os.environ.get("STAAN_API_KEY", "") STAAN_BASE_URL = os.environ.get("STAAN_BASE_URL", "https://api.staan.ai/v2") # 2 Euro je 1000 Anfragen, als USD-Näherung für die interne Kostenkontrolle. STAAN_COST_PER_QUERY_USD = float(os.environ.get("STAAN_COST_PER_QUERY_USD", "0.0023")) # Pflicht-Ausschlussliste je Suchanfrage (Befund aus Phase 0, hebt die # Trefferqualität deutlich). staan erlaubt maximal 10 Domains je Anfrage, # Nutzer-Ausschlüsse füllen die restlichen Plätze auf. STAAN_EXCLUDE_DOMAINS = [ "youtube.com", "wikipedia.org", "facebook.com", "reddit.com", "pinterest.com", "tiktok.com", "instagram.com", "x.com", ] # Rundenbegrenzung der EU-Recherche-Schleife (Planungsaufrufe an das Modell, # ohne die Abschlussrunde). research bildet die 4-Phasen-Tiefenrecherche nach. EU_RESEARCH_MAX_ROUNDS_ADHOC = int(os.environ.get("EU_RESEARCH_MAX_ROUNDS_ADHOC", "3")) EU_RESEARCH_MAX_ROUNDS_RESEARCH = int(os.environ.get("EU_RESEARCH_MAX_ROUNDS_RESEARCH", "5")) # --- Verbrauchssaetze (Credits je Aktion) ------------------------------------- # Beschlossen 2026-07-23, der Verbrauchsrechner im Verwaltungsportal nutzt # dieselben Werte: Live-Lauf 45, Recherche je Durchlauf 40 (das Anlegen faehrt # drei Durchlaeufe, also 120), 1 Credit = 0,20 USD. Per ENV ueberschreibbar. # (Die gemessenen Mediane aus refresh_log lagen bei 24 bzw. 33 Credits, die # Verkaufssaetze sind bewusst hoeher angesetzt.) CREDITS_PER_ADHOC_REFRESH = float(os.environ.get("CREDITS_PER_ADHOC_REFRESH", "45")) CREDITS_PER_RESEARCH_PASS = float(os.environ.get("CREDITS_PER_RESEARCH_PASS", "40")) # --- Abrechnungsmodus --------------------------------------------------------- # 'flat' = feste Credits je Aktion nach CREDIT_TARIFF. Der Kunde kann seinen # Verbrauch vorher ausrechnen, und eine spaetere Verbilligung des # Modell-Backends veraendert sein Kontingent nicht. # 'actual' = bisheriges Verhalten, echte Kosten geteilt durch cost_per_credit. # Damit haengt das Kundenerlebnis am Einkaufspreis, ein Refresh einer # grossen Lage zieht ein Vielfaches eines Refreshs einer frischen. # Die echten Kosten wandern in beiden Modi unveraendert nach token_usage_monthly, # die interne Kostenkontrolle bleibt also erhalten. BILLING_MODE = os.environ.get("BILLING_MODE", "flat").lower() # Credits je Aktion im Modus 'flat'. Schluessel ist die Abrechnungsquelle, # beim Refresh zusaetzlich nach Lagentyp getrennt. # # monitor_* sind die beschlossenen Verkaufssaetze (siehe oben). # analysis/factcheck sind Einzelbausteine aus dem Studio und liegen mangels # eigener Messreihe bei rund einem Viertel eines Live-Laufs. Sobald # token_usage_monthly dafuer Zahlen hat, hier nachziehen. # chat/enhance/globe kosten real 0,02 bis 0,03 USD je Aufruf, also unter einem # halben Credit. Der Satz 1 verhindert Missbrauch, ohne echte Nutzung spuerbar # zu belasten. CREDIT_TARIFF = { "monitor_adhoc": CREDITS_PER_ADHOC_REFRESH, "monitor_research": CREDITS_PER_RESEARCH_PASS, "analysis": float(os.environ.get("CREDITS_PER_ANALYSIS", "12")), "factcheck": float(os.environ.get("CREDITS_PER_FACTCHECK", "12")), "chat": 1.0, "enhance": 1.0, "globe": 1.0, } # Voreinstellung fuer neue Lizenzen, wenn die Verwaltung nichts anderes setzt. # 'monthly' = Kontingent gilt je Kalendermonat und wird zum Monatswechsel neu # gefuellt. 'total' = Kontingent gilt fuer die gesamte Lizenzlaufzeit. CREDITS_PERIOD_DEFAULT = os.environ.get("CREDITS_PERIOD_DEFAULT", "monthly") # Ausgabesprache wird pro Organisation gesteuert -- siehe services/org_settings.py # (organization_settings-Tabelle, Key 'output_language', Werte 'de' | 'en'). # Default-Fallback in den Agent-Methoden ist 'Deutsch', sodass Calls ohne # explizite Org-Bindung weiterhin deutsch produzieren. # Dev-Modus: ausfuehrliches Logging (DEBUG-Level, HTTP-Request-Log) # In Kundenversion auf False setzen oder Env-Variable entfernen DEV_MODE = os.environ.get("DEV_MODE", "true").lower() == "true" # Feature-Flag: Translator-Agent (Haiku) komplett deaktivieren. # False = keine Uebersetzungen mehr, fremdsprachige Artikel bleiben unuebersetzt. TRANSLATOR_ENABLED = os.environ.get("TRANSLATOR_ENABLED", "true").lower() == "true" # RSS-Feeds (Fallback, primär aus DB geladen) RSS_FEEDS = { "deutsch": [ {"name": "tagesschau", "url": "https://www.tagesschau.de/index~rss2.xml"}, {"name": "ZDF heute", "url": "https://www.zdf.de/rss/zdf/nachrichten"}, {"name": "Spiegel", "url": "https://www.spiegel.de/schlagzeilen/index.rss"}, {"name": "Zeit", "url": "https://newsfeed.zeit.de/index"}, {"name": "FAZ", "url": "https://www.faz.net/rss/aktuell/"}, {"name": "Süddeutsche", "url": "https://rss.sueddeutsche.de/rss/Topthemen"}, {"name": "NZZ", "url": "https://www.nzz.ch/recent.rss"}, {"name": "Deutsche Welle", "url": "https://rss.dw.com/rdf/rss-de-all"}, ], "international": [ {"name": "Reuters", "url": "https://www.reutersagency.com/feed/"}, {"name": "AP News", "url": "https://rsshub.app/apnews/topics/apf-topnews"}, {"name": "BBC World", "url": "https://feeds.bbci.co.uk/news/world/rss.xml"}, {"name": "Al Jazeera", "url": "https://www.aljazeera.com/xml/rss/all.xml"}, {"name": "France24", "url": "https://www.france24.com/en/rss"}, ], "behoerden": [ {"name": "BMI", "url": "https://www.bmi.bund.de/SiteGlobals/Functions/RSSFeed/BMI/RSSNewsfeed/RSSNewsfeed_Nachrichten.xml"}, {"name": "Europol", "url": "https://www.europol.europa.eu/rss.xml"}, ], } # Ausgeschlossene Quellen (Fallback, primär aus DB geladen) EXCLUDED_SOURCES = ["bild.de", "bild", "twitter", "x.com", "facebook", "instagram", "tiktok", "reddit"] # SMTP (E-Mail-Versand für Magic Links und Einladungen) SMTP_HOST = os.environ.get("SMTP_HOST", "") SMTP_PORT = int(os.environ.get("SMTP_PORT", "587")) SMTP_USER = os.environ.get("SMTP_USER", "") SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "") SMTP_FROM_EMAIL = os.environ.get("SMTP_FROM_EMAIL", "noreply@aegis-sight.de") SMTP_FROM_NAME = os.environ.get("SMTP_FROM_NAME", "AegisSight Monitor") SMTP_USE_TLS = os.environ.get("SMTP_USE_TLS", "true").lower() == "true" # Quellenvielfalt: Domain-Begrenzungen MAX_FEEDS_PER_DOMAIN = 3 # Max. Feeds pro Domain bei Feed-Selektion MAX_ARTICLES_PER_DOMAIN_RSS = 10 # Max. Artikel pro Domain nach RSS-Fetch # Magic Link MAGIC_LINK_EXPIRE_MINUTES = 10 MAGIC_LINK_BASE_URL = os.environ.get("MAGIC_LINK_BASE_URL", "https://monitor.aegis-sight.de") # Telegram (Telethon) TELEGRAM_API_ID = int(os.environ.get("TELEGRAM_API_ID", "0")) TELEGRAM_API_HASH = os.environ.get("TELEGRAM_API_HASH", "") TELEGRAM_SESSION_PATH = os.environ.get("TELEGRAM_SESSION_PATH", "/home/claude-dev/.telegram/telegram_session") # X / Twitter (twscrape) -- siehe feeds/x_parser.py # Scraper liest Account-Timelines konfigurierter X-Quellen (source_type='x_account'). X_SCRAPER_ENABLED = os.environ.get("X_SCRAPER_ENABLED", "true").lower() == "true" # twscrape-Account-Store (SQLite). Liegt ausserhalb des Repos. X_ACCOUNTS_DB_PATH = os.environ.get("X_ACCOUNTS_DB_PATH", "/home/claude-dev/.x-scraper/accounts.db") # HTTP-Proxy fuer den X-Egress (tinyproxy am RUTX11 ueber WireGuard). # Leer = direkter Abruf ueber die Server-IP. Bei gesetztem Wert prueft der # Parser den Proxy vor jedem Lauf und faellt bei Ausfall auf direkt zurueck. X_PROXY_URL = os.environ.get("X_PROXY_URL", "") # Max. Posts pro Account-Timeline und Recency-Fenster in Tagen. X_POST_CAP_PER_ACCOUNT = int(os.environ.get("X_POST_CAP_PER_ACCOUNT", "40")) X_RECENCY_DAYS = int(os.environ.get("X_RECENCY_DAYS", "14")) # Health-Check (genutzt von services/source_health.py) HEALTH_CHECK_USER_AGENT = os.environ.get( "HEALTH_CHECK_USER_AGENT", "Mozilla/5.0 (compatible; AegisSight-HealthCheck/1.0)", ) HEALTH_CHECK_TIMEOUT_S = float(os.environ.get("HEALTH_CHECK_TIMEOUT_S", "15.0"))