update
This commit is contained in:
@@ -28,6 +28,7 @@ import json
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
import shutil
|
||||
import unicodedata
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
@@ -63,7 +64,7 @@ from textkit import _norm_title, _parse_selection, _title, clean_title
|
||||
log = logging.getLogger("creator.board_inventory")
|
||||
|
||||
BOARD = "inventory"
|
||||
RESEARCH_RUNTIME = 900 # one research agent, one round — the tail ingests live while it writes
|
||||
from config import RESEARCH_RUNTIME # noqa: E402 — zentral tunebar
|
||||
_POLL_RESEARCH = 3 # seconds between live reads of a running research file
|
||||
|
||||
_ingest_lock = asyncio.Lock() # serializes the read-modify-write title upserts
|
||||
@@ -84,15 +85,37 @@ def _line(i: int, p: dict, mark: str = "") -> str:
|
||||
return f"{i}. {mark}{p['title']} — {d}" if d else f"{i}. {mark}{p['title']}"
|
||||
|
||||
|
||||
def _naming_schema(data, count: int) -> int | None:
|
||||
"""{"best": N} → 1-based member index in [1, count] · otherwise None."""
|
||||
def _naming_schema(data, count: int) -> tuple[int | None, str | None, bool] | None:
|
||||
"""{"ok":true} → (None,None,True) · {"best":N[,"name":…]} → (N, name|None, False).
|
||||
name über 80 Zeichen wird verworfen (Kürze ist der Zweck der Abstraktion)."""
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
if data.get("ok") is True:
|
||||
return None, None, True
|
||||
try:
|
||||
n = int(data.get("best"))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
return n if 1 <= n <= count else None
|
||||
if not 1 <= n <= count:
|
||||
return None
|
||||
name = str(data.get("name", "")).strip() or None
|
||||
if name and len(name) > 80:
|
||||
name = None
|
||||
return n, name, False
|
||||
|
||||
|
||||
def _name_verankert(name: str, rows: list[dict], ctoks: set[str] | None) -> bool:
|
||||
"""Abstraction guard: a free-formed title must be anchored — in the corpus (uni/projekt)
|
||||
or in the members' own words (thema). Unanchored names drift to textbook canon
|
||||
(measured: 'Königsberger Brückenproblem', 0 corpus hits) → fall back to best-of-members."""
|
||||
if ctoks is not None:
|
||||
return _hat_anker(name, ctoks)
|
||||
from qa import _STOP
|
||||
basis = " ".join(f"{r.get('title', '')} {r.get('description') or ''}" for r in rows)
|
||||
fold = lambda s: unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode().casefold() # noqa: E731
|
||||
btoks = set(re.findall(r"\w{3,}", fold(basis)))
|
||||
ntoks = {t for t in re.findall(r"\w{3,}", fold(name)) if t not in _STOP}
|
||||
return bool(ntoks) and ntoks <= btoks
|
||||
|
||||
|
||||
# ── Embedding cache (per flow) ─────────────────────────────────────────────────────
|
||||
@@ -450,15 +473,25 @@ def _hat_anker(title: str, ctoks: set[str]) -> bool:
|
||||
async def _anker_beleg(ctx: GenContext, flow: Flow, kandidaten: list[tuple[str, dict]]) -> set[str]:
|
||||
"""Evidence judge for quorum titles WITHOUT any corpus anchor — two readers naming the
|
||||
same famous canon independently beat the quorum although the material never mentions it
|
||||
(measured: 'Königsberger Brückenproblem', 0 corpus hits). FAIL-OPEN: the titles carry
|
||||
2-reader backing, only an explicit 'nein' rejects. → card_ids to reject."""
|
||||
(measured: 'Königsberger Brückenproblem', 0 corpus hits). FAIL-OPEN for judge errors —
|
||||
but an EMPTY evidence pack (no distinctive token anywhere in the corpus) rejects
|
||||
deterministically: the judge approved 3 canon titles on sham excerpts. → card_ids to reject."""
|
||||
from qa import _distinctive
|
||||
topic = flow.topic
|
||||
folder = source_folder(topic)
|
||||
lines = []
|
||||
hart_nein: set[str] = set()
|
||||
for k, (cid, p) in enumerate(kandidaten, 1):
|
||||
ev = _evidence_pack(folder, None, [p.get("title", ""), p.get("description") or ""], budget=4000)
|
||||
# distinctive tokens only: title+description as raw queries pulled generic sections
|
||||
# („Beispiel", „klassisch") — the judge saw sham excerpts and waved canon through
|
||||
toks = _distinctive(p.get("title", "")) | _distinctive(p.get("description") or "")
|
||||
ev = _evidence_pack(folder, None, [" ".join(sorted(toks))], budget=4000) if toks else ""
|
||||
if not ev: # nothing to attest — deterministic reject, no judge to sweet-talk
|
||||
hart_nein.add(cid)
|
||||
lines.append(f"{k}. {p.get('title', '')} — {p.get('description') or ''}\nAUSZÜGE:\n"
|
||||
f"{ev or '(keine passenden Auszüge im Material gefunden)'}")
|
||||
if len(hart_nein) == len(kandidaten):
|
||||
return hart_nein
|
||||
h = _h(*[cid for cid, _ in kandidaten])
|
||||
path = flow.work_dir / f"anker-beleg-{h}.json"
|
||||
ids = set(range(1, len(kandidaten) + 1))
|
||||
@@ -473,7 +506,8 @@ async def _anker_beleg(ctx: GenContext, flow: Flow, kandidaten: list[tuple[str,
|
||||
timeout=_timeout("selection_mapping", len(kandidaten)))
|
||||
if status != OK or not isinstance(verdict, dict):
|
||||
verdict = {} # fail-open
|
||||
return {cid for k, (cid, _p) in enumerate(kandidaten, 1) if str(verdict.get(k, "ja")) == "nein"}
|
||||
return hart_nein | {cid for k, (cid, _p) in enumerate(kandidaten, 1)
|
||||
if str(verdict.get(k, "ja")) == "nein"}
|
||||
|
||||
|
||||
async def _proc_consensus_gate(ctx: GenContext, flow: Flow, cards):
|
||||
@@ -608,32 +642,42 @@ async def _proc_clarify(ctx: GenContext, flow: Flow, cards):
|
||||
|
||||
|
||||
async def _choose_title(ctx: GenContext, flow: Flow, cid: str, rows: list[dict],
|
||||
template: str, current: int | None = None) -> str:
|
||||
"""Judge picks the best member title (index). Parse-fail → survivorship fallback."""
|
||||
template: str, current_title: str | None = None) -> tuple[str, str | None]:
|
||||
"""Judge picks the best member title and MAY propose a short abstracted name — kept
|
||||
only when anchored (_name_verankert). → (member_norm, custom_title|None);
|
||||
("", None) on cancel; {"ok":true} in the check keeps the current title."""
|
||||
topic = flow.topic
|
||||
h = _h(*[r["norm"] for r in rows], template)
|
||||
path = flow.work_dir / f"naming-{cid}-{h}.json"
|
||||
best = _naming_schema(_json_file(path), len(rows))
|
||||
if best is None:
|
||||
verdict = _naming_schema(_json_file(path), len(rows))
|
||||
if verdict is None:
|
||||
lines = "\n".join(f"{k + 1}. {_t_text(r)}" for k, r in enumerate(rows))
|
||||
kw = dict(topic=topic, members=lines, out_path=path)
|
||||
if current is not None:
|
||||
kw["current"] = current
|
||||
status, best = await run_single_slot(
|
||||
kw = dict(topic=topic, members=lines)
|
||||
if current_title is not None:
|
||||
kw["current_title"] = current_title
|
||||
status, verdict = await run_single_slot(
|
||||
ctx, f"Naming {cid}", key=f"blocks-{topic}-naming-{cid}-{h}",
|
||||
prompt=_prompt(template, **kw), role="judge", capabilities="files",
|
||||
payload=lambda result, p=path, n=len(rows): _naming_schema(_json_file(p), n),
|
||||
prompt=_prompt(template, **kw), role="judge", capabilities="none",
|
||||
payload=lambda result, p=path, n=len(rows): _sink_json(result, p, lambda d: _naming_schema(d, n)),
|
||||
timeout=_timeout("selection_mapping", len(rows)))
|
||||
if status == CANCELLED:
|
||||
return ""
|
||||
return "", None
|
||||
if status == FAILED:
|
||||
best = None
|
||||
if best is None:
|
||||
verdict = None
|
||||
if verdict is None:
|
||||
rep = _rep(rows)
|
||||
w = _norm_title(rep["title"])
|
||||
norms = [r["norm"] for r in rows]
|
||||
return w if w in norms else norms[0]
|
||||
return rows[best - 1]["norm"]
|
||||
return (w if w in norms else norms[0]), None
|
||||
best, name, ok = verdict
|
||||
if ok: # check judge confirms the current (possibly custom) title: change nothing
|
||||
return "", (current_title or "")
|
||||
custom = None
|
||||
if name:
|
||||
ctoks = flow.state.get("korpus_tokens")
|
||||
if _name_verankert(name, rows, ctoks):
|
||||
custom = clean_title(name)
|
||||
return rows[best - 1]["norm"], custom
|
||||
|
||||
|
||||
async def _proc_naming(ctx: GenContext, flow: Flow, cards):
|
||||
@@ -650,13 +694,15 @@ async def _name_one(ctx: GenContext, flow: Flow, c):
|
||||
p = c["payload"]
|
||||
rows = await _member_rows(topic, cid)
|
||||
if len(rows) > 1 and not p.get("renamed"):
|
||||
winner = await _choose_title(ctx, flow, cid, rows, "Blocks-Naming")
|
||||
if not winner: # cancelled
|
||||
winner, custom = await _choose_title(ctx, flow, cid, rows, "Blocks-Naming")
|
||||
if not winner and custom is None: # cancelled
|
||||
return
|
||||
p["main_norm"] = winner
|
||||
w = next(r for r in rows if r["norm"] == winner)
|
||||
p["title"], p["description"] = w["title"], w.get("description") or p.get("description", "")
|
||||
await db.kanban_set_payload(topic, BOARD, cid, p)
|
||||
if winner:
|
||||
p["main_norm"] = winner
|
||||
w = next(r for r in rows if r["norm"] == winner)
|
||||
p["title"] = custom or w["title"]
|
||||
p["description"] = w.get("description") or p.get("description", "")
|
||||
await db.kanban_set_payload(topic, BOARD, cid, p)
|
||||
await db.kanban_advance(topic, BOARD, cid, "naming_check")
|
||||
|
||||
|
||||
@@ -675,14 +721,14 @@ async def _namecheck_one(ctx: GenContext, flow: Flow, c):
|
||||
p = c["payload"]
|
||||
rows = await _member_rows(topic, cid)
|
||||
if len(rows) > 1 and not p.get("renamed"):
|
||||
norms = [r["norm"] for r in rows]
|
||||
cur = p.get("main_norm")
|
||||
current = norms.index(cur) + 1 if cur in norms else 1
|
||||
winner = await _choose_title(ctx, flow, cid, rows, "Blocks-Naming-Check", current=current)
|
||||
if not winner:
|
||||
winner, custom = await _choose_title(ctx, flow, cid, rows, "Blocks-Naming-Check",
|
||||
current_title=p.get("title", ""))
|
||||
if not winner and custom is None: # cancelled
|
||||
return
|
||||
w = next(r for r in rows if r["norm"] == winner)
|
||||
p["title"], p["description"] = w["title"], w.get("description") or p.get("description", "")
|
||||
if winner: # ok-verdict ("" + custom) keeps title AND description untouched
|
||||
w = next(r for r in rows if r["norm"] == winner)
|
||||
p["title"] = custom or w["title"]
|
||||
p["description"] = w.get("description") or p.get("description", "")
|
||||
readers = sorted(set().union(*[set(r.get("readers") or []) for r in rows])) if rows else []
|
||||
sources = sorted(set().union(*[set(r.get("sources") or []) for r in rows])) if rows else []
|
||||
await db.kanban_upsert_card(topic, BOARD, f"b-{cid}", "block", "fragment_filter", {
|
||||
@@ -1872,6 +1918,13 @@ async def reset_board_from_stage(topic: str, board: str, stage: str, files: dict
|
||||
await db.delete_subblocks(topic)
|
||||
await db.delete_question_pattern(topic)
|
||||
await db.delete_sub_artefakte(topic)
|
||||
# global sidecar files and per-block resume slots: leftovers of the previous
|
||||
# derive would re-merge/resume into the fresh run — nothing here is reused
|
||||
for k in ("sidecar", "facts", "sub_roh", "question_pattern", "artefakte"):
|
||||
files[k].unlink(missing_ok=True)
|
||||
for d in files["arbeit"].glob("ab-*"):
|
||||
if d.is_dir():
|
||||
shutil.rmtree(d, ignore_errors=True)
|
||||
await db.add_event(topic, "reset", key=f"{board}:from-{stage}", status=str(moved))
|
||||
return moved
|
||||
|
||||
|
||||
Reference in New Issue
Block a user