This commit is contained in:
team3
2026-07-12 20:02:13 +02:00
parent 31a42bbf1d
commit cde1721d10
23 changed files with 731 additions and 219 deletions

View File

@@ -14,8 +14,8 @@ import embedding
import korpus
import llm
import textkit
from config import (ANKER_OVERLAP_MERGE, LUECKEN_RUNDEN_MAX, MERGE_KANDIDAT_COS,
MERGE_KANDIDAT_JACCARD,
from config import (ANKER_OVERLAP_MERGE, BRAUCHT_KANDIDATEN, LUECKEN_RUNDEN_MAX,
MERGE_KANDIDAT_COS, MERGE_KANDIDAT_JACCARD,
MERGE_PANEL, READER_JE_ABSCHNITT, ZIELE_CHUNK_ATOME)
log = logging.getLogger("creator2.inventar")
@@ -266,6 +266,101 @@ def _kanten_aufloesen(topic: str) -> None:
" VALUES(?,?,?,'braucht')", (topic, a["id"], ziel))
def _braucht_unaufgeloest(atome: list[dict]) -> list[tuple[dict, str]]:
"""(Atom, braucht-Titel), die _kanten_aufloesen (norm/kern) NICHT traf. Selbe
Auflösungslogik — was sie fand, ist erledigt und bleibt außen vor."""
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"])
offen = []
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 not ziel or ziel == a["id"]:
offen.append((a, titel))
return offen
def _trigramm(s: str) -> set[str]:
s = textkit.norm(s)
return {s[i:i + 3] for i in range(len(s) - 2)} or {s}
def _tri_dice(a: str, b: str) -> float:
"""Zeichen-Trigramm-Dice — fuzzy Titel-Ähnlichkeit, die geteilte Wortstämme UND
Kopfnomen erfasst („Approximationsalgorithmus" ~ „Approximativer Algorithmus")."""
A, B = _trigramm(a), _trigramm(b)
return 2 * len(A & B) / (len(A) + len(B)) if A or B else 0.0
def _braucht_kandidaten(titel: str, atome: list[dict], selbst_id: int, cap: int) -> list[dict]:
"""Expliziter Kandidatengenerator für Kompositum↔Expansion (Lektion 28: Cosinus
~0.53, Embedding schlägt sie nie vor). Zulassung (hohe Trefferquote): ein
signifikantes Token (≥5 Zeichen) ist Substring des anderen normierten Titels
(„algorithmus" ⊂ „approximationsalgorithmus"). Rang nach Trigramm-Dice — der
ganze Titel zählt, nicht nur das Kopfnomen, so steht der Grundbegriff vor
generischen Distraktoren („Algorithmus"). Der Judge trennt dann fein."""
nt = textkit.norm(titel)
bt = {t for t in textkit.tokens(titel) if len(t) >= 5}
treffer = []
for a in atome:
if a["id"] == selbst_id:
continue
na = textkit.norm(a["titel"])
at = {t for t in textkit.tokens(a["titel"]) if len(t) >= 5}
if {t for t in at if t in nt} | {t for t in bt if t in na}:
treffer.append((_tri_dice(titel, a["titel"]), a["id"], a))
treffer.sort(key=lambda x: (-x[0], x[1]))
return [a for _, _, a in treffer[:cap]]
async def _braucht_fallback(ctx: llm.Kontext) -> None:
"""braucht-Titel, die norm/kern nicht auflösten, per Judge auf Atome mappen.
Deterministischer Substring-Prefilter (Kandidaten) → EIN Judge (n=1) als
Präzisions-Gate. braucht-Kanten sind reversibel/billiger als Merges → kein Panel
(Lektion 78). Im Zweifel null: eine Falsch-Kante verschiebt Ordnung + Level.
Idempotent (INSERT OR IGNORE); läuft nur in bauen, damit ein Bausteine-Reset ihn
ohne Re-Extraktion mitnimmt."""
topic = ctx.topic
atome = aktive_atome(topic)
offen = _braucht_unaufgeloest(atome)
if not offen:
return
angaben = [(a, titel, kand) for a, titel in offen
if (kand := _braucht_kandidaten(titel, atome, a["id"], BRAUCHT_KANDIDATEN))]
if not angaben:
log.info("braucht-Fallback: %d unaufgelöste Titel, keine Kandidaten", len(offen))
return
geloest = 0
async def chunk_loesen(chunk: list) -> None:
nonlocal geloest
liste = "\n\n".join(
f"ANGABE {n}: Voraussetzung „{titel}“ (gebraucht von Atom {a['id']}"
f"{a['titel']}“)\nKandidaten:\n"
+ "\n".join(f" - atom {k['id']}: {k['titel']}{k['definition']}" for k in kand)
for n, (a, titel, kand) in enumerate(chunk, 1))
res = await llm.call(ctx, stage="braucht", template="Braucht-Aufloesung",
werte={"angaben": liste}, role="judge",
n=len(chunk), item=f"br{chunk[0][0]['id']}", erwartet=list)
wahl = {e.get("phrase"): e.get("atom") for e in res or [] if isinstance(e, dict)}
for n, (a, titel, kand) in enumerate(chunk, 1):
zid = wahl.get(n)
if zid in {k["id"] for k in kand} and zid != a["id"]:
db.execute("INSERT OR IGNORE INTO kanten(topic, von_atom, zu_atom, art)"
" VALUES(?,?,?,'braucht')", (topic, a["id"], zid))
geloest += 1
chunks = [angaben[i:i + PAAR_CHUNK] for i in range(0, len(angaben), PAAR_CHUNK)]
await llm.alle(chunk_loesen(c) for c in chunks)
log.info("braucht-Fallback: %d/%d Titel aufgelöst (%d ohne Kandidaten)",
geloest, len(offen), len(offen) - len(angaben))
# ── Soll-Zuordnung ────────────────────────────────────────────────────────────
async def _soll_zuordnen(ctx: llm.Kontext, nur_offene: bool = True) -> None: