This commit is contained in:
Team3
2026-07-04 19:20:48 +02:00
parent 92c69c1561
commit c05421a8c1
14 changed files with 695 additions and 277 deletions

View File

@@ -31,7 +31,7 @@ from jsonio import parse_json_text, 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
from crawl import crawl
from pipeline import (
CANCELLED, FAILED, OK, GenContext, _extra, _gather_progress, _yesno_schema, _log, _prompt, _race,
CANCELLED, FAILED, OK, GenContext, _detached, _extra, _gather_progress, _yesno_schema, _log, _prompt, _race,
_relevance_schema, _runde_schema, _semaphore, _str_list, _levels_schema, _timeout, run_single_slot,
)
from textkit import (
@@ -597,6 +597,39 @@ def _sink_json(result, path: Path, schema):
return val
async def _panel_2of3(tasks: dict, sink, outs_now, norm) -> None:
"""Panel-Welle „first 2 agree": kehrt zurück, sobald zwei vorliegende Verdicts
übereinstimmen — die dritte Stimme kann die Mehrheit dann nicht mehr kippen. Sonst
(Dissens) wird weiter gewartet. Der Langsamste bestimmte jede Welle (gemessen: 98 s
bei ok-p50 ~50 s). Nachzügler laufen detached weiter; ihr File dient nur dem Resume.
tasks: {Task: judge_nr} · sink(j, result) persistiert · outs_now() liest Verdicts ·
norm(verdict) macht sie vergleichbar."""
offen = dict(tasks)
while offen:
done, _rest = await asyncio.wait(list(offen), return_when=asyncio.FIRST_COMPLETED)
for t in done:
j = offen.pop(t)
try:
r = t.result()
except Exception: # noqa: BLE001 — Panel ist fail-open, Ausfall = fehlende Stimme
continue
if isinstance(r, tuple):
sink(j, r)
outs = [norm(s) for s in outs_now()]
if len(outs) >= 2 and any(outs[a] == outs[b]
for a in range(len(outs)) for b in range(a + 1, len(outs))):
break
for t, j in offen.items(): # Dritter läuft weiter — sein File zählt fürs Resume
async def _warte(t=t, j=j):
try:
r = await t
if isinstance(r, tuple):
sink(j, r)
except (asyncio.CancelledError, Exception): # noqa: BLE001
pass
_detached(asyncio.create_task(_warte()))
def _sink_subs(result, path: Path):
"""Finder reply as TEXT (marker format), persisted to `path` for audit/diagnosis.
File fallback: a tool-capable agent (thema web mode) that wrote the file despite the
@@ -796,7 +829,23 @@ async def _subblocks_block(ctx: GenContext, set_p, files: dict, entries: dict, i
"role": "quick", "capabilities": round_caps,
"payload": (lambda result, p=p: _sink_subs(result, p)),
} 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)
async def _fold_late(d: dict) -> None:
"""Dritte Stimme nachbuchen statt warten (ersetzte grace=300, gemessen 73 s/Runde):
Mentions sind additiv; ein Fund, den nur der Nachzügler hat, bleibt Einzelfund
und läuft durchs Clarify-Quellen-Gate — verfälscht wird nichts."""
for marker, subs in d.items():
num = _resolve_title(chunk_idx, marker)
if num is None:
continue
seen_late = set()
for sub in subs:
sn = _norm_title(sub)
if sn and sn not in seen_late:
seen_late.add(sn)
await db.upsert_subblock(topic, norm_by_num[num], sn, title_by_num[num], sub)
agent_texts = await _race(topic, label, slots, 2, _timeout("subblock", len(subset)), provider, cancelled=is_cancelled, late=_fold_late)
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}
@@ -1846,16 +1895,29 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
fk[f] = ek[f]
return raw
def _inline_source(queries: list[str]) -> tuple[str, str]:
"""Korpus-Auszüge INLINE statt Datei-Recherche → (source, capabilities). Die
Tool-Agenten (bash/read) verloren sich messbar in Reasoning-Schleifen (bis 39k
Zeichen) und endeten mit leerem Turn — Retry-Wellen à 6090 s seriell pro Block.
No-Tool-Calls mit Inline-Material hatten 0 solcher Fälle (Muster: Facts-Check).
Fail-open: ohne Treffer bleibt die alte Selbst-Recherche."""
ev = _evidence_pack(folder, sources, queries) if folder else ""
if ev:
return _prompt("Blocks-Source-Inline", excerpts=ev), "none"
return source, caps
# Phase "Facts find": 1 generator per chunk.
async def _find(ci, idxs):
fp = raw_path(ci)
if _facts_schema(_json_file(fp)):
return True
subs_total = sum(len(blocks[i][1]) for i in idxs)
f_source, f_caps = await asyncio.to_thread(
_inline_source, [blocks[i][0] for i in idxs] + [s for i in idxs for s in blocks[i][1]])
status, _r = await run_single_slot(
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,
prompt=_prompt("Facts-Research", topic=topic, source=f_source, blocks=block_text(idxs), out_path=fp, extra=_extra(instructions)),
role="quick", capabilities=f_caps,
payload=lambda result, p=fp: _sink_or_file(result, p, _facts_schema),
timeout=_timeout("content", subs_total))
return status != FAILED and _facts_schema(_json_file(fp)) is not None
@@ -1883,10 +1945,12 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
for fk in fm.values())
for bt, fm in per.items())
subs_total = sum(len(blocks[i][1]) for i in idxs)
e_source, e_caps = await asyncio.to_thread(
_inline_source, [blocks[i][0] for i in idxs] + [s for i in idxs for s in blocks[i][1]])
await run_single_slot(
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,
prompt=_prompt("Facts-Supplement", topic=topic, source=e_source, blocks=block, out_path=ep, extra=_extra(instructions)),
role="quick", capabilities=e_caps,
payload=lambda result, p=ep: _sink_or_file(result, p, _facts_schema),
timeout=_timeout("content", subs_total))
@@ -1913,16 +1977,17 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
ev = _cited_evidence(folder, sources, cites, fallback) if folder else ""
c_source = _prompt("Blocks-Source-Inline", excerpts=ev) if ev else source
pending = [j for j in panel if _facts_check_schema(_json_file(chk_path(ci, j))) is None]
rs = await asyncio.gather(*[
tmap = {asyncio.create_task(
run_agent(f"blocks-{topic}-{ns}facts-check-c{ci}-j{j}",
_prompt("Facts-Check", topic=topic, source=c_source, facts=facts_text, out_path=chk_path(ci, j), extra=_extra(instructions)),
_timeout("content_check", len(per)), provider=provider, role="judge",
capabilities="none" if ev else caps,
scope=topic, label=f"{lbl}Facts check {ci}/{j}")
for j in pending], return_exceptions=True)
for j, r in zip(pending, rs):
if isinstance(r, tuple):
_sink_json(r, chk_path(ci, j), _facts_check_schema)
scope=topic, label=f"{lbl}Facts check {ci}/{j}")): j
for j in pending}
await _panel_2of3(tmap, lambda j, r: _sink_json(r, chk_path(ci, j), _facts_check_schema),
lambda: [s for j in panel
if (s := _facts_check_schema(_json_file(chk_path(ci, j)))) is not None],
lambda s: {tuple(x) for x in s})
outs = [s for j in panel if (s := _facts_check_schema(_json_file(chk_path(ci, j)))) is not None]
bvotes: dict[str, int] = {}
vvotes: dict[str, int] = {}
@@ -1970,10 +2035,13 @@ async def _facts_block(ctx, set_p, files: dict, raw: dict, q: dict, folder, inst
goal.append(f"BLOCK: {bt}\nSUBBAUSTEINE:\n" + "\n".join(f"- {s}" for s in affected_subs))
if not goal:
return
x_source, x_caps = await asyncio.to_thread(
_inline_source, [bt for bt in rel_by] + [s for subs in rel_by.values()
for s in subs if _norm_title(s) in subs_norm])
await run_single_slot(
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,
prompt=_prompt("Facts-Research", topic=topic, source=x_source, blocks="\n\n".join(goal), out_path=fix_path(ci), extra=_extra(instructions)),
role="quick", capabilities=x_caps,
payload=lambda result, p=fix_path(ci): _sink_or_file(result, p, _facts_schema),
timeout=_timeout("content", len(subs_norm)))
await _gather_progress([_fix(ci) for ci in correctable], len(correctable), _report_p(set_p, topic, "Facts fix"))
@@ -3266,15 +3334,16 @@ async def _artefacts_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
pending = [j for j in (1, 2, 3)[:FACTS_CHECK_PANEL] if _example_check_schema(_json_file(cpath(j))) is None]
if pending:
# ground truth (facts) is fully inline → no tools, text reply, engine persists
rs = await asyncio.gather(*[
tmap = {asyncio.create_task(
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="none",
scope=topic, label=f"{lbl}Beispiel-Check {ci}/{j}")
for j in pending], return_exceptions=True)
for j, r in zip(pending, rs):
if isinstance(r, tuple):
_sink_json(r, cpath(j), _example_check_schema)
scope=topic, label=f"{lbl}Beispiel-Check {ci}/{j}")): j
for j in pending}
await _panel_2of3(tmap, lambda j, r: _sink_json(r, cpath(j), _example_check_schema),
lambda: [s for j in (1, 2, 3)[:FACTS_CHECK_PANEL]
if (s := _example_check_schema(_json_file(cpath(j)))) is not None],
lambda s: frozenset(s))
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:
return items # no exam possible → keep (best-effort)