Files
creator/backend/config.py
2026-07-01 20:00:57 +00:00

172 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TEMPLATES_DIR = PROJECT_ROOT / "templates"
STORAGE_DIR = PROJECT_ROOT / "storage"
FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"
DB_PATH = STORAGE_DIR / "creator.db"
PROJECTS_DIR = PROJECT_ROOT / "projects"
UNI_DIR = PROJECT_ROOT / "uni"
MAX_CONCURRENT_GENERATIONS = 10
# Readability gate: deterministic checker (small German complexity model,
# scale 17). Sections that are too hard go into the read-exam revision.
# If transformers/torch or the model are missing → gate silently off.
READABILITY_ACTIVE = True
READABILITY_MODEL = "MiriUll/distilbert-german-text-complexity"
# Anchors on the 17 scale (TextComplexityDE): plain language ~1.2; Wikipedia average
# ~3.22; from MOS > 4 a sentence counts as "truly complex" (the paper's simplification cutoff).
READABILITY_MAX = 3.5 # section too hard when the sentence average is above this
READABILITY_HARD = 4.0 # an individual sentence is "hard" from here on
READABILITY_HARD_SHARE = 0.30 # … OR when this share of sentences is hard
# Block consolidation: semantic embedding clustering instead of an LLM list merge.
# A small multilingual sentence embedding (mean-pool) builds the candidate clusters
# GLOBALLY (no chunk loss) via cosine + union-find. Title variants of the same concept
# ("Vertex Cover" / "Vertex Cover Definition") merge; the consensus then counts the
# real readers per cluster (≥2 = consensus). If transformers/torch are missing or the model
# won't load → embedding silently off, `_consolidate` falls back to the old panel-judge path.
EMBEDDING_AKTIV = True
EMBEDDING_MODELL = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" # CPU, multilingual, ~470 MB
# Stronger (larger) CPU alternative if needed: "BAAI/bge-m3".
# Consolidation = two-stage: (1) the embedding builds COARSE similarity blocks (high recall),
# (2) an LLM judge groups EACH block into the real blocks (merge paraphrases, split
# over-merges). Pure threshold blocking creates a giant component (everything chained) →
# hence "capped blocking": greedily merge by cosine, but cap the block size. This keeps the
# LLM lists short and stable (evidenced: embedding block + LLM judge ≈ 95% precision).
EMBEDDING_BLOCK_FLOOR = 0.5 # minimum cosine for two candidates to share ONE block
EMBEDDING_BLOCK_CAP = 25 # max. titles per block (keep the LLM list short/stable)
# Subblock dedup: purely deterministic (no LLM). Subblocks are short statements IN THE SAME
# block context — from this cosine on two are the same statement (checked on aak: ≥0.88 are
# without exception true duplicates). Conservative 0.90 so different aspects (∈NP ≠ NP-hard) stay separate.
EMBEDDING_SUB_DUP = 0.90
# Umbrella grouping (block granularity level 2, step "Blocks-Gruppierung", AFTER the filter):
# collapse sibling DEFINITIONS that are components of ONE umbrella concept (TM model:
# Konfiguration/Start-/Folge-/Stopkonfiguration/Berechnung/Alphabet/δ; KNF: Literale/Klauseln/
# Variablen) into ONE block whose description ENUMERATES the children — so the later subblock step
# re-derives them from the source under the umbrella scope (demotion is re-derivation, not transfer).
# If the flag is off or no embedding model → step is silently skipped (like Dedup).
BLOCKS_GRUPPIERUNG_AKTIV = True
# Lower floor than consolidation (0.5, paraphrase-tuned) for higher sibling recall; the LLM judge is
# the precision gate. Smaller cap since a lower floor pulls in more nodes → keep the judge lists short.
EMBEDDING_SIBLING_FLOOR = 0.35 # heterogeneous facets of one model co-cluster weakly → low floor (recall)
EMBEDDING_SIBLING_CAP = 18 # a rich model (TM) can have many constituent parts
# Reconcile pass: two independently-judged clusters can emit the SAME parent concept under different
# titles (e.g. two „Turingmaschine"-umbrellas). Merge umbrella pairs whose title+description cosine is
# ≥ this (conservative → only true same-parent duplicates, never two distinct umbrellas).
GROUP_RECONCILE_FLOOR = 0.75
# Over-merge backstop ONLY (no-structure floor). Research (meronymy ≠ similarity): parts of ONE model are
# legitimately DISSIMILAR (TM: Alphabet/Konfiguration/δ ~0.22), while distinct same-type concepts (P/NP/…)
# are SIMILAR (~0.85) — so member-vs-member cosine is the WRONG instrument for over-merge (empirically
# inverted: TM 0.218 < the P/NP bundle 0.227). The real precision floor is the ATOMICITY type-guard
# (_GROUP_STANDALONE: a member that is a named algorithm/problem/theorem/complexity-class dissolves the
# umbrella). This floor is demoted to a near-zero backstop that only rejects a literally structureless
# chain (random-pair baseline), set BELOW the legitimate heterogeneous minimum so it never kills a real model.
GROUP_MIN_COS_FLOOR = 0.15
# Cap for concurrent CLI agent processes (across all generations).
# Own lane for interactive calls (chat, elements) so they don't hang behind
# running writers in the queue.
MAX_CONCURRENT_AGENTS = 10
MAX_CONCURRENT_INTERACTIVE = 8
# Grace window of the consensus races (blocks, guide, OnePager): after the first
# valid result the remaining agents may still become done for this many seconds
# (kill only once the minimum is already in).
CONSENSUS_GRACE = 300
# Research race: longer grace window. Research drives the whole block count;
# with slow providers (e.g. MiniMax) ALL 5 agents should become done, not just
# the quorum of 3. The per-agent timeout (TIMEOUTS["research"]=1800s) caps real hangs.
RESEARCH_GRACE = 900
# Cap of the clarification and check loops: maximum rounds until everything must be
# decided. In the last round the mapping agent MUST decide every entry;
# check loops leave any remaining objections standing after that.
CONSENSUS_MAX_ROUNDS = 3
# Crawler triage (content/noise) — deterministic rule filter instead of an LLM.
# Match: substring (lowercase) against URL AND file name. Order: keep > noise > min_chars > keep.
# Just add special rules here.
CRAWL_KEEP_PATTERNS = ["learn-unit", "learn-course"] # always content
CRAWL_NOISE_PATTERNS = [ # clearly off-topic → out
"clubs", "events", "podcasts", "resources", "-u-",
"academy", "pricing", "/plans", "career", "newsletter", "impressum", "login",
]
CRAWL_MIN_CHARS = 400 # too little text → out
# LLM topic relevance gate (after the rule filter): per content page yes/no against the spec.
# Separates the subject area (e.g. backend vs frontend), which the global CRAWL_* rules can't.
QUELLE_RELEVANZ_CHUNK = 12 # pages per rater package (small, since a snippet ships per page)
QUELLE_RELEVANZ_SNIPPET = 800 # body characters per page in the prompt (URL is the primary signal)
# Timeouts per agent step: (base seconds, seconds per block/section).
# Applies equally to all providers — whoever is too slow gets restarted or overtaken.
TIMEOUTS = {
"research": (1800, 0), # fixed 30 min
"research_mapping": (600, 3), # n = pre-merged entries
"selection_mapping": (600, 2), # n = remaining entries (block inventory)
"ergaenzung": (900, 0), # subject-field extension for projects (web research)
"plan": (300, 5),
"plan_judge": (600, 5), # judge reads up to 5 outlines, n = sections
"content": (600, 90), # identify content per block in the chunk (web search)
"content_check": (300, 10), # content exam per block in the package
"subblock": (900, 45), # find subblocks per block in the chunk (web search)
"subblock_check": (300, 15), # judge decides contested subblocks in the chunk
"level": (300, 10), # classify subblocks per chunk
"level_check": (300, 10), # judge decides contested levels in the chunk
"relevance": (300, 10), # subblocks relevant/peripheral per chunk
"relevance_check": (300, 10), # judge decides contested relevance in the chunk
"question_pattern": (300, 15), # question patterns per block (subblocks × types)
"question_pattern_check": (300, 10), # critic cleans up the pattern table per block
"writer": (600, 120), # per section in the chunk
"lese_check": (300, 10), # per section in the package
}
# Purpose per format — flows into the outline judge (what the guide should achieve).
# German strings: these are inserted verbatim into the judge prompt → kept German on purpose.
FORMAT_PURPOSE = {
"Guide": "einen fokussierten Guide — alles Relevante ohne Randthemen",
"FullGuide": "einen Komplett-Guide — das ganze Thema inkl. Randthemen",
"Rest": "einen Ergänzungs-Guide — nur die Randthemen",
}
# Provider stacks: completely independent, any one can be removed at any time.
# Roles: "quick" = bulk work (research, classification),
# "fast" = interaction + voting (chat, exam, clarification, elements),
# "judge" = mapping/judge/check agents — cold (low temperature,
# no thinking) for stable verdicts; Claude/local map to "fast",
# "guide" = large generation (proposals, writer).
DEFAULT_PROVIDER = "claude"
PROVIDERS = {
"claude": {
"cli": "claude",
"guide": "claude-opus-4-8[1m]",
"fast": "claude-sonnet-4-6",
"judge": "claude-sonnet-4-6", # the CLI has no temperature setting
"quick": "claude-sonnet-4-6",
"env_key": None, # auth via CLAUDE_CODE_OAUTH_TOKEN or ~/.claude
},
# "minimax-kalt/…" is NOT its own stack, just an opencode provider entry
# (dev-ops/opencode.json) with low temperature; M3 there without thinking.
"minimax": {
"cli": "opencode",
"guide": "minimax/MiniMax-M3",
"fast": "minimax-kalt/MiniMax-M2.7-highspeed",
"judge": "minimax-kalt/MiniMax-M3",
"quick": "minimax/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", # Ollama reachable?
},
}