This commit is contained in:
team3
2026-07-24 11:23:18 +02:00
parent cb68cd671b
commit 1b054497ed
49 changed files with 935 additions and 408 deletions

View File

@@ -1,7 +1,7 @@
"""Stufe 2: Atome extrahieren (2 Reader, Anker-Pflicht), Anker-Fix
(deterministisch vor LLM), Soll-Zuordnung, Lücken schließen (Nachextraktion →
Web-Nachrecherche → Freispruch-Panel). Gate zählt Coverage deterministisch."""
from . import config, db, engine, laden, llm, panels, textkit
from . import config, db, engine, graph, laden, llm, panels, textkit
def _quelle(qid: int) -> dict:
@@ -50,7 +50,7 @@ async def atom_extraktion(task: dict) -> engine.Ergebnis:
antwort = await llm.call(
run_id=task["run_id"], stufe="inventar", knoten="atom_extraktion",
item=task["item"], role="extraktion", n=len(aktive),
skill_namen=["extraktor", "deutsch-praezise", "atom-extraktion"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"thema": thema,
"abschnitte": "\n\n".join(f"=== ABSCHNITT {i} ===\n{c}"
for i, (_, _, c) in aktive.items())})
@@ -124,7 +124,7 @@ async def anker_fix(task: dict) -> engine.Ergebnis:
antwort = await llm.call(
run_id=task["run_id"], stufe="inventar", knoten="anker_fix",
item=f"{task['item']}:b{i}", role="extraktion", n=len(batch),
skill_namen=["extraktor", "deutsch-praezise", "anker-fix"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"atome": liste, "quelltext": text[:config.SOLL_CHUNK_CHARS]})
if antwort is None: # Call-Ausfall: neuer Versuch statt stilles Auslassen
raise RuntimeError("anker_fix ohne Ergebnis")
@@ -132,12 +132,12 @@ async def anker_fix(task: dict) -> engine.Ergebnis:
textkit.bloecke(antwort, "FIX") if b.get("id", "").isdigit()}
for z in batch:
neu = antworten.get(z["atom_id"], "")
span = textkit.finde_zitat(text, neu) if neu and neu != "VERWERFEN" else None
span = textkit.finde_zitat(text, neu) if neu and neu != "KEINER" else None
if span:
db.update("anker", "id=?", (z["anker_id"],),
start=span[0], ende=span[1], zitat=neu[:600])
llm_fix += 1
elif neu == "VERWERFEN":
elif neu == "KEINER":
db.update("atome", "id=?", (z["atom_id"],), status="verworfen")
verworfen += 1
# Verankerte sofort aktivieren — Dedup/Zuordnung sehen sonst nichts
@@ -162,7 +162,7 @@ async def soll_zuordnung(task: dict) -> engine.Ergebnis:
antwort = await llm.call(
run_id=task["run_id"], stufe="inventar", knoten="soll_zuordnung",
item=task["item"], role="judge", n=len(atome),
skill_namen=["richter", "deutsch-praezise", "soll-zuordnung"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"atome": "\n".join(f"[{a['id']}] {a['titel']}: {a['definition'][:150]}"
for a in atome),
"soll": "\n".join(f"[{s['id']}] {s['punkt']}" for s in soll)})
@@ -191,7 +191,7 @@ async def _freispruch(task: dict, soll: dict) -> bool:
text = await llm.call(
run_id=task["run_id"], stufe="inventar", knoten="luecke",
item=f"{task['item']}:p{i}", role="judge",
skill_namen=["richter", "deutsch-praezise", "luecke-freispruch"],
skill_namen=graph.skills_von(task["knoten"], extra="freispruch"),
werte={"soll_punkt": soll["punkt"], "auszuege": auszuege[:20000]})
urteile = textkit.bloecke(text or "", "URTEIL")
return urteile[0].get("urteil", "") if urteile else None
@@ -239,7 +239,7 @@ async def luecke(task: dict) -> engine.Ergebnis:
antwort = await llm.call(
run_id=task["run_id"], stufe="inventar", knoten="luecke",
item=task["item"], role="extraktion", n=len(aktive),
skill_namen=["extraktor", "deutsch-praezise", "luecke-nachextraktion"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"luecken": "\n\n".join(
f"=== LUECKE ===\nSOLL: {sid}\nPUNKT: {s['punkt']}\nFENSTER:\n{f}"
for sid, (_, s, f) in aktive.items())})
@@ -250,7 +250,7 @@ async def luecke(task: dict) -> engine.Ergebnis:
s = block.get("soll", "").strip()
if s.isdigit():
je_soll.setdefault(int(s), []).append(block)
nichts = {int(b["soll"]) for b in textkit.bloecke(antwort, "NICHTS")
nichts = {int(b["soll"]) for b in textkit.bloecke(antwort, "LEER")
if b.get("soll", "").strip().isdigit()}
gesamt = 0
for sid, (m, soll, _) in aktive.items():
@@ -306,7 +306,7 @@ async def verdichtung(task: dict) -> engine.Ergebnis:
text = await llm.call(
run_id=task["run_id"], stufe="inventar", knoten="verdichtung",
item=f"{task['item']}:p{i}", role="judge", n=len(atome),
skill_namen=["richter", "deutsch-praezise", "verdichtung"],
skill_namen=graph.skills_von(task["knoten"]),
werte={"soll_punkt": soll["punkt"], "atome": liste,
"ziel": config.VERDICHTUNG_ZIEL})
if text is None:
@@ -450,7 +450,8 @@ async def gate(task: dict) -> engine.Ergebnis:
sorted(textkit.tokens(zitat))[:3])
neue.append({"knoten": "suche", "item": f"luecke:{s['id']}:r{runde + 1}",
"art": "ruecklauf", "runde": runde + 1,
"payload": {"query": query, "zweck": f"luecke:{s['id']}"}})
"payload": {"query": query, "zweck": "luecke",
"soll_id": s["id"]}})
else:
neue.append({"knoten": "luecke",
"item": f"r{runde + 1}:frei:soll:{s['id']}",
@@ -461,6 +462,16 @@ async def gate(task: dict) -> engine.Ergebnis:
# misst sich am Soll; alles andere bläht nur (32-Bausteine-Lektion)
db.execute("UPDATE atome SET status='verworfen' WHERE topic=? AND "
"status='aktiv' AND soll_id IS NULL AND soll_geprueft=1", topic)
# Harter Vollständigkeits-Stop (Lektion 103): kippt über die Hälfte der
# extrahierten Atome als Beifang, ist das Soll zu eng → PAUSE statt
# löchriger Guide.
geprueft = db.one("SELECT COUNT(*) c FROM atome WHERE topic=? AND "
"soll_geprueft=1 AND merged_into IS NULL", topic)["c"]
beifang = db.one("SELECT COUNT(*) c FROM atome WHERE topic=? AND "
"status='verworfen' AND soll_id IS NULL AND "
"soll_geprueft=1 AND merged_into IS NULL", topic)["c"]
if geprueft and beifang / geprueft > config.BEIFANG_MAX_QUOTE:
raise llm.LaufPause(f"vollstaendigkeit:beifang {beifang}/{geprueft}")
# (d2) Facetten HART: jede Beleg-Stelle braucht ein Atom in der Nähe
# (Gutachten: Namensherkunft fehlte trotz 7 Belegen). Cap: 2 gezielte