190 lines
9.8 KiB
Python
190 lines
9.8 KiB
Python
"""Zentrale Konfiguration. Tuning-Konstanten leben hier, QA-Messlatten in qa.py
|
|
(die Messlatte darf nie Teil eines Suchraums werden)."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
TEMPLATES_DIR = PROJECT_ROOT / "templates"
|
|
STORAGE_DIR = PROJECT_ROOT / "storage"
|
|
TOPICS_DIR = PROJECT_ROOT / "topics" # Uni-Quelldateien: topics/<name>/*.txt|md|pdf
|
|
KORPUS_DIR = STORAGE_DIR / "korpus" # Snapshots: korpus/<topic>/q<id>.md
|
|
FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"
|
|
|
|
|
|
def _load_env(path: Path) -> None:
|
|
"""Mini-.env-Loader. Die DATEI gewinnt über geerbtes Env — ein --reload-Master
|
|
behält sonst sein Start-Environment und pinnt stille Altwerte über .env-Edits."""
|
|
try:
|
|
text = path.read_text(encoding="utf-8")
|
|
except OSError:
|
|
return
|
|
for line in text.splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, value = line.partition("=")
|
|
key, value = key.strip(), value.strip().strip('"').strip("'")
|
|
if key:
|
|
os.environ[key] = value
|
|
|
|
|
|
_load_env(PROJECT_ROOT / ".env")
|
|
|
|
# NACH _load_env lesen — sonst ist ein in .env gesetztes CREATOR_DB wirkungslos
|
|
# (der Loader-Kommentar verspricht „die Datei gewinnt").
|
|
DB_PATH = STORAGE_DIR / os.getenv("CREATOR_DB", "creator2.db") # Override für Smoke-Läufe
|
|
|
|
# ── Topic-Namen ───────────────────────────────────────────────────────────────
|
|
import re as _re
|
|
|
|
# Erstes Zeichen alphanumerisch (kein führendes „-" = rsync/CLI-Option), danach
|
|
# nur Wortzeichen/Umlaute/- /_. Kein „.", kein „/" → sicher als Pfadkomponente
|
|
# (rmtree, Snapshot-Ordner) UND als Shell-Argument (Transfer-Makefile).
|
|
TOPIC_NAME_RE = _re.compile(r"^[A-Za-z0-9äöüÄÖÜß][\wäöüÄÖÜß-]{0,63}$")
|
|
|
|
|
|
def topic_name_ok(name: str) -> bool:
|
|
return bool(name) and TOPIC_NAME_RE.match(name) is not None
|
|
|
|
|
|
# ── Nebenläufigkeit ───────────────────────────────────────────────────────────
|
|
MAX_CONCURRENT_AGENTS = int(os.getenv("MAX_CONCURRENT_AGENTS", "16")) # CLI-Prozesse (~310 MB RSS)
|
|
MAX_CONCURRENT_API_AGENTS = int(os.getenv("MAX_CONCURRENT_API_AGENTS", "28")) # direkte API, ~0 RAM
|
|
RAM_MIN_FREE_PCT = int(os.getenv("RAM_MIN_FREE_PCT", "20")) # unter x % frei warten CLI-Spawns
|
|
OPENCODE_START_DELAY = float(os.getenv("OPENCODE_START_DELAY", "0.5")) # Session-DB-Kollisionen
|
|
|
|
# ── Infra-Fehler: nie fail-open ───────────────────────────────────────────────
|
|
INFRA_MAX_RETRIES = 3 # 429/Timeout/Netz: Retries pro Call, dann Lauf-PAUSE
|
|
INFRA_BACKOFF_BASE = 8.0 # Pause = base · 2^(n-1) → 8/16/32 s
|
|
HEDGE_NACH_S = 90 # Slot ohne Ergebnis nach max(dies, timeout/2) → Zwilling; 0 = aus
|
|
|
|
# ── Budget ────────────────────────────────────────────────────────────────────
|
|
RUN_BUDGET_TOKENS = int(os.getenv("RUN_BUDGET_TOKENS", "0")) # in+out pro Lauf; 0 = aus
|
|
|
|
# ── Ebene 0: Korpus & Soll ────────────────────────────────────────────────────
|
|
RECHERCHE_LENSES = ("curriculum", "lehrbuch", "syllabus", "uebungen") # 1 Agent je Lens/Runde
|
|
RECHERCHE_RUNDEN_MAX = 3 # Sättigungsloop-Cap (Themen)
|
|
SOLL_MIN_BELEGE = 2 # Punkt bestätigt ab so vielen unabhängigen Quellen
|
|
SOLL_CHUNK_CHARS = 40_000 # Quelltext pro Soll-Extraktions-Call
|
|
# Themenband der Soll-Punkte: k = c·√(Kandidaten), weiches Band [0.7k, 1.3k] (Lektion 80).
|
|
# Gemessen (aak): ohne Band faltete der Konsens 254 Atome auf 7 Punkte → Kapitel ohne
|
|
# Leitstruktur, Grundlagen mischten sich in Spezialthemen.
|
|
SOLL_PUNKTE_PER_SQRT = 1.0
|
|
SOLL_PUNKTE_MIN = 5
|
|
|
|
# ── Ebene 1: Inventar ─────────────────────────────────────────────────────────
|
|
ABSCHNITT_CHARS = 12_000 # lost-in-the-middle-Guard
|
|
READER_JE_ABSCHNITT = 2 # unabhängige Reader (Konsens entsteht über Dedup, nicht Union)
|
|
ANKER_OVERLAP_MERGE = 0.5 # Span-Überlappung ab der zwei Atome automatisch mergen
|
|
# Fuzzy-Anker-Stufe 4 (Lektion 83, Hypothes.is-Muster exakt→locker→fuzzy):
|
|
FUZZY_MIN_ZEICHEN = 40 # kürzere Zitate nie fuzzy (alnum-gefaltet nicht mehr eindeutig)
|
|
FUZZY_FEHLERQUOTE = 0.08 # max. Edit-Distanz als Anteil der Zitatlänge
|
|
MERGE_PANEL = 2 # Judges pro Merge-Kandidat — Merge nur einstimmig
|
|
MERGE_KANDIDAT_COS = 0.75 # Definitions-Ähnlichkeit ab der ein Paar zum Panel geht
|
|
MERGE_KANDIDAT_JACCARD = 0.3 # Schwelle im lexikalischen Fallback (misst viel niedriger als Kosinus)
|
|
LUECKEN_RUNDEN_MAX = 2 # gezielte Nachextraktion pro Soll-Lücke
|
|
# braucht-Titel, die norm/kern nicht auflösen (Kompositum↔Expansion misst ~0.53 Cosinus,
|
|
# Embedding schlägt sie NIE vor — Lektion 28): Substring-Prefilter → 1 Judge (n=1, reversibel).
|
|
BRAUCHT_KANDIDATEN = 8 # Kandidaten-Cap je unaufgelöstem braucht-Titel (Lektion 34)
|
|
|
|
# ── Ebene 2: Artefakte ────────────────────────────────────────────────────────
|
|
BEISPIEL_FORMEN = ("text", "mathe", "code", "tabelle") # Worked-Example-Formen
|
|
CODE_SPRACHEN = ("python", "javascript") # Syntax-Gate parse-only (Container: python3, node)
|
|
ARTEFAKT_CHUNK_ATOME = 8 # Atome pro Generate-Call (ein Soll-Punkt, gestückelt)
|
|
VERIFY_PANEL = 2 # unabhängige Prüfer, einstimmig = verifiziert
|
|
|
|
# ── Ebene 3: Struktur ─────────────────────────────────────────────────────────
|
|
LEVEL_RANG = {"E": 0, "M": 1, "S": 2} # Durchgänge: Grundgerüst / Standard / Feinheiten
|
|
DURCHGANG = { # Leser-Kontext je Durchgang (Templates: {durchgang})
|
|
"E": "Grundgerüst, erster Durchgang — der Leser kennt noch nichts,"
|
|
" alles voraussetzungsfrei erklären",
|
|
"M": "Standardstoff, zweiter Durchgang — der Leser kennt das Grundgerüst",
|
|
"S": "Feinheiten, dritter Durchgang — der Leser kennt Grundgerüst und Standardstoff",
|
|
}
|
|
BAUSTEIN_MIN_ATOME = 4
|
|
BAUSTEIN_MAX_ATOME = 8
|
|
ZIELE_CHUNK_ATOME = 30 # Atome pro Lernziel-Call
|
|
KAPITEL_BAUSTEINE = 5 # Richtwert Bausteine je Kapitel (Kapitel-Schnitt-Judge)
|
|
|
|
# ── Ebene 4: Guide ────────────────────────────────────────────────────────────
|
|
SECTION_WOERTER_PRO_ATOM = (40, 220) # Längenband ausführlicher Teil je Atom
|
|
|
|
# ── Auto-Loop ─────────────────────────────────────────────────────────────────
|
|
LOOP_MAX_ITER = 10
|
|
FIX_MAX_VERSUCHE = 3 # Section einfrieren nach so vielen Fixes OHNE Textänderung
|
|
|
|
# ── Timeouts je Schritt: (Basis-Sekunden, Sekunden pro Item) ─────────────────
|
|
TIMEOUTS = {
|
|
"recherche": (900, 0),
|
|
"soll": (300, 0),
|
|
"soll_konsens": (300, 2),
|
|
"extraktion": (450, 0),
|
|
"merge": (150, 8),
|
|
"braucht": (200, 0),
|
|
"soll_zuordnung": (300, 8),
|
|
"luecke": (450, 0),
|
|
"artefakt": (450, 20),
|
|
"verify": (200, 10),
|
|
"fix": (300, 15),
|
|
"ziele": (300, 6),
|
|
"level": (200, 4),
|
|
"kapitel": (200, 2),
|
|
"zyklus": (150, 0),
|
|
"writer": (450, 60),
|
|
"pruefer": (600, 5),
|
|
"qa_judge": (600, 0),
|
|
}
|
|
|
|
|
|
def timeout_fuer(step: str, n: int = 0) -> int:
|
|
base, per = TIMEOUTS[step]
|
|
return int(base + per * n)
|
|
|
|
|
|
# ── Provider-Stacks (unabhängig; jeder kann jederzeit fehlen) ─────────────────
|
|
# Rollen: quick = Massenarbeit (Extraktion), judge = kalte Urteile,
|
|
# guide = große Generierung (Writer), fast = Interaktion/Leichtes.
|
|
DEFAULT_PROVIDER = os.getenv("DEFAULT_PROVIDER", "")
|
|
PROVIDERS = {
|
|
"claude": {
|
|
"cli": "claude",
|
|
"guide": "claude-opus-4-8[1m]",
|
|
"fast": "claude-sonnet-4-6",
|
|
"judge": "claude-sonnet-4-6",
|
|
"quick": "claude-sonnet-4-6",
|
|
"env_key": None,
|
|
},
|
|
"minimax": {
|
|
"cli": "opencode",
|
|
"guide": "minimax/MiniMax-M3",
|
|
"fast": "minimax-kalt/MiniMax-M2.7-highspeed",
|
|
# native Route für Judges: der kalt-Endpoint stallte 20 % der Judge-Calls (Vorgänger, 2026-07-04)
|
|
"judge": "minimax/MiniMax-M3",
|
|
"quick": "minimax-kalt/MiniMax-M2.7-highspeed",
|
|
"env_key": "MINIMAX_API_KEY",
|
|
},
|
|
"lokal": {
|
|
"cli": "opencode",
|
|
"guide": "ollama/qwen3.6:27b",
|
|
"fast": "ollama/qwen3.5:9b",
|
|
"judge": "ollama/qwen3.5:9b",
|
|
"quick": "ollama/qwen3.5:9b",
|
|
"env_key": None,
|
|
"check_url": "http://localhost:11434/api/tags",
|
|
},
|
|
}
|
|
|
|
ROLE_ROUTING = {r: os.getenv(f"ROLE_{r.upper()}", "") for r in ("quick", "judge", "guide", "fast")}
|
|
|
|
|
|
def resolve_role(run_provider: str, role: str) -> tuple[str, str]:
|
|
"""→ (provider, model). Reines Routing; Verfügbarkeits-Fallback macht agents.run_agent."""
|
|
target = ROLE_ROUTING.get(role, "") or run_provider
|
|
provider, _, model = target.partition(":")
|
|
if provider not in PROVIDERS:
|
|
provider, model = run_provider, ""
|
|
if not model:
|
|
model = PROVIDERS.get(provider, {}).get(role, "")
|
|
return provider, model
|