151 lines
6.1 KiB
Python
151 lines
6.1 KiB
Python
"""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"
|