Im EU-Modus laeuft die Recherche jetzt europaeisch: agents/eu_researcher.py steuert eine Schleife aus Bedrock-Planung (Opus, EU-Profile) und staan-Suchen (services/staan_client.py, Suche + Volltexte via full_content=markdown, Pflicht-Domain-Ausschlussliste je Anfrage). Bildet die 4-Phasen-Tiefenrecherche nach und liefert exakt das JSON der bisherigen CLI-WebSearch, Parsing und Filter in researcher.search bleiben unveraendert. Anti-Halluzination: nur URLs aus echten staan-Treffern werden akzeptiert; published_at nur wenn aus Inhalt oder URL ableitbar (staan liefert keine Daten). Doppelspur im Orchestrator: Lane-Schluessel ist jetzt (Organisation, Backend), Aufloesung zentral in _resolve_ai_backend (Lage > Org-Setting > ENV). CLI- und EU-Lauf derselben Organisation laufen gleichzeitig, gleiche Backends seriell. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
216 Zeilen
11 KiB
Python
216 Zeilen
11 KiB
Python
"""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"))
|