Re-freeze before the rest of phase 1: point the ledger at the experiment price table after it failed on a model the old table did not know

This commit is contained in:
Claude (backend session) 2026-08-08 15:47:13 +03:00
parent 9ca51da0a8
commit 930a3792c3
5 changed files with 937 additions and 0 deletions

View file

@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""Ф3 — ПАНЕЛЬ РЕДАКТОРОВ поверх ОДНОЙ якорной базы.
База ЗАМОРОЖЕННЫЙ боевой черновик `deepseek-v4-flash` из Ф2 (тот самый, что прошёл гейт эха).
Все редакторы правят побайтно один текст, поэтому контраст между ними чист от разницы черновиков
ровно того конфаунда, который эксп-21 назвал носителем 70% эффекта.
Конфигурация редактора одинакова у всех: full-regen боевым `editor.md` + ЗАКОН-БЛОК (D39.104).
Единственное различие клеток модель. Это проверяется `--isolation` до покупок.
`gemini-3.1-pro-preview` ОБЯЗАТЕЛЬНЫЙ кандидат промта: он идёт в Ф3 независимо от скрина
(гипотеза эксп-04 «gemini-pro аутлаер прозы»), скрин-числа при нём печатаются.
Вторая база (лучший черновик Ф2) ОПЦИЯ промта: берётся только если Ф2 дал отрыв выше пола И
смета влезает; решение и его основание печатаются `--plan`.
Запуск: --plan состав панели, смета, изоляция, $0
--isolation чем клетки различаются сверка сообщений, $0
--run покупка редактур
--score батарея, канон, цена, $0
"""
from __future__ import annotations
import json
import os
import statistics as st
import sys
import time
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"))
from dotenv import load_dotenv # noqa: E402
from openai import OpenAI # noqa: E402
import bank as BANK # noqa: E402
import battery as B # noqa: E402
import material as M # noqa: E402
import money as MONEY # noqa: E402
import panel as P2 # noqa: E402
import prompts as PR # noqa: E402
from roster import CANDIDATES, call_kwargs # noqa: E402
load_dotenv(REPO / "eval" / ".env")
OUT = MONEY.OUT
BATTLE = "deepseek-v4-pro" # R0 — боевой редактор, база сравнения
MANDATORY = "gemini-3.1-pro-preview" # обязательный кандидат промта
MAX_EDITORS = 5
def client(model: str) -> OpenAI:
base, env, *_ = CANDIDATES[model]
return OpenAI(api_key=os.environ[env], base_url=base, timeout=900)
def panel() -> list[str]:
"""R0 = боевой редактор · затем обязательный кандидат · затем прошедшие скрин по цене.
Порядок фиксирован, потому что имена армов (R0, R1, ) впечатываются в ключ судейского
прохода: сдвиг порядка после выпуска заданий сделал бы семейство неповторимым.
"""
res = P2.screen_result()
out = [BATTLE, MANDATORY]
ok = [m for m, r in res.items()
if r["verdict"] == "прошёл" and m not in out and r.get("price_editor") is not None]
ok.sort(key=lambda m: res[m]["price_editor"])
return (out + ok)[:MAX_EDITORS]
def base_text(uid: str) -> str:
"""Якорная база: замороженный боевой черновик Ф2."""
return P2.draft_text(P2.ANCHOR, uid)
def tag(model: str, uid: str) -> str:
return f"tp3-ed-{model}-{uid}"
def text_of(arm: str, uid: str) -> str:
if arm == "F":
return base_text(uid)
model = panel()[int(arm[1:])]
f = OUT / f"{tag(model, uid)}.json"
return json.loads(f.read_text(encoding="utf-8")).get("content", "") if f.exists() else ""
def msgs_for(model: str, u: dict) -> list[dict]:
return PR.editor_msgs(u["source"], base_text(u["uid"]),
PR.law_block(PR.terms_in(u["source"])))
def cmd_isolation() -> None:
"""Клетки обязаны различаться ТОЛЬКО моделью. Проверяется сборкой сообщений, а не верой."""
u = M.units_zh()[0]
ms = {m: msgs_for(m, u) for m in panel()}
ref = ms[BATTLE]
bad = 0
for m, v in ms.items():
same = [a["content"] == b["content"] for a, b in zip(ref, v, strict=True)]
ok = all(same) and len(ref) == len(v)
bad += not ok
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {m:26s} сообщения совпадают с базой "
f"({len(v)} блоков)")
print(f" закон-блок в сообщениях: {'да' if any('КАНОНИЧЕСКИЕ ПЕРЕВОДЫ' in x['content'] for x in ref) else 'НЕТ'}")
print(f" база — замороженный черновик {P2.ANCHOR}, {len(base_text(u['uid']))} знаков")
print(f"\n{'ИЗОЛЯЦИЯ ЧИСТА' if not bad else f'ПРОВАЛОВ: {bad}'}")
def cmd_plan() -> None:
pan, res, us = panel(), P2.screen_result(), M.units_zh()
print(f"ПАНЕЛЬ Ф3: {len(pan)} редакторов × {len(us)} единиц поверх ОДНОЙ якорной базы")
est = 0.0
for i, m in enumerate(pan):
r = res.get(m, {})
price = r.get("price_editor")
free = " (куплено в Ф2, переиспользуется)" if m == BATTLE else ""
if m != BATTLE:
est += (price or 0.02) * len(us)
print(f" R{i} {m:26s} скрин={r.get('verdict', 'не скринился'):12s} "
f"$/ед={price if price is not None else float('nan'):.5f}{free}")
print(f"\nсмета ${est:.4f} при потолке ${MONEY.CEILINGS['Ф3']:.2f}")
if est > MONEY.CEILINGS["Ф3"]:
print("СМЕТА НЕ ВЛЕЗАЕТ — сократить панель или пинг владельцу ДО покупок")
def cmd_run() -> None:
led = MONEY.Guarded("Ф3", default_expect=0.03)
for m in panel():
if m == BATTLE:
print(f" {m}: клетка куплена в Ф2 (tp2-edit) — не перекупается")
continue
for u in M.units_zh():
if not base_text(u["uid"]):
print(f"{u['uid'][:6]}: якорной базы нет — клетка пропущена")
continue
rec = MONEY.purchase(led, tag(m, u["uid"]), m, client(m),
call_kwargs(m, msgs_for(m, u)),
role="editor3", uid=u["uid"], editor=m)
if rec.get("skipped"):
print("⛔ потолок Ф3 — стоп")
return
print(f" {m:26s} {u['uid'][:6]} finish={str(rec.get('finish')):9s} "
f"{len(rec.get('content', '')):5d} зн ${rec.get('cost_usd', 0):.5f}")
time.sleep(0.15)
print(f"потрачено Ф3 ${led.spent():.6f}")
def cmd_score() -> None:
pan, us, cards = panel(), M.units_zh(), B.load_cards()
print(f"{'арм':30s}{'ед.':>5s}{'ханьцзы':>9s}{'типогр':>8s}{'канон':>14s}"
f"{'покрытие':>10s}{'$/ед':>10s}")
for i, m in enumerate(pan + ["БАЗА"]):
rows, k, s, costs = [], 0, 0, []
for u in us:
t = base_text(u["uid"]) if m == "БАЗА" else text_of(f"R{i}", u["uid"])
if not t.strip():
continue
rows.append(B.run_unit(B.Unit(source=u["source"], draft=t, final=t, cards=cards)))
kk, ss = BANK.coverage(u["source"], t)
k += kk
s += ss
if m == "БАЗА":
costs.append(P2.draft_cost(P2.ANCHOR, u["uid"]))
elif m == BATTLE:
f = OUT / f"{P2.tag_edit(P2.ANCHOR, u['uid'])}.json"
if f.exists():
costs.append(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0))
else:
f = OUT / f"{tag(m, u['uid'])}.json"
if f.exists():
costs.append(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0))
if not rows:
continue
label = "БАЗА (черновик)" if m == "БАЗА" else f"R{i} {m}"
print(f"{label:30s}{len(rows):5d}"
f"{sum(r['cjk_leak']['han_chars'] for r in rows):9d}"
f"{sum(r['typography']['violations'] for r in rows):8d}"
f"{f'{k}/{s}':>14s}{(k / s if s else 0):>10.3f}"
f"{(st.median(costs) if costs else 0):>10.5f}")
led = MONEY.Guarded("Ф3")
print(f"\nпотрачено Ф3 ${led.spent():.6f} из ${led.ceiling:.2f}")
if __name__ == "__main__":
a = sys.argv[1:] or ["--plan"]
fn = {"--plan": cmd_plan, "--isolation": cmd_isolation, "--run": cmd_run,
"--score": cmd_score}.get(a[0])
fn() if fn else print(__doc__)

193
eval/tenant_panel/judge.py Normal file
View file

@ -0,0 +1,193 @@
#!/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 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"])
b = P2.edit_text(P2.ANCHOR, u["uid"], floor=True)
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(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:
rs = runs_used()
rs.append(dict(n=len(rs) + 1, pass_=pass_, tokens=tokens, note=note))
RUNS.write_text(json.dumps(rs, ensure_ascii=False, indent=1), encoding="utf-8")
if len(rs) > CAP:
raise SystemExit(f"⛔ КАП АГЕНТ-ЗАПУСКОВ ПРОБИТ: {len(rs)} > {CAP} — стоп и пинг")
return len(rs)
def cmd_plan(pass_: str) -> None:
setup(pass_)
us = units(pass_)
amap = arm_map(pass_)
print(f"проход {pass_} · единиц {len(us)} · армов {len(amap)}")
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}")
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,
"--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()

View file

@ -28,9 +28,17 @@ sys.path.insert(0, str(REPO / "eval" / "role_topology"))
import buy as BUY # noqa: E402
import roster as ROSTER # noqa: E402
OUT = Path.home() / "books" / "tenant-panel"
OUT.mkdir(parents=True, exist_ok=True)
BUY.OUT = OUT # сырьё пака — своё, не эксп-21
# ⚠ ПРАЙС-ТАБЛИЦА ТОЖЕ СВОЯ. Поймано исполнением на середине скрина: `buy.priced` звал
# `role_topology/prices.cost`, где нет ни одной новой модели ростера — прогон упал на `kimi-k2.6`
# c KeyError. Хуже тихого падения было бы его отсутствие: у пересекающихся моделей таблица
# эксп-21 ставит кэш-цену Gemini равной входной, то есть цена считалась бы по устаревшему прайсу
# и расходилась с объявленным в пре-реге. Подмена делается ЗДЕСЬ, один раз и явно.
BUY.cost = ROSTER.cost
# Теги покупок по фазам. ⚠ Заводишь новый тег — вписываешь СЮДА тем же коммитом: ровно этого
# не сделали трижды в эксп-21, и расход фазы печатался заниженным.
@ -170,6 +178,15 @@ def selftest() -> int:
ok, why = g.afford()
bad += ok
print(f"[{'OK ' if not ok else 'ПРОВАЛ'}] проекционный гард отказывает при нулевом потолке")
# Прайс: КАЖДАЯ модель ростера обязана считаться кассой, иначе покупка падает на середине.
for m in ROSTER.CANDIDATES:
try:
BUY.cost(m, 1000, 0, 1000, 0, 2000)
except Exception as e: # noqa: BLE001
bad += 1
print(f"[ПРОВАЛ] {m}: касса не умеет считать цену ({type(e).__name__})")
print(f"[OK ] цена считается для всех {len(ROSTER.CANDIDATES)} моделей ростера"
if not bad else "")
# Фриз-гейт: НЕ-файл (встроенный питон) обязан быть отвергнут.
try:
exec(compile("import money; money._refuse_unfrozen()", "<string>", "exec"), # noqa: S102

290
eval/tenant_panel/panel.py Normal file
View file

@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""Ф2 — ПАНЕЛЬ ЧЕРНОВИКОВ. Главный рычаг пака.
Почему именно эта роль. Эксп-21 §16.2: тот же редактор над ДРУГИМ черновиком той же длины даёт
7.66 ошибки против 5.91 это ~70% заголовочного эффекта топологии. Черновиком за всю историю
проекта была одна модель. Самая чувствительная роль ни разу не перебиралась.
Что меряется на КАЖДОМ жильце (заказ промта, стр.55):
(а) черновик как ТЕКСТ абсолютный агент-суд (`judge.py`, проход `p2zh`/`p2en`);
(б) боевой редактор `A_law`-конфига ПОВЕРХ каждого черновика доезжает ли качество до финала;
(в) частота эхо-мины доля единиц, потребовавших ре-гена;
(г) батарея + покрытие канона (детерминированно, $0);
(д) цена/единица и проекция на книгу.
ШУМОВОЙ ПОЛ. Один арм гоняется ДВАЖДЫ end-to-end на тех же единицах выбран ЯКОРНЫЙ
(`deepseek-v4-flash` `deepseek-v4-pro`), потому что именно с ним сравниваются все перевесы, и
пол обязан быть измерен на боевой паре, а не на удобной. Пол применяется к Ф2 И Ф3.
Черновики ФРИЗЯТСЯ: Ф3 правит замороженный якорный черновик, клетка «dspro над flash» покупается
здесь ОДИН раз и в Ф3 переиспользуется.
Запуск: --plan состав панели и смета, $0
--drafts черновики всех жильцов (zh + en)
--edit боевой редактор поверх каждого черновика
--floor вторая генерация якорного арма end-to-end
--score батарея, канон, эхо, цена $0
"""
from __future__ import annotations
import json
import os
import statistics as st
import sys
import time
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"))
from dotenv import load_dotenv # noqa: E402
from openai import OpenAI # noqa: E402
import bakeoff as BO # noqa: E402 гейт годности черновика
import bank as BANK # noqa: E402
import battery as B # noqa: E402
import material as M # noqa: E402
import money as MONEY # noqa: E402
import prompts as PR # noqa: E402
from roster import CANDIDATES, call_kwargs # noqa: E402
load_dotenv(REPO / "eval" / ".env")
OUT = MONEY.OUT
ANCHOR = "deepseek-v4-flash" # боевой черновик — якорь панели
EDITOR = "deepseek-v4-pro" # боевой редактор, конфиг `A_law` (full-regen + закон-блок)
MAX_TENANTS = 5 # потолок промта: до 5 жильцов + якорь
RETRIES = 3
def client(model: str) -> OpenAI:
base, env, *_ = CANDIDATES[model]
return OpenAI(api_key=os.environ[env], base_url=base, timeout=900)
def screen_result() -> dict:
f = OUT / "screen.json"
return json.loads(f.read_text(encoding="utf-8")) if f.exists() else {}
def tenants() -> list[str]:
"""Состав панели по ОБЪЯВЛЕННОМУ правилу (пре-рег §0.6).
Берутся прошедшие скрин кандидаты черновой роли. Если смета не влезает панель режется так,
чтобы СОХРАНИТЬ ЦЕНОВУЮ ОСЬ: обязательны якорь, самый дешёвый прошедший и самый дорогой
прошедший; середина добирается по возрастанию цены. Причина: вопрос пака «покупает ли
дорогой черновик качество», и панель, срезанная по цене сверху, на него не отвечает.
"""
res = screen_result()
ok = [m for m, r in res.items()
if r["verdict"] == "прошёл" and r.get("price_translator") is not None]
ok.sort(key=lambda m: res[m]["price_translator"])
if ANCHOR in ok:
ok.remove(ANCHOR)
if len(ok) <= MAX_TENANTS - 1:
return [ANCHOR, *ok]
keep = [ok[0], ok[-1]]
for m in ok[1:-1]:
if len(keep) >= MAX_TENANTS - 1:
break
keep.append(m)
return [ANCHOR, *sorted(keep, key=lambda m: res[m]["price_translator"])]
def units_all() -> list[tuple[dict, bool]]:
"""(единица, en?) — обе оси обязательны."""
return [(u, False) for u in M.units_zh()] + [(u, True) for u in M.units_en()]
def tag_draft(model: str, uid: str, attempt: int = 1, floor: bool = False) -> str:
base = "tp2-floor" if floor else "tp2-draft"
return f"{base}-D-{model}-{uid}" + ("" if attempt == 1 else f"-r{attempt}")
def tag_edit(model: str, uid: str, floor: bool = False) -> str:
return f"{'tp2-floor' if floor else 'tp2-edit'}-E-{model}-{uid}"
def draft_text(model: str, uid: str, floor: bool = False) -> str:
"""ГОДНЫЙ черновик: последняя попытка, прошедшая гейт. Пусто — годного нет."""
pre = tag_draft(model, uid, floor=floor)
best = ""
for f in sorted(OUT.glob(f"{pre}*.json")):
c = json.loads(f.read_text(encoding="utf-8")).get("content", "")
if c and not BO.draft_reject_reason(c):
best = c
return best
def edit_text(model: str, uid: str, floor: bool = False) -> str:
f = OUT / f"{tag_edit(model, uid, floor)}.json"
return json.loads(f.read_text(encoding="utf-8")).get("content", "") if f.exists() else ""
def buy_draft(led, model: str, u: dict, en: bool, floor: bool = False) -> dict:
blk = None if en else PR.glossary_block(PR.terms_in(u["source"]))
msgs = PR.translator_msgs(u["source"], blk, en=en)
rec = {}
for attempt in range(1, RETRIES + 1):
rec = MONEY.purchase(led, tag_draft(model, u["uid"], attempt, floor), model, client(model),
call_kwargs(model, msgs), role="draft", uid=u["uid"], en=en,
tenant=model, attempt=attempt, floor=floor)
if rec.get("skipped") or not BO.draft_reject_reason(rec.get("content", "")):
return rec
print(f"{BO.draft_reject_reason(rec['content'])}ре-ген {attempt + 1}")
return rec
def buy_edit(led, model: str, u: dict, en: bool, floor: bool = False) -> dict:
draft = draft_text(model, u["uid"], floor)
if not draft:
return dict(skipped=False, content="", no_draft=True)
blk = None if en else PR.law_block(PR.terms_in(u["source"]))
msgs = PR.editor_msgs(u["source"], draft, blk, en=en)
return MONEY.purchase(led, tag_edit(model, u["uid"], floor), EDITOR, client(EDITOR),
call_kwargs(EDITOR, msgs), role="edit", uid=u["uid"], en=en,
tenant=model, floor=floor)
def cmd_plan() -> None:
ts = tenants()
res = screen_result()
us = units_all()
print(f"ПАНЕЛЬ Ф2: {len(ts)} жильцов черновой роли · единиц {len(us)} "
f"({sum(1 for _u, e in us if not e)} zh + {sum(1 for _u, e in us if e)} en)")
for m in ts:
r = res.get(m, {})
print(f" {m:26s} скрин={r.get('verdict', ''):10s} $/ед transl="
f"{r.get('price_translator') or 0:.5f} edit={r.get('price_editor') or 0:.5f}")
print("\nне вошли: " + ", ".join(f"{m}({r['verdict']})" for m, r in res.items()
if m not in ts) or "")
n_dr = len(ts) * len(us)
n_ed = len(ts) * len(us)
n_fl = 2 * len(M.units_zh())
est = sum((res.get(m, {}).get("price_translator") or 0.005) * len(us) for m in ts) \
+ (res.get(EDITOR, {}).get("price_editor") or 0.005) * len(ts) * len(us) \
+ ((res.get(ANCHOR, {}).get("price_translator") or 0.001)
+ (res.get(EDITOR, {}).get("price_editor") or 0.005)) * len(M.units_zh())
print(f"\nвызовов: черновики {n_dr} + редактуры {n_ed} + пол {n_fl} = {n_dr + n_ed + n_fl}")
print(f"смета по ФАКТИЧЕСКИМ ценам скрина: ${est:.4f} при потолке ${MONEY.CEILINGS['Ф2']:.2f}")
if est > MONEY.CEILINGS["Ф2"]:
print("СМЕТА НЕ ВЛЕЗАЕТ — резать панель по объявленному правилу или пинг владельцу")
def cmd_drafts() -> None:
led = MONEY.Guarded("Ф2", default_expect=0.01)
for m in tenants():
for u, en in units_all():
rec = buy_draft(led, m, u, en)
if rec.get("skipped"):
print("⛔ потолок Ф2 — стоп")
return
print(f" {m:24s} {'en' if en else 'zh'} {u['uid'][:6]} "
f"finish={str(rec.get('finish')):8s} {len(rec.get('content', '')):5d} зн "
f"${rec.get('cost_usd', 0):.5f}")
time.sleep(0.15)
print(f"потрачено Ф2 ${led.spent():.6f}")
def cmd_edit() -> None:
led = MONEY.Guarded("Ф2", default_expect=0.01)
for m in tenants():
for u, en in units_all():
rec = buy_edit(led, m, u, en)
if rec.get("no_draft"):
print(f" {m:24s} {u['uid'][:6]} — годного черновика нет, редактура пропущена")
continue
if rec.get("skipped"):
print("⛔ потолок Ф2 — стоп")
return
print(f" E[{m:22s}] {'en' if en else 'zh'} {u['uid'][:6]} "
f"finish={str(rec.get('finish')):8s} {len(rec.get('content', '')):5d} зн "
f"${rec.get('cost_usd', 0):.5f}")
time.sleep(0.15)
print(f"потрачено Ф2 ${led.spent():.6f}")
def cmd_floor() -> None:
"""Вторая генерация ЯКОРНОГО арма end-to-end на тех же zh-единицах."""
led = MONEY.Guarded("Ф2", default_expect=0.01)
for u in M.units_zh():
d = buy_draft(led, ANCHOR, u, False, floor=True)
if d.get("skipped"):
print("⛔ потолок Ф2 — стоп")
return
e = buy_edit(led, ANCHOR, u, False, floor=True)
print(f" пол {u['uid'][:6]} черновик {len(d.get('content', '')):5d} зн · "
f"редактура {len(e.get('content', '')):5d} зн")
time.sleep(0.15)
print(f"потрачено Ф2 ${led.spent():.6f}")
def draft_cost(model: str, uid: str, floor: bool = False) -> float:
"""Цена черновика = ВСЕ попытки: ре-ген оплачен, значит он в цене роли."""
pre = tag_draft(model, uid, floor=floor)
return sum(json.loads(f.read_text(encoding="utf-8")).get("cost_usd", 0.0)
for f in OUT.glob(f"{pre}*.json"))
def regen_rate(model: str, en: bool = False) -> tuple[int, int]:
us = [u for u, e in units_all() if e == en]
need = 0
for u in us:
pre = tag_draft(model, u["uid"])
if len(list(OUT.glob(f"{pre}-r*.json"))):
need += 1
return need, len(us)
def cmd_score() -> None:
ts = tenants()
zh = M.units_zh()
cards = B.load_cards()
print("═══ Ф2 (в) ЭХО-МИНА: доля единиц, потребовавших ре-гена ═══")
for m in ts:
nz, tz = regen_rate(m, en=False)
ne, te = regen_rate(m, en=True)
print(f" {m:26s} zh {nz}/{tz} = {nz / max(1, tz):.0%} · en {ne}/{te} = {ne / max(1, te):.0%}")
print("\n═══ Ф2 (г) БАТАРЕЯ И ПОКРЫТИЕ КАНОНА (zh, детерминированно) ═══")
print(f"{'арм':30s}{'ед.':>5s}{'ханьцзы':>9s}{'типогр':>8s}{'потеря':>8s}"
f"{'канон':>14s}{'покрытие':>10s}")
for m in ts:
for kind in ("D", "E"):
rows, k, s = [], 0, 0
for u in zh:
t = draft_text(m, u["uid"]) if kind == "D" else edit_text(m, u["uid"])
if not t.strip():
continue
rows.append(B.run_unit(B.Unit(source=u["source"], draft=t, final=t, cards=cards)))
kk, ss = BANK.coverage(u["source"], t)
k += kk
s += ss
if not rows:
continue
print(f"{kind + '[' + m + ']':30s}{len(rows):5d}"
f"{sum(r['cjk_leak']['han_chars'] for r in rows):9d}"
f"{sum(r['typography']['violations'] for r in rows):8d}"
f"{sum(r['numbers']['lost_n'] for r in rows):8d}"
f"{f'{k}/{s}':>14s}{(k / s if s else 0):>10.3f}")
print("\n═══ Ф2 (д) ЦЕНА/ЕДИНИЦА И ПРОЕКЦИЯ НА КНИГУ ═══")
print(f"{'жилец':26s}{'черновик':>11s}{'редактор':>11s}{'связка':>11s}{'книга 1500 ед.':>16s}")
for m in ts:
dc = [draft_cost(m, u["uid"]) for u in zh]
ec = [json.loads((OUT / f"{tag_edit(m, u['uid'])}.json").read_text(encoding="utf-8"))
.get("cost_usd", 0.0) for u in zh if (OUT / f"{tag_edit(m, u['uid'])}.json").exists()]
if not dc:
continue
d50, e50 = st.median(dc), (st.median(ec) if ec else 0.0)
print(f"{m:26s}{d50:11.5f}{e50:11.5f}{d50 + e50:11.5f}{(d50 + e50) * 1500:16.2f}")
led = MONEY.Guarded("Ф2")
print(f"\nпотрачено Ф2 ${led.spent():.6f} из ${led.ceiling:.2f} · "
f"пак ${MONEY.Pack().spent():.6f} из ${MONEY.PACK_CEILING:.2f}")
if __name__ == "__main__":
a = sys.argv[1:] or ["--plan"]
fn = {"--plan": cmd_plan, "--drafts": cmd_drafts, "--edit": cmd_edit,
"--floor": cmd_floor, "--score": cmd_score}.get(a[0])
fn() if fn else print(__doc__)

