init
This commit is contained in:
172
backend/artefakte.py
Normal file
172
backend/artefakte.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Ebene 2: Artefakte. Pro Atom 2 Flashcards + 1 Worked Example, generiert in
|
||||
Soll-Punkt-Gruppen (Kontext teilen spart Tokens — Lektion 52), verifiziert von einem
|
||||
2er-Panel GEGEN DIE ANKER-ZITATE (inline, Lektion 48). Einstimmig ok → verifiziert;
|
||||
sonst ein Fix + Re-Verify durch einen Judge; danach verifiziert oder verworfen."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
import db
|
||||
import llm
|
||||
from config import ARTEFAKT_CHUNK_ATOME, VERIFY_PANEL
|
||||
|
||||
log = logging.getLogger("creator2.artefakte")
|
||||
|
||||
EBENE = "artefakte"
|
||||
|
||||
# Karten müssen ohne den Quelltext funktionieren (aak Lauf 8: 17 verifizierte
|
||||
# Karten fragten „was steht im Beleg"). Eng gefasst, damit Fachwörter nicht
|
||||
# matchen: „Belegung" (Aussagenlogik), „Quelle" (Flussnetzwerke), „belegen".
|
||||
_QUELLEN_REFERENZ = re.compile(
|
||||
r"\bbeleg(e|s)?\b|musterlösung|quelltext|laut (quelle|text|skript)"
|
||||
r"|(aus|in) de[rm] (quelle|text|skript)|aufgabenkontext|hausaufgabe"
|
||||
r"|präsenzaufgabe|klausur\w*|\baufgabe \d|\bserie \d|\bblatt \d",
|
||||
re.IGNORECASE)
|
||||
|
||||
|
||||
def _referenziert_quelle(inhalt: dict) -> bool:
|
||||
return _QUELLEN_REFERENZ.search(" ".join(str(v) for v in inhalt.values())) is not None
|
||||
|
||||
|
||||
def _zitate(atom_id: int) -> str:
|
||||
rows = db.query("SELECT zitat FROM anker WHERE atom_id=? AND start>=0", (atom_id,))
|
||||
return "\n".join(f"> {r['zitat']}" for r in rows) or "(kein Anker)"
|
||||
|
||||
|
||||
def _atom_block(a: dict) -> str:
|
||||
return (f"ATOM {a['id']}: {a['titel']} [{a['typ']}]\n"
|
||||
f"Definition: {a['definition']}\nBelege:\n{_zitate(a['id'])}")
|
||||
|
||||
|
||||
def _chunks(topic: str, nur_ohne: bool) -> list[list[dict]]:
|
||||
"""Atome je Soll-Punkt gruppiert, gestückelt. nur_ohne: nur Atome ohne Artefakte."""
|
||||
atome = db.query("SELECT * FROM atome WHERE topic=? AND status NOT IN"
|
||||
" ('gemerged','verworfen') ORDER BY soll_id, id", (topic,))
|
||||
if nur_ohne:
|
||||
# verworfene zählen nicht — sonst kann der Repair nach Verwurf nie
|
||||
# nachgenerieren (Lauf 8: 5 Atome blieben dauerhaft ohne Flashcard)
|
||||
atome = [a for a in atome if not db.one(
|
||||
"SELECT id FROM artefakte WHERE atom_id=? AND status!='verworfen'"
|
||||
" LIMIT 1", (a["id"],))]
|
||||
gruppen: dict = {}
|
||||
for a in atome:
|
||||
gruppen.setdefault(a["soll_id"], []).append(a)
|
||||
out = []
|
||||
for gruppe in gruppen.values():
|
||||
out += [gruppe[i:i + ARTEFAKT_CHUNK_ATOME]
|
||||
for i in range(0, len(gruppe), ARTEFAKT_CHUNK_ATOME)]
|
||||
return out
|
||||
|
||||
|
||||
async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
bloecke = "\n\n".join(_atom_block(a) for a in chunk)
|
||||
res = await llm.call(ctx, stage="artefakt", template="Artefakt-Generate",
|
||||
werte={"atome": bloecke}, role="guide",
|
||||
n=len(chunk), item=f"g{chunk[0]['id']}", erwartet=list)
|
||||
gueltig = {a["id"] for a in chunk}
|
||||
for e in res or []:
|
||||
atom_id = e.get("atom")
|
||||
typ = str(e.get("typ", "")).strip()
|
||||
if atom_id not in gueltig or typ not in ("flashcard", "beispiel"):
|
||||
continue
|
||||
inhalt = {k: str(e.get(k, "")) for k in ("frage", "antwort", "text")}
|
||||
if _referenziert_quelle(inhalt):
|
||||
log.info("Artefakt zu Atom %s nicht übernommen: referenziert die Quelle", atom_id)
|
||||
continue
|
||||
db.insert("artefakte", atom_id=atom_id, typ=typ, inhalt=db.j(inhalt), status="kandidat")
|
||||
|
||||
|
||||
async def _verifizieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
"""Panel prüft alle Kandidaten des Chunks gegen die Belege. Verwerfen ist
|
||||
destruktiv → nur nach Fix + Re-Verify (im Zweifel behalten, Lektion 67)."""
|
||||
kandidaten = []
|
||||
for a in chunk:
|
||||
for k in db.query("SELECT * FROM artefakte WHERE atom_id=? AND status='kandidat'",
|
||||
(a["id"],)):
|
||||
if _referenziert_quelle(db.uj(k["inhalt"])): # deterministisch, vor dem Panel
|
||||
db.update("artefakte", "id", k["id"], status="verworfen")
|
||||
else:
|
||||
kandidaten.append(k)
|
||||
if not kandidaten:
|
||||
return
|
||||
je_atom = {a["id"]: a for a in chunk}
|
||||
liste = "\n\n".join(
|
||||
f"ARTEFAKT {k['id']} (Atom {k['atom_id']}, {k['typ']}): {k['inhalt']}\n"
|
||||
f"Belege:\n{_zitate(k['atom_id'])}" for k in kandidaten)
|
||||
stimmen = await llm.panel(ctx, VERIFY_PANEL, stage="verify", template="Artefakt-Verify",
|
||||
werte={"artefakte": liste}, role="judge",
|
||||
n=len(kandidaten), item=f"v{chunk[0]['id']}", erwartet=list)
|
||||
for k in kandidaten:
|
||||
urteile = []
|
||||
maengel = []
|
||||
for stimme in stimmen:
|
||||
for e in stimme:
|
||||
if isinstance(e, dict) and e.get("artefakt") == k["id"]:
|
||||
urteile.append(bool(e.get("ok")))
|
||||
if e.get("mangel"):
|
||||
maengel.append(str(e["mangel"]))
|
||||
break
|
||||
if len(urteile) >= VERIFY_PANEL and all(urteile):
|
||||
db.update("artefakte", "id", k["id"], status="verifiziert")
|
||||
elif urteile:
|
||||
await _fixen(ctx, k, maengel, je_atom.get(k["atom_id"], {}))
|
||||
# keine gültige Stimme (Panel-Ausfall) → Kandidat bleibt, nächste Runde prüft
|
||||
|
||||
|
||||
async def _fixen(ctx: llm.Kontext, k: dict, maengel: list[str], atom: dict) -> None:
|
||||
res = await llm.call(ctx, stage="artefakt_fix", template="Artefakt-Fix",
|
||||
schritt="fix", role="guide", item=f"f{k['id']}",
|
||||
werte={"artefakt": k["inhalt"], "typ": k["typ"],
|
||||
"maengel": "\n".join(f"- {m}" for m in maengel) or "-",
|
||||
"belege": _zitate(k["atom_id"])},
|
||||
erwartet=dict)
|
||||
if res:
|
||||
inhalt = {kk: str(res.get(kk, "")) for kk in ("frage", "antwort", "text")}
|
||||
if _referenziert_quelle(inhalt):
|
||||
db.update("artefakte", "id", k["id"], status="verworfen")
|
||||
return
|
||||
db.update("artefakte", "id", k["id"], inhalt=db.j(inhalt))
|
||||
urteil = await llm.call(ctx, stage="reverify", template="Artefakt-Verify",
|
||||
schritt="verify", role="judge", item=f"rv{k['id']}",
|
||||
werte={"artefakte": f"ARTEFAKT {k['id']} (Atom {k['atom_id']},"
|
||||
f" {k['typ']}): {db.j(res) if res else k['inhalt']}\n"
|
||||
f"Belege:\n{_zitate(k['atom_id'])}"},
|
||||
erwartet=list)
|
||||
ok = any(isinstance(e, dict) and e.get("artefakt") == k["id"] and e.get("ok")
|
||||
for e in urteil or [])
|
||||
db.update("artefakte", "id", k["id"], status="verifiziert" if ok else "verworfen")
|
||||
|
||||
|
||||
async def bauen(ctx: llm.Kontext) -> None:
|
||||
ctx.ebene = EBENE
|
||||
neu = _chunks(ctx.topic, nur_ohne=True)
|
||||
await asyncio.gather(*(_generieren(ctx, c) for c in neu))
|
||||
alle = _chunks(ctx.topic, nur_ohne=False)
|
||||
await asyncio.gather(*(_verifizieren(ctx, c) for c in alle))
|
||||
|
||||
|
||||
def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
befunde = []
|
||||
atome = db.query("SELECT * FROM atome WHERE topic=? AND status NOT IN"
|
||||
" ('gemerged','verworfen')", (ctx.topic,))
|
||||
for a in atome:
|
||||
karten = db.query("SELECT status FROM artefakte WHERE atom_id=? AND typ='flashcard'",
|
||||
(a["id"],))
|
||||
if not any(k["status"] == "verifiziert" for k in karten):
|
||||
befunde.append({"art": "atom_ohne_flashcard", "item": str(a["id"]),
|
||||
"detail": a["titel"]})
|
||||
offen = db.query("SELECT id FROM artefakte WHERE atom_id=? AND status='kandidat'",
|
||||
(a["id"],))
|
||||
for k in offen:
|
||||
befunde.append({"art": "artefakt_unentschieden", "item": str(k["id"]),
|
||||
"detail": a["titel"]})
|
||||
return befunde
|
||||
|
||||
|
||||
async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
ctx.ebene = EBENE
|
||||
if not befunde:
|
||||
return False
|
||||
await bauen(ctx) # idempotent: generiert Fehlendes nach, prüft Offenes
|
||||
return True
|
||||
Reference in New Issue
Block a user