"""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 / MiniMax (R3/R9) — alles M3; M2.x kann Thinking nicht abschalten ── LLM_ENDPOINT = "https://api.minimax.io/anthropic/v1/messages" ROLLEN = { # thinking IMMER explizit ("enabled" gibt 400; Default widersprüchlich dokumentiert) "extraktion": dict(model="MiniMax-M3", thinking="disabled", temperature=0.2), "judge": dict(model="MiniMax-M3", thinking="disabled", temperature=0.1), "writer": dict(model="MiniMax-M3", thinking="adaptive", temperature=0.7), } 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 je Knoten: (basis_s, pro_item_s) — creator2-Lektion 14: nie fix; # pro_item skaliert mit der Bündel-Größe (n in llm.call) TIMEOUTS = { "recherche_plan": (60, 20), "soll_extraktion": (180, 60), "soll_konsens": (240, 2), "atom_extraktion": (240, 60), "anker_fix": (180, 4), "dedup": (120, 8), "soll_zuordnung": (120, 3), "luecke": (180, 45), "lernziele": (120, 3), "bausteine": (120, 3), "ordnung": (180, 3), "writer": (420, 0), "kapitel_intro": (90, 0), "llm_pruefung": (240, 60), "fix": (300, 0), "redundanz": (120, 8), "klaerung": (180, 0), } 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 # ── Korpus ── RECHERCHE_LENSES = ("ueberblick", "tutorial", "referenz", "praxis") # generisch! RECHERCHE_RUNDEN_MAX = int(os.getenv("RECHERCHE_RUNDEN_MAX", "4")) QUERIES_JE_LENS = 3 SOLL_CHUNK_CHARS = 40_000 SOLL_MIN_BELEGE = 2 # ≥2 unabhängige Quellen je bestätigtem Punkt 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) # ── 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 SOLL_MIN_BELEGE_WIKI = 1 # kuratierte Quelle (Wikipedia) = 1 Beleg genügt # ── 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_MAX = {"gate_korpus": 4, "gate_inventar": 8, "gate_struktur": 3, "gate_guide": 12} # gate_inventar 6: Anker-Fix, 2× Dedup/Lücke, Web-Nachrecherche, Nachzügler-Dedup # gate_guide 8: Echtlauf konvergierte 45→28→10 — der Stillstand-Fingerprint # fängt echte Schleifen, das Runden-Limit ist nur die Notbremse dahinter MAX_PARALLEL_GLOBAL = 24 def timeout_fuer(knoten: str, n: int = 0) -> float: basis, pro = TIMEOUTS.get(knoten, TIMEOUT_DEFAULT) return basis + pro * n def user_agent() -> str: mail = os.getenv("KONTAKT_MAIL", "") return f"creator3/0.1 (+mailto:{mail})" if mail else "creator3/0.1"