#!/usr/bin/env python3 """АГЕНТ-СУДЕЙСТВО ЭКСП-22 — тот же абсолютный риг, другая панель. $0 по ключам провайдеров. Риг НЕ пере-изобретается: методика (абсолютная шкала · слепые метки Т1..Тn · перемешивание хешем соли · декой · пара пола · обоснование ЦИТАТОЙ на каждую ненулевую ось · семейство, впечатанное в ключ ДО первого ответа · Холм) живёт в `role_topology/absjudge.py` и подключается через `configure()`. Здесь только панель эксп-22: свои единицы, свои армы, свои семейства и свой каталог сырья. ЧЕТЫРЕ ПРОХОДА, каждый со своим семейством, объявленным ДО выпуска заданий: p2zh — панель черновиков на zh: доезжает ли качество черновика до финала после боевого редактора; p2en — та же панель на оси переноса en; p3 — панель редакторов поверх ОДНОЙ якорной базы; dva — довесок на купленном сырье эксп-21: прямой контраст `D_` против `A` агент-голосами. ЗНАК: перевес > 0 означает «ПЕРВЫЙ арм лучше» (меньше ошибок). КАП АГЕНТ-ЗАПУСКОВ ≤60 на пак. Счётчик ведётся файлом `agent-runs.json`: одна судейская сессия = одна запись. Превышение — стоп и пинг, а не тихое продолжение. Запуск: --plan PASS что будет выпущено, $0 --emit PASS выпустить задания судьям, $0 --ingest PASS разобрать ответы, $0 --controls PASS контроли (читать ДО боевых чисел), $0 --score PASS контрасты с поправкой Холма, $0 --axes PASS разложение по осям (разведочно), $0 --runs сколько агент-запусков израсходовано """ from __future__ import annotations import json import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") ZONE = Path(__file__).resolve().parent sys.path.insert(0, str(ZONE)) sys.path.insert(0, str(REPO / "eval" / "role_topology")) import absjudge as AJ # noqa: E402 import material as M # noqa: E402 import money as MONEY # noqa: E402 import panel as P2 # noqa: E402 OUT = MONEY.OUT KEYS = OUT / "blind-keys" RUNS = OUT / "agent-runs.json" CAP = 60 def arm_map(pass_: str) -> dict[str, str]: """Арм → модель. Имена армов НЕЙТРАЛЬНЫ (D0/E0/R0): в задание судье имя арма не попадает вовсе, но и в ключе оно не должно подсказывать «дорогая модель», если ключ кто-то откроет.""" if pass_ in ("p2zh", "p2en"): ts = P2.tenants() return {**{f"D{i}": m for i, m in enumerate(ts)}, **{f"E{i}": m for i, m in enumerate(ts)}} if pass_ == "p3": import editors as ED # noqa: PLC0415 return {f"R{i}": m for i, m in enumerate(ED.panel())} | {"F": P2.ANCHOR} return {"A": "эксп-21 arm A", "D_": "эксп-21 arm D_"} def family(pass_: str) -> list[tuple[str, str, str]]: """Семейство контрастов прохода. Объявляется ДО выпуска и впечатывается в ключ.""" if pass_ in ("p2zh", "p2en"): ts = P2.tenants() fam = [(f"E{i}", "E0", f"финал поверх черновика жильца {i} против финала поверх якоря") for i in range(1, len(ts))] fam.append(("E0", "D0", "КОНТРОЛЬ: покупает ли боевой редактор поверх якорного черновика")) return fam if pass_ == "p3": import editors as ED # noqa: PLC0415 pan = ED.panel() fam = [(f"R{i}", "R0", f"редактор {i} против боевого над ТОЙ ЖЕ базой") for i in range(1, len(pan))] fam.append(("R0", "F", "КОНТРОЛЬ: покупает ли боевой редактор поверх якорной базы")) return fam return [("D_", "A", "однопроходка без мандата против связки (сырьё эксп-21)")] def family_desc(pass_: str) -> list[tuple[str, str, str]]: """ОПИСАТЕЛЬНОЕ семейство — ось (а) заказа: черновик КАК ТЕКСТ, до редактора. Пре-рег Ф2 сузил РЕШАЮЩЕЕ семейство до финалов (решение владельца принимается по тому, что уедет в книгу), и эти контрасты поправку Холма не несут и вердиктов не определяют. Но печатать их обязательно: тот же пре-рег требует печатать перевес ниже пола ЧИСЛОМ, а не замалчивать, а промт заказал эту ось явным пунктом. Молчание о купленном и отсуженном замере — та самая ловушка «след отчёта», от которой пак и защищается. """ if pass_ in ("p2zh", "p2en"): ts = P2.tenants() return [(f"D{i}", "D0", f"черновик жильца {i} КАК ТЕКСТ против якорного черновика") for i in range(1, len(ts))] return [] def arms(pass_: str) -> tuple[str, ...]: return tuple(arm_map(pass_)) def units(pass_: str) -> list[dict]: if pass_ == "p2en": return M.units_en() if pass_ == "dva": import bakeoff as BO # noqa: PLC0415 return BO.units() return M.units_zh() def text_of(pass_: str): amap = arm_map(pass_) def hook(arm: str, u: dict) -> str: if pass_ == "dva": import bakeoff as BO # noqa: PLC0415 return BO.text3_of(arm, u) if pass_ == "p3": import editors as ED # noqa: PLC0415 return ED.text_of(arm, u["uid"]) model = amap[arm] return (P2.draft_text(model, u["uid"]) if arm.startswith("D") else P2.edit_text(model, u["uid"])) return hook def floor_of(pass_: str): """Пара повторов ОДНОГО арма на одном входе — истинная разница ноль.""" def hook(u: dict): if pass_ not in ("p2zh", "p3"): return None a = P2.edit_text(P2.ANCHOR, u["uid"], floor=1) b = P2.edit_text(P2.ANCHOR, u["uid"], floor=2) return (a, b) if a.strip() and b.strip() else None return hook def decoy_base(slots: dict) -> str: """В декой сажается порча ЧИСТОГО текста. Берётся финал якоря — он есть во всех проходах.""" for k in ("E0", "R0", "A", "D0"): if slots.get(k, "").strip(): return slots[k] return next(t for t in slots.values() if t.strip()) def setup(pass_: str) -> None: AJ.configure(DIRS_HOOK=None, JUDGES_HOOK=None, # крючки — глобали ОБЩЕГО рига: # не сбросишь — соседний пак прочитает чужие каталоги молча (ревью 10.08) OUT=OUT, KEYS=KEYS, PASS=pass_, ARMS=arms(pass_), ARMS_2B=(), FAMILY=family(pass_), TEXT_HOOK=text_of(pass_), UNITS_HOOK=lambda: units(pass_), FLOOR_HOOK=floor_of(pass_), DECOY_HOOK=decoy_base, SRC_LANG="английском" if pass_ == "p2en" else "китайском") KEYS.mkdir(parents=True, exist_ok=True) def runs_used() -> list[dict]: return json.loads(RUNS.read_text(encoding="utf-8")) if RUNS.exists() else [] def log_run(pass_: str, tokens: list[str], note: str = "") -> int: """Запись агент-запуска + карта «токен → судья». Карта нужна не для отчётности: без идентичности судьи нельзя ни посчитать согласие судей, ни разложить вердикты по популяциям — эксп-21 не персистировал её и потому не мог этого сделать (найдено его же ревью). Карта лежит рядом с ключом и судье не выдаётся. """ rs = runs_used() n = len(rs) + 1 rs.append(dict(n=n, pass_=pass_, tokens=tokens, note=note)) RUNS.write_text(json.dumps(rs, ensure_ascii=False, indent=1), encoding="utf-8") jf = KEYS / f"{pass_}-JUDGES.json" m = json.loads(jf.read_text(encoding="utf-8")) if jf.exists() else {} for t in tokens: m[t] = f"agent-{n:02d}" jf.write_text(json.dumps(m, ensure_ascii=False, indent=1), encoding="utf-8") if n > CAP: raise SystemExit(f"⛔ КАП АГЕНТ-ЗАПУСКОВ ПРОБИТ: {n} > {CAP} — стоп и пинг") return n def cmd_plan(pass_: str) -> None: setup(pass_) us = units(pass_) amap = arm_map(pass_) print(f"проход {pass_} · единиц {len(us)} · армов {len(amap)}") if pass_.startswith("p2"): print(" (D_i — черновик жильца i; E_i — БОЕВОЙ редактор deepseek-v4-pro поверх ЭТОГО " "черновика,\n то есть буква справа называет жильца ЧЕРНОВОЙ роли, а не редактора)") for a, m in amap.items(): print(f" {a:4s} → {m}") print("\nсемейство (впечатывается в ключ ДО ответов):") for a, b, w in family(pass_): print(f" {a:4s} vs {b:4s} {w}") have = sum(1 for u in us for a in amap if (text_of(pass_)(a, u) or "").strip()) print(f"\nтекстов готово {have} из {len(us) * len(amap)}") print(f"агент-запусков израсходовано {len(runs_used())} из {CAP}") # Сколько единиц отдаётся ОДНОЙ судейской сессии. Кап промта — «пакет до ~20 текстов»; здесь # считается по фактическому числу меток прохода, а не на глаз. def per_run(pass_: str) -> int: n_arms = len(arm_map(pass_)) + 3 # + декой и пара пола return max(1, 20 // max(1, n_arms)) def cmd_dispatch(pass_: str) -> None: """Группы заданий на агент-запуски + дословный промт судьи. Ничего не запускает сам.""" setup(pass_) tasks = sorted((OUT / f"aj-{pass_}-tasks").glob("*.txt")) if not tasks: print("заданий нет — сначала --emit") return k = per_run(pass_) groups = [tasks[i:i + k] for i in range(0, len(tasks), k)] used = len(runs_used()) print(f"проход {pass_}: заданий {len(tasks)} · по {k} на сессию · сессий {len(groups)}") print(f"агент-запусков израсходовано {used}, останется {CAP - used - len(groups)} из {CAP}") if used + len(groups) > CAP: print("⛔ КАП НЕ ВМЕЩАЕТ ПРОХОД — стоп и пинг, не резать молча") return print("\nПРОМТ СУДЬИ (дословно, один на сессию; список файлов подставляется):\n") print(JUDGE_PROMPT.format(files="<файлы сессии>", answers=OUT / f"aj-{pass_}")) print("\nГРУППЫ:") for i, g in enumerate(groups, 1): print(f" сессия {i}: " + " ".join(str(p) for p in g)) JUDGE_PROMPT = """Ты — литературный редактор-эксперт в роли СУДЬИ. Работай сам, не делегируй \ задачу другим агентам: раздача пакетов разным исполнителям смешивает их пороги строгости и \ делает числа несравнимыми. Файлы заданий: {files} Для КАЖДОГО файла: прочитай его целиком и выполни ровно то, что в нём написано — оцени каждый \ помеченный вариант ОТДЕЛЬНО против исходника, посчитай ошибки по четырём осям и запиши ответ \ файлом ровно по тому пути, который назван внутри задания (каталог {answers}). Жёсткие правила: * обоснование ПОЧЕМУ обязательно для каждой НЕНУЛЕВОЙ оси и обязано содержать цитату в кавычках; \ число без цитаты не принимается разбором и на диск не попадает; * ноль по оси обоснования не требует; * НЕ открывай ничего, кроме файлов заданий: ни ключей раскладки, ни кода, ни соседних каталогов. \ Метки Т1..Тn слепые намеренно; * порядок меток ничего не значит и перемешан; * не сравнивай варианты между собой — только каждый против исходника. Верни одной строкой: сколько файлов заданий обработано и куда записаны ответы.""" def main() -> None: a = sys.argv[1:] if not a: print(__doc__) return if a[0] == "--runs": rs = runs_used() print(f"агент-запусков {len(rs)} из {CAP}") for r in rs: print(f" {r['n']:3d} {r['pass_']:6s} токенов {len(r['tokens'])} {r.get('note', '')}") return if len(a) < 2: print("нужен PASS: p2zh | p2en | p3 | dva") return cmd, pass_ = a[0], a[1] setup(pass_) fn = {"--plan": lambda: cmd_plan(pass_), "--emit": AJ.emit, "--ingest": AJ.ingest, "--dispatch": lambda: cmd_dispatch(pass_), "--controls": AJ.controls, "--score": AJ.score, "--axes": AJ.axes, "--position": AJ.position, "--agree": AJ.judges_agree}.get(cmd) if fn: fn() else: print(__doc__) if __name__ == "__main__": main()