textmachine/eval/tenant_panel/judge.py

278 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()