278 lines
15 KiB
Python
278 lines
15 KiB
Python
#!/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()
|