168 lines
7.2 KiB
Python
168 lines
7.2 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 (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"
|