update
This commit is contained in:
@@ -13,7 +13,8 @@ import tempfile
|
||||
import belege
|
||||
import db
|
||||
import llm
|
||||
from config import ARTEFAKT_CHUNK_ATOME, AUSSAGEN_JE_ATOM, BEISPIEL_FORMEN, VERIFY_PANEL
|
||||
from config import (ARTEFAKT_CHUNK_ATOME, AUSSAGEN_JE_ATOM, BEISPIEL_FORMEN,
|
||||
NACHFUELL_CAP, VERIFY_PANEL)
|
||||
|
||||
log = logging.getLogger("creator2.artefakte")
|
||||
|
||||
@@ -51,17 +52,78 @@ def _atom_block(a: dict) -> str:
|
||||
f"Definition: {a['definition']}\nBelege:\n{_zitate(a['id'])}")
|
||||
|
||||
|
||||
FC_ZIEL = 2 # Flashcards je Atom; Beispiel-Ziel ist 1, Aussagen-Ziel AUSSAGEN_JE_ATOM Paare
|
||||
|
||||
# Zeilen-Ziel je Typ (Aussagen: 2 Zeilen pro Paar) — Basis für den Nachfüll-Cap
|
||||
ZIEL_ZEILEN = {"flashcard": FC_ZIEL, "beispiel": 1, "aussage": 2 * AUSSAGEN_JE_ATOM}
|
||||
|
||||
|
||||
def _lebende(atom_id: int, typ: str) -> list[dict]:
|
||||
return db.query("SELECT * FROM artefakte WHERE atom_id=? AND typ=?"
|
||||
" AND status!='verworfen'", (atom_id, typ))
|
||||
|
||||
|
||||
def _aufgegeben(atom_id: int, typ: str) -> bool:
|
||||
"""Anti-Churn: ab NACHFUELL_CAP × Ziel Verwürfen nicht mehr nachfüllen —
|
||||
messen meldet dann `artefakt_aufgegeben` (sichtbar, aber kein Repair)."""
|
||||
n = db.one("SELECT COUNT(*) AS n FROM artefakte WHERE atom_id=? AND typ=?"
|
||||
" AND status='verworfen'", (atom_id, typ))["n"]
|
||||
return n >= NACHFUELL_CAP * ZIEL_ZEILEN[typ]
|
||||
|
||||
|
||||
def _gen_bedarf(atom_id: int) -> bool:
|
||||
"""Braucht das Atom einen Artefakt-Generate-Call? (Flashcards unterm Ziel
|
||||
oder Beispiel fehlt — jeweils solange nicht aufgegeben.)"""
|
||||
if (len(_lebende(atom_id, "flashcard")) < FC_ZIEL
|
||||
and not _aufgegeben(atom_id, "flashcard")):
|
||||
return True
|
||||
return not _lebende(atom_id, "beispiel") and not _aufgegeben(atom_id, "beispiel")
|
||||
|
||||
|
||||
def _paar_lage(atom_id: int) -> tuple[int, int, int]:
|
||||
"""→ (komplette lebende Paare, einseitig lebende Paare, höchster Paar-Index).
|
||||
Der Index zählt über ALLE Zeilen (auch verworfene) — neue Paare dürfen alte
|
||||
`paar`-Keys nie wiederverwenden."""
|
||||
hoechster = -1
|
||||
seiten: dict[str, set[bool]] = {}
|
||||
for k in db.query("SELECT inhalt, status FROM artefakte WHERE atom_id=?"
|
||||
" AND typ='aussage'", (atom_id,)):
|
||||
inh = db.uj(k["inhalt"], {})
|
||||
paar = str(inh.get("paar", ""))
|
||||
try:
|
||||
hoechster = max(hoechster, int(paar.rsplit("-", 1)[1]))
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
if k["status"] != "verworfen":
|
||||
seiten.setdefault(paar, set()).add(bool(inh.get("wahr")))
|
||||
komplett = sum(1 for s in seiten.values() if s == {True, False})
|
||||
return komplett, len(seiten) - komplett, hoechster
|
||||
|
||||
|
||||
def _paare_bereinigen(atom_ids: list[int]) -> None:
|
||||
"""Unvollständige Paar-Gruppen verwerfen: die lebenden Seiten müssen genau
|
||||
{wahr, falsch} ergeben. Fängt halbe Paare UND Deformierte (fehlender
|
||||
paar-Key, doppelt gleiche Seite). Nachfüllen erzeugt nur KOMPLETTE Paare."""
|
||||
for atom_id in atom_ids:
|
||||
gruppen: dict[str, list[dict]] = {}
|
||||
for k in db.query("SELECT * FROM artefakte WHERE atom_id=? AND typ='aussage'",
|
||||
(atom_id,)):
|
||||
paar = str(db.uj(k["inhalt"], {}).get("paar", ""))
|
||||
gruppen.setdefault(paar, []).append(k)
|
||||
for zeilen in gruppen.values():
|
||||
lebend = [z for z in zeilen if z["status"] != "verworfen"]
|
||||
seiten = {bool(db.uj(z["inhalt"], {}).get("wahr")) for z in lebend}
|
||||
if lebend and seiten != {True, False}:
|
||||
for z in lebend:
|
||||
db.update("artefakte", "id", z["id"], status="verworfen")
|
||||
|
||||
|
||||
def _chunks(topic: str, nur_ohne: bool) -> list[list[dict]]:
|
||||
"""Atome je Soll-Punkt gruppiert, gestückelt. nur_ohne: nur Atome ohne Artefakte."""
|
||||
"""Atome je Soll-Punkt gruppiert, gestückelt. nur_ohne: nur Atome mit Bedarf."""
|
||||
atome = db.query("SELECT * FROM atome WHERE topic=? AND status NOT IN"
|
||||
" ('gemerged','verworfen') ORDER BY soll_id, id", (topic,))
|
||||
if nur_ohne:
|
||||
# Die echte Invariante ist „≥1 lebende FLASHCARD" (Beispiel zählt nicht).
|
||||
# Prüfte man „irgendein lebendes Artefakt", blockierte ein überlebendes
|
||||
# Beispiel die Flashcard-Nachgenerierung dauerhaft (Lauf 8: 5 Atome ohne).
|
||||
atome = [a for a in atome if not db.one(
|
||||
"SELECT id FROM artefakte WHERE atom_id=? AND typ='flashcard'"
|
||||
" AND status!='verworfen' LIMIT 1", (a["id"],))]
|
||||
atome = [a for a in atome if _gen_bedarf(a["id"])]
|
||||
gruppen: dict = {}
|
||||
for a in atome:
|
||||
gruppen.setdefault(a["soll_id"], []).append(a)
|
||||
@@ -91,6 +153,9 @@ async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
werte={"atome": bloecke}, role="guide",
|
||||
n=len(chunk), item=f"g{chunk[0]['id']}", erwartet=list)
|
||||
gueltig = {a["id"] for a in chunk}
|
||||
# Nachfüllen bis zum Ziel, nie darüber: das Template liefert immer voll
|
||||
# (2 Karten + Beispiel), der Guard nimmt nur, was dem Atom noch fehlt.
|
||||
fc_frei = {a["id"]: FC_ZIEL - len(_lebende(a["id"], "flashcard")) for a in chunk}
|
||||
for e in res or []:
|
||||
atom_id = e.get("atom")
|
||||
typ = str(e.get("typ", "")).strip()
|
||||
@@ -103,12 +168,14 @@ async def _generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
if _referenziert_quelle(inhalt):
|
||||
log.info("Artefakt zu Atom %s nicht übernommen: referenziert die Quelle", atom_id)
|
||||
continue
|
||||
# Nachgenerierung (nur_ohne) läuft auch, wenn nur die Flashcard fehlt —
|
||||
# dann kein zweites Beispiel anlegen (Verify-Tokens/Leitner-Dubletten).
|
||||
if typ == "beispiel" and db.one(
|
||||
"SELECT id FROM artefakte WHERE atom_id=? AND typ='beispiel'"
|
||||
" AND status!='verworfen' LIMIT 1", (atom_id,)):
|
||||
continue
|
||||
if typ == "flashcard":
|
||||
if fc_frei.get(atom_id, 0) <= 0:
|
||||
continue
|
||||
fc_frei[atom_id] -= 1
|
||||
db.insert("artefakte", atom_id=atom_id, typ=typ, inhalt=db.j(inhalt), status="kandidat")
|
||||
|
||||
|
||||
@@ -117,8 +184,10 @@ async def _verifizieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
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"],)):
|
||||
# Aussagen haben ihr EIGENES Panel (+Paar-Logik) — landeten sie hier,
|
||||
# überschrieb _fixen ihren Inhalt mit dem Flashcard-Schema (leere Hülle)
|
||||
for k in db.query("SELECT * FROM artefakte WHERE atom_id=? AND status='kandidat'"
|
||||
" AND typ!='aussage'", (a["id"],)):
|
||||
if _referenziert_quelle(db.uj(k["inhalt"])): # deterministisch, vor dem Panel
|
||||
db.update("artefakte", "id", k["id"], status="verworfen")
|
||||
else:
|
||||
@@ -280,7 +349,14 @@ async def _aussagen_generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
role="guide", n=len(chunk), item=f"as{chunk[0]['id']}",
|
||||
erwartet=list)
|
||||
gueltig = {a["id"] for a in chunk}
|
||||
zaehler: dict[int, int] = {}
|
||||
# Nachfüllen: nur fehlende Paare übernehmen; Index hinter ALLEN bestehenden
|
||||
# Zeilen fortsetzen (alte `paar`-Keys — auch verworfene — nie wiederverwenden)
|
||||
bedarf: dict[int, int] = {}
|
||||
naechster: dict[int, int] = {}
|
||||
for a in chunk:
|
||||
komplett, _, hoechster = _paar_lage(a["id"])
|
||||
bedarf[a["id"]] = max(0, AUSSAGEN_JE_ATOM - komplett)
|
||||
naechster[a["id"]] = hoechster + 1
|
||||
for e in res or []:
|
||||
if not isinstance(e, dict) or e.get("atom") not in gueltig:
|
||||
continue
|
||||
@@ -290,8 +366,11 @@ async def _aussagen_generieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
continue
|
||||
if _referenziert_quelle({"text": f"{wahr} {falsch}"}):
|
||||
continue
|
||||
i = zaehler.get(atom_id, 0)
|
||||
zaehler[atom_id] = i + 1
|
||||
if bedarf.get(atom_id, 0) <= 0:
|
||||
continue
|
||||
bedarf[atom_id] -= 1
|
||||
i = naechster[atom_id]
|
||||
naechster[atom_id] = i + 1
|
||||
paar = f"{atom_id}-{i}"
|
||||
db.insert("artefakte", atom_id=atom_id, typ="aussage", status="kandidat",
|
||||
inhalt=db.j({"text": wahr, "wahr": True, "paar": paar,
|
||||
@@ -334,28 +413,37 @@ async def _aussagen_verifizieren(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
return None
|
||||
return urteil
|
||||
|
||||
entschieden: dict[int, str] = {}
|
||||
for k in kandidaten:
|
||||
votes = [u for s in stimmen if (u := urteil_fuer(k["id"])(s)) is not None]
|
||||
if len(votes) < VERIFY_PANEL:
|
||||
continue
|
||||
db.update("artefakte", "id", k["id"],
|
||||
status="verifiziert" if all(votes) else "verworfen")
|
||||
entschieden[k["id"]] = "verifiziert" if all(votes) else "verworfen"
|
||||
# Paare fallen GANZ: kippt eine Seite, fällt auch die andere (frisch
|
||||
# verifiziert oder unentschieden) — ein halbes Paar prüft schief.
|
||||
paar_von = {k["id"]: str(db.uj(k["inhalt"], {}).get("paar", "")) for k in kandidaten}
|
||||
gefallen = {paar_von[kid] for kid, st in entschieden.items() if st == "verworfen"}
|
||||
for k in kandidaten:
|
||||
status = entschieden.get(k["id"])
|
||||
if paar_von[k["id"]] in gefallen:
|
||||
status = "verworfen"
|
||||
if status:
|
||||
db.update("artefakte", "id", k["id"], status=status)
|
||||
|
||||
|
||||
async def _kette(ctx: llm.Kontext, chunk: list[dict]) -> None:
|
||||
"""gen→gate→verify EINES Chunks verkettet — der Verify braucht nur die eigenen
|
||||
Kandidaten, nicht die Generierung fremder Chunks (Stage-Barriere kostete
|
||||
Wall-Clock: 1 Nachzügler blockierte alle Verifies)."""
|
||||
ohne = [a for a in chunk if not db.one(
|
||||
"SELECT id FROM artefakte WHERE atom_id=? AND typ='flashcard'"
|
||||
" AND status!='verworfen' LIMIT 1", (a["id"],))]
|
||||
ohne = [a for a in chunk if _gen_bedarf(a["id"])]
|
||||
if ohne:
|
||||
await _generieren(ctx, ohne)
|
||||
_formen_gate(ctx.topic, [a["id"] for a in chunk])
|
||||
await _verifizieren(ctx, chunk)
|
||||
ohne_aussage = [a for a in chunk if not db.one(
|
||||
"SELECT id FROM artefakte WHERE atom_id=? AND typ='aussage'"
|
||||
" AND status!='verworfen' LIMIT 1", (a["id"],))]
|
||||
_paare_bereinigen([a["id"] for a in chunk])
|
||||
ohne_aussage = [a for a in chunk
|
||||
if _paar_lage(a["id"])[0] < AUSSAGEN_JE_ATOM
|
||||
and not _aufgegeben(a["id"], "aussage")]
|
||||
if ohne_aussage:
|
||||
await _aussagen_generieren(ctx, ohne_aussage)
|
||||
await _aussagen_verifizieren(ctx, chunk)
|
||||
@@ -367,19 +455,37 @@ async def bauen(ctx: llm.Kontext) -> None:
|
||||
|
||||
|
||||
def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
"""Vollständigkeit lebend-basiert (kandidat zählt mit — der ist unterwegs und
|
||||
wird als `artefakt_unentschieden` separat gemeldet). Ziel: 2 Flashcards,
|
||||
1 Beispiel, AUSSAGEN_JE_ATOM komplette Paare je Atom; am Cap stattdessen
|
||||
`artefakt_aufgegeben` (sichtbar, aber reparieren fasst es nicht mehr an)."""
|
||||
befunde = []
|
||||
|
||||
def fehlt(atom: dict, typ: str, art: str) -> None:
|
||||
if _aufgegeben(atom["id"], typ):
|
||||
befunde.append({"art": "artefakt_aufgegeben", "item": f"{atom['id']}-{typ}",
|
||||
"detail": atom["titel"]})
|
||||
else:
|
||||
befunde.append({"art": art, "item": str(atom["id"]), "detail": atom["titel"]})
|
||||
|
||||
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"]})
|
||||
if not db.one("SELECT id FROM artefakte WHERE atom_id=? AND typ='aussage'"
|
||||
" AND status='verifiziert' LIMIT 1", (a["id"],)):
|
||||
befunde.append({"art": "atom_ohne_aussage", "item": str(a["id"]),
|
||||
fc = len(_lebende(a["id"], "flashcard"))
|
||||
if fc < FC_ZIEL:
|
||||
fehlt(a, "flashcard",
|
||||
"atom_ohne_flashcard" if fc == 0 else "flashcard_unvollstaendig")
|
||||
if not _lebende(a["id"], "beispiel"):
|
||||
fehlt(a, "beispiel", "atom_ohne_beispiel")
|
||||
komplett, einseitig, _ = _paar_lage(a["id"])
|
||||
if einseitig:
|
||||
# immer reparierbar: _paare_bereinigen ist deterministisch (kein LLM),
|
||||
# der Cap begrenzt nur die NACHgenerierung (ohne_aussage-Filter in _kette)
|
||||
befunde.append({"art": "paar_unvollstaendig", "item": str(a["id"]),
|
||||
"detail": a["titel"]})
|
||||
elif komplett < AUSSAGEN_JE_ATOM:
|
||||
fehlt(a, "aussage",
|
||||
"atom_ohne_aussage" if komplett == 0 else "paar_unvollstaendig")
|
||||
offen = db.query("SELECT id FROM artefakte WHERE atom_id=? AND status='kandidat'",
|
||||
(a["id"],))
|
||||
for k in offen:
|
||||
@@ -388,9 +494,15 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
return befunde
|
||||
|
||||
|
||||
# Arten, bei denen bauen() etwas bewegen kann. aufgegeben (Cap erreicht) und
|
||||
# unentschieden (Panel-Ausfall) allein → kein Lauf, sonst churnt der Auto-Loop leer.
|
||||
_REPARIERBAR = {"atom_ohne_flashcard", "flashcard_unvollstaendig", "atom_ohne_beispiel",
|
||||
"atom_ohne_aussage", "paar_unvollstaendig"}
|
||||
|
||||
|
||||
async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
ctx.ebene = EBENE
|
||||
if not befunde:
|
||||
if not any(b["art"] in _REPARIERBAR for b in befunde):
|
||||
return False
|
||||
await bauen(ctx) # idempotent: generiert Fehlendes nach, prüft Offenes
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user