Files
creator3/backend/config.py
2026-07-24 11:23:18 +02:00

168 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Konstanten + .env-Loader. Regel: .env-Datei gewinnt über geerbtes Env
(creator2-Lektion 12: --reload-Master pinnte sonst stale Werte)."""
import os
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def _load_env() -> None:
datei = ROOT / ".env"
if not datei.exists():
return
for zeile in datei.read_text().splitlines():
zeile = zeile.strip()
if not zeile or zeile.startswith("#") or "=" not in zeile:
continue
k, v = zeile.split("=", 1)
v = v.split("#", 1)[0].strip()
if v:
os.environ[k.strip()] = v
_load_env()
FAKE = bool(os.getenv("CREATOR_FAKE"))
LLM_LOG = bool(os.getenv("LLM_LOG")) # 1 = Prompt+Antwort je Call in call_texte
DB_PFAD = Path(os.getenv("CREATOR_DB") or ROOT / "storage" / "creator3.db")
KORPUS_DIR = ROOT / "storage" / "korpus"
SKILLS_DIR = ROOT / "skills"
PIPELINE_YAML = ROOT / "pipeline.yaml"
TOPIC_NAME_RE = re.compile(r"^[a-zA-Z0-9äöüÄÖÜß][a-zA-Z0-9äöüÄÖÜß ._-]{0,63}$")
# ── LLM (R3/R9) — Anbieter/Modell per env überschreibbar, nicht im Code ──
LLM_ENDPOINT = os.getenv("LLM_ENDPOINT",
"https://api.minimax.io/anthropic/v1/messages")
LLM_MODEL = os.getenv("LLM_MODEL", "MiniMax-M3")
ROLLEN = {
# thinking IMMER explizit ("enabled" gibt 400; Default widersprüchlich dokumentiert)
"extraktion": dict(model=LLM_MODEL, thinking="disabled", temperature=0.2),
"judge": dict(model=LLM_MODEL, thinking="disabled", temperature=0.1),
"writer": dict(model=LLM_MODEL, thinking="adaptive", temperature=0.7),
}
# Anbieter-spezifische Infra-Marker (MiniMax-Statuscodes) getrennt von generischen
INFRA_MARKER = ("429", "rate limit", "rate_limit", "timeout", "timed out",
"connection", "network", "overloaded", "unavailable",
'"status_code":1002', '"status_code":1041', '"status_code":2045')
LLM_MAX_TOKENS = 32_000
MAX_PARALLEL_LLM = int(os.getenv("MAX_PARALLEL_LLM", "12")) # M3: 200 RPM → 8-16 sicher
TAKT_SEKUNDEN = float(os.getenv("TAKT_SEKUNDEN", "3")) # 1 Call-Start je Takt; 0 = Breiten-Drossel
BUENDEL = {"lang": 1, "mittel": 3, "kurz": 10} # Tasks je Call nach Knoten-Klasse
INFRA_MAX_RETRIES = 3
INFRA_BACKOFF_BASE = 8.0 # → 8/16/32 s, mit Jitter (Retry-After unzuverlässig)
INHALT_MAX_RESTARTS = 2
DROSSEL_MIN_BREITE = 4 # 429/1002/1041/2045: Breite halbieren, 20 Erfolge = +1
DROSSEL_ERFOLGE_JE_PLUS = 20
HEDGE_NACH_S = int(os.getenv("HEDGE_NACH_S", "90")) # Zwilling nach max(90, timeout/2); 0 = aus
RUN_BUDGET_TOKENS = int(os.getenv("RUN_BUDGET_TOKENS", "10000000")) # DEFAULT AN; 0 = aus
PREIS_IN, PREIS_OUT = 0.60, 2.40 # $/1M konservativ (R9), real messen
# Timeouts stehen als Knoten-Property in pipeline.yaml (graph.timeout_fuer);
# hier nur der Default für Knoten ohne Angabe
TIMEOUT_DEFAULT = (180, 0)
# ── Suche (R2) ──
SUCHE_TIMEOUT_S = 10
SUCHE_RETRIES = 1
CIRCUIT_BREAKER_N = 3
WIKI_MAX_PARALLEL = 3
DDGS_PAUSE_S = 1.5
SUCHE_MAX_TREFFER = 8
# ── Laden (R6) ──
FETCH_CONNECT_S = 10
FETCH_READ_S = 30
DOMAIN_RATE_S = 1.0
SNAPSHOT_MIN_ZEICHEN = 500
# Privates Lese-Tool, ~20 Seiten/Lauf mit Domain-Bremse — kein robots.txt-Check
# mehr (Lektion 101); Browser-UA, sonst sperren Bot-Filter trotzdem
BROWSER_UA = ("Mozilla/5.0 (X11; Linux x86_64; rv:128.0) "
"Gecko/20100101 Firefox/128.0")
URTEIL_AUSZUG_ZEICHEN = 3_000 # Inhalts-Urteil je geladener Quelle
# ── Korpus ──
RECHERCHE_LENSES = ("ueberblick", "tutorial", "referenz", "praxis") # generisch!
QUERIES_JE_LENS = 3
SOLL_CHUNK_CHARS = 40_000
SOLL_PUNKTE_MIN = int(os.getenv("SOLL_PUNKTE_MIN", "5"))
SOLL_BAND_FAKTOR = 1.0 # Ziel ≈ √Kandidaten (creator2-Band — Lektion 90)
SOLL_BAND_TOLERANZ = 1.5 # mehr als Ziel×1.5 bestätigt → nochmal falten
QUELLEN_MAX = int(os.getenv("QUELLEN_MAX", "24")) # harter Korpus-Deckel
EXTRAKT_FENSTER = 4_000 # Atome nur ±Fenster um Soll-Belege (belegnah)
# Harte Vollständigkeits-Stops (Lektion 103): lieber PAUSE als Mini-Guide
SEED_PFLICHT = True # keine Startquelle nützlich geladen → PAUSE
QUELLEN_MIN = 5 # weniger geladene nützliche Quellen → PAUSE
BEIFANG_MAX_QUOTE = 0.5 # >50 % Atome als Beifang verworfen → PAUSE
# ── Inventar ──
ABSCHNITT_CHARS = 6_000 # 12k riss das 32k-Output-Cap (creator2 empirisch)
READER_JE_ABSCHNITT = 2 # Konsens entsteht über Dedup, nicht Union
CHUNK_HALBIER_TIEFE = 2
ANKER_OVERLAP_MERGE = 0.5
FUZZY_MIN_ZEICHEN = 40
FUZZY_FEHLERQUOTE = 0.08
WORT_MIN = 8
WORT_FEHLERQUOTE = 0.25
ANKER_FIX_BATCH = 20
MERGE_PANEL = 2 # Merge nur einstimmig (Falsch-Merge >> Dublette)
DEDUP_JACCARD = 0.3 # gemessener Paraphrasen-Floor
DEDUP_TITEL_FUZZY = 85 # nur bei ≥2 Inhaltstoken (Subset-100-Falle)
DEDUP_PAARE_PRO_CALL = 10
DEDUP_KANDIDATEN_CAP = 400
VERDICHTUNG_ZIEL = 8 # weiches Ziel Atome je Soll-Punkt (kein Cap!)
LUECKEN_RUNDEN_MAX = 2
WEB_NACHRECHERCHE_RUNDEN = 1
LUECKE_FENSTER = 6_000
BELEG_FENSTER_VOR, BELEG_FENSTER_NACH = 300, 1200
# ── Struktur ──
KAPITEL_SOLL_PUNKTE = 8
BAUSTEIN_MIN_ATOME, BAUSTEIN_MAX_ATOME = 4, 8
ZIELE_CHUNK_ATOME = 30
# ── Guide ──
SECTION_WOERTER_PRO_ATOM = (40, 220) # QA-Band = ×1.25
PRUEF_PANEL = 2
KLAERUNG_PANEL = 3
FIX_MAX_VERSUCHE = 3
REDUNDANZ_JACCARD = 0.5 # Satzpaare zwischen Bausteinen (Paraphrasen!)
REDUNDANZ_SHINGLE = 5 # gemeinsame 5-Wort-Folge = Kandidat
REDUNDANZ_SATZ_MIN_TOKEN = 6
ZIEL_DEDUP_JACCARD = 0.5 # synonyme Lernziele deterministisch mergen
ECHO_JACCARD = 0.7 # 1. Satz nach Marker ≈ Atom-Definition = Echo
FAKTEN_SIGNATUR_MIN = 2 # geteilte Zahlen+Namen je Satzpaar = Kandidat
# ── Engine (R8) ──
POLL_SEKUNDEN = 2.0
MAX_VERSUCHE = 2 # Inhalts-Versuche je Task; Infra zählt separat
STILLSTAND_N = 2 # identischer Befund-Fingerprint n× → PAUSE
# Gate-Runden-Caps stehen als runden_max am Gate-Knoten in pipeline.yaml
MAX_PARALLEL_GLOBAL = 24
# ── Sprache: EIN Parameter, alles referenziert ihn (Lektion 106) ──
SPRACHE = os.getenv("SPRACHE", "de")
SPRACHEN_SUCHE = tuple(os.getenv("SPRACHEN_SUCHE", "de,en").split(","))
STEMMER_NAME = {"de": "german", "en": "english", "fr": "french",
"es": "spanish", "it": "italian"} # sonst: kein Stemming
NEGATIONEN = {"de": {"nicht", "kein", "keine", "keinen", "keiner", "ohne",
"nie", "niemals"},
"en": {"not", "no", "none", "never", "without"}}
NEGATIONS_PRAEFIX = {"de": "nicht", "en": "non"}
SUCHE_LOCALE = {"de": {"country": "DE", "search_lang": "de", "region": "de-de"},
"en": {"country": "US", "search_lang": "en", "region": "us-en"}}
# Ausgabe-Literale, die im Guide landen — eine Stelle, pro Sprache tauschbar
TEXTE = {
"sammel_titel": "Grundlagen",
"sammel_punkt": "Weitere Grundlagen zum Thema",
"sammel_ziel": "Der Lernende kann Grundlagen zu '{punkt}' erklären.",
"teil": "{titel} (Teil {n})",
"abschnitt_verweis": "- {titel} (im Abschnitt '{abschnitt}')",
}
def user_agent() -> str:
mail = os.getenv("KONTAKT_MAIL", "")
return f"creator3/0.1 (+mailto:{mail})" if mail else "creator3/0.1"