update
This commit is contained in:
156
backend/guide.py
156
backend/guide.py
@@ -19,8 +19,8 @@ import jsonx
|
||||
import llm
|
||||
import belege
|
||||
import textkit
|
||||
from config import (AUFTRAG_RUNDEN_MAX, DURCHGANG, FIX_MAX_VERSUCHE, LEVEL_RANG,
|
||||
SECTION_WOERTER_PRO_ATOM)
|
||||
from config import (AUFTRAG_RUNDEN_MAX, DURCHGANG, FIX_MAX_VERSUCHE, KLAERUNG_PANEL,
|
||||
LEVEL_RANG, SECTION_WOERTER_PRO_ATOM)
|
||||
|
||||
log = logging.getLogger("creator2.guide")
|
||||
|
||||
@@ -186,13 +186,14 @@ def _laenge_band(n_atome: int) -> tuple[int, int]:
|
||||
|
||||
def _anknuepf_kontext(b: dict) -> str:
|
||||
"""Was der Leser aus früheren Durchgängen schon kennt: tieferstufige Atome
|
||||
desselben Ziels. Der Writer knüpft an, statt neu zu erklären
|
||||
(Dopplungs-Schutz zwischen den Durchgängen)."""
|
||||
desselben Ziels — aber NIE Atome dieses Bausteins selbst (seit Level =
|
||||
dominantes Atom-Level enthält ein M-Baustein auch eigene E-Atome; die
|
||||
stünden sonst doppelt im Writer-Prompt: als bekannt UND als zu schreiben)."""
|
||||
rang = LEVEL_RANG.get(b["level"], 1)
|
||||
kontext: dict[int, dict] = {}
|
||||
for a in db.query("SELECT * FROM atome WHERE ziel_id=? AND status NOT IN"
|
||||
" ('gemerged','verworfen')", (b["ziel_id"],)):
|
||||
if LEVEL_RANG.get(a["level"], 1) < rang:
|
||||
if a["baustein_id"] != b["id"] and LEVEL_RANG.get(a["level"], 1) < rang:
|
||||
kontext[a["id"]] = a
|
||||
return "\n".join(f"- {a['titel']}: {a['definition']}" for a in kontext.values())
|
||||
|
||||
@@ -454,6 +455,14 @@ def _offene_auftraege(b_id: int) -> list[dict]:
|
||||
" ORDER BY id", (b_id,))
|
||||
|
||||
|
||||
def _klaerung_kandidaten(b_id: int) -> list[dict]:
|
||||
"""Eskalierte falsch/luecke, die noch nie geklärt wurden. eskaliert+geklaert=1
|
||||
ist der terminale Endzustand (unauflösbar) — Klärung läuft je Auftrag genau
|
||||
einmal, auch nach Re-Eskalation (strukturell, kein Sondercode)."""
|
||||
return db.query("SELECT * FROM auftraege WHERE baustein_id=? AND status='eskaliert'"
|
||||
" AND art IN ('falsch','luecke') AND geklaert=0 ORDER BY id", (b_id,))
|
||||
|
||||
|
||||
def _fakten_von(atome: list[dict]) -> str:
|
||||
return "\n\n".join(f"Atom {a['id']} ({a['titel']}): {a['definition']}\n"
|
||||
+ "\n".join(_quote(z) for z in _zitate(a["id"])) for a in atome)
|
||||
@@ -499,6 +508,54 @@ async def _auftraege_urteilen(ctx: llm.Kontext, b: dict, sec: dict,
|
||||
db.update("auftraege", "id", a["id"], runden=a["runden"] + 1)
|
||||
|
||||
|
||||
async def _klaeren(ctx: llm.Kontext, b: dict) -> None:
|
||||
"""Stichentscheid (Lektion 19) über eskalierte falsch/luecke: EIN 3er-Panel
|
||||
je Baustein, Mehrheit entscheidet. text_falsch → Auftrag wieder offen (der
|
||||
Fix bekommt ein Streich-Mandat); unbegruendet → kein_mangel; quelle_unklar
|
||||
oder Voll-Patt → terminal unauflösbar. Unter 3 gültigen Stimmen KEINE
|
||||
Entscheidung (fail-closed, Lektion 20) — nächste Runde erneut."""
|
||||
kand = _klaerung_kandidaten(b["id"])
|
||||
if not kand:
|
||||
return
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||||
if not sec or not sec["text_lang"]:
|
||||
return
|
||||
atome = _atome_von(b["id"])
|
||||
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],)) or {"text": b["titel"]}
|
||||
liste = "\n".join(f"{i}: [{a['art']}] {a['detail']}" for i, a in enumerate(kand, 1))
|
||||
stimmen = await llm.panel(ctx, KLAERUNG_PANEL, stage="klaerung",
|
||||
template="Guide-Konflikt-Klaerung", schritt="klaerung",
|
||||
role="judge", n=len(kand), item=f"b{b['id']}-klaerung",
|
||||
werte={"ziel": ziel["text"], "fakten": _fakten_von(atome),
|
||||
"kompakt": sec["text_kompakt"], "lang": sec["text_lang"],
|
||||
"auftraege": liste},
|
||||
erwartet=list)
|
||||
je: dict[int, list[str]] = {}
|
||||
for s in stimmen:
|
||||
for e in s:
|
||||
if isinstance(e, dict):
|
||||
try:
|
||||
urteil = str(e.get("urteil", "")).strip()
|
||||
if urteil in ("text_falsch", "unbegruendet", "quelle_unklar"):
|
||||
je.setdefault(int(e.get("auftrag")), []).append(urteil)
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
for i, a in enumerate(kand, 1):
|
||||
urteile = je.get(i, [])
|
||||
if len(urteile) < KLAERUNG_PANEL:
|
||||
continue # Rumpf-Panel → keine Entscheidung, bleibt geklaert=0
|
||||
mehrheit = max(set(urteile), key=urteile.count)
|
||||
if urteile.count(mehrheit) * 2 <= len(urteile):
|
||||
mehrheit = "quelle_unklar" # Voll-Patt (1/1/1) → terminal
|
||||
# EIN atomares Update je Auftrag → crash-/resume-sicher
|
||||
if mehrheit == "text_falsch":
|
||||
db.update("auftraege", "id", a["id"], status="offen", runden=0, geklaert=1)
|
||||
elif mehrheit == "unbegruendet":
|
||||
db.update("auftraege", "id", a["id"], status="kein_mangel", geklaert=1)
|
||||
else:
|
||||
db.update("auftraege", "id", a["id"], geklaert=1) # bleibt eskaliert
|
||||
|
||||
|
||||
async def _auftraege_finden(ctx: llm.Kontext, b: dict, sec: dict) -> None:
|
||||
"""Fund-Modus: läuft nur bei leerer Auftragsliste. Bereits verworfene Details
|
||||
gehen als Negativ-Liste mit, Inserts dedupen exakt gegen alles Nicht-Behobene
|
||||
@@ -545,9 +602,19 @@ async def _stage_fix(ctx: llm.Kontext, b: dict) -> str:
|
||||
sec = db.one("SELECT * FROM sections WHERE baustein_id=?", (b["id"],))
|
||||
offene = _offene_auftraege(b["id"])
|
||||
det = _det_auftraege(ctx.topic, b, sec["text_lang"], sec["text_kompakt"])
|
||||
auftraege = det + [f"KRITISCH ({a['art']}): {a['detail']}"
|
||||
if a["art"] in ("falsch", "luecke") else a["detail"]
|
||||
for a in offene]
|
||||
|
||||
def _auftrag_zeile(a: dict) -> str:
|
||||
zeile = (f"KRITISCH ({a['art']}): {a['detail']}"
|
||||
if a["art"] in ("falsch", "luecke") else a["detail"])
|
||||
# Klärungs-Vermerk generiert, NICHT im detail persistiert — der eingefrorene
|
||||
# Wortlaut ist die Dedup-Invariante der auftraege-Tabelle
|
||||
if a["geklaert"] and a["art"] in ("falsch", "luecke"):
|
||||
zeile += (" [KLÄRUNG: 3er-Panel bestätigt — der Text widerspricht hier der"
|
||||
" Faktenbasis. Anhand der Faktenbasis korrigieren; ist die Passage"
|
||||
" dort nicht belegbar, STREICHEN oder aufs Belegbare abschwächen.]")
|
||||
return zeile
|
||||
|
||||
auftraege = det + [_auftrag_zeile(a) for a in offene]
|
||||
if not auftraege:
|
||||
return "done"
|
||||
atome = _atome_von(b["id"])
|
||||
@@ -718,15 +785,22 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
befunde.append({"art": "det_check", "item": str(b["id"]),
|
||||
"detail": auftrag[:300]})
|
||||
# Offene Aufträge sichtbar machen — sonst passiert eine bekannte Lücke das
|
||||
# Gate. Eskalierte falsch/luecke = Faktenbasis-Konflikt: ehrliche Rest-
|
||||
# Schuld, wird nie geroutet; eskalierter Stil bleibt stumm (nur DB).
|
||||
# Gate. Eskalierte dreiteilig: ungeklärt → klaerung_offen (reparierbar,
|
||||
# Stichentscheid), geklärt → fakten_konflikt (terminal, kleine ehrliche
|
||||
# Rest-Schuld), Stil → stil_rest (terminal, war vorher komplett stumm).
|
||||
for a in db.query("SELECT art, detail FROM auftraege WHERE baustein_id=?"
|
||||
" AND status='offen'", (b["id"],)):
|
||||
befunde.append({"art": "auftrag_offen", "item": str(b["id"]),
|
||||
"detail": f"({a['art']}) {a['detail']}"[:300]})
|
||||
for a in db.query("SELECT detail FROM auftraege WHERE baustein_id=? AND"
|
||||
" status='eskaliert' AND art IN ('falsch','luecke')", (b["id"],)):
|
||||
befunde.append({"art": "fakten_konflikt", "item": str(b["id"]),
|
||||
for a in db.query("SELECT art, detail, geklaert FROM auftraege WHERE"
|
||||
" baustein_id=? AND status='eskaliert'", (b["id"],)):
|
||||
if a["art"] == "stil":
|
||||
art = "stil_rest"
|
||||
elif a["geklaert"]:
|
||||
art = "fakten_konflikt"
|
||||
else:
|
||||
art = "klaerung_offen"
|
||||
befunde.append({"art": art, "item": str(b["id"]),
|
||||
"detail": a["detail"][:300]})
|
||||
ziel = db.one("SELECT text FROM lernziele WHERE id=?", (b["ziel_id"],))
|
||||
if ziel:
|
||||
@@ -748,13 +822,15 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
|
||||
def _qa_auftrag(b_id: int, claim: str) -> None:
|
||||
"""Panel-bestätigter Fakten-Claim → Auftragszeile (quelle qa). Exakter Dedup
|
||||
gegen alles Nicht-Behobene; trifft er eine kein_mangel-Zeile, widersprechen
|
||||
sich die Richter → eskaliert (Faktenbasis klären, kein weiteres Ping-Pong)."""
|
||||
gegen alles Nicht-Behobene; trifft er eine offene ODER kein_mangel-Zeile,
|
||||
widersprechen sich zwei Panels → eskaliert = ab in den Klärungs-Stichentscheid.
|
||||
geklaert wird NIE zurückgesetzt: war der Auftrag schon geklärt, ist das
|
||||
terminal (deckelt QA↔Klärung-Ping-Pong hart auf einen Durchlauf)."""
|
||||
alt = db.one("SELECT id, status FROM auftraege WHERE baustein_id=? AND detail=?"
|
||||
" AND status != 'behoben'", (b_id, claim))
|
||||
if alt is None:
|
||||
db.insert("auftraege", baustein_id=b_id, art="falsch", detail=claim, quelle="qa")
|
||||
elif alt["status"] == "kein_mangel":
|
||||
elif alt["status"] in ("offen", "kein_mangel"):
|
||||
db.update("auftraege", "id", alt["id"], status="eskaliert")
|
||||
|
||||
|
||||
@@ -790,8 +866,10 @@ async def messen_llm(ctx: llm.Kontext) -> list[dict]:
|
||||
erwartet=list)
|
||||
if verdacht is None:
|
||||
return # Verdachts-Call ausgefallen → nächste Runde erneut, kein Hash
|
||||
claims = [str(c.get("claim", "")).strip() for c in verdacht
|
||||
if isinstance(c, dict) and c.get("claim")]
|
||||
# dedupen — ein doppelt gemeldeter Claim eskalierte sonst im selben
|
||||
# Durchlauf seine eigene frisch eingefügte Zeile
|
||||
claims = list(dict.fromkeys(str(c.get("claim", "")).strip() for c in verdacht
|
||||
if isinstance(c, dict) and c.get("claim")))
|
||||
if not claims:
|
||||
hash_setzen(b["id"], h) # sauber geprüft
|
||||
return
|
||||
@@ -828,14 +906,16 @@ def _text_sig(b_id: int) -> str:
|
||||
|
||||
|
||||
def _auftrag_stand(b_ids: list[int]) -> set:
|
||||
"""Zustands-Snapshot der Aufträge dieser Bausteine — Urteile sind Fortschritt,
|
||||
auch ohne Textänderung (die Note bewegt sich erst unter der min(1, n/basis)-
|
||||
Sättigung; ohne dies bräche auto_loop reine Urteilsrunden als Stillstand ab)."""
|
||||
"""Zustands-Snapshot der Aufträge dieser Bausteine. Status- und geklaert-
|
||||
Übergänge sind Fortschritt (auch ohne Textänderung); ein reines runden-
|
||||
Inkrement zählt NICHT — identisches Panel-Patt auf identischem Text ist
|
||||
per Definition Leer-Churn und hielt auto_loop sonst bis LOOP_MAX_ITER am
|
||||
Leben. Echte Urteilsrunden schließen/eskalieren etwas → Statuswechsel."""
|
||||
if not b_ids:
|
||||
return set()
|
||||
marks = ",".join("?" * len(b_ids))
|
||||
return {(r["id"], r["status"], r["runden"]) for r in db.query(
|
||||
f"SELECT id, status, runden FROM auftraege WHERE baustein_id IN ({marks})",
|
||||
return {(r["id"], r["status"], r["geklaert"]) for r in db.query(
|
||||
f"SELECT id, status, geklaert FROM auftraege WHERE baustein_id IN ({marks})",
|
||||
tuple(b_ids))}
|
||||
|
||||
|
||||
@@ -846,16 +926,29 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
Statusübergang — sonst erkennt auto_loop nie Stillstand."""
|
||||
ctx.ebene = EBENE
|
||||
betroffen: dict[int, list[dict]] = {}
|
||||
klaerbar: set[int] = set()
|
||||
for b in befunde:
|
||||
if b["art"] == "fakten_konflikt":
|
||||
continue # eskaliert = Faktenbasis-Problem, kein Rewrite kann das lösen
|
||||
if b["art"] in ("fakten_konflikt", "stil_rest"):
|
||||
continue # terminal: geklärt-unauflösbar bzw. Stil-Rest — keine Route
|
||||
try:
|
||||
b_id = int(b["item"])
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if b["art"] == "klaerung_offen":
|
||||
klaerbar.add(b_id) # Stichentscheid statt Stage-Routing
|
||||
continue
|
||||
betroffen.setdefault(b_id, []).append(b)
|
||||
if not betroffen:
|
||||
if not betroffen and not klaerbar:
|
||||
return False
|
||||
alle_ids = list(betroffen.keys() | klaerbar)
|
||||
text_vorher = {b_id: _text_sig(b_id) for b_id in alle_ids}
|
||||
stand_vorher = _auftrag_stand(alle_ids)
|
||||
# Klärung VOR dem Routing: text_falsch öffnet Aufträge wieder → die routen
|
||||
# unten in den Fix; unbegruendet/quelle_unklar brauchen keine Stage.
|
||||
if klaerbar:
|
||||
je_baustein = {bb["id"]: bb for bb in _bausteine(ctx.topic)}
|
||||
await llm.alle(_klaeren(ctx, je_baustein[b_id])
|
||||
for b_id in sorted(klaerbar) if b_id in je_baustein)
|
||||
geroutet: list[int] = []
|
||||
for b_id, liste in betroffen.items():
|
||||
arten = {x["art"] for x in liste}
|
||||
@@ -874,10 +967,15 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
db.update("sections", "baustein_id", b_id, stage="fix")
|
||||
db.update("bausteine", "id", b_id, status="repair")
|
||||
geroutet.append(b_id)
|
||||
text_vorher = {b_id: _text_sig(b_id) for b_id in geroutet}
|
||||
stand_vorher = _auftrag_stand(geroutet)
|
||||
for b_id in sorted(klaerbar):
|
||||
# wiedereröffnete Aufträge → direkt in den Fix (der Re-Check urteilt danach;
|
||||
# umgeht den Freeze bewusst — das Streich-Mandat erzeugt eine Textänderung)
|
||||
if b_id not in geroutet and _offene_auftraege(b_id):
|
||||
db.update("sections", "baustein_id", b_id, stage="fix")
|
||||
db.update("bausteine", "id", b_id, status="repair")
|
||||
geroutet.append(b_id)
|
||||
await bauen(ctx)
|
||||
bewegt = _auftrag_stand(geroutet) != stand_vorher
|
||||
bewegt = _auftrag_stand(alle_ids) != stand_vorher
|
||||
for b_id in geroutet: # Fix-Cap-Zähler pflegen: Änderung → reset, sonst +1
|
||||
if _text_sig(b_id) != text_vorher[b_id]:
|
||||
db.execute("UPDATE sections SET fix_versuche=0 WHERE baustein_id=?", (b_id,))
|
||||
|
||||
Reference in New Issue
Block a user