update
This commit is contained in:
@@ -129,7 +129,7 @@ async def _recherche_runde(ctx: llm.Kontext, runde: int) -> int:
|
||||
erwartet=list)
|
||||
return res or []
|
||||
|
||||
ergebnisse = await asyncio.gather(*(eine(lens) for lens in RECHERCHE_LENSES))
|
||||
ergebnisse = await llm.alle(eine(lens) for lens in RECHERCHE_LENSES)
|
||||
neu = 0
|
||||
for lens, quellen in zip(RECHERCHE_LENSES, ergebnisse):
|
||||
for q in quellen:
|
||||
@@ -159,6 +159,15 @@ async def _soll_extrahieren(ctx: llm.Kontext) -> None:
|
||||
offene = db.query("SELECT * FROM quellen WHERE topic=? AND status='neu'", (ctx.topic,))
|
||||
|
||||
async def eine(q: dict) -> None:
|
||||
# Resume-Idempotenz: Kandidaten eines abgebrochenen Passes dieser Quelle
|
||||
# zuerst entfernen (Status='neu' ⇒ Konsens lief noch nicht ⇒ diese
|
||||
# Kandidaten stammen nur aus dem unterbrochenen Lauf) — sonst blähen
|
||||
# Doppel-Kandidaten das √n-Konsensband auf.
|
||||
for r in db.query("SELECT id, belege FROM soll WHERE topic=? AND status='kandidat'",
|
||||
(ctx.topic,)):
|
||||
belege = db.uj(r["belege"])
|
||||
if belege and belege[0].get("quelle") == q["id"]:
|
||||
db.execute("DELETE FROM soll WHERE id=?", (r["id"],))
|
||||
text = quelltext(q)
|
||||
for i, (_, chunk) in enumerate(textkit.abschnitte(text, SOLL_CHUNK_CHARS)):
|
||||
res = await llm.call(ctx, stage="soll", template="Korpus-Soll",
|
||||
@@ -173,7 +182,7 @@ async def _soll_extrahieren(ctx: llm.Kontext) -> None:
|
||||
belege=db.j([{"quelle": q["id"], "zitat": zitat}]))
|
||||
db.update("quellen", "id", q["id"], status="extrahiert")
|
||||
|
||||
await asyncio.gather(*(eine(q) for q in offene))
|
||||
await llm.alle(eine(q) for q in offene)
|
||||
|
||||
|
||||
def _min_belege(topic: str) -> int:
|
||||
@@ -267,11 +276,37 @@ async def _konsens(ctx: llm.Kontext) -> int:
|
||||
belege = [b for i in p["ids"] for b in db.uj(je_id[i]["belege"])]
|
||||
if len({b["quelle"] for b in belege}) >= min_belege:
|
||||
bestaetigt.append((p, belege))
|
||||
for r in db.query("SELECT id FROM soll WHERE topic=? AND status='bestaetigt'", (ctx.topic,)):
|
||||
db.execute("DELETE FROM soll WHERE id=?", (r["id"],)) # idempotent: neu aufbauen
|
||||
# Bestehende bestätigte Zeilen NICHT löschen und neu bauen (das warf die von
|
||||
# _belege_nachsuchen ergänzten Belege + den geprueft-Cache weg und churnte die
|
||||
# soll.id). Stattdessen abgleichen: Match über punkt-Kern, sonst über einen
|
||||
# gemeinsamen (quelle,zitat)-Beleg. Treffer → Belege-Union, geprueft bleibt.
|
||||
alt = db.query("SELECT * FROM soll WHERE topic=? AND status='bestaetigt'", (ctx.topic,))
|
||||
alt_kern: dict[str, dict] = {}
|
||||
alt_beleg: dict[tuple, dict] = {}
|
||||
for r in alt:
|
||||
alt_kern.setdefault(r["punkt"].casefold(), r)
|
||||
for b in db.uj(r["belege"]):
|
||||
alt_beleg.setdefault((b["quelle"], b["zitat"]), r)
|
||||
behalten: set[int] = set()
|
||||
for p, belege in bestaetigt:
|
||||
db.insert("soll", topic=ctx.topic, punkt=p["punkt"], status="bestaetigt",
|
||||
belege=db.j(belege))
|
||||
treffer = alt_kern.get(p["punkt"].casefold())
|
||||
if treffer is None:
|
||||
for b in belege:
|
||||
if (treffer := alt_beleg.get((b["quelle"], b["zitat"]))):
|
||||
break
|
||||
if treffer is not None:
|
||||
vorhanden = db.uj(treffer["belege"])
|
||||
gesehen = {(b["quelle"], b["zitat"]) for b in vorhanden}
|
||||
vereint = vorhanden + [b for b in belege
|
||||
if (b["quelle"], b["zitat"]) not in gesehen]
|
||||
db.update("soll", "id", treffer["id"], punkt=p["punkt"], belege=db.j(vereint))
|
||||
behalten.add(treffer["id"])
|
||||
else:
|
||||
behalten.add(db.insert("soll", topic=ctx.topic, punkt=p["punkt"],
|
||||
status="bestaetigt", belege=db.j(belege)))
|
||||
for r in alt: # verwaiste bestätigte Zeilen (kein Konsens mehr) entfernen
|
||||
if r["id"] not in behalten:
|
||||
db.execute("DELETE FROM soll WHERE id=?", (r["id"],))
|
||||
ok_ids = {i for p, _ in bestaetigt for i in p["ids"]}
|
||||
for i, k in je_id.items(): # Buchführung: kein Kandidat verschwindet still
|
||||
status = ("abgelehnt" if i in abgelehnt else
|
||||
@@ -310,7 +345,12 @@ async def bauen(ctx: llm.Kontext) -> None:
|
||||
if jetzt <= vorher >= 0: # Sättigung: Runde ohne neuen bestätigten Punkt
|
||||
break
|
||||
vorher = jetzt
|
||||
db.update("topics", "name", ctx.topic, status="korpus_fertig") # Auto-Freeze
|
||||
# Auto-Freeze nur bei ≥1 bestätigtem Punkt — sonst bleibt Status 'korpus', der
|
||||
# QA-Loop läuft an (soll_leer) und das Gate stoppt den Lauf fail-closed, statt
|
||||
# E1 mit leerem Soll zu starten (dort würde jedes Atom kaskadenartig verworfen).
|
||||
if db.one("SELECT COUNT(*) AS n FROM soll WHERE topic=? AND status='bestaetigt'",
|
||||
(ctx.topic,))["n"]:
|
||||
db.update("topics", "name", ctx.topic, status="korpus_fertig")
|
||||
|
||||
|
||||
# ── QA + Repair ───────────────────────────────────────────────────────────────
|
||||
@@ -361,22 +401,31 @@ async def _belege_nachsuchen(ctx: llm.Kontext, soll_ids: list[int]) -> bool:
|
||||
if not faellig:
|
||||
return
|
||||
text = quelltext(q)
|
||||
liste = "\n".join(f"{p['id']}: {p['punkt']}" for p in faellig)
|
||||
res = await llm.call(ctx, stage="soll_beleg", template="Korpus-Soll-Beleg",
|
||||
schritt="soll", role="judge", item=f"q{q['id']}",
|
||||
n=len(faellig),
|
||||
werte={"punkte": liste, "text": text[:SOLL_CHUNK_CHARS]},
|
||||
erwartet=list)
|
||||
if res is None:
|
||||
return # Ausfall: nicht als „geprüft" verbuchen
|
||||
offen_ids = {p["id"] for p in faellig}
|
||||
gefunden: dict[int, str] = {}
|
||||
for e in res:
|
||||
if not isinstance(e, dict):
|
||||
ausfall = False # ein Fenster-Ausfall → Quelle nicht vollständig geprüft
|
||||
# ALLE Fenster durchsuchen (nicht nur die ersten 40k) — sonst nagelt der
|
||||
# geprueft-Cache Belege hinter dem ersten Fenster dauerhaft fest.
|
||||
for fi, (_, chunk) in enumerate(textkit.abschnitte(text, SOLL_CHUNK_CHARS)):
|
||||
rest = [p for p in faellig if p["id"] in offen_ids]
|
||||
if not rest:
|
||||
break
|
||||
liste = "\n".join(f"{p['id']}: {p['punkt']}" for p in rest)
|
||||
res = await llm.call(ctx, stage="soll_beleg", template="Korpus-Soll-Beleg",
|
||||
schritt="soll", role="judge", item=f"q{q['id']}-{fi}",
|
||||
n=len(rest),
|
||||
werte={"punkte": liste, "text": chunk}, erwartet=list)
|
||||
if res is None:
|
||||
ausfall = True
|
||||
continue
|
||||
zitat = str(e.get("zitat", "")).strip()
|
||||
if zitat and e.get("soll") in {p["id"] for p in faellig} \
|
||||
and textkit.finde_zitat(text, zitat) is not None:
|
||||
gefunden[e["soll"]] = zitat
|
||||
for e in res:
|
||||
if not isinstance(e, dict):
|
||||
continue
|
||||
zitat = str(e.get("zitat", "")).strip()
|
||||
if zitat and e.get("soll") in offen_ids \
|
||||
and textkit.finde_zitat(chunk, zitat) is not None:
|
||||
gefunden[e["soll"]] = zitat
|
||||
offen_ids.discard(e["soll"])
|
||||
for p in faellig:
|
||||
frisch = db.one("SELECT * FROM soll WHERE id=?", (p["id"],))
|
||||
if p["id"] in gefunden:
|
||||
@@ -384,7 +433,7 @@ async def _belege_nachsuchen(ctx: llm.Kontext, soll_ids: list[int]) -> bool:
|
||||
"zitat": gefunden[p["id"]]}]
|
||||
db.update("soll", "id", p["id"], belege=db.j(belege))
|
||||
bewegt = True
|
||||
else:
|
||||
elif not ausfall: # nur eine lückenlos durchsuchte Quelle gilt als geprüft
|
||||
geprueft = sorted(set(db.uj(frisch["geprueft"])) | {q["id"]})
|
||||
db.update("soll", "id", p["id"], geprueft=db.j(geprueft))
|
||||
|
||||
@@ -414,10 +463,33 @@ async def reparieren(ctx: llm.Kontext, befunde: list[dict]) -> bool:
|
||||
(ctx.topic,))
|
||||
bewegt = bewegt or len(noch) < len(offene)
|
||||
if any(b["art"] in ("korpus_leer", "soll_leer") for b in befunde):
|
||||
topic = db.one("SELECT art FROM topics WHERE name=?", (ctx.topic,))
|
||||
if topic["art"] == "thema":
|
||||
art = db.one("SELECT art FROM topics WHERE name=?", (ctx.topic,))["art"]
|
||||
if art == "thema":
|
||||
await _recherche_runde(ctx, runde=99)
|
||||
await _soll_extrahieren(ctx)
|
||||
await _konsens(ctx)
|
||||
bewegt = True
|
||||
else: # uni: keine Recherche möglich — vorhandene Quellen neu falten,
|
||||
# ohne Kandidaten sogar frisch extrahieren; ehrliches bewegt
|
||||
vorher = db.one("SELECT COUNT(*) AS n FROM soll WHERE topic=?"
|
||||
" AND status='bestaetigt'", (ctx.topic,))["n"]
|
||||
if not db.query("SELECT id FROM soll WHERE topic=? AND"
|
||||
" status IN ('kandidat','gefaltet')", (ctx.topic,)):
|
||||
db.execute("UPDATE quellen SET status='neu' WHERE topic=?", (ctx.topic,))
|
||||
await _soll_extrahieren(ctx)
|
||||
await _konsens(ctx)
|
||||
nachher = db.one("SELECT COUNT(*) AS n FROM soll WHERE topic=?"
|
||||
" AND status='bestaetigt'", (ctx.topic,))["n"]
|
||||
bewegt = bewegt or nachher > vorher
|
||||
return bewegt
|
||||
|
||||
|
||||
def gate(ctx: llm.Kontext) -> str | None:
|
||||
"""Fail-closed-Gate vor dem Ebenen-Marker: ohne Quellen oder ohne bestätigten
|
||||
Soll-Punkt darf E1 nicht starten (leeres Soll verwirft in E1 jedes Atom)."""
|
||||
if not db.one("SELECT id FROM quellen WHERE topic=? LIMIT 1", (ctx.topic,)):
|
||||
return "keine Quellen"
|
||||
if not db.one("SELECT id FROM soll WHERE topic=? AND status='bestaetigt' LIMIT 1",
|
||||
(ctx.topic,)):
|
||||
return "kein bestätigter Soll-Punkt"
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user