This commit is contained in:
team3
2026-07-10 15:43:11 +02:00
commit 0a41166cca
72 changed files with 7772 additions and 0 deletions

158
backend/config.py Normal file
View File

@@ -0,0 +1,158 @@
"""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"
DB_PATH = STORAGE_DIR / os.getenv("CREATOR_DB", "creator2.db") # Override für Smoke-Läufe
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")
# ── 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
# ── Ebene 2: Artefakte ────────────────────────────────────────────────────────
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 ─────────────────────────────────────────────────────────
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
# ── 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),
"soll_zuordnung": (300, 8),
"luecke": (450, 0),
"artefakt": (450, 20),
"verify": (200, 10),
"fix": (300, 15),
"ziele": (300, 6),
"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