update
This commit is contained in:
@@ -34,15 +34,15 @@ def aktive_atome(topic: str) -> list[dict]:
|
||||
# ── Extraktion ────────────────────────────────────────────────────────────────
|
||||
|
||||
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. 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 and not force:
|
||||
db.update("quellen", "id", quelle["id"], status="atome")
|
||||
# Idempotenz-Guard über `atome_stand` (nicht „hat irgendein Anker-Atom"): dieser
|
||||
# Merker wird erst NACH einem vollständigen Extraktions-Pass gesetzt. Bricht der
|
||||
# Lauf mitten in der Extraktion ab (einige Reader haben schon eingefügt, Merker
|
||||
# noch leer), liest der Resume die Quelle komplett neu — sonst gingen die Atome
|
||||
# der ungelesenen Abschnitte still verloren. Doppelte fängt der Anker-Overlap-Merge.
|
||||
# force=True (Repair quelle_unvollstaendig) liest ohnehin neu.
|
||||
stand = db.one("SELECT atome_stand FROM quellen WHERE id=?", (quelle["id"],))
|
||||
if stand and stand["atome_stand"] and not force:
|
||||
db.update("quellen", "id", quelle["id"], status=stand["atome_stand"])
|
||||
return
|
||||
text = korpus.quelltext(quelle)
|
||||
# aufgaben-Quellen: das GEÜBTE Konzept destillieren, nie die Aufgabeninstanz
|
||||
@@ -61,8 +61,8 @@ async def _extrahiere_quelle(ctx: llm.Kontext, quelle: dict, force: bool = False
|
||||
# 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))
|
||||
await llm.alle((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
|
||||
@@ -91,12 +91,14 @@ async def _extrahiere_quelle(ctx: llm.Kontext, quelle: dict, force: bool = False
|
||||
start=-1, ende=-1, zitat=zitat)
|
||||
|
||||
stuecke = textkit.abschnitte(text)
|
||||
await asyncio.gather(*(reader(off, chunk, r)
|
||||
for off, chunk in stuecke
|
||||
for r in range(READER_JE_ABSCHNITT)))
|
||||
await llm.alle(reader(off, chunk, r)
|
||||
for off, chunk in stuecke
|
||||
for r in range(READER_JE_ABSCHNITT))
|
||||
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")
|
||||
endstand = "teilweise" if fehl else "atome"
|
||||
# Merker erst JETZT setzen (nach vollständigem Pass) — er ist das Resume-Gate
|
||||
db.update("quellen", "id", quelle["id"], status=endstand, atome_stand=endstand)
|
||||
|
||||
|
||||
# ── Dedup ─────────────────────────────────────────────────────────────────────
|
||||
@@ -161,6 +163,9 @@ def _anker_dedup(topic: str) -> None:
|
||||
if passt:
|
||||
gew, ver = (a_id, b_id) if len(je_def[a_id]) >= len(je_def[b_id]) else (b_id, a_id)
|
||||
_merge(topic, gew, ver)
|
||||
# Verlierer-Anker in-memory erben, sonst übersteht eine transitive
|
||||
# Dublette (C überlappt nur mit B, B ging in A) diesen Pass.
|
||||
anker[gew].extend(anker[ver])
|
||||
|
||||
|
||||
def _geprueft(a: int, b: int) -> bool:
|
||||
@@ -233,7 +238,7 @@ async def _judge_dedup(ctx: llm.Kontext) -> None:
|
||||
" VALUES(?,?,?,'verwandt','nein')", (topic, lo, hi))
|
||||
|
||||
chunks = [offen[i:i + PAAR_CHUNK] for i in range(0, len(offen), PAAR_CHUNK)]
|
||||
await asyncio.gather(*(chunk_urteil(c) for c in chunks))
|
||||
await llm.alle(chunk_urteil(c) for c in chunks)
|
||||
|
||||
|
||||
def _erster_anker(atom_id: int) -> str:
|
||||
@@ -283,13 +288,13 @@ async def _soll_zuordnen(ctx: llm.Kontext, nur_offene: bool = True) -> None:
|
||||
db.update("atome", "id", a["id"], soll_id=sid)
|
||||
|
||||
chunks = [atome[i:i + ZIELE_CHUNK_ATOME] for i in range(0, len(atome), ZIELE_CHUNK_ATOME)]
|
||||
await asyncio.gather(*(chunk_zuordnen(c) for c in chunks))
|
||||
await llm.alle(chunk_zuordnen(c) for c in chunks)
|
||||
|
||||
|
||||
async def bauen(ctx: llm.Kontext) -> None:
|
||||
ctx.ebene = EBENE
|
||||
offene = db.query("SELECT * FROM quellen WHERE topic=? AND status='extrahiert'", (ctx.topic,))
|
||||
await asyncio.gather(*(_extrahiere_quelle(ctx, q) for q in offene))
|
||||
await llm.alle(_extrahiere_quelle(ctx, q) for q in offene)
|
||||
_anker_dedup(ctx.topic)
|
||||
await _judge_dedup(ctx)
|
||||
_kanten_aufloesen(ctx.topic)
|
||||
@@ -351,8 +356,19 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
if b["art"] == "quelle_unvollstaendig":
|
||||
q = db.one("SELECT * FROM quellen WHERE id=?", (item,))
|
||||
if q:
|
||||
vorher = db.one(
|
||||
"SELECT COUNT(*) AS n FROM anker k JOIN atome a ON a.id=k.atom_id"
|
||||
" WHERE a.topic=? AND k.quelle_id=? AND a.status NOT IN"
|
||||
" ('gemerged','verworfen')", (ctx.topic, q["id"]))["n"]
|
||||
await _extrahiere_quelle(ctx, q, force=True)
|
||||
return True
|
||||
nachher = db.one(
|
||||
"SELECT COUNT(*) AS n FROM anker k JOIN atome a ON a.id=k.atom_id"
|
||||
" WHERE a.topic=? AND k.quelle_id=? AND a.status NOT IN"
|
||||
" ('gemerged','verworfen')", (ctx.topic, q["id"]))["n"]
|
||||
# ehrliches bewegt: blieb die Quelle 'teilweise' ohne neue Anker,
|
||||
# erkennt auto_loop Stillstand statt 10 Token-Runden zu drehen
|
||||
nq = db.one("SELECT status FROM quellen WHERE id=?", (q["id"],))
|
||||
return nq["status"] == "atome" or nachher > vorher
|
||||
return False
|
||||
|
||||
# Anker-Fixes gebündelt (Batch je Quelle statt Atom×Quelle — war 83 % der
|
||||
@@ -411,6 +427,8 @@ async def _anker_fixen_batch(ctx: llm.Kontext, atom_ids: list[int]) -> bool:
|
||||
offen[atom_id] = a
|
||||
quellen = db.query("SELECT * FROM quellen WHERE topic=?", (ctx.topic,))
|
||||
texte = {q["id"]: korpus.quelltext(q) for q in quellen}
|
||||
start_ids = set(offen)
|
||||
ausgefallen: set[int] = set() # Atome, deren zuständiger Call ausfiel (None)
|
||||
|
||||
for atom_id in list(offen):
|
||||
for roh in db.query("SELECT * FROM anker WHERE atom_id=? AND start<0", (atom_id,)):
|
||||
@@ -432,6 +450,9 @@ async def _anker_fixen_batch(ctx: llm.Kontext, atom_ids: list[int]) -> bool:
|
||||
schritt="fix", role="judge", n=len(ids),
|
||||
item=f"q{q['id']}-o{offset}",
|
||||
werte={"atome": liste, "text": chunk_text}, erwartet=list)
|
||||
if res is None: # Call-Ausfall ≠ „keine Quellstelle" → nicht verwerfen
|
||||
ausgefallen.update(i for i in ids if i in offen)
|
||||
return
|
||||
for e in res or []:
|
||||
atom_id = e.get("atom")
|
||||
zitat = str(e.get("zitat", "")).strip()
|
||||
@@ -453,9 +474,13 @@ async def _anker_fixen_batch(ctx: llm.Kontext, atom_ids: list[int]) -> bool:
|
||||
break
|
||||
await llm.alle(abschnitt_call(q, offset, chunk_text, ids[i:i + ANKER_FIX_CHUNK])
|
||||
for i in range(0, len(ids), ANKER_FIX_CHUNK))
|
||||
for atom_id in offen:
|
||||
# Verwerfen nur, wo ALLE zuständigen Calls liefen (kein Ausfall) und keiner
|
||||
# eine Quellstelle fand. Ausgefallene bleiben ohne_anker → nächste Runde erneut.
|
||||
verworfen = [i for i in offen if i not in ausgefallen]
|
||||
for atom_id in verworfen:
|
||||
db.update("atome", "id", atom_id, status="verworfen")
|
||||
return bool(atom_ids)
|
||||
verankert = start_ids - set(offen)
|
||||
return bool(verankert or verworfen) # ehrliches „bewegt" (kein Ausfall-Dauerschleifen)
|
||||
|
||||
|
||||
async def _soll_stichentscheid(ctx: llm.Kontext, atom_id: int) -> bool:
|
||||
@@ -471,14 +496,16 @@ async def _soll_stichentscheid(ctx: llm.Kontext, atom_id: int) -> bool:
|
||||
werte={"titel": a["titel"], "definition": a["definition"],
|
||||
"beleg": _erster_anker(atom_id), "soll": soll_liste},
|
||||
erwartet=dict)
|
||||
urteile = [s.get("soll", "fremd") for s in stimmen]
|
||||
if urteile and all(u == "fremd" for u in urteile):
|
||||
# Verwerfen ist destruktiv → nur bei vollzähligem „fremd"-Panel. Eine Stimme
|
||||
# ohne soll-Feld ist KEIN fremd-Votum (zählte bisher fälschlich mit).
|
||||
if llm.einstimmig(stimmen, 2, lambda s: (s.get("soll") == "fremd")
|
||||
if "soll" in s else None):
|
||||
db.update("atome", "id", atom_id, status="verworfen")
|
||||
return True
|
||||
gueltig = {p["id"] for p in punkte}
|
||||
for u in urteile:
|
||||
if u in gueltig:
|
||||
db.update("atome", "id", atom_id, soll_id=u)
|
||||
for s in stimmen: # Zuordnung ist nicht destruktiv → Mehrheit/erste gültige reicht
|
||||
if s.get("soll") in gueltig:
|
||||
db.update("atome", "id", atom_id, soll_id=s["soll"])
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -490,16 +517,24 @@ async def _luecke_schliessen(ctx: llm.Kontext, soll_id: int) -> bool:
|
||||
p = db.one("SELECT * FROM soll WHERE id=?", (soll_id,))
|
||||
if not p or p["status"] != "bestaetigt":
|
||||
return False
|
||||
runden = db.one( # bisherige Nachextraktions-Versuche: aus dem Ledger, topic-weit
|
||||
# bisherige ECHTE Nachextraktions-Runden: nur erfolgreiche Calls (status='ok';
|
||||
# Parse-/Infra-Retries schreiben eigene Zeilen) und nur seit dem letzten
|
||||
# Inventar-Reset (Alt-Läufe zählten sonst mit → zu früher Freispruch).
|
||||
resets = db.uj(db.one("SELECT resets FROM topics WHERE name=?", (ctx.topic,))["resets"], {})
|
||||
seit = resets.get("inventar", 0)
|
||||
runden = db.one(
|
||||
"SELECT COUNT(*) AS n FROM events e JOIN runs r ON e.run_id=r.id"
|
||||
" WHERE r.topic=? AND e.stage='luecke' AND e.item=?", (ctx.topic, f"s{soll_id}"))["n"]
|
||||
" WHERE r.topic=? AND e.stage='luecke' AND e.item=? AND e.status='ok' AND e.id>?",
|
||||
(ctx.topic, f"s{soll_id}", seit))["n"]
|
||||
if runden >= LUECKEN_RUNDEN_MAX:
|
||||
atome = aktive_atome(ctx.topic)
|
||||
liste = "\n".join(f"- {a['titel']}: {a['definition']}" for a in atome[:200])
|
||||
stimmen = await llm.panel(ctx, 2, stage="luecke_stich", template="Soll-Abgedeckt",
|
||||
schritt="soll_zuordnung", role="judge", item=f"s{soll_id}",
|
||||
werte={"punkt": p["punkt"], "atome": liste}, erwartet=dict)
|
||||
if stimmen and all(bool(s.get("abgedeckt")) for s in stimmen):
|
||||
# Freispruch ist destruktiv fürs Soll → nur bei vollzähligem Panel
|
||||
if llm.einstimmig(stimmen, 2, lambda s: bool(s.get("abgedeckt"))
|
||||
if "abgedeckt" in s else None):
|
||||
db.update("soll", "id", soll_id, status="abgedeckt")
|
||||
return True
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user