update
This commit is contained in:
@@ -7,6 +7,7 @@ Danach Soll-Zuordnung je Atom; Soll-Punkte ohne Atom lösen gezielte Nachextrakt
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
import db
|
||||
import embedding
|
||||
@@ -22,7 +23,7 @@ log = logging.getLogger("creator2.inventar")
|
||||
EBENE = "inventar"
|
||||
TYPEN = ("begriff", "aussage", "verfahren")
|
||||
LEVELS = ("E", "M", "S")
|
||||
PAAR_CHUNK = 40 # Merge-Paare pro Judge-Call (Lektion 34/55)
|
||||
PAAR_CHUNK = 10 # Merge-Paare pro Judge-Call (40 riss das Output-Cap: 58–71 % Parse-Fehler)
|
||||
|
||||
|
||||
def aktive_atome(topic: str) -> list[dict]:
|
||||
@@ -32,25 +33,39 @@ def aktive_atome(topic: str) -> list[dict]:
|
||||
|
||||
# ── Extraktion ────────────────────────────────────────────────────────────────
|
||||
|
||||
async def _extrahiere_quelle(ctx: llm.Kontext, quelle: dict) -> None:
|
||||
async def _extrahiere_quelle(ctx: llm.Kontext, quelle: dict, force: bool = False) -> None:
|
||||
# Idempotenz-Guard: hat die Quelle schon verankerte Atome (z. B. nach Soll-Reset),
|
||||
# wird sie nicht erneut gelesen — sonst Doppel-Extraktion.
|
||||
# wird sie nicht erneut gelesen — sonst Doppel-Extraktion. force=True (Repair
|
||||
# quelle_unvollstaendig) liest trotzdem; Doppelte fängt der Anker-Dedup.
|
||||
schon = db.one(
|
||||
"SELECT a.id FROM atome a JOIN anker k ON k.atom_id=a.id"
|
||||
" WHERE a.topic=? AND k.quelle_id=? AND a.status NOT IN ('gemerged','verworfen')"
|
||||
" LIMIT 1", (ctx.topic, quelle["id"]))
|
||||
if schon:
|
||||
if schon and not force:
|
||||
db.update("quellen", "id", quelle["id"], status="atome")
|
||||
return
|
||||
text = korpus.quelltext(quelle)
|
||||
# aufgaben-Quellen: das GEÜBTE Konzept destillieren, nie die Aufgabeninstanz
|
||||
template = ("Atom-Extraktion-Aufgaben" if quelle.get("rolle") == "aufgaben"
|
||||
else "Atom-Extraktion")
|
||||
fehl: list[int] = []
|
||||
|
||||
async def reader(offset: int, chunk: str, r: int) -> None:
|
||||
async def reader(offset: int, chunk: str, r: int, tiefe: int = 0) -> None:
|
||||
res = await llm.call(ctx, stage="extraktion", template=template,
|
||||
werte={"topic": ctx.topic, "quelle": quelle["titel"], "text": chunk},
|
||||
role="quick", item=f"q{quelle['id']}-o{offset}-r{r}", erwartet=list)
|
||||
role="quick",
|
||||
item=f"q{quelle['id']}-o{offset}-r{r}" + "-h" * tiefe,
|
||||
erwartet=list)
|
||||
if res is None:
|
||||
# Leerantwort/Parse-Fehler ist meist das Output-Cap (stop=max_tokens,
|
||||
# deterministisch — stumpfes Wiederholen hilft nie): Chunk halbieren.
|
||||
if tiefe < 2 and len(chunk) > 2000:
|
||||
mitte = len(chunk) // 2
|
||||
await asyncio.gather(reader(offset, chunk[:mitte], r, tiefe + 1),
|
||||
reader(offset + mitte, chunk[mitte:], r, tiefe + 1))
|
||||
else:
|
||||
fehl.append(offset) # nicht still: Quelle bleibt unvollständig
|
||||
return
|
||||
for a in res or []:
|
||||
titel = str(a.get("titel", "")).strip()
|
||||
definition = str(a.get("definition", "")).strip()
|
||||
@@ -79,7 +94,9 @@ async def _extrahiere_quelle(ctx: llm.Kontext, quelle: dict) -> None:
|
||||
await asyncio.gather(*(reader(off, chunk, r)
|
||||
for off, chunk in stuecke
|
||||
for r in range(READER_JE_ABSCHNITT)))
|
||||
db.update("quellen", "id", quelle["id"], status="atome")
|
||||
if fehl: # stiller Chunk-Verlust hieß bisher trotzdem „atome" (Lernen: 41 Calls leer)
|
||||
log.warning("Quelle %s: %d Abschnitt(e) ohne Extraktion", quelle["id"], len(fehl))
|
||||
db.update("quellen", "id", quelle["id"], status="teilweise" if fehl else "atome")
|
||||
|
||||
|
||||
# ── Dedup ─────────────────────────────────────────────────────────────────────
|
||||
@@ -202,7 +219,11 @@ async def _judge_dedup(ctx: llm.Kontext) -> None:
|
||||
if isinstance(e, dict) and e.get("paar") == n:
|
||||
urteile.append(bool(e.get("gleich")))
|
||||
break
|
||||
if len(urteile) == MERGE_PANEL and all(urteile):
|
||||
if len(urteile) < MERGE_PANEL:
|
||||
continue # Panel unvollständig (Parse-/Infra-Ausfall): Paar bleibt
|
||||
# offen für die nächste Runde — ein dauerhaftes „nein" ließ
|
||||
# Dubletten systematisch überleben (aak: 42/73 Calls unparsbar)
|
||||
if all(urteile):
|
||||
gew, ver = ((a["id"], b["id"]) if len(a["definition"]) >= len(b["definition"])
|
||||
else (b["id"], a["id"]))
|
||||
_merge(topic, gew, ver)
|
||||
@@ -221,12 +242,20 @@ def _erster_anker(atom_id: int) -> str:
|
||||
|
||||
|
||||
def _kanten_aufloesen(topic: str) -> None:
|
||||
"""braucht-Titel → Atom-IDs (Norm-Gleichheit; Unauflösbares fällt still weg)."""
|
||||
"""braucht-Titel → Atom-IDs. Norm-Gleichheit zuerst, Titel-Kern als zweite
|
||||
Stufe (Norm allein löste nur 72–79 % auf — der Rest fiel still weg und
|
||||
Ordnung/Level-Logik arbeiteten auf einem lückigen Graph)."""
|
||||
atome = aktive_atome(topic)
|
||||
je_norm = {textkit.norm(a["titel"]): a["id"] for a in atome}
|
||||
je_kern: dict[str, int] = {}
|
||||
for a in atome:
|
||||
if kern := textkit.titel_kern(a["titel"]):
|
||||
je_kern.setdefault(kern, a["id"])
|
||||
for a in atome:
|
||||
for titel in db.uj(a["braucht"]):
|
||||
ziel = je_norm.get(textkit.norm(titel))
|
||||
if not ziel and (kern := textkit.titel_kern(titel)):
|
||||
ziel = je_kern.get(kern)
|
||||
if ziel and ziel != a["id"]:
|
||||
db.execute("INSERT OR IGNORE INTO kanten(topic, von_atom, zu_atom, art)"
|
||||
" VALUES(?,?,?,'braucht')", (topic, a["id"], ziel))
|
||||
@@ -269,6 +298,20 @@ async def bauen(ctx: llm.Kontext) -> None:
|
||||
|
||||
# ── QA + Repair ───────────────────────────────────────────────────────────────
|
||||
|
||||
_TITEL_KATALOG = re.compile(r"^(Satz|Lemma|Korollar|Bemerkung|Definition|Aufgabe"
|
||||
r"|Übung|Transformation|Theorem)\s+[IVX]*\d", re.IGNORECASE)
|
||||
|
||||
|
||||
def _titel_kaputt(titel: str) -> bool:
|
||||
"""Katalognummern-Titel (Satz 6.26 — der Leser hat kein nummeriertes Skript)
|
||||
und abgerissene Fragmente (aak: „Teste für alle V 0 ⊆ V mit |")."""
|
||||
if _TITEL_KATALOG.match(titel):
|
||||
return True
|
||||
if titel.rstrip().endswith(("|", ",", ";", ":", "-", "–", "(", "{", "⊆", "=")):
|
||||
return True
|
||||
return titel.count("(") != titel.count(")")
|
||||
|
||||
|
||||
def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
befunde = []
|
||||
atome = aktive_atome(ctx.topic)
|
||||
@@ -281,9 +324,15 @@ def messen(ctx: llm.Kontext) -> list[dict]:
|
||||
befunde.append({"art": "atom_ohne_soll", "item": str(a["id"]), "detail": a["titel"]})
|
||||
else:
|
||||
belegte_soll.add(a["soll_id"])
|
||||
if _titel_kaputt(a["titel"]):
|
||||
befunde.append({"art": "titel_katalog", "item": str(a["id"]), "detail": a["titel"]})
|
||||
for p in db.query("SELECT * FROM soll WHERE topic=? AND status='bestaetigt'", (ctx.topic,)):
|
||||
if p["id"] not in belegte_soll:
|
||||
befunde.append({"art": "soll_ohne_atom", "item": str(p["id"]), "detail": p["punkt"]})
|
||||
for q in db.query("SELECT * FROM quellen WHERE topic=? AND status='teilweise'",
|
||||
(ctx.topic,)):
|
||||
befunde.append({"art": "quelle_unvollstaendig", "item": str(q["id"]),
|
||||
"detail": q["titel"]})
|
||||
return befunde
|
||||
|
||||
|
||||
@@ -299,6 +348,11 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
return await _soll_stichentscheid(ctx, item)
|
||||
if b["art"] == "soll_ohne_atom":
|
||||
return await _luecke_schliessen(ctx, item)
|
||||
if b["art"] == "quelle_unvollstaendig":
|
||||
q = db.one("SELECT * FROM quellen WHERE id=?", (item,))
|
||||
if q:
|
||||
await _extrahiere_quelle(ctx, q, force=True)
|
||||
return True
|
||||
return False
|
||||
|
||||
# Anker-Fixes gebündelt (Batch je Quelle statt Atom×Quelle — war 83 % der
|
||||
@@ -306,7 +360,11 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
anker_ids = [int(b["item"]) for b in befunde
|
||||
if b["art"] == "atom_ohne_anker" and str(b["item"]).isdigit()]
|
||||
bewegt = await _anker_fixen_batch(ctx, anker_ids) if anker_ids else False
|
||||
rest = [b for b in befunde if b["art"] != "atom_ohne_anker"]
|
||||
titel_ids = [int(b["item"]) for b in befunde
|
||||
if b["art"] == "titel_katalog" and str(b["item"]).isdigit()]
|
||||
if titel_ids:
|
||||
bewegt |= await _titel_fixen(ctx, titel_ids)
|
||||
rest = [b for b in befunde if b["art"] not in ("atom_ohne_anker", "titel_katalog")]
|
||||
bewegt |= any(await llm.alle(einer(b) for b in rest))
|
||||
if bewegt:
|
||||
_anker_dedup(ctx.topic)
|
||||
@@ -316,6 +374,27 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
return bewegt
|
||||
|
||||
|
||||
async def _titel_fixen(ctx: llm.Kontext, atom_ids: list[int]) -> bool:
|
||||
"""Kaputte Titel (Katalognummern, Fragmente) aus der Definition neu benennen."""
|
||||
atome = [a for i in atom_ids if (a := db.one("SELECT * FROM atome WHERE id=?", (i,)))
|
||||
and a["status"] not in ("gemerged", "verworfen")]
|
||||
if not atome:
|
||||
return False
|
||||
liste = "\n".join(f"{a['id']}: {a['titel']} — {a['definition']}" for a in atome)
|
||||
res = await llm.call(ctx, stage="titel_fix", template="Atom-Titel-Fix",
|
||||
schritt="fix", werte={"atome": liste}, role="judge",
|
||||
n=len(atome), item=f"t{atome[0]['id']}", erwartet=list)
|
||||
gueltig = {a["id"] for a in atome}
|
||||
bewegt = False
|
||||
for e in res or []:
|
||||
if isinstance(e, dict) and e.get("atom") in gueltig:
|
||||
titel = str(e.get("titel", "")).strip()
|
||||
if titel and not _titel_kaputt(titel):
|
||||
db.update("atome", "id", e["atom"], titel=titel)
|
||||
bewegt = True
|
||||
return bewegt
|
||||
|
||||
|
||||
ANKER_FIX_CHUNK = 20 # Atome pro Batch-Call (ein Quelltext-Abschnitt trägt viele Fixes)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user