This commit is contained in:
team3
2026-07-07 00:25:23 +02:00
parent f2feb1dbd0
commit f0c1bdacfa
15 changed files with 1363 additions and 367 deletions

View File

@@ -220,104 +220,60 @@ async def _generate_block(ctx: GenContext, files: dict, title: str, description:
instructions: str = "", ns: str = "", lbl: str = "",
sources: list[str] | None = None,
seeds: list[str] | None = None, melde=None) -> dict | None:
"""GEN_PANEL unabhängige Generatoren liefern je Subs+Facts+Level/Relevanz in EINEM Call;
Konsens im Code (Variant-Cluster über beide Ausgaben, ≥2 unabhängige Generatoren =
consensus). Einzelnennungen und ungedeckte Seeds werden „unsicher" — der Prüfer
entscheidet mit Material (ersetzt Sättigungsrunden + Clarify-Panel). Degraded: liefert
nur EIN Generator, wird alles unsicher. → {raw, facts, unsicher, votes} | None."""
topic, provider = ctx.topic, ctx.provider
"""Bottom-up-Anreicherung: die Subs stehen FEST — Board 1 hat die Atome geclustert und
als Subs persistiert. EIN Call liefert pro vorgegebenem Sub die Lern-Facts + Level +
Relevanz aus dem Material. Er entdeckt und entfernt KEINE Subs (Discovery/Konsens/Seeds
entfallen; die Vollständigkeit kommt aus Board 1). → {raw, facts, unsicher, votes} | None."""
topic = ctx.topic
work_dir = files["arbeit"]
bnorm = _norm_title(title)
source, caps = await asyncio.to_thread(
_inline_source, topic, sources, [f"{title} {description}"])
seeds_txt = ""
if seeds:
seeds_txt = ("\nAlready identified sub-point CANDIDATES of this block (verify against "
"the material; if backed AND not already covered by another entry, include "
"them — rephrased as a standalone statement):\n"
+ "\n".join(f"- {s}" for s in dict.fromkeys(seeds) if s) + "\n")
# feste Subs aus Board 1 (Reihenfolge erhalten, dublettenfrei)
subs: list[str] = []
seen: set[str] = set()
for r in await db.list_subblocks(topic, bnorm):
st = str(r.get("sub_title") or "").strip()
sn = _norm_title(st)
if st and sn and sn not in seen and r.get("status") in ("consensus", "candidate"):
seen.add(sn)
subs.append(st)
if not subs:
return {"raw": {title: []}, "facts": {title: {}}, "unsicher": [], "votes": {}}
source, caps = await asyncio.to_thread(_inline_source, topic, sources, [title] + subs)
if melde:
melde("Generate")
h = _h8(title, description, "gen")
paths = [work_dir / f"gen-{h}-g{g}.json" for g in range(1, GEN_PANEL + 1)]
prompt = _prompt("Subblock-Generate", topic=topic,
melde("Anreichern")
h = _h8(title, description, "enrich")
pfad = work_dir / f"enrich-{h}.json"
sub_liste = "\n".join(f"{i}. {t}" for i, t in enumerate(subs, 1))
prompt = _prompt("Subblock-Anreichern", topic=topic,
block=f"{title}{description}" if description else title,
source=source, seeds=seeds_txt, extra=_extra(instructions))
pending = [(g, p) for g, p in enumerate(paths, 1) if _gen_schema(_json_file(p)) is None]
if pending:
slots = [{
"key": f"blocks-{topic}-{ns}sb-gen-{h}-g{g}",
"prompt": prompt, "role": "quick", "capabilities": caps,
"payload": (lambda result, p=p: _sink_json(result, p, _gen_schema)),
} for g, p in pending]
await _race(topic, f"{lbl}Generate", slots, len(slots),
_timeout("generate", 10), provider, cancelled=ctx.is_cancelled)
subs=sub_liste, source=source, extra=_extra(instructions))
if _gen_schema(_json_file(pfad)) is None:
status, _v = await run_single_slot(
ctx, f"{lbl}Anreichern", key=f"blocks-{topic}-{ns}sb-enrich-{h}",
prompt=prompt, role="quick", capabilities=caps,
payload=lambda result, p=pfad: _sink_json(result, p, _gen_schema),
timeout=_timeout("generate", len(subs)))
if status == FAILED:
_log(topic, f"Anreichern {title} ohne Ergebnis — Facts bleiben leer, Prüfer misst")
if ctx.is_cancelled():
return None
outs = [o for p in paths if (o := _gen_schema(_json_file(p))) is not None]
if not outs:
return None
if len(outs) < len(paths):
_log(topic, f"Generate {title}: nur {len(outs)}/{len(paths)} Generatoren — alles unsicher, Prüfer entscheidet")
# Mentions in die DB (QA-Beleg-Signal), Karten-Re-Spawn darf nicht kumulieren
await db.delete_subblocks(topic, bnorm)
alle: list[tuple[dict, int]] = [] # (sub-Eintrag, Generator-Index)
for gi, subs in enumerate(outs):
seen: set[str] = set()
for e in subs:
sn = _norm_title(e["title"])
if not sn or sn in seen:
continue
seen.add(sn)
alle.append((e, gi))
await db.upsert_subblock(topic, bnorm, sn, title, e["title"])
if not alle:
return {"raw": {title: []}, "facts": {title: {}}, "unsicher": [], "votes": {}}
sims = await _sims_of([e["title"] for e, _ in alle])
raw: list[str] = []
out = _gen_schema(_json_file(pfad)) or []
by_norm = {_norm_title(e["title"]): e for e in out}
facts: dict[str, dict] = {}
votes: dict[str, dict] = {}
unsicher: list[dict] = []
for c in _variant_clusters([e["title"] for e, _ in alle], [1] * len(alle), sims):
rep = alle[c["rep"]][0]
sn = _norm_title(rep["title"])
fk = _fk_of(rep)
for m in c["members"]:
if m != c["rep"]:
_facts_union(fk, _fk_of(alle[m][0]))
await db.set_subblock_fields(topic, bnorm, _norm_title(alle[m][0]["title"]),
status="variant")
votes[sn] = {"level": [v for m in c["members"] if (v := alle[m][0]["level"])],
"relevance": [v for m in c["members"] if (v := alle[m][0]["relevance"])]}
gens = {alle[m][1] for m in c["members"]}
# degraded (1 Generator): kein Konsens möglich — alles unsicher, Prüfer entscheidet
if len(gens) >= 2 and len(outs) >= 2:
raw.append(rep["title"])
facts[sn] = fk
await db.set_subblock_fields(topic, bnorm, sn, status="consensus")
else:
unsicher.append({**rep, **fk})
# Seed-Garantie: ungedeckte Seeds gehen als unsicher zum Prüfer (der ist das Beleg-Gate)
for seed in dict.fromkeys(s for s in (seeds or []) if s):
st = _sub_tokens(seed)
gedeckt = [t for t in raw + [u["title"] for u in unsicher]]
if not st or any(st <= _sub_tokens(t) for t in gedeckt):
continue
if gedeckt and EMBEDDING_AKTIV and await asyncio.to_thread(embedding.available):
sims = await asyncio.to_thread(embedding.embed_sims, [seed] + gedeckt)
if sims is not None and max(float(sims[0][j]) for j in range(1, len(gedeckt) + 1)) >= SEED_COVER_COS:
continue
unsicher.append({"title": seed, "level": "", "relevance": "", "key_points": [],
"prerequisites": "", "hurdles": "", "cited_facts": [], "example_idea": ""})
raw_map = {title: raw}
await _dedup_subblocks(topic, raw_map) # deterministischer Near-Dup-Filter
facts = {sn: fk for sn, fk in facts.items()
if sn in {_norm_title(s) for s in raw_map[title]}}
return {"raw": raw_map, "facts": {title: facts}, "unsicher": unsicher, "votes": votes}
for st in subs:
sn = _norm_title(st)
e = by_norm.get(sn)
if e:
fk = _fk_of(e)
votes[sn] = {"level": [e["level"]] if e.get("level") else [],
"relevance": [e["relevance"]] if e.get("relevance") else []}
else: # der Call ließ diesen Sub aus — leere Facts, der Prüfer/QA sieht die Lücke
fk = {k: ([] if k in ("key_points", "cited_facts") else "") for k in _FACTS_FIELDS}
votes[sn] = {"level": [], "relevance": []}
facts[sn] = fk
await db.put_subblock(topic, bnorm, sn, title, st, status="consensus")
return {"raw": {title: subs}, "facts": {title: facts}, "unsicher": [], "votes": votes}
# ── Verify (+ Fix-Tail) ─────────────────────────────────────────────────────────────
@@ -336,11 +292,14 @@ def _default_vote(stimmen: list[str], default: str) -> str:
async def _verify_block(ctx: GenContext, files: dict, title: str, gen: dict, q: dict,
instructions: str = "", ns: str = "", lbl: str = "",
sources: list[str] | None = None, melde=None) -> dict | None:
sources: list[str] | None = None, melde=None,
keep_all: bool = False) -> dict | None:
"""VERIFY_PANEL unabhängige Prüfer auditieren den Block in EINEM Call (MECE-Faltung,
Fremd, Lücken, Unsicher-Übernahme, Facts-Korrektheit, Level/Relevanz). Auswertung mit
Schnittmengen-Semantik pro Befundklasse (Faltung/Fremd/Übernahme einstimmig, Discard
2/2, Korrektur ≥1 Stimme); Fix-Tail ist EIN Call für Korrekturen + belegte Lücken.
keep_all=True (Bottom-up): kein Atom wird entfernt/gefaltet/als neu erfunden — nur
Facts-Korrektur + Level/Relevanz bleiben (Board 1 hat schon MECE geclustert).
{raw, facts, sidecar} | None (nur bei Cancel)."""
topic = ctx.topic
work_dir = files["arbeit"]
@@ -427,6 +386,18 @@ async def _verify_block(ctx: GenContext, files: dict, title: str, gen: dict, q:
if einstimmig:
v1, v2 = verdicts[0], verdicts[1]
if keep_all:
# Bottom-up: jedes Board-1-Atom bleibt. Destruktive/erzeugende Befunde
# neutralisieren — die Loops unten laufen dann leer. Nur Facts-Korrektur
# (ohne discard) + Level/Relevanz überleben.
for v in (v1, v2):
v["fremd"] = set()
v["gruppen"] = []
v["kataloge"] = []
v["uebernehmen"] = {}
v["luecken"] = []
# discard abschalten (kein Atom entfernen), aber den Korrektur-Hinweis behalten
v["facts_probleme"] = [{**p, "discard": False} for p in v["facts_probleme"]]
# 1. Fremd (einstimmig): fürs THEMA fremde Aussagen → discarded
for k in sorted(v1["fremd"] & v2["fremd"]):
t = _titel(k)