This commit is contained in:
Team3
2026-07-05 10:38:24 +02:00
parent db58d52567
commit 9b6bfa45c6
9 changed files with 56 additions and 22 deletions

View File

@@ -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.

View File

@@ -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 (01 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"])

View File

@@ -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"]

View File

@@ -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",

View File

@@ -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)

View File

@@ -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):

View File

@@ -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}

View File

@@ -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)

View File

@@ -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, "",