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:
parent
9ca51da0a8
commit
930a3792c3
5 changed files with 937 additions and 0 deletions
192
eval/tenant_panel/editors.py
Normal file
192
eval/tenant_panel/editors.py
Normal 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
193
eval/tenant_panel/judge.py
Normal 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()
|
||||
|
|
@ -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
290
eval/tenant_panel/panel.py
Normal 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
245
eval/tenant_panel/sol.py
Normal 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__)
|
||||
Loading…
Add table
Reference in a new issue