init
This commit is contained in:
158
backend/config.py
Normal file
158
backend/config.py
Normal 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
|
||||
Reference in New Issue
Block a user