This commit is contained in:
team3
2026-07-04 12:21:45 +02:00
parent 2f5d5b9ca1
commit 8d8f6c8e51
43 changed files with 1920 additions and 236 deletions

View File

@@ -21,7 +21,8 @@ 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 config import (FORMAT_PURPOSE, GUIDE_LAENGE_MAX, GUIDE_LAENGE_MIN, READABILITY_ACTIVE,
TEMPLATES_DIR, MAX_CONCURRENT_AGENTS_PER_TOPIC)
from fsutil import atomic_write_json
from jsonio import read_json_file as _json_file
from pipeline import (CANCELLED, FAILED, OK, GenContext, _extra, _log, _prompt,
@@ -34,9 +35,7 @@ GUIDE_STAGES = ("lernziele", "zuweisung", "writer", "fakten_gate", "coverage", "
STAGE_LABELS = {"lernziele": "Lernziele", "zuweisung": "Zuweisung", "writer": "Writer",
"fakten_gate": "Fakten-Gate", "coverage": "Coverage",
"lesbarkeit": "Lesbarkeit", "done": "Fertig"}
MAX_WRITER_ROUNDS = 2 # coverage → writer feedback loop cap (gains die after round 12)
GATE_FIX_MIN = 3 # fact-gate claims below this: log only — a full fix rewrite for
# 12 residual claims fired on 19/20 blocks (40 agent-minutes)
from config import GATE_FIX_MIN, MAX_WRITER_ROUNDS, WRITER_SPLIT_SUBS # zentral tunebar
# Simultaneous cards = the per-topic agent cap: every card busies exactly ONE agent at a
# time (its stages run serially), so a lower number just idles slots (was hardcoded 10
# from the old 10-slot era while the .env already allowed 24).
@@ -65,7 +64,10 @@ def _ziele_schema(data):
def _gate_schema(data):
"""{"ok":true} → [] · {"claims":[{text,grund}]} → list · None invalid."""
"""{"ok":true} → [] · {"claims":[{text,grund,urteil}]} → list · None invalid.
urteil "falsch" (contradicts the facts/itself) vs "unbelegt" (true but underivable) —
default unbelegt. Entries whose grund starts with "belegt" are dropped: one judge
returned a 65-entry full inventory including SUPPORTED claims."""
if not isinstance(data, dict):
return None
if data.get("ok") is True:
@@ -76,7 +78,12 @@ def _gate_schema(data):
out = []
for c in claims:
if isinstance(c, dict) and str(c.get("text", "")).strip():
out.append({"text": str(c["text"]).strip(), "grund": str(c.get("grund", "")).strip()})
grund = str(c.get("grund", "")).strip()
if grund.casefold().startswith("belegt"):
continue
urteil = str(c.get("urteil", "")).strip().casefold()
out.append({"text": str(c["text"]).strip(), "grund": grund,
"urteil": urteil if urteil == "falsch" else "unbelegt"})
return out
@@ -241,7 +248,7 @@ async def _stage_zuweisung(env: _Env, card: dict) -> bool:
# A single section over ~45 subs measurably breaks the writer/coverage (Front Matter:
# 4/6 objectives open after 2 rounds). First drafts of oversized cards are written in two
# halves and merged back into ONE canonical section (all gates/assembly read one section).
WRITER_SPLIT_SUBS = 30
# WRITER_SPLIT_SUBS: siehe config.py
def _merge_split_sections(sec_a: dict, sec_b: dict) -> str:
@@ -397,13 +404,15 @@ async def _stage_fakten_gate(env: _Env, card: dict) -> bool:
if status == FAILED:
claims = [] # gate failure must not block the card — logged, text stands
_log(env.topic, f"Fakten-Gate {card['block']}: kein Ergebnis — Text bleibt ungeprüft")
if claims and len(claims) < GATE_FIX_MIN:
# 12 Rest-Claims rechtfertigen keinen Voll-Rewrite: der Fix-Pass lief für 19/20
# Blöcke und kostete 40 Agent-Minuten; die Guide-QA-Fachlichkeits-Stichprobe misst nach
falsch = [c for c in claims if c.get("urteil") == "falsch"] if claims else []
if claims and not falsch and len(claims) < GATE_FIX_MIN:
# 12 merely UNSUPPORTED claims don't justify a fix pass (it ran for 19/20 blocks,
# 40 agent-minutes) — but a WRONG claim always does: one slipped through this
# threshold and cost the guide 1.5 QA points
_log(env.topic, f"Fakten-Gate {card['block']}: {len(claims)} Claim(s) unter Schwelle — kein Fix")
claims = []
if claims:
_log(env.topic, f"Fakten-Gate {card['block']}: {len(claims)} unbelegte Claims → Fix")
_log(env.topic, f"Fakten-Gate {card['block']}: {len(claims)} Claims ({len(falsch)} falsch) → Fix")
fixp = env.slot(f"gatefix-{_safe(norm)}-r{card['writer_rounds']}.md")
fixp.unlink(missing_ok=True)
claims_text = "\n".join(f"- {c['text']}" + (f" ({c['grund']})" if c["grund"] else "")
@@ -501,6 +510,18 @@ async def _stage_lesbarkeit(env: _Env, card: dict) -> bool:
hints = await asyncio.to_thread(readability.rate_sections, {1: sec["md"]})
if hints.get(1):
problems.append(hints[1])
# deterministic length trigger, same formula as the QA detector: prompt guidelines
# alone left writers 2.74.1× over target — a measured overshoot forces the fix pass
subs_all = env.subs_by_title.get(card["block"], [])
n_rel = max(sum(1 for s in subs_all if s.get("relevance") != "peripheral"), 1)
aus = re.split(r"<!--\s*ausführlich\s*-->", sec["md"], maxsplit=1)
pro_sub = len(aus[1] if len(aus) == 2 else sec["md"]) / n_rel
if not (GUIDE_LAENGE_MIN <= pro_sub <= GUIDE_LAENGE_MAX * 0.9):
ziel = _writer_budget(len(subs_all))
problems.append(
f"Länge {round(pro_sub)} Zeichen/Sub (Rahmen {GUIDE_LAENGE_MIN}{round(GUIDE_LAENGE_MAX * 0.9)}): "
f"schreibe den ausführlich-Teil auf etwa {ziel} Zeichen GESAMT um — Sockel-Prosa und "
f"Wiederholungen streichen, alle Sub-Marker und Beispiele behalten")
if problems:
from guide import _level_label
subs = env.subs_by_title.get(card["block"], [])