diff --git a/.env.example b/.env.example index ccbbc4b..9d88157 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# Standard-Provider für alle Agenten (Pflicht — es gibt keinen Code-Default) +DEFAULT_PROVIDER=minimax + # Datei nach .env kopieren (wird nicht committet). # Claude-Provider: lokal einmal 'claude setup-token' ausführen, Token eintragen. diff --git a/backend/block_calls.py b/backend/block_calls.py index 2339595..5c3abb4 100644 --- a/backend/block_calls.py +++ b/backend/block_calls.py @@ -220,7 +220,7 @@ def _fk_of(e: dict) -> dict: async def _generate_block(ctx: GenContext, files: dict, title: str, description: str, instructions: str = "", ns: str = "", lbl: str = "", sources: list[str] | None = None, - seeds: list[str] | None = None) -> dict | None: + seeds: list[str] | None = None, melde=None) -> dict | None: """GEN_PANEL unabhängige Generatoren liefern je Subs+Facts+Level/Relevanz in EINEM Call; Konsens im Code (Variant-Cluster über beide Ausgaben, ≥2 unabhängige Generatoren = consensus). Einzelnennungen und ungedeckte Seeds werden „unsicher" — der Prüfer @@ -237,6 +237,8 @@ async def _generate_block(ctx: GenContext, files: dict, title: str, description: "the material; if backed AND not already covered by another entry, include " "them — rephrased as a standalone statement):\n" + "\n".join(f"- {s}" for s in dict.fromkeys(seeds) if s) + "\n") + if melde: + melde("Generate") h = _h8(title, description, "gen") paths = [work_dir / f"gen-{h}-g{g}.json" for g in range(1, GEN_PANEL + 1)] prompt = _prompt("Subblock-Generate", topic=topic, @@ -335,7 +337,7 @@ def _default_vote(stimmen: list[str], default: str) -> str: async def _verify_block(ctx: GenContext, files: dict, title: str, gen: dict, q: dict, instructions: str = "", ns: str = "", lbl: str = "", - sources: list[str] | None = None) -> dict | None: + sources: list[str] | None = None, melde=None) -> dict | None: """VERIFY_PANEL unabhängige Prüfer auditieren den Block in EINEM Call (MECE-Faltung, Fremd, Lücken, Unsicher-Übernahme, Facts-Korrektheit, Level/Relevanz). Auswertung mit Schnittmengen-Semantik pro Befundklasse (Faltung/Fremd/Übernahme einstimmig, Discard @@ -356,6 +358,8 @@ async def _verify_block(ctx: GenContext, files: dict, title: str, gen: dict, q: (u for u in unsicher if u["title"] == t), {}) return (fk.get("key_points") or [])[:3] + if melde: + melde("Verify") verdicts: list[dict] = [] if n: zeilen = "\n".join(f"{k}. {t}" + "".join(f"\n - {p}" for p in _kp(t)) @@ -524,6 +528,8 @@ async def _verify_block(ctx: GenContext, files: dict, title: str, gen: dict, q: # 8. Fix-Tail (0–1 Call): Korrekturen + belegte Lücken if (korrekturen or luecken) and not ctx.is_cancelled(): + if melde: + melde("Fix") neu = await _fix_befunde(ctx, files, title, korrekturen, luecken, [s["title"] for s in sidecar_subs], instructions, ns, lbl, sources) for e in neu or []: @@ -603,7 +609,7 @@ def _subs_text(title: str, sidecar_subs: list[dict]) -> str: async def _artefakte_block(ctx: GenContext, files: dict, title: str, sidecar_subs: list[dict], instructions: str = "", - ns: str = "", lbl: str = "") -> dict | None: + ns: str = "", lbl: str = "", melde=None) -> dict | None: """EIN Generator-Call liefert Fragen + Flashcards + Beispiele (Split in 2 parallele Calls bei > ART_SPLIT_SUBS Subs), EIN Prüfer-Call verifiziert Beispiele, bereinigt die Fragen und ergänzt fehlende. → {pattern, artefacts} | None (nur Cancel).""" @@ -611,6 +617,8 @@ async def _artefakte_block(ctx: GenContext, files: dict, title: str, work_dir = files["arbeit"] if not sidecar_subs: return {"pattern": {title: []}, "artefacts": {"flashcard": [], "example": []}} + if melde: + melde("Artefakte gen") sh = _subs_hash({title: sidecar_subs}) haelften = ([sidecar_subs] if len(sidecar_subs) <= ART_SPLIT_SUBS else [sidecar_subs[:len(sidecar_subs) // 2], sidecar_subs[len(sidecar_subs) // 2:]]) @@ -647,6 +655,8 @@ async def _artefakte_block(ctx: GenContext, files: dict, title: str, fehlend = [t for t in sub_titles if _norm_title(t) not in {_norm_title(p["subblock"]) for p in pattern}] if pattern or examples: + if melde: + melde("Artefakte check") tabelle = "\n".join(f"({p['subblock']}) {p['question']}" for p in pattern) or "(keine)" beisp = "\n\n".join( f"{k}. PROBLEM: {e['problem']}\n SCHRITTE: " + " | ".join(e["steps"]) diff --git a/backend/board_artefacts.py b/backend/board_artefacts.py index 2b9d495..1c3d053 100644 --- a/backend/board_artefacts.py +++ b/backend/board_artefacts.py @@ -167,6 +167,19 @@ async def _seed_map(topic: str) -> dict[str, list[str]]: return seeds +def _melder(flow: Flow, norm: str): + """Live-Stepper der Karte: Phasenname → card_info (ging bei der Call-Verschmelzung + verloren — Karten liefen ohne Badge/Stepper durch das Board).""" + set_p = _card_set_p(flow, norm) + + def melde(schritt: str) -> None: + try: + set_p(f"{schritt}…", step=blocks._step_idx(flow.topic, schritt)) + except ValueError: + set_p(f"{schritt}…") + return melde + + async def _proc_generate(ctx: GenContext, flow: Flow, files: dict, instructions: str, cards): """Verschmolzener Erzeuger: 2 unabhängige Generatoren liefern Subs+Facts+Einstufung in EINEM Call, Konsens im Code (block_calls._generate_block).""" @@ -181,7 +194,8 @@ async def _proc_generate(ctx: GenContext, flow: Flow, files: dict, instructions: p.get("description", ""), instructions, ns=f"{_safe(norm)}-", lbl=f"{p.get('title', norm)} · ", sources=p.get("sources"), - seeds=[s for s in seeds.get(norm, []) if s] or None) + seeds=[s for s in seeds.get(norm, []) if s] or None, + melde=_melder(flow, norm)) if gen is None: return _fail_or_cancel(ctx, f"Generate {p.get('title', norm)}") p["raw"], p["facts"] = gen["raw"], gen["facts"] @@ -207,7 +221,7 @@ async def _proc_verify(ctx: GenContext, flow: Flow, files: dict, q: dict, instru "unsicher": p.get("unsicher") or [], "votes": p.get("votes") or {}} res = await _verify_block(ctx, _pfiles(files, norm), title, gen, q, instructions, ns=f"{_safe(norm)}-", lbl=f"{title or norm} · ", - sources=p.get("sources")) + sources=p.get("sources"), melde=_melder(flow, norm)) if res is None: return None # nur Cancel — Karte bleibt liegen p["raw"], p["facts"], p["sidecar"] = res["raw"], res["facts"], res["sidecar"] @@ -230,7 +244,8 @@ async def _proc_artefakte(ctx: GenContext, flow: Flow, files: dict, instructions title = p.get("title", "") res = await _artefakte_block(ctx, _pfiles(files, norm), title, (p.get("sidecar") or {}).get(title) or [], - instructions, ns=f"{_safe(norm)}-", lbl=f"{title or norm} · ") + instructions, ns=f"{_safe(norm)}-", lbl=f"{title or norm} · ", + melde=_melder(flow, norm)) if res is None: return None # nur Cancel p["pattern"] = res["pattern"] diff --git a/backend/config.py b/backend/config.py index 935252c..1e5eb88 100644 --- a/backend/config.py +++ b/backend/config.py @@ -266,7 +266,10 @@ FORMAT_PURPOSE = { # "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" +# Kein Provider-Default im Code (Betreiber-Vorgabe): die .env entscheidet. +DEFAULT_PROVIDER = os.getenv("DEFAULT_PROVIDER", "") +if not DEFAULT_PROVIDER: + raise RuntimeError("DEFAULT_PROVIDER fehlt in der .env (z. B. DEFAULT_PROVIDER=minimax)") PROVIDERS = { "claude": { "cli": "claude", diff --git a/backend/fake_agents.py b/backend/fake_agents.py index 6458fbb..97a5954 100644 --- a/backend/fake_agents.py +++ b/backend/fake_agents.py @@ -290,7 +290,7 @@ def aktivieren(welt: Welt, setattr_fn=setattr) -> None: import qa import repair - async def fake_run_agent(agent_key, prompt, timeout, provider="claude", role="fast", + async def fake_run_agent(agent_key, prompt, timeout, provider="", role="fast", capabilities="none", lane="batch", scope=None, on_line=None, label=""): return welt.respond(agent_key, prompt, capabilities) diff --git a/backend/models.py b/backend/models.py index 115af74..0324a00 100644 --- a/backend/models.py +++ b/backend/models.py @@ -7,6 +7,8 @@ FormatType = Literal[ "Rest", ] +from config import DEFAULT_PROVIDER + ProviderType = Literal["claude", "minimax", "lokal"] SourceType = Literal["thema", "projekt", "uni", "link"] @@ -16,7 +18,7 @@ class GuideCreateRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) format: FormatType instructions: str = Field(default="", max_length=2000) - provider: ProviderType = "claude" + provider: ProviderType = DEFAULT_PROVIDER ab_step: int | None = Field(default=None, ge=0, le=5) # re-run from board stage (0 lernziele … 5 lesbarkeit); None = full/resume @@ -42,7 +44,7 @@ class RepairRequest(BaseModel): class BlocksCreateRequest(BaseModel): topic: str = Field(min_length=1, max_length=100) instructions: str = Field(default="", max_length=2000) - provider: ProviderType = "claude" + provider: ProviderType = DEFAULT_PROVIDER source_type: SourceType = "thema" source_location: str = Field(default="", max_length=2000) research: bool = True # False = Continue: drain the existing kanban queue, no new search @@ -157,7 +159,7 @@ class GuideChatRequest(BaseModel): section: str = Field(default="", max_length=20000) outline: str = Field(default="", max_length=8000) messages: list[ChatMessage] = Field(min_length=1) - provider: ProviderType = "claude" + provider: ProviderType = DEFAULT_PROVIDER class GuideChatResponse(BaseModel): @@ -172,7 +174,7 @@ class BlockChatRequest(BaseModel): section: str = Field(default="", max_length=20000) # detailed version section_compact: str = Field(default="", max_length=20000) # compact version (mnemonics) messages: list[ChatMessage] = Field(min_length=1) - provider: ProviderType = "claude" + provider: ProviderType = DEFAULT_PROVIDER class BlockChatResponse(BaseModel): @@ -203,7 +205,7 @@ class BlockExamRequest(BaseModel): # Base + cap are kept server-side (anchor / subs×25) — the client cap is only a hint. cap: int = Field(default=10, ge=1, le=10000) # score cap = unlocked subblocks × 25 messages: list[ChatMessage] = [] # dialog so far; empty = first question - provider: ProviderType = "claude" + provider: ProviderType = DEFAULT_PROVIDER thorough: bool = False # "thorough check": rating with a strong model (role guide) @@ -246,7 +248,7 @@ class BlockPruefenRequest(BaseModel): spot: str = "ausführlich" # "compact" | "ausführlich" (displayed field) snippet: str = Field(min_length=1, max_length=20000) # raw markdown block hint: str = Field(default="", max_length=2000) # optional addition (✏️) - provider: ProviderType = "claude" + provider: ProviderType = DEFAULT_PROVIDER class BlockPruefenResponse(BaseModel): @@ -258,7 +260,7 @@ class BlockUebernehmenRequest(BaseModel): spot: str = "ausführlich" alt: str = Field(min_length=1, max_length=20000) revised: str = Field(default="", max_length=20000) - provider: ProviderType = "claude" + provider: ProviderType = DEFAULT_PROVIDER class BlockUebernehmenResponse(BaseModel): diff --git a/backend/routes.py b/backend/routes.py index 9b13ca1..27eb939 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -217,7 +217,7 @@ async def run_repair_route(req: RepairRequest): @router.post("/blocks/research") -async def add_blocks_research(topic: str, provider: str = "claude"): +async def add_blocks_research(topic: str, provider: str = DEFAULT_PROVIDER): """Attach one more research agent — to the live flow, or attach-or-start.""" if add_research_agent(topic): return {"ok": True, "attached": True} diff --git a/backend/tests/test_board_inventory.py b/backend/tests/test_board_inventory.py index 34e4b21..236c79b 100644 --- a/backend/tests/test_board_inventory.py +++ b/backend/tests/test_board_inventory.py @@ -71,14 +71,14 @@ async def board_env(testdb, tmp_path, monkeypatch): monkeypatch.setattr(bi, "_emb_ok", no_emb) async def fake_generate(ctx, files, title, description, instructions="", ns="", lbl="", - sources=None, seeds=None): + sources=None, seeds=None, melde=None): subs = ["Sub Eins", "Sub Zwei"] return {"raw": {title: list(subs)}, "facts": {title: {_norm_title(s): {"key_points": [f"Fakt zu {s}"], "cited_facts": []} for s in subs}}, "unsicher": [], "votes": {}} - async def fake_verify(ctx, files, title, gen, q, instructions="", ns="", lbl="", sources=None): + async def fake_verify(ctx, files, title, gen, q, instructions="", ns="", lbl="", sources=None, melde=None): subs = gen["raw"].get(title) or [] bfacts = gen["facts"].get(title) or {} sidecar = [{"title": s, "level": "beginner", @@ -87,7 +87,7 @@ async def board_env(testdb, tmp_path, monkeypatch): for i, s in enumerate(subs)] return {"raw": {title: list(subs)}, "facts": {title: bfacts}, "sidecar": {title: sidecar}} - async def fake_artefakte(ctx, files, title, sidecar_subs, instructions="", ns="", lbl=""): + async def fake_artefakte(ctx, files, title, sidecar_subs, instructions="", ns="", lbl="", melde=None): if not sidecar_subs: return {"pattern": {title: []}, "artefacts": {"flashcard": [], "example": []}} first = sidecar_subs[0]["title"] @@ -242,7 +242,7 @@ async def test_empty_subblocks_completes_without_deadletter(board_env, monkeypat db, ctx, files = board_env async def empty_gen(ctx, files, title, description, instructions="", ns="", lbl="", - sources=None, seeds=None): + sources=None, seeds=None, melde=None): return {"raw": {title: []}, "facts": {title: {}}, "unsicher": [], "votes": {}} monkeypatch.setattr(ba, "_generate_block", empty_gen) await db.kanban_upsert_card(TOPIC, "artefacts", "leer", "ablock", "generate", @@ -719,7 +719,7 @@ async def test_outline_runs_before_artefacts_finish(board_env, monkeypatch): base_verify = ba._verify_block snapshot = {} - async def slow_verify(ctx, files, title, gen, q, instructions="", ns="", lbl="", sources=None): + async def slow_verify(ctx, files, title, gen, q, instructions="", ns="", lbl="", sources=None, melde=None): await asyncio.sleep(0.8) # keeps one card in `verify` while the outline fires return await base_verify(ctx, files, title, gen, q, instructions, ns=ns, lbl=lbl, sources=sources) diff --git a/backend/train_f0.py b/backend/train_f0.py index 60011ed..a44b69b 100644 --- a/backend/train_f0.py +++ b/backend/train_f0.py @@ -16,6 +16,7 @@ from pathlib import Path # WICHTIG: config (mit CREATOR_PARAMS) lädt vor allen Pipeline-Modulen import database +from config import DEFAULT_PROVIDER from fake_agents import Welt, aktivieren from fsutil import atomic_write_json @@ -40,7 +41,7 @@ async def f0(out: str) -> None: "artefakte": tmp / "artefakte.json", "outline": tmp / "outline.json", "outline_slots": [tmp / f"outline-{i}.json" for i in (1, 2, 3)], "research": [work / f"research-{i}.md" for i in (1, 2, 3, 4, 5)]} - ctx = GenContext(topic="f0", provider="claude", is_cancelled=lambda: False) + ctx = GenContext(topic="f0", provider=DEFAULT_PROVIDER, is_cancelled=lambda: False) start = time.monotonic() ok = await asyncio.wait_for( bi.run_boards(ctx, lambda *a, **k: None, files, {"type": "thema"}, None, "",