245
eval/tenant_panel/sol.py Normal file
View file

@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Ф0.4 — ПАКЕТЫ ДЛЯ SOL: внешний судейский контур руками владельца. $0.
Что это. Sol флагманская reasoning-модель OpenAI в ЧАТ-подписке владельца. Она вне обеих
популяций судей пака (платные ключи + агенты сессии), поэтому её голос третий независимый
инструмент. Сессия готовит paste-ready пакеты, владелец вставляет их руками и возвращает ответы;
транскрипты персистятся дословно.
**Пакет (а) АРБИТРАЖ ЭКСП-21.** Там два платных судьи разошлись на величину, которая
собственным шумом не объясняется: разброс судьи-замены на повторе даёт sd перевеса 0.81 на голос,
то есть ±0.15 на контраст из 15 единиц. Отобраны ТРИ контраста по ФАКТИЧЕСКОМУ расхождению:
контраст luna gemini разрыв в sd собственного шума
A vs F +0.84 +3.03 2.19 ~14
B vs F +1.44 +3.33 1.90 ~12
A_law vs A +0.91 0.20 1.11 ~7 ЗНАК РАСХОДИТСЯ
Почему именно эти. `A/F` заказан промтом. `B/F` тот же заголовочный вопрос («покупает ли второй
проход качество») и второй по величине разрыв. `A_law/A` взят НЕ по величине: это единственный
контраст, где судьи расходятся ЗНАКОМ, а расхождение знака хуже расхождения величины оно меняет
вердикт, а не точность. `D/A` (разрыв 0.74) и `D/D_` (1.23) не берутся: там знак у обоих судей
один, и арбитраж уточнил бы величину, которая решения не несёт.
Протокол ПАРНЫЙ, потому что арбитражу подлежит парный замер: слепые метки, ОБА порядка, декой.
Правило зеркальности для пар-протокола живо (для абсолютного снято, D39.117 п.2в).
**Семейный запрет.** Sol не судит клетки OpenAI-моделей. В армах эксп-21 их нет (deepseek, glm),
поэтому запрет здесь не связывает но он объявлен, и в пакете (б) по финалистам эксп-22 будет
применён механически.
Запуск: --emit выпустить пакеты и инструкцию, $0
--ingest принять ответы владельца из sol/answers/, $0
--score что сказал Sol против платных судей, $0
"""
from __future__ import annotations
import hashlib
import json
import statistics as st
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 bakeoff as BO # noqa: E402
import judges as J # noqa: E402
import money as MONEY # noqa: E402
SOL = MONEY.OUT / "sol"
ANSW = SOL / "answers"
KEYS = MONEY.OUT / "blind-keys"
CONTRASTS = [("A", "F"), ("B", "F"), ("A_law", "A")]
N_UNITS = 3
SALT_FILE = KEYS / "SALT-sol.txt"
def _salt() -> str:
if not SALT_FILE.exists():
import secrets # noqa: PLC0415
SALT_FILE.parent.mkdir(parents=True, exist_ok=True)
SALT_FILE.write_text(secrets.token_hex(16), encoding="utf-8")
return SALT_FILE.read_text(encoding="utf-8").strip()
def pick_units(a: str, b: str) -> list[dict]:
"""Единицы с НАИБОЛЬШИМ расхождением судей на этом контрасте — там арбитраж и нужен."""
us = BO.units()
scored = []
for u in us:
per = []
for judge in J.JUDGES:
bo = BO.margins_by_order(a, b, u["uid"], judge)
if len(bo) == 2:
per.append(st.mean([st.mean(bo[0]), st.mean(bo[1])]))
if len(per) == len(J.JUDGES):
scored.append((abs(per[0] - per[1]), u))
scored.sort(key=lambda x: (-x[0], x[1]["uid"]))
return [u for _g, u in scored[:N_UNITS]]
def _packet(src: str, v1: str, v2: str, tok: str) -> str:
return "\n".join([
f"ПАКЕТ {tok}", "",
J.RUBRIC.split("---", 1)[0].strip() if "---" in J.RUBRIC else J.RUBRIC.strip(), "",
"=" * 60, "ИСХОДНИК:", src, "",
"=" * 60, "ВАРИАНТ 1:", v1, "",
"=" * 60, "ВАРИАНТ 2:", v2, ""])
def emit() -> None:
SOL.mkdir(parents=True, exist_ok=True)
ANSW.mkdir(parents=True, exist_ok=True)
key: dict[str, dict] = {}
n = 0
for a, b in CONTRASTS:
for u in pick_units(a, b):
ta, tb = BO.text_of(a, u), BO.text_of(b, u)
if not ta.strip() or not tb.strip():
continue
for order in (0, 1):
tok = hashlib.sha1(f"{_salt()}|{a}|{b}|{u['uid']}|{order}".encode()).hexdigest()[:10] # noqa: S324
v1, v2 = (ta, tb) if order == 0 else (tb, ta)
(SOL / f"{tok}.txt").write_text(_packet(u["source"], v1, v2, tok), encoding="utf-8")
key[tok] = dict(contrast=f"{a}v{b}", first=a if order == 0 else b,
second=b if order == 0 else a, uid=u["uid"], order=order,
kind="боевой")
n += 1
# ДЕКОЙ: посаженная деградация против чистого текста. Сессия, не поймавшая декой, аннулируется.
u0 = BO.units()[0]
clean = BO.text3_of("A", u0)
planted, k = AJ._plant(clean) # noqa: SLF001
if k >= 2:
for order in (0, 1):
tok = hashlib.sha1(f"{_salt()}|decoy|{u0['uid']}|{order}".encode()).hexdigest()[:10] # noqa: S324
v1, v2 = (clean, planted) if order == 0 else (planted, clean)
(SOL / f"{tok}.txt").write_text(_packet(u0["source"], v1, v2, tok), encoding="utf-8")
key[tok] = dict(contrast="ДЕКОЙ", first="чистый" if order == 0 else "порченый",
second="порченый" if order == 0 else "чистый", uid=u0["uid"],
order=order, kind="контроль-декой", plants=k)
n += 1
(KEYS / "sol-KEY.json").write_text(json.dumps(key, ensure_ascii=False, indent=1),
encoding="utf-8")
(SOL / "ИНСТРУКЦИЯ.md").write_text(_instr(len(key)), encoding="utf-8")
print(f"пакетов {n}{SOL} · ключ → {KEYS}/sol-KEY.json (владельцу и Sol НЕ давать)")
print(f"инструкция → {SOL}/ИНСТРУКЦИЯ.md")
def _instr(n: int) -> str:
return f"""# Пакеты для Sol — инструкция владельцу
Всего пакетов: **{n}**. Каждый отдельный файл `<токен>.txt` в этом каталоге.
Порядок вставки НЕ важен; частичный возврат тоже годится что вернётся, то и посчитается.
## Что делать
1. Открыть новый чат с Sol (high reasoning).
2. Вставить содержимое ОДНОГО файла целиком, без правок и без пояснений от себя.
3. Скопировать ответ Sol **дословно** в файл `answers/<тот же токен>.txt`.
4. Следующий пакет **новым чатом**: иначе Sol узнаёт зеркальные пары и позиционного контроля
не остаётся (эта беда измерена на агенте-судье в эксп-21 §10).
## Формат ответа, который ждёт разбор
Sol просят вернуть ровно такие строки (это часть текста пакета, отдельно требовать не нужно):
```
В1-ВЕРНОСТЬ: <число>
В1-ТЕРМИН: <число>
В1-ЯЗЫК: <число>
В1-ФОРМА: <число>
В2-ВЕРНОСТЬ: <число>
В2-ТЕРМИН: <число>
В2-ЯЗЫК: <число>
В2-ФОРМА: <число>
```
Если Sol добавит рассуждение не удалять: транскрипт персистится дословно, лишнее разбор
игнорирует.
## Чего делать НЕ надо
* не сообщать Sol, какая модель какой вариант писала (в пакетах этого нет, и это важно);
* не менять порядок вариантов внутри пакета;
* не сводить несколько пакетов в один чат.
## Что это даёт
Два платных судьи эксп-21 разошлись на 2.19 ошибки там, где собственный шум судьи ±0.15.
Направление вывода «второй проход покупает качество» держится, значимость нет: её несёт ОДИН
судья, аварийная лёгкая замена. Голос Sol третий независимый инструмент, и он решает, к кому
из двух ближе истина.
Среди пакетов есть **контрольные** (в них одна сторона заведомо испорчена). Если Sol их не
ловит его голоса по боевым парам не читаются. Какие именно контрольные не сказано намеренно.
"""
def ingest() -> None:
key = json.loads((KEYS / "sol-KEY.json").read_text(encoding="utf-8"))
got = {}
for f in sorted(ANSW.glob("*.txt")):
if f.stem not in key:
print(f" ⚠ токена {f.stem} нет в ключе — пропуск")
continue
p = J.parse(f.read_text(encoding="utf-8"))
if p["В1-ВЕРНОСТЬ"] is None:
print(f"{f.stem}: ответ не разобран")
continue
e1 = sum(p[f"В1-{ax}"] or 0 for ax in J.AXES)
e2 = sum(p[f"В2-{ax}"] or 0 for ax in J.AXES)
got[f.stem] = dict(**key[f.stem], e1=e1, e2=e2)
(MONEY.OUT / "sol-votes.json").write_text(json.dumps(got, ensure_ascii=False, indent=1),
encoding="utf-8")
print(f"принято голосов {len(got)} из {len(key)} пакетов")
def score() -> None:
f = MONEY.OUT / "sol-votes.json"
if not f.exists() or not json.loads(f.read_text(encoding="utf-8")):
print("Sol не вернулся — голосов нет. Это НЕ блокер: отчёт сдаётся с явной строкой.")
return
votes = json.loads(f.read_text(encoding="utf-8"))
dec = [v for v in votes.values() if v["kind"] == "контроль-декой"]
if dec:
caught = sum(1 for v in dec
if (v["e1"] < v["e2"]) == (v["first"] == "чистый"))
print(f"ДЕКОЙ: поймано {caught}/{len(dec)}"
+ ("" if caught == len(dec) else " ⛔ сессия Sol аннулируется целиком"))
if caught != len(dec):
return
print(f"\n{'контраст':14s}{'ед.':>5s}{'Sol':>9s}{'luna':>9s}{'gemini':>9s}")
for a, b in CONTRASTS:
per: dict[str, list[float]] = {}
for v in votes.values():
if v["contrast"] != f"{a}v{b}":
continue
m = (v["e2"] - v["e1"]) if v["first"] == a else (v["e1"] - v["e2"])
per.setdefault(v["uid"], []).append(m)
vals = [st.mean(x) for x in per.values() if len(x) == 2]
if not vals:
continue
paid = {}
for judge in J.JUDGES:
pv = []
for u in BO.units():
if u["uid"] not in per:
continue
bo = BO.margins_by_order(a, b, u["uid"], judge)
if len(bo) == 2:
pv.append(st.mean([st.mean(bo[0]), st.mean(bo[1])]))
paid[judge] = st.mean(pv) if pv else float("nan")
print(f"{a + ' vs ' + b:14s}{len(vals):5d}{st.mean(vals):+9.2f}"
+ "".join(f"{paid[j]:+9.2f}" for j in J.JUDGES))
print("\n⚠ единицы Sol — подмножество (наибольшее расхождение судей), поэтому платные колонки"
"\n пере-считаны на ТЕХ ЖЕ единицах, а не взяты из свода по всем 15.")
if __name__ == "__main__":
a = sys.argv[1:] or ["--emit"]
fn = {"--emit": emit, "--ingest": ingest, "--score": score}.get(a[0])
fn() if fn else print(__doc__)