update
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user