This commit is contained in:
team3
2026-07-04 02:32:31 +02:00
parent 91b0d00aa1
commit c4caf31ed0
38 changed files with 3849 additions and 118 deletions

View File

@@ -20,6 +20,7 @@ import re
import database as db
import readability
from blocks import _sink_json
from config import FORMAT_PURPOSE, READABILITY_ACTIVE, TEMPLATES_DIR, MAX_CONCURRENT_AGENTS_PER_TOPIC
from fsutil import atomic_write_json
from jsonio import read_json_file as _json_file
@@ -195,22 +196,34 @@ async def _set(env: _Env, card: dict, **fields):
async def _stage_lernziele(env: _Env, card: dict) -> bool:
norm = card["block_norm"]
if not await db.list_lernziele(env.topic, norm):
path = env.slot(f"ziele-{_safe(norm)}.json")
subs = "\n".join(f"- [{s.get('level', 'beginner')}] {s['title']}"
for s in env.subs_by_title.get(card["block"], [])) or "(keine)"
status, ziele = await run_single_slot(
env.ctx, f"Lernziele {card['block']}", key=f"{env.guide_id}-ziele-{_safe(norm)}",
prompt=_prompt("Guide-Lernziele", topic=env.topic, block=card["block"],
subs=subs, facts=_card_facts(env, card["block"]),
out_path=path, extra=_extra(env.instructions)),
role="judge", capabilities="files",
payload=lambda result: _ziele_schema(_json_file(path)),
timeout=_timeout("lernziele", len(env.subs_by_title.get(card["block"], []))))
async def _versuch(suffix: str):
path = env.slot(f"ziele-{_safe(norm)}{suffix}.json")
return await run_single_slot(
env.ctx, f"Lernziele {card['block']}", key=f"{env.guide_id}-ziele-{_safe(norm)}{suffix}",
prompt=_prompt("Guide-Lernziele", topic=env.topic, block=card["block"],
subs=subs, facts=_card_facts(env, card["block"]),
out_path=path, extra=_extra(env.instructions)),
role="judge", capabilities="files",
payload=lambda result, p=path: _ziele_schema(_json_file(p)),
timeout=_timeout("lernziele", len(env.subs_by_title.get(card["block"], []))))
status, ziele = await _versuch("")
if status == CANCELLED:
return False
if status == FAILED:
await _set(env, card, status="error", gate_info="Lernziele ohne Ergebnis")
return False
if not ziele: # ein Ersatz-Versuch — leere Liste heißt: das Coverage-Gate läuft leer
status, ziele = await _versuch("-2")
if status == CANCELLED:
return False
if not isinstance(ziele, list):
ziele = []
if not ziele:
_log(env.topic, f"Lernziele {card['block']}: zweimal leer — Block ohne Coverage-Gate")
for z in ziele:
await db.put_lernziel(env.topic, norm, z["id"], z["text"], _norm_title(z["sub"]))
await _set(env, card, stage="zuweisung", status="open")
@@ -254,6 +267,12 @@ def _merge_split_sections(sec_a: dict, sec_b: dict) -> str:
return "\n\n".join(lines)
def _writer_budget(n_subs: int, sockel: int = 800) -> int:
"""Length guideline (chars) for the detailed version — unguided sections measured 24×
too long (23k) or, after the readability fix, far too thin (180 chars/sub)."""
return sockel + 400 * max(n_subs, 1)
async def _write_split(env: _Env, card: dict, ziele_text: str):
"""First draft in two halves (parallel), merged into one section.
→ merged text | None (failed) | False (cancelled)."""
@@ -290,6 +309,7 @@ async def _write_split(env: _Env, card: dict, ziele_text: str):
examples=await _card_examples(env, norm, parts[i],
include_unmatched=(i == 0)),
gaps="\n" + hints[i] + "\n",
budget=_writer_budget(len(parts[i]), sockel=400),
spec=env.spec, out_path=path, extra=_extra(env.instructions)),
role="guide", capabilities="files", payload=_payload,
timeout=_timeout("writer", 1))
@@ -338,6 +358,7 @@ async def _stage_writer(env: _Env, card: dict) -> bool:
facts=_card_facts(env, card["block"]),
examples=await _card_examples(env, norm, env.subs_by_title.get(card["block"], [])),
gaps=gaps, spec=env.spec,
budget=_writer_budget(len(env.subs_by_title.get(card["block"], []))),
out_path=path, extra=_extra(env.instructions)),
role="guide", capabilities="files", payload=_payload,
timeout=_timeout("writer", 1))
@@ -455,13 +476,15 @@ async def _stage_lesbarkeit(env: _Env, card: dict) -> bool:
return False
problems: list[str] = []
path = env.slot(f"lese-{_safe(norm)}-r{card['writer_rounds']}.json")
# Text-Antwort + Engine-Sink: Datei-schreibende Judges lieferten invalides JSON
# (3 kaputte Check-Dateien im Messlauf) — der Sink validiert vor dem Persistieren
status, res = await run_single_slot(
env.ctx, f"Lese-Check {card['block']}", key=f"{env.guide_id}-lese-{_safe(norm)}",
prompt=_prompt("Guide-Lese-Check", topic=env.topic, format_name=env.format,
spec=env.spec, sections=f"SECTION: {card['block']}\n{sec['md']}",
out_path=path, extra=_extra(env.instructions)),
role="judge", capabilities="files",
payload=lambda result: _problems_schema(_json_file(path)),
extra=_extra(env.instructions)),
role="judge", capabilities="none",
payload=lambda result: _sink_json(result, path, _problems_schema),
timeout=_timeout("lese_check", 1))
if status == CANCELLED:
return False
@@ -477,6 +500,8 @@ async def _stage_lesbarkeit(env: _Env, card: dict) -> bool:
sub_list = "\n".join(f"- [{_level_label(s)}] {s['title']}" for s in subs) or "(none)"
tasks = (f"SECTION: {card['block']}\n"
f"SUBBLOCKS (set one `<!-- sub: LABEL | title -->` marker each, label/order as here):\n{sub_list}\n"
f"LENGTH TARGET: about {_writer_budget(len(subs))} characters for the detailed "
f"version (guideline — covering every subblock beats brevity).\n"
f"PROBLEM: {' · '.join(problems)}\nCURRENT CONTENT:\n{sec['md']}")
fixp = env.slot(f"lesefix-{_safe(norm)}.md")
fixp.unlink(missing_ok=True)
@@ -562,6 +587,9 @@ async def run_guide_board(guide_id: str, topic: str, format_name: str, entries:
from guide import _load_subblocks
ctx = GenContext(topic=topic, provider=provider,
is_cancelled=lambda: is_guide_cancelled(guide_id), guide_id=guide_id)
import uuid
from datetime import datetime, timezone
db.set_current_run(topic, f"{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M')}-g{uuid.uuid4().hex[:4]}")
spec = (TEMPLATES_DIR / "Format" / "Section.md").read_text(encoding="utf-8")
subs_raw = await _load_subblocks(topic)
project = source_folder(topic)
@@ -590,6 +618,9 @@ async def run_guide_board(guide_id: str, topic: str, format_name: str, entries:
await asyncio.gather(*[_run_card(env, c, sem) for c in open_cards])
finally:
reporter.cancel()
db.set_current_run(topic, None)
else:
db.set_current_run(topic, None)
if is_guide_cancelled(guide_id):
return None
# assembly — identical shape to the legacy pipeline
@@ -617,6 +648,14 @@ async def run_guide_board(guide_id: str, topic: str, format_name: str, entries:
})
for ch in order:
chapters.append({"title": ch, "sections": by_chapter[ch]})
if chapters:
try: # Abschluss-Guide-QA (best-effort): speist das Badge mit einer frischen Note
import guide_qa
rep = await guide_qa.guide_qa_report(topic, llm=True)
if rep:
await asyncio.to_thread(guide_qa._write_report, rep)
except Exception:
log.exception("[%s] Abschluss-Guide-QA fehlgeschlagen", topic)
return chapters or None
@@ -655,7 +694,11 @@ async def board_snapshot(topic: str, format_name: str, limit: int = 20) -> dict:
"ziele": f"{zc[0]}/{zc[1]}" if zc else ""})
columns.append({"key": stage, "label": STAGE_LABELS[stage],
"total": len(in_stage), "cards": views})
return {"columns": columns}
import qa as qa_mod # lazy wie in board_inventory
tdir = qa_mod.QA_DIR / topic
greports = sorted(tdir.glob("guide-*.json"), key=lambda p: p.stat().st_mtime) if tdir.is_dir() else []
note_guide = (_json_file(greports[-1]) or {}).get("note_guide") if greports else None
return {"columns": columns, "qa_guide": note_guide}
async def reset_card(topic: str, format_name: str, block_norm: str, ab_stage: int) -> bool: