update
This commit is contained in:
@@ -24,7 +24,7 @@ from pathlib import Path
|
||||
import database as db
|
||||
import embedding
|
||||
from agents import kill_process, cancel_scope, clear_scope, run_agent
|
||||
from config import CONSENSUS_GRACE, CONSENSUS_MAX_ROUNDS, DEFAULT_PROVIDER, CRAWL_KEEP_PATTERNS, CRAWL_NOISE_PATTERNS, CRAWL_MIN_CHARS, QUELLE_RELEVANZ_CHUNK, QUELLE_RELEVANZ_SNIPPET, EMBEDDING_AKTIV, EMBEDDING_SUB_DUP, BLOCKS_GRUPPIERUNG_AKTIV, EMBEDDING_SIBLING_FLOOR, EMBEDDING_SIBLING_CAP, GROUP_RECONCILE_FLOOR, GROUP_MIN_COS_FLOOR
|
||||
from config import CONSENSUS_GRACE, CONSENSUS_MAX_ROUNDS, DEFAULT_PROVIDER, CRAWL_KEEP_PATTERNS, CRAWL_NOISE_PATTERNS, CRAWL_MIN_CHARS, QUELLE_RELEVANZ_CHUNK, QUELLE_RELEVANZ_SNIPPET, EMBEDDING_AKTIV, EMBEDDING_SUB_DUP, BLOCKS_GRUPPIERUNG_AKTIV, EMBEDDING_SIBLING_FLOOR, EMBEDDING_SIBLING_CAP, GROUP_RECONCILE_FLOOR, GROUP_MIN_COS_FLOOR, SUB_VARIANT_COS, SEED_COVER_COS
|
||||
from fsutil import atomic_write_text, atomic_write_json
|
||||
from jsonio import read_json_file as _json_file
|
||||
from paths import arbeit_dir, blocks_path, question_pattern_path, project_dir, subblocks_path, source_path, source_crawl_dir, safe_folder
|
||||
@@ -53,6 +53,10 @@ RESEARCH_THEMA_AGENTS = 5 # web mode (source "thema", no crawl folder)
|
||||
RESEARCH_SECTION_CHARS = 12000
|
||||
# Triage (content/noise) is now a deterministic rule filter (config.CRAWL_*).
|
||||
SUBBLOCK_CAP = 900 # subblock find loop per chunk (15 min)
|
||||
SUBBLOCK_MIN = 5 # below this consensus count a block gets focused catch-up rounds
|
||||
SUBBLOCK_EXTRA_ROUNDS = 2 # max catch-up rounds (saturation stop still applies — thin stays thin)
|
||||
SUBBLOCK_MAX_ROUNDS = 5 # hard round cap: measured runs hit 5–9 rounds purely on paraphrases
|
||||
# before the variant-robust `new` count converges — never search longer
|
||||
CONSOLIDATION_CHUNK = 600 # up to here ONE global judge (dedups everything); above that chunked + merge pass — fallback path only
|
||||
DEDUP_PAIR_FLOOR = 0.6 # min cosine for a candidate pair (complete-link aggregates → no chaining)
|
||||
DEDUP_PAIRS_CHUNK = 40 # pairs per judge package (pairwise verification instead of a block mixer)
|
||||
@@ -61,7 +65,10 @@ FILTER_CHUNK = 35 # blocks to assess per judge in the degrade pass (fu
|
||||
# Balance question-pattern chunks by sub load via LPT (makespan), not by block count.
|
||||
QUESTION_CHUNK_SUBS = 50 # target sum of relevant subs per chunk
|
||||
QUESTION_MAX_ROUNDS = 3 # catch-up rounds for subs without a pattern (the LLM omits ~18 % per chunk)
|
||||
FACTS_CHUNK_SUBS = 25 # facts extraction: smaller chunks (facts are bulkier than patterns)
|
||||
FACTS_CHUNK_SUBS = 10 # facts extraction: small chunks — the 4 phases (find/erg/check/fix) are
|
||||
# serial PER CHUNK, so chunk count = parallelism; the makespan tail of a
|
||||
# late block is bounded by ONE chunk's phase chain, not the whole block
|
||||
ARTEFACT_CHUNK_SUBS = 25 # flashcards/examples: bulk generation, phases are cheap → bigger packages
|
||||
FACTS_CHECK_PANEL = 3 # judges per chunk in the facts check (majority objects)
|
||||
CONSOLIDATION_PANEL = 3 # mapping judges per chunk (panel → reconcile instead of a single judge)
|
||||
SUBBLOCK_PANEL = 3 # source judges in the subblock clarification (majority instead of a single judge)
|
||||
@@ -556,10 +563,51 @@ def _lpt_chunks(weights: list[int], target: int) -> list[list[int]]:
|
||||
|
||||
|
||||
|
||||
_NEG_TOKENS = {"nicht", "kein", "keine", "keinen", "keiner", "ohne", "nie"}
|
||||
|
||||
|
||||
def _neg_set(title: str) -> frozenset:
|
||||
"""Negation tokens of a title — antonym statements measure cos 0.91–0.95 (above any usable
|
||||
variant threshold), so equal negation sets are a hard merge precondition."""
|
||||
return frozenset(t for t in re.findall(r"\w+", _norm_title(title)) if t in _NEG_TOKENS)
|
||||
|
||||
|
||||
def _sub_tokens(title: str) -> set:
|
||||
return set(re.findall(r"\w+", _norm_title(title)))
|
||||
|
||||
|
||||
def _variant_clusters(titles: list[str], mentions: list[int], sims) -> list[dict]:
|
||||
"""Fold phrasing VARIANTS of one concept BEFORE the consensus count: finders rephrase per
|
||||
round, so exact-norm counting starves real concepts. Union-find over cos ≥ SUB_VARIANT_COS
|
||||
with the negation guard. → [{"rep": idx, "members": [idx…], "mentions": sum}]."""
|
||||
n = len(titles)
|
||||
parent = list(range(n))
|
||||
|
||||
def find(x):
|
||||
while parent[x] != x:
|
||||
parent[x] = parent[parent[x]]
|
||||
x = parent[x]
|
||||
return x
|
||||
|
||||
negs = [_neg_set(t) for t in titles]
|
||||
for i in range(n):
|
||||
for j in range(i + 1, n):
|
||||
if float(sims[i][j]) >= SUB_VARIANT_COS and negs[i] == negs[j]:
|
||||
parent[find(i)] = find(j)
|
||||
groups: dict[int, list[int]] = {}
|
||||
for i in range(n):
|
||||
groups.setdefault(find(i), []).append(i)
|
||||
return [{"rep": max(g, key=lambda k: (len(titles[k]), -k)), "members": g,
|
||||
"mentions": sum(mentions[k] for k in g)} for g in groups.values()]
|
||||
|
||||
|
||||
async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str,
|
||||
wipe: bool = True, ns: str = "") -> dict | None:
|
||||
wipe: bool = True, ns: str = "", seeds: list[str] | None = None,
|
||||
lbl: str = "") -> dict | None:
|
||||
"""Block B (DB + loop): per package, find subblocks in rounds (3 finders, until 0 new/cap),
|
||||
collect in the DB (≥2 mentions = consensus, 1× discarded), a judge cleans up per package.
|
||||
collect in the DB (variant-clustered mentions ≥2 = consensus), a judge panel cleans up per
|
||||
package; blocks below SUBBLOCK_MIN get focused catch-up rounds; `seeds` (demoted fragment
|
||||
titles, single-block kanban calls) are guaranteed to reach the facts evidence gate.
|
||||
→ {block title: [subblock, …]} (consensus) or None. Fills DB table `subblocks`.
|
||||
wipe=False (kanban board: one call per block) keeps the other blocks' rows."""
|
||||
topic, provider, is_cancelled = ctx.topic, ctx.provider, ctx.is_cancelled
|
||||
@@ -574,8 +622,12 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
|
||||
n = len(chunks)
|
||||
title_by_num = {num: _title(entries[num]) for num in nums}
|
||||
norm_by_num = {num: _norm_title(title_by_num[num]) for num in nums}
|
||||
emb_on = EMBEDDING_AKTIV and await asyncio.to_thread(embedding.available)
|
||||
if wipe:
|
||||
await db.delete_subblocks(topic) # fresh start of the block (idempotent counter)
|
||||
else:
|
||||
for num in nums: # per-block wipe: a re-spawned card must not accumulate mentions
|
||||
await db.delete_subblocks(topic, norm_by_num[num])
|
||||
|
||||
async def _known_block(chunk):
|
||||
known = []
|
||||
@@ -589,48 +641,82 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
|
||||
# self-bias/echo) — only add what's missing. This keeps the counter an honest consensus signal.
|
||||
return ("\n\nBEREITS ERFASST — liste diese NICHT erneut. Finde nur, was FEHLT:\n" + "\n".join(known))
|
||||
|
||||
# ONE finder round (3 slots, quorum 2) → count of NEW sub norms; None = no result/cancel.
|
||||
async def _one_round(label, subset, assignment, paths, keys, known, extra_instr):
|
||||
chunk_idx = _title_index({num: title_by_num[num] for num in subset})
|
||||
for p in paths:
|
||||
p.unlink(missing_ok=True)
|
||||
slots = [{
|
||||
"key": k,
|
||||
"prompt": _prompt("Subblock-Research", topic=topic, assignment=assignment, known=known, out_path=p, extra=_extra(extra_instr)),
|
||||
"role": "quick", "capabilities": caps,
|
||||
"payload": (lambda result, p=p: _parse_subblocks(_read(p)) or None),
|
||||
} for k, p in zip(keys, paths)]
|
||||
agent_texts = await _race(topic, label, slots, 2, _timeout("subblock", len(subset)), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE)
|
||||
if is_cancelled() or not agent_texts:
|
||||
return None
|
||||
rows_before = {num: await db.list_subblocks(topic, norm_by_num[num]) for num in subset}
|
||||
existing = {num: {s["sub_norm"] for s in rows_before[num]} for num in subset}
|
||||
fresh: dict[int, list[str]] = {}
|
||||
for d in agent_texts:
|
||||
for marker, subs in d.items():
|
||||
num = _resolve_title(chunk_idx, marker)
|
||||
if num is None:
|
||||
continue
|
||||
seen_set = set()
|
||||
for sub in subs:
|
||||
sn = _norm_title(sub)
|
||||
if not sn or sn in seen_set:
|
||||
continue
|
||||
seen_set.add(sn)
|
||||
if sn not in existing[num]:
|
||||
existing[num].add(sn)
|
||||
fresh.setdefault(num, []).append(sub)
|
||||
await db.upsert_subblock(topic, norm_by_num[num], sn, title_by_num[num], sub)
|
||||
# "New" is variant-robust: a paraphrase of an existing sub (or of another fresh find)
|
||||
# still gets stored above (its mention feeds the cluster consensus), but it must not
|
||||
# keep the saturation loop spinning — finders rephrase every round (measured: 5–9
|
||||
# rounds without this fold). Model off → exact counting (status quo).
|
||||
new = 0
|
||||
for num, cands in fresh.items():
|
||||
sims = None
|
||||
base = [s["sub_title"] for s in rows_before[num]]
|
||||
if emb_on and base + cands:
|
||||
sims = await asyncio.to_thread(embedding.embed_sims, base + cands)
|
||||
if sims is None:
|
||||
new += len(cands)
|
||||
continue
|
||||
negs = [_neg_set(t) for t in base + cands]
|
||||
nb = len(base)
|
||||
kept: list[int] = []
|
||||
for i in range(nb, nb + len(cands)):
|
||||
dup = any(float(sims[i][j]) >= SUB_VARIANT_COS and negs[i] == negs[j]
|
||||
for j in [*range(nb), *kept])
|
||||
if not dup:
|
||||
kept.append(i)
|
||||
new += len(kept)
|
||||
return new
|
||||
|
||||
# Phase "Subblocks find": per package loop until 0 new subs / time cap.
|
||||
async def _find(c, chunk):
|
||||
assignment = "\n".join(f"- {entries[num]}" for num in chunk)
|
||||
chunk_idx = _title_index({num: title_by_num[num] for num in chunk})
|
||||
start = time.monotonic()
|
||||
round_n = 0
|
||||
while not is_cancelled():
|
||||
round_n += 1
|
||||
bekannt = await _known_block(chunk) if round_n > 1 else ""
|
||||
paths = [work_dir / f"subblock-c{c}-r{round_n}-{i}.md" for i in (1, 2, 3)]
|
||||
for p in paths:
|
||||
p.unlink(missing_ok=True)
|
||||
slots = [{
|
||||
"key": f"blocks-{topic}-{ns}subblock-c{c}-r{round_n}-{i}",
|
||||
"prompt": _prompt("Subblock-Research", topic=topic, assignment=assignment, known=bekannt, out_path=p, extra=_extra(instructions)),
|
||||
"role": "quick", "capabilities": caps,
|
||||
"payload": (lambda result, p=p: _parse_subblocks(_read(p)) or None),
|
||||
} for i, p in enumerate(paths, 1)]
|
||||
agent_texts = await _race(topic, f"Subblocks package {c} R{round_n}", slots, 2, _timeout("subblock", len(chunk)), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE)
|
||||
if is_cancelled():
|
||||
return False
|
||||
if not agent_texts:
|
||||
keys = [f"blocks-{topic}-{ns}subblock-c{c}-r{round_n}-{i}" for i in (1, 2, 3)]
|
||||
new = await _one_round(f"{lbl}Subblocks package {c} R{round_n}", chunk, assignment, paths, keys, bekannt, instructions)
|
||||
if new is None:
|
||||
if is_cancelled():
|
||||
return False
|
||||
return round_n > 1 # round 1 without result = error; later = simply the end
|
||||
existing = {num: {s["sub_norm"] for s in await db.list_subblocks(topic, norm_by_num[num])} for num in chunk}
|
||||
new = 0
|
||||
for d in agent_texts:
|
||||
for marker, subs in d.items():
|
||||
num = _resolve_title(chunk_idx, marker)
|
||||
if num is None:
|
||||
continue
|
||||
seen_set = set()
|
||||
for sub in subs:
|
||||
sn = _norm_title(sub)
|
||||
if not sn or sn in seen_set:
|
||||
continue
|
||||
seen_set.add(sn)
|
||||
if sn not in existing[num]:
|
||||
new += 1
|
||||
existing[num].add(sn)
|
||||
await db.upsert_subblock(topic, norm_by_num[num], sn, title_by_num[num], sub)
|
||||
if new == 0:
|
||||
break
|
||||
if round_n >= SUBBLOCK_MAX_ROUNDS:
|
||||
_log(topic, f"Subblocks package {c}: round cap reached ({round_n})")
|
||||
break
|
||||
if time.monotonic() - start > SUBBLOCK_CAP:
|
||||
_log(topic, f"Subblocks package {c}: time cap reached (round {round_n})")
|
||||
break
|
||||
@@ -643,49 +729,117 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
|
||||
_blocks_errors[topic] = "Subblocks failed (research)"
|
||||
return None
|
||||
|
||||
# Phase "Subblocks select": ≥2 mentions = consensus, 1× discarded (code).
|
||||
# Phase "Subblocks select": variant-clustered mentions ≥2 = consensus — the cluster
|
||||
# representative carries the status, folded members become `variant` (NOT discarded:
|
||||
# the clarify panel's uncertain group must not re-list them). Model off → exact counter.
|
||||
async def _select(subset, keep_consensus=False):
|
||||
for num in subset:
|
||||
rows = await db.list_subblocks(topic, norm_by_num[num])
|
||||
clusters = None
|
||||
if emb_on and len(rows) >= 2:
|
||||
sims = await asyncio.to_thread(embedding.embed_sims, [r["sub_title"] for r in rows])
|
||||
if sims is not None:
|
||||
clusters = _variant_clusters([r["sub_title"] for r in rows],
|
||||
[r["mentions"] for r in rows], sims)
|
||||
if clusters is None:
|
||||
for r in rows:
|
||||
if keep_consensus and r["status"] == "consensus":
|
||||
continue
|
||||
await db.set_subblock_fields(topic, norm_by_num[num], r["sub_norm"],
|
||||
status=("consensus" if r["mentions"] >= 2 else "discarded"))
|
||||
continue
|
||||
for cl in clusters:
|
||||
# a re-select (catch-up) never demotes panel-confirmed subs — an existing
|
||||
# consensus member stays the representative, new variants fold under it.
|
||||
kept = [k for k in cl["members"] if keep_consensus and rows[k]["status"] == "consensus"]
|
||||
for k in cl["members"]:
|
||||
if kept:
|
||||
st = "consensus" if k in kept else "variant"
|
||||
elif cl["mentions"] >= 2:
|
||||
st = "consensus" if k == cl["rep"] else "variant"
|
||||
else:
|
||||
st = "discarded"
|
||||
if keep_consensus and rows[k]["status"] == "consensus" and st != "consensus":
|
||||
continue
|
||||
await db.set_subblock_fields(topic, norm_by_num[num], rows[k]["sub_norm"], status=st)
|
||||
|
||||
set_p(f"Subblocks select ({n} packages)…", step=_step_idx(topic, "Subblocks select"))
|
||||
for num in nums:
|
||||
for s in await db.list_subblocks(topic, norm_by_num[num]):
|
||||
await db.set_subblock_fields(topic, norm_by_num[num], s["sub_norm"],
|
||||
status=("consensus" if s["mentions"] >= 2 else "discarded"))
|
||||
await _select(nums)
|
||||
|
||||
# Judge formulation → shown candidate (best cos ≥ SUB_VARIANT_COS, negation-guarded).
|
||||
# Judges demonstrably paraphrase; without canonicalizing, the exact-norm majority vote
|
||||
# splinters across formulations (measured: 0.993-duplicates in a final list).
|
||||
async def _canon_map(shown: list[str], judge_titles: list[str]) -> dict[str, tuple[str, str]]:
|
||||
if not emb_on or not shown or not judge_titles:
|
||||
return {}
|
||||
texts = shown + list(judge_titles)
|
||||
sims = await asyncio.to_thread(embedding.embed_sims, texts)
|
||||
if sims is None:
|
||||
return {}
|
||||
negs = [_neg_set(t) for t in texts]
|
||||
m: dict[str, tuple[str, str]] = {}
|
||||
for a in range(len(shown), len(texts)):
|
||||
best, bv = None, 0.0
|
||||
for b in range(len(shown)):
|
||||
v = float(sims[a][b])
|
||||
if v >= SUB_VARIANT_COS and v > bv and negs[a] == negs[b]:
|
||||
best, bv = b, v
|
||||
if best is not None:
|
||||
m[_norm_title(texts[a])] = (_norm_title(shown[best]), shown[best])
|
||||
return m
|
||||
|
||||
# Phase "Subblocks clarify": source panel (SUBBAUSTEIN_PANEL judges) checks consensus + uncertain (1×)
|
||||
# against the source; code majority per sub. External, multi-voice gate against single-judge bias + echo.
|
||||
async def _clarify(c, chunk):
|
||||
fp = work_dir / f"subblock-final-c{c}.md"
|
||||
async def _clarify(c, chunk, tag=""):
|
||||
fp = work_dir / f"subblock-final-c{c}{tag}.md"
|
||||
if _parse_subblocks(_read(fp)):
|
||||
return
|
||||
block_texts, has_any = [], False
|
||||
consensus_by_num: dict[int, list[str]] = {}
|
||||
shown_by_num: dict[int, list[str]] = {}
|
||||
for num in chunk:
|
||||
rows = await db.list_subblocks(topic, norm_by_num[num])
|
||||
consensus_subs = [s["sub_title"] for s in rows if s["status"] == "consensus"]
|
||||
uncertain = [s["sub_title"] for s in rows if s["status"] != "consensus" and s["mentions"] == 1]
|
||||
# folded variants (status `variant`) are already counted — only true singles are uncertain
|
||||
uncertain = [s["sub_title"] for s in rows if s["status"] == "discarded" and s["mentions"] == 1]
|
||||
consensus_by_num[num] = consensus_subs
|
||||
shown_by_num[num] = consensus_subs + uncertain
|
||||
if not consensus_subs and not uncertain:
|
||||
continue
|
||||
has_any = True
|
||||
k_lines = "\n".join(f"- {s}" for s in consensus_subs) if consensus_subs else "- (keiner)"
|
||||
u_lines = "\n".join(f"- {s}" for s in uncertain) if uncertain else "- (keiner)"
|
||||
block_texts.append(f"BLOCK: {title_by_num[num]}\nKonsens (≥2 finders):\n{k_lines}\nUnsicher (1× — streng gegen Source check):\n{u_lines}")
|
||||
band = ""
|
||||
shown = shown_by_num[num]
|
||||
if emb_on and len(shown) >= 2: # near-dup pairs BELOW the fold threshold → explicit panel hint
|
||||
sims = await asyncio.to_thread(embedding.embed_sims, shown)
|
||||
if sims is not None:
|
||||
pairs = [f"- „{shown[i]}“ ↔ „{shown[j]}“"
|
||||
for i in range(len(shown)) for j in range(i + 1, len(shown))
|
||||
if 0.75 <= float(sims[i][j]) < SUB_VARIANT_COS]
|
||||
if pairs:
|
||||
band = ("\nMögliche Duplikate — prüfen und ggf. zu EINEM Eintrag zusammenführen:\n"
|
||||
+ "\n".join(pairs[:12]))
|
||||
block_texts.append(f"BLOCK: {title_by_num[num]}\nKonsens (≥2 finders):\n{k_lines}\nUnsicher (1× — streng gegen Source check):\n{u_lines}{band}")
|
||||
if not has_any:
|
||||
return
|
||||
|
||||
chunk_idx = _title_index({num: title_by_num[num] for num in chunk})
|
||||
paths = [work_dir / f"subblock-final-c{c}-j{j}.md" for j in range(1, SUBBLOCK_PANEL + 1)]
|
||||
pending = [(j, p) for j, p in enumerate(paths, 1) if _parse_subblocks(_read(p)) is None]
|
||||
paths = [work_dir / f"subblock-final-c{c}{tag}-j{j}.md" for j in range(1, SUBBLOCK_PANEL + 1)]
|
||||
# truthiness, NOT `is None`: a missing file parses to {} — with `is None` the whole
|
||||
# panel silently never ran (fallback adopted the raw consensus unchecked).
|
||||
pending = [(j, p) for j, p in enumerate(paths, 1) if not _parse_subblocks(_read(p))]
|
||||
for _, p in pending:
|
||||
p.unlink(missing_ok=True)
|
||||
if pending:
|
||||
slots = [{
|
||||
"key": f"blocks-{topic}-{ns}subblock-final-c{c}-j{j}",
|
||||
"key": f"blocks-{topic}-{ns}subblock-final-c{c}{tag}-j{j}",
|
||||
"prompt": _prompt("Subblock-Mapping", topic=topic, source=source, blocks="\n\n".join(block_texts), out_path=p, extra=_extra(instructions)),
|
||||
"role": "judge", "capabilities": caps,
|
||||
"payload": (lambda result, p=p: _parse_subblocks(_read(p)) or None),
|
||||
} for j, p in pending]
|
||||
existing = SUBBLOCK_PANEL - len(pending)
|
||||
await _race(topic, f"Subblock-Clarification {c}", slots, max(1, 2 - existing),
|
||||
await _race(topic, f"{lbl}Subblock-Clarification {c}", slots, max(1, 2 - existing),
|
||||
_timeout("subblock_check", len(chunk)), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE)
|
||||
if is_cancelled():
|
||||
return
|
||||
@@ -698,22 +852,34 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
|
||||
return
|
||||
|
||||
# code majority per block/sub-norm: keep if a majority of judges list it (tie → keep).
|
||||
# Votes are canonicalized onto the shown candidates first (paraphrase-robust).
|
||||
block_texts_out = []
|
||||
for num in chunk:
|
||||
raw_votes: list[list[str]] = []
|
||||
for d in outs:
|
||||
subs_of_num: list[str] = []
|
||||
for marker, subs in d.items():
|
||||
if _resolve_title(chunk_idx, marker) == num:
|
||||
subs_of_num.extend(subs)
|
||||
raw_votes.append(subs_of_num)
|
||||
judge_titles = list(dict.fromkeys(s for subs in raw_votes for s in subs
|
||||
if _norm_title(s) not in {_norm_title(t) for t in shown_by_num[num]}))
|
||||
canon = await _canon_map(shown_by_num[num], judge_titles)
|
||||
votes: dict[str, int] = {}
|
||||
form: dict[str, str] = {}
|
||||
for d in outs:
|
||||
for subs_of_num in raw_votes:
|
||||
seen = set()
|
||||
for marker, subs in d.items():
|
||||
if _resolve_title(chunk_idx, marker) != num:
|
||||
for sub in subs_of_num:
|
||||
sn = _norm_title(sub)
|
||||
if not sn:
|
||||
continue
|
||||
for sub in subs:
|
||||
sn = _norm_title(sub)
|
||||
if not sn or sn in seen:
|
||||
continue
|
||||
seen.add(sn)
|
||||
form.setdefault(sn, sub)
|
||||
votes[sn] = votes.get(sn, 0) + 1
|
||||
if sn in canon:
|
||||
sn, sub = canon[sn]
|
||||
if sn in seen:
|
||||
continue
|
||||
seen.add(sn)
|
||||
form.setdefault(sn, sub)
|
||||
votes[sn] = votes.get(sn, 0) + 1
|
||||
kept = [form[sn] for sn in form if votes[sn] * 2 >= len(outs)]
|
||||
if kept:
|
||||
block_texts_out.append(f"<!-- block: {title_by_num[num]} -->\n" + "\n".join(f"- {s}" for s in kept))
|
||||
@@ -725,8 +891,9 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
|
||||
|
||||
# Final list per block: judge output, otherwise consensus fallback. Reconcile DB + build raw.
|
||||
raw: dict[str, list[str]] = {}
|
||||
for c, chunk in enumerate(chunks, 1):
|
||||
final = _parse_subblocks(_read(work_dir / f"subblock-final-c{c}.md")) or {}
|
||||
|
||||
async def _align(c, chunk, tag=""):
|
||||
final = _parse_subblocks(_read(work_dir / f"subblock-final-c{c}{tag}.md")) or {}
|
||||
chunk_idx = _title_index({num: title_by_num[num] for num in chunk})
|
||||
final_by_num = {_resolve_title(chunk_idx, m): subs for m, subs in final.items() if _resolve_title(chunk_idx, m) is not None}
|
||||
for num in chunk:
|
||||
@@ -740,14 +907,83 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
|
||||
final_norms = {_norm_title(s) for s in subs}
|
||||
have = {s["sub_norm"] for s in await db.list_subblocks(topic, norm_by_num[num])}
|
||||
for s in await db.list_subblocks(topic, norm_by_num[num]):
|
||||
await db.set_subblock_fields(topic, norm_by_num[num], s["sub_norm"],
|
||||
status=("consensus" if s["sub_norm"] in final_norms else "discarded"))
|
||||
if s["sub_norm"] in final_norms:
|
||||
st = "consensus"
|
||||
elif s["status"] == "variant":
|
||||
st = "variant" # folded members stay marked — a catch-up clarify must not
|
||||
else: # re-list them as "uncertain singles"
|
||||
st = "discarded"
|
||||
await db.set_subblock_fields(topic, norm_by_num[num], s["sub_norm"], status=st)
|
||||
for s in subs:
|
||||
sn = _norm_title(s)
|
||||
if sn and sn not in have:
|
||||
await db.upsert_subblock(topic, norm_by_num[num], sn, title, s)
|
||||
await db.set_subblock_fields(topic, norm_by_num[num], sn, status="consensus")
|
||||
|
||||
for c, chunk in enumerate(chunks, 1):
|
||||
await _align(c, chunk)
|
||||
|
||||
# Minimum catch-up: a block below SUBBLOCK_MIN gets up to SUBBLOCK_EXTRA_ROUNDS focused
|
||||
# finder rounds. Saturation stop stays — a thin block REMAINS thin if nothing verifiable.
|
||||
async def _catchup(c, chunk):
|
||||
for k in range(1, SUBBLOCK_EXTRA_ROUNDS + 1):
|
||||
lacking = [num for num in chunk if len(raw.get(title_by_num[num]) or []) < SUBBLOCK_MIN]
|
||||
if not lacking or is_cancelled():
|
||||
return
|
||||
assignment = "\n".join(f"- {entries[num]}" for num in lacking)
|
||||
known = await _known_block(lacking) # ALL rows of the block, incl. variants/discarded
|
||||
focus = (instructions + "\n\nDieser Block hat bisher nur sehr wenige belegte "
|
||||
"Subbausteine. Suche gezielt nach WEITEREN belegbaren Kernaspekten, die "
|
||||
"oben fehlen. Nimm NUR auf, was die Quellen wirklich hergeben — nicht aufblähen.")
|
||||
paths = [work_dir / f"subblock-x{k}-c{c}-{i}.md" for i in (1, 2, 3)]
|
||||
keys = [f"blocks-{topic}-{ns}subblock-x{k}-c{c}-{i}" for i in (1, 2, 3)]
|
||||
new = await _one_round(f"{lbl}Subblocks catch-up {c} X{k}", lacking, assignment, paths, keys, known, focus)
|
||||
if not new:
|
||||
return
|
||||
await _select(lacking, keep_consensus=True)
|
||||
await _clarify(c, chunk, tag=f"-x{k}")
|
||||
if is_cancelled():
|
||||
return
|
||||
await _align(c, chunk, tag=f"-x{k}")
|
||||
|
||||
# progress reuses the clarify step label — catch-up has no own registry entry
|
||||
await _gather_progress([_catchup(c, chunk) for c, chunk in enumerate(chunks, 1)], n,
|
||||
_report_p(set_p, topic, "Subblocks clarify"))
|
||||
if is_cancelled():
|
||||
return None
|
||||
await _dedup_subblocks(topic, raw) # near-dup filter per block (deterministic, no LLM)
|
||||
|
||||
# Seed guarantee (single-block kanban calls): every demoted-fragment seed must reach the
|
||||
# facts evidence gate — covered by a consensus sub, promoted from a single find, or
|
||||
# inserted as its own sub. Unverifiable seeds die at the facts discard, not silently here.
|
||||
for num in (nums if seeds else []):
|
||||
title = title_by_num[num]
|
||||
for seed in dict.fromkeys(s for s in seeds if s):
|
||||
st = _sub_tokens(seed)
|
||||
have = raw.get(title) or []
|
||||
if not st or any(st <= _sub_tokens(s) for s in have):
|
||||
continue
|
||||
if emb_on and have: # embedding backup for rephrased covers (lexical is primary)
|
||||
sims = await asyncio.to_thread(embedding.embed_sims, [seed] + have)
|
||||
if sims is not None and max(float(sims[0][j]) for j in range(1, len(have) + 1)) >= SEED_COVER_COS:
|
||||
continue
|
||||
rows = [r for r in await db.list_subblocks(topic, norm_by_num[num]) if r["status"] != "consensus"]
|
||||
cand = next((r for r in rows if st <= _sub_tokens(r["sub_title"])), None)
|
||||
if cand is None and emb_on and rows:
|
||||
sims = await asyncio.to_thread(embedding.embed_sims, [seed] + [r["sub_title"] for r in rows])
|
||||
if sims is not None:
|
||||
j = max(range(1, len(rows) + 1), key=lambda x: float(sims[0][x]))
|
||||
if float(sims[0][j]) >= SEED_COVER_COS and _neg_set(seed) == _neg_set(rows[j - 1]["sub_title"]):
|
||||
cand = rows[j - 1]
|
||||
if cand is not None:
|
||||
await db.set_subblock_fields(topic, norm_by_num[num], cand["sub_norm"], status="consensus")
|
||||
raw.setdefault(title, []).append(cand["sub_title"])
|
||||
_log(topic, f"Seed „{seed}“: Einzelfund „{cand['sub_title']}“ übernommen ({title})")
|
||||
elif (sn := _norm_title(seed)):
|
||||
await db.put_subblock(topic, norm_by_num[num], sn, title, seed, status="consensus")
|
||||
raw.setdefault(title, []).append(seed)
|
||||
_log(topic, f"Seed „{seed}“ als Subbaustein eingefügt ({title}) — Facts-Gate prüft")
|
||||
|
||||
if not raw:
|
||||
# Finders ran but nothing survived the consensus/evidence gates: a legitimately
|
||||
# thin block (e.g. a bare named reduction). {} = done-without-subs — the guide
|
||||
@@ -772,8 +1008,9 @@ async def _dedup_subblocks(topic: str, raw: dict[str, list[str]]) -> None:
|
||||
return
|
||||
keepers: list[int] = []
|
||||
discarded: list[int] = []
|
||||
negs = [_neg_set(s) for s in subs]
|
||||
for i in sorted(range(len(subs)), key=lambda x: (-len(subs[x]), x)): # most informative first
|
||||
if any(float(sims[i][j]) >= EMBEDDING_SUB_DUP for j in keepers):
|
||||
if any(float(sims[i][j]) >= EMBEDDING_SUB_DUP and negs[i] == negs[j] for j in keepers):
|
||||
discarded.append(i)
|
||||
else:
|
||||
keepers.append(i)
|
||||
@@ -812,7 +1049,7 @@ def _disputed_lines(items, item_idxs, disputed: dict) -> str:
|
||||
)
|
||||
|
||||
|
||||
async def _levels_block(ctx: GenContext, set_p, files: dict, raw: dict, instructions: str, ns: str = "") -> dict | None:
|
||||
async def _levels_block(ctx: GenContext, set_p, files: dict, raw: dict, instructions: str, ns: str = "", lbl: str = "") -> dict | None:
|
||||
"""Block C: three phases with a barrier — find (classify), select (vote), clarify.
|
||||
Local IDs 1..n per package, mapped to global gid afterwards.
|
||||
→ {block title: [{title, level}, …]} or None."""
|
||||
@@ -868,7 +1105,7 @@ async def _levels_block(ctx: GenContext, set_p, files: dict, raw: dict, instruct
|
||||
"role": "quick", "capabilities": "files",
|
||||
"payload": (lambda result, p=p, ids=local_set: _levels_schema(_json_file(p), ids)),
|
||||
} for i, p in pending]
|
||||
new = await _race(topic, f"Levels package {c}", slots, 2 - existing, _timeout("level", len(item_idxs)), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE)
|
||||
new = await _race(topic, f"{lbl}Levels package {c}", slots, 2 - existing, _timeout("level", len(item_idxs)), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE)
|
||||
return not is_cancelled() and new is not None
|
||||
|
||||
oks = await _gather_progress([_rate(c, idxs) for c, idxs in enumerate(chunks, 1)], len(chunks), _report_p(set_p, topic, "Levels find"))
|
||||
@@ -895,7 +1132,7 @@ async def _levels_block(ctx: GenContext, set_p, files: dict, raw: dict, instruct
|
||||
if decision is None:
|
||||
disputed_block = _disputed_lines(items, item_idxs, strittig)
|
||||
status, decision = await run_single_slot(
|
||||
ctx, f"Levels-Clarification {c}",
|
||||
ctx, f"{lbl}Levels-Clarification {c}",
|
||||
key=f"blocks-{topic}-{ns}level-final-c{c}",
|
||||
prompt=_prompt("Levels-Mapping", topic=topic, disputed=disputed_block, out_path=judge_path, extra=_extra(instructions)),
|
||||
role="judge", capabilities="files",
|
||||
@@ -1008,7 +1245,7 @@ def _facts_complete(files: dict) -> bool:
|
||||
return isinstance(d, dict) and bool(d)
|
||||
|
||||
|
||||
async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, instructions: str, ns: str = "") -> tuple | None:
|
||||
async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, instructions: str, ns: str = "", lbl: str = "") -> tuple | None:
|
||||
"""Block: per sub extract source facts (find) → verify (check) → correct/discard (fix).
|
||||
Extract-once grounding: the result feeds level/relevance/questions/guide.
|
||||
→ (facts_map, discarded_map) — facts_map {block: {sub_norm: facts}}, discarded_map
|
||||
@@ -1081,7 +1318,7 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
|
||||
return True
|
||||
subs_total = sum(len(blocks[i][1]) for i in idxs)
|
||||
status, _r = await run_single_slot(
|
||||
ctx, f"Facts {ci}", key=f"blocks-{topic}-{ns}facts-c{ci}",
|
||||
ctx, f"{lbl}Facts {ci}", key=f"blocks-{topic}-{ns}facts-c{ci}",
|
||||
prompt=_prompt("Facts-Research", topic=topic, source=source, blocks=block_text(idxs), out_path=fp, extra=_extra(instructions)),
|
||||
role="quick", capabilities=caps,
|
||||
payload=lambda result, p=fp: _facts_schema(_json_file(p)),
|
||||
@@ -1112,7 +1349,7 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
|
||||
for bt, fm in per.items())
|
||||
subs_total = sum(len(blocks[i][1]) for i in idxs)
|
||||
await run_single_slot(
|
||||
ctx, f"Facts supplement {ci}", key=f"blocks-{topic}-{ns}facts-erg-c{ci}",
|
||||
ctx, f"{lbl}Facts supplement {ci}", key=f"blocks-{topic}-{ns}facts-erg-c{ci}",
|
||||
prompt=_prompt("Facts-Supplement", topic=topic, source=source, blocks=block, out_path=ep, extra=_extra(instructions)),
|
||||
role="quick", capabilities=caps,
|
||||
payload=lambda result, p=ep: _facts_schema(_json_file(p)),
|
||||
@@ -1134,7 +1371,8 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
|
||||
await asyncio.gather(*[
|
||||
run_agent(f"blocks-{topic}-{ns}facts-check-c{ci}-j{j}",
|
||||
_prompt("Facts-Check", topic=topic, source=source, facts=facts_text, out_path=chk_path(ci, j), extra=_extra(instructions)),
|
||||
_timeout("content_check", len(per)), provider=provider, role="judge", capabilities=caps)
|
||||
_timeout("content_check", len(per)), provider=provider, role="judge", capabilities=caps,
|
||||
scope=topic, label=f"{lbl}Facts check {ci}/{j}")
|
||||
for j in pending], return_exceptions=True)
|
||||
outs = [s for j in (1, 2, 3)[:FACTS_CHECK_PANEL] if (s := _facts_check_schema(_json_file(chk_path(ci, j)))) is not None]
|
||||
bvotes: dict[str, int] = {}
|
||||
@@ -1183,7 +1421,7 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
|
||||
if not goal:
|
||||
return
|
||||
await run_single_slot(
|
||||
ctx, f"Facts-Fix {ci}", key=f"blocks-{topic}-{ns}facts-fix-c{ci}",
|
||||
ctx, f"{lbl}Facts-Fix {ci}", key=f"blocks-{topic}-{ns}facts-fix-c{ci}",
|
||||
prompt=_prompt("Facts-Research", topic=topic, source=source, blocks="\n\n".join(goal), out_path=fix_path(ci), extra=_extra(instructions)),
|
||||
role="quick", capabilities=caps,
|
||||
payload=lambda result, p=fix_path(ci): _facts_schema(_json_file(p)),
|
||||
@@ -1211,7 +1449,7 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
|
||||
return outcome, discarded_map
|
||||
|
||||
|
||||
async def _relevance_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str, ns: str = "") -> dict | None:
|
||||
async def _relevance_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str, ns: str = "", lbl: str = "") -> dict | None:
|
||||
"""Block D: three phases with a barrier — find (relevant/peripheral), select (vote), clarify.
|
||||
Items from the sidecar; local IDs 1..n per package → global gid.
|
||||
→ {gid: relevance} or None on cancel/research error. Default on gap/dispute: 'relevant'."""
|
||||
@@ -1249,7 +1487,7 @@ async def _relevance_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
|
||||
"role": "quick", "capabilities": "files",
|
||||
"payload": (lambda result, p=p, ids=local_set: _relevance_schema(_json_file(p), ids)),
|
||||
} for i, p in pending]
|
||||
new = await _race(topic, f"Relevance package {c}", slots, 2 - existing, _timeout("relevance", len(item_idxs)), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE)
|
||||
new = await _race(topic, f"{lbl}Relevance package {c}", slots, 2 - existing, _timeout("relevance", len(item_idxs)), provider, cancelled=is_cancelled, grace=CONSENSUS_GRACE)
|
||||
return not is_cancelled() and new is not None
|
||||
|
||||
oks = await _gather_progress([_rate(c, idxs) for c, idxs in enumerate(chunks, 1)], len(chunks), _report_p(set_p, topic, "Relevance find"))
|
||||
@@ -1276,7 +1514,7 @@ async def _relevance_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
|
||||
if decision is None:
|
||||
disputed_block = _disputed_lines(items, item_idxs, strittig)
|
||||
status, decision = await run_single_slot(
|
||||
ctx, f"Relevance-Clarification {c}",
|
||||
ctx, f"{lbl}Relevance-Clarification {c}",
|
||||
key=f"blocks-{topic}-{ns}relevance-final-c{c}",
|
||||
prompt=_prompt("Relevance-Mapping", topic=topic, disputed=disputed_block, out_path=judge_path, extra=_extra(instructions)),
|
||||
role="judge", capabilities="files",
|
||||
@@ -1321,7 +1559,7 @@ def _match_sub(agent_sub: str, rel: list[str]) -> str:
|
||||
return agent_sub
|
||||
|
||||
|
||||
async def _question_pattern_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str, ns: str = "") -> dict | None:
|
||||
async def _question_pattern_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str, ns: str = "", lbl: str = "") -> dict | None:
|
||||
"""Block E (chunks of 10): find (1 generator per ~10 blocks, parallel), select (code:
|
||||
group per block + dedup), clarify (1 critic per chunk), check (catch-up round).
|
||||
Assignment per entry via the `block` field (a chunk file carries several blocks).
|
||||
@@ -1371,7 +1609,7 @@ async def _question_pattern_block(ctx: GenContext, set_p, files: dict, sidecar:
|
||||
)
|
||||
subs_total = sum(len(blocks[i][1]) for i in idxs)
|
||||
status, _ = await run_single_slot(
|
||||
ctx, f"Question-Pattern {ci}",
|
||||
ctx, f"{lbl}Question-Pattern {ci}",
|
||||
key=f"blocks-{topic}-{ns}question-pattern-c{ci}",
|
||||
prompt=_prompt("Question-Pattern-Research", topic=topic, blocks=block,
|
||||
out_path=fp, extra=_extra(instructions)),
|
||||
@@ -1436,7 +1674,7 @@ async def _question_pattern_block(ctx: GenContext, set_p, files: dict, sidecar:
|
||||
return # nothing to clarify in this chunk
|
||||
subs_total = sum(len(blocks[i][1]) for i in idxs)
|
||||
status, _ = await run_single_slot(
|
||||
ctx, f"Question-Pattern-Clarification {ci}",
|
||||
ctx, f"{lbl}Question-Pattern-Clarification {ci}",
|
||||
key=f"blocks-{topic}-{ns}question-pattern-final-c{ci}",
|
||||
prompt=_prompt("Question-Pattern-Critique", topic=topic, table="\n\n".join(block_texts), out_path=fp, extra=_extra(instructions)),
|
||||
role="judge", capabilities="files",
|
||||
@@ -1505,7 +1743,7 @@ async def _question_pattern_block(ctx: GenContext, set_p, files: dict, sidecar:
|
||||
return # resume
|
||||
subs_total = sum(len(s) for _, s in items)
|
||||
await run_single_slot(
|
||||
ctx, f"Question pattern catch-up R{round_n}/{pi}",
|
||||
ctx, f"{lbl}Question pattern catch-up R{round_n}/{pi}",
|
||||
key=f"blocks-{topic}-{ns}question-pattern-nach{round_n}-c{pi}",
|
||||
prompt=_prompt("Question-Pattern-Research", topic=topic, blocks=_followup_block(items),
|
||||
out_path=fp, extra=_extra(instructions)),
|
||||
@@ -2144,6 +2382,26 @@ def _outline_complete(files: dict) -> bool:
|
||||
return isinstance(d, dict) and isinstance(d.get("chapters"), list) and bool(d.get("chapters"))
|
||||
|
||||
|
||||
def _outline_review_schema(data, valid: set[int], n_chapters: int, n_blocks: int):
|
||||
"""{"moves": {"<blocknr>": <chapter-idx>}} → {nr: idx} (may be {}) · None if broken/invalid.
|
||||
A mass rewrite (more than a third of all blocks) is rejected — the reviewer's job is
|
||||
spotting misplacements, not re-designing the outline."""
|
||||
if not isinstance(data, dict) or not isinstance(data.get("moves"), dict):
|
||||
return None
|
||||
out: dict[int, int] = {}
|
||||
for k, v in data["moves"].items():
|
||||
try:
|
||||
nr, ch = int(k), int(v)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
if nr not in valid or not (1 <= ch <= n_chapters):
|
||||
return None
|
||||
out[nr] = ch
|
||||
if len(out) * 3 > n_blocks:
|
||||
return None
|
||||
return out
|
||||
|
||||
|
||||
def _outline_schema(data, valid: set[int]):
|
||||
"""{"chapters":[{title,numbers}]} → cleaned (valid numbers, each exactly once) ·
|
||||
None at <80 % coverage (agent/judge omitted too much)."""
|
||||
@@ -2302,6 +2560,34 @@ async def _outline_block(ctx: GenContext, set_p, files: dict, entries: dict, ins
|
||||
timeout=_timeout("plan_judge", len(entries)))
|
||||
plan = _outline_schema(_json_file(files["outline"]), valid) or proposals[0]
|
||||
|
||||
# Placement review (best-effort): ONE judge checks every block→chapter assignment and
|
||||
# reports ONLY misplacements as moves. Invalid/mass output → plan unchanged.
|
||||
if proposals and len(plan["chapters"]) >= 2 and not is_cancelled():
|
||||
rp = files["arbeit"] / "outline-review.json"
|
||||
moves = _outline_review_schema(_json_file(rp), valid, len(plan["chapters"]), len(entries))
|
||||
if moves is None:
|
||||
chapter_text = "\n\n".join(
|
||||
f"KAPITEL {k}: {ch['title']}\n" + "\n".join(f" {n}. {_title(entries[n])}" for n in ch["numbers"])
|
||||
for k, ch in enumerate(plan["chapters"], 1))
|
||||
set_p("Outline review…", step=step)
|
||||
await run_single_slot(
|
||||
ctx, "Outline-Review", key=f"blocks-{topic}-outline-review",
|
||||
prompt=_prompt("Guide-Outline-Review", topic=topic, chapters=chapter_text,
|
||||
out_path=rp, extra=_extra(instructions)),
|
||||
role="judge", capabilities="files",
|
||||
payload=lambda result: _outline_review_schema(
|
||||
_json_file(rp), valid, len(plan["chapters"]), len(entries)),
|
||||
timeout=_timeout("plan_judge", len(entries)))
|
||||
moves = _outline_review_schema(_json_file(rp), valid, len(plan["chapters"]), len(entries))
|
||||
for nr, target in (moves or {}).items():
|
||||
for ch in plan["chapters"]:
|
||||
if nr in ch["numbers"]:
|
||||
ch["numbers"].remove(nr)
|
||||
plan["chapters"][target - 1]["numbers"].append(nr)
|
||||
if moves:
|
||||
plan["chapters"] = [ch for ch in plan["chapters"] if ch["numbers"]]
|
||||
_log(topic, f"Outline-Review: {len(moves)} Block/Blöcke umsortiert")
|
||||
|
||||
# Completeness: every block appears — missing in "Other" (against omitting agents/judge).
|
||||
included = {n for ch in plan["chapters"] for n in ch["numbers"]}
|
||||
missing = [n for n in entries if n not in included]
|
||||
@@ -2372,7 +2658,7 @@ def _artefacts_complete(files: dict) -> bool:
|
||||
return isinstance(d, dict) and all(t in d for t in ARTEFACT_TYPES)
|
||||
|
||||
|
||||
async def _artefacts_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str, ns: str = "") -> dict | None:
|
||||
async def _artefacts_block(ctx: GenContext, set_p, files: dict, sidecar: dict, instructions: str, ns: str = "", lbl: str = "") -> dict | None:
|
||||
"""Generate learning artefacts per type from the stored facts — one generation pass
|
||||
per type over chunks. Worked examples are verified against the facts (wrong ones discarded);
|
||||
flashcards are low-risk and stay unchecked. → {type: [entries]} (also in files)."""
|
||||
@@ -2400,7 +2686,7 @@ async def _artefacts_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
|
||||
atomic_write_json(files["artefakte"], empty_map, indent=1)
|
||||
return empty_map
|
||||
|
||||
chunks = _lpt_chunks([len(z) for _, z in blocks], FACTS_CHUNK_SUBS)
|
||||
chunks = _lpt_chunks([len(z) for _, z in blocks], ARTEFACT_CHUNK_SUBS)
|
||||
def block_text(idxs):
|
||||
return "\n\n".join(f"BLOCK: {blocks[i][0]}\nSUBBAUSTEINE:\n" + "\n".join(blocks[i][1]) for i in idxs)
|
||||
|
||||
@@ -2419,7 +2705,8 @@ async def _artefacts_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
|
||||
await asyncio.gather(*[
|
||||
run_agent(f"blocks-{topic}-{ns}artifact-example-check-c{ci}-j{j}",
|
||||
_prompt("Artifact-Example-Check", topic=topic, facts=block_text(idxs), examples=examples_txt, out_path=cpath(j), extra=_extra(instructions)),
|
||||
_timeout("content_check", len(items)), provider=provider, role="judge", capabilities=caps)
|
||||
_timeout("content_check", len(items)), provider=provider, role="judge", capabilities=caps,
|
||||
scope=topic, label=f"{lbl}Beispiel-Check {ci}/{j}")
|
||||
for j in pending], return_exceptions=True)
|
||||
outs = [s for j in (1, 2, 3)[:FACTS_CHECK_PANEL] if (s := _example_check_schema(_json_file(cpath(j)))) is not None]
|
||||
if not outs:
|
||||
@@ -2446,7 +2733,7 @@ async def _artefacts_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
|
||||
if schema(_json_file(p)) is not None:
|
||||
return True
|
||||
await run_single_slot(
|
||||
ctx, f"{_ARTEFACT_STEP[typ]} {ci}", key=f"blocks-{topic}-{ns}artifact-{typ}-c{ci}",
|
||||
ctx, f"{lbl}{_ARTEFACT_STEP[typ]} {ci}", key=f"blocks-{topic}-{ns}artifact-{typ}-c{ci}",
|
||||
prompt=_prompt(_ARTEFACT_PROMPT[typ], topic=topic, blocks=block_text(idxs), out_path=p, extra=_extra(instructions)),
|
||||
role="guide", capabilities="files",
|
||||
payload=lambda result, p=p: schema(_json_file(p)),
|
||||
|
||||
Reference in New Issue
Block a user