This commit is contained in:
team3
2026-07-24 11:23:18 +02:00
parent cb68cd671b
commit 1b054497ed
49 changed files with 935 additions and 408 deletions

View File

@@ -4,7 +4,7 @@
import hashlib
import re
from . import belege, config, db, engine, llm, panels, textkit
from . import arten, belege, config, db, engine, graph, llm, panels, textkit
_MARKER_RE = re.compile(r"<!--\s*atom:\s*(\d+)\s*-->")
@@ -25,7 +25,7 @@ def _band(n_atome: int) -> tuple[int, int]:
def _slug(titel: str) -> str:
s = textkit.norm(titel).replace(" ", "-")
return re.sub(r"[^a-z0-9äöüß-]", "", s)
return "".join(c for c in s if c.isalnum() or c == "-") # Unicode-fähig
def _bekannt_kontext(topic: str, baustein: dict) -> str:
@@ -39,8 +39,10 @@ def _bekannt_kontext(topic: str, baustein: dict) -> str:
baustein["id"], topic)
if not fruehere:
return "-"
return "\n".join(f"- {f['titel']} (im Abschnitt '{f['bt']}')"
for f in fruehere[:60])
return "\n".join(
config.TEXTE["abschnitt_verweis"].format(titel=f["titel"],
abschnitt=f["bt"])
for f in fruehere[:60])
@engine.worker("guide.writer")
@@ -64,7 +66,7 @@ async def writer(task: dict) -> engine.Ergebnis:
antwort = await llm.call(
run_id=task["run_id"], stufe="guide", knoten="writer", item=task["item"],
role="writer",
skill_namen=["autor", "deutsch-praezise", "guide-lesbarkeit", "writer"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"baustein": b["titel"], "atome": atome_text,
"belege": belege.fenster_fuer_atome([a["id"] for a in atome])[:40000],
"bekannt": _bekannt_kontext(task["topic"], b),
@@ -167,10 +169,8 @@ async def det_pruefung(task: dict) -> engine.Ergebnis:
frisch = {(b["art"], b["item"], b["detail"][:400]) for b in befunde}
for row in db.query(
"SELECT id, art, item, detail FROM befunde WHERE item=? AND "
"status='offen' AND art IN ('marker_fehlend','marker_fremd',"
"'marker_doppelt','vorwaerts','laenge','format_leak',"
"'link_im_text','atom_echo','h_ebene')",
f"baustein:{bid}"):
f"status='offen' AND art IN ({','.join('?' * len(arten.DET_ARTEN))})",
f"baustein:{bid}", *arten.DET_ARTEN):
if (row["art"], row["item"], row["detail"][:400]) not in frisch:
db.update("befunde", "id=?", (row["id"],), status="veraltet")
for b in befunde:
@@ -210,14 +210,14 @@ async def llm_pruefung(task: dict) -> engine.Ergebnis:
text = await llm.call(
run_id=task["run_id"], stufe="guide", knoten="llm_pruefung",
item=f"{task['item']}:p{i}", role="judge", n=len(aktive),
skill_namen=["richter", "deutsch-praezise", "guide-pruefung"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"sections": sections_text})
if text is None:
return None
return {"befunde": [b for b in textkit.bloecke(text, "BEFUND")
if b.get("art") in ("falsch", "luecke", "stil")
and b.get("section", "").strip().isdigit()],
"ok": {int(b["section"]) for b in textkit.bloecke(text, "OK")
"ok": {int(b["section"]) for b in textkit.bloecke(text, "LEER")
if b.get("section", "").strip().isdigit()}}
stimmen = await panels.panel(ruf, config.PRUEF_PANEL)
@@ -277,7 +277,7 @@ async def fix(task: dict) -> engine.Ergebnis:
antwort = await llm.call(
run_id=task["run_id"], stufe="guide", knoten="fix", item=task["item"],
role="writer",
skill_namen=["autor", "deutsch-praezise", "guide-lesbarkeit", "guide-fix"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"section": sec["text"],
"befunde": "\n".join(f"- [{b['art']}] {b['detail']}" for b in offen),
"belege": belege.fenster_fuer_atome(
@@ -308,7 +308,7 @@ async def _klaerung(task, bid, sec, offen) -> engine.Ergebnis:
text = await llm.call(
run_id=task["run_id"], stufe="guide", knoten="fix",
item=f"{task['item']}:k{b['id']}:p{i}", role="judge",
skill_namen=["richter", "deutsch-praezise", "klaerung"],
skill_namen=graph.skills_von(task["knoten"], extra="klaerung"),
werte={"befund": f"[{b['art']}] {b['detail']}", "belege": beleg_text})
urteile = textkit.bloecke(text or "", "URTEIL")
return urteile[0].get("urteil") if urteile else None
@@ -339,7 +339,7 @@ async def kapitel_intro(task: dict) -> engine.Ergebnis:
antwort = await llm.call(
run_id=task["run_id"], stufe="guide", knoten="kapitel_intro",
item=task["item"], role="writer",
skill_namen=["autor", "deutsch-praezise", "kapitel-intro"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"kapitel": kap["titel"],
"sections": "\n".join(f"- {t['titel']}" for t in titel),
"vorherige": "\n".join(f"- {v['titel']}" for v in vorherige)
@@ -352,6 +352,46 @@ async def kapitel_intro(task: dict) -> engine.Ergebnis:
{"knoten": "redundanz", "item": f"r{task['runde']}:redundanz"}])
@engine.worker("guide.lektorat")
async def lektorat(task: dict) -> engine.Ergebnis:
"""Gesamt-Wächter (Kurzbuch-Ziel): EIN Blick über den ganzen Guide —
section-übergreifende Dopplungen, Splitter-Sections, zähe Übergänge.
Läuft einmal je Topic; Befunde laufen durch die normale Fix-Maschine."""
topic = task["topic"]
if db.one("SELECT id FROM tasks WHERE topic=? AND knoten='lektorat' AND "
"status='fertig' AND id!=?", topic, task["id"]):
return engine.Ergebnis(daten={"skip": True})
teile = []
for kap in db.query("SELECT * FROM kapitel WHERE topic=? ORDER BY ord",
topic):
teile.append(f"## {kap['titel']}")
for b in db.query(
"SELECT b.id, b.titel, s.text FROM bausteine b JOIN sections s "
"ON s.baustein_id=b.id WHERE b.kapitel_id=? AND s.text!='' "
"ORDER BY b.ord", kap["id"]):
teile.append(f"[SECTION {b['id']}] {b['titel']}\n{b['text']}")
if len(teile) < 2:
return engine.Ergebnis(daten={"skip": True})
antwort = await llm.call(
run_id=task["run_id"], stufe="guide", knoten="lektorat",
item=task["item"], role="judge",
skill_namen=graph.skills_von(task["knoten"]),
werte={"guide": "\n\n".join(teile)[:120_000]})
if antwort is None:
raise RuntimeError("lektorat ohne Ergebnis")
befunde_bloecke = textkit.bloecke(antwort, "BEFUND")
if not befunde_bloecke and not textkit.bloecke(antwort, "LEER"):
raise RuntimeError("lektorat: weder BEFUND noch LEER") # Format kaputt
n = 0
for b in befunde_bloecke:
sid, art = b.get("section", "").strip(), b.get("art", "").strip()
if sid.isdigit() and art in ("redundanz", "stil") and befund_merken(
task["run_id"], "lektorat", art, f"baustein:{sid}",
b.get("detail", "")[:300], task["runde"]):
n += 1
return engine.Ergebnis(daten={"befunde": n})
def befund_merken(run_id: int, knoten: str, art: str, item: str, detail: str,
runde: int) -> bool:
"""Befund-Dedupe (creator2-Schlüssel art+item+detail): existiert derselbe
@@ -392,7 +432,8 @@ async def gate(task: dict) -> engine.Ergebnis:
continue
alle_marker += [int(m) for m in _MARKER_RE.findall(sec["text"])]
for f in _det_befunde(topic, b["id"], sec["text"]):
if f["art"] == "laenge": # Länge blockiert nicht hart (nur Befund)
if not arten.BEFUND_ARTEN.get(f["art"], {"gate_blockiert": True})[
"gate_blockiert"]: # z. B. laenge: nur Kennzahl
continue
if (f["art"], f["item"], f["detail"][:400]) in final:
continue
@@ -402,7 +443,10 @@ async def gate(task: dict) -> engine.Ergebnis:
"detail": "Atom in mehreren Sections gelehrt"})
# Kaputte Marker mit erschöpftem Fix → Section NEU schreiben (max 1×!
# sonst resettet jedes Neuschreiben den Fix-Cap → Endlos-Karussell)
for f in [f for f in befunde if f["art"].startswith("marker")]:
# NUR Section-Befunde (item baustein:*) — marker_global_doppelt trägt
# item atom:<marke>, dessen Zahl ist KEINE Baustein-Id (latenter Bug)
for f in [f for f in befunde if f["art"].startswith("marker")
and f["item"].startswith("baustein:")]:
bid = f["item"].split(":")[-1]
sec = db.one("SELECT fix_versuche FROM sections WHERE baustein_id=?", bid)
if sec and (sec["fix_versuche"] or 0) >= config.FIX_MAX_VERSUCHE \
@@ -414,7 +458,7 @@ async def gate(task: dict) -> engine.Ergebnis:
offene = db.query(
"SELECT * FROM befunde b WHERE b.run_id IN (SELECT id FROM runs WHERE "
"topic=?) AND b.stufe='guide' AND b.status='offen' AND b.art IN "
"('falsch','luecke','redundanz','vorwaerts','format_leak')", topic)
f"({','.join('?' * len(arten.FIX_ARTEN))})", topic, *arten.FIX_ARTEN)
for b in offene:
befunde.append({"art": b["art"], "item": b["item"],
"detail": b["detail"][:120]})