textmachine/eval/tenant_panel/editors.py

232 lines
13 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
"""Ф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
# ⚠ ПЕР-МОДЕЛЬНЫЙ ТАЙМАУТ И ЗАПРЕТ SDK-РЕТРАЕВ. Поймано исполнением: `gemini-3.1-pro-preview`
# держал ОДИН вызов 70.8 минуты без ошибки — таймаут 900 с умножался на внутренние ретраи
# клиента, и клетка не закрывалась вовсе (за то же время он отдал 1 клетку из 16). Это тот же
# класс, что его серия HTTP 503 на скрине: модель в этой роли на проводе не работоспособна.
# Здесь она получает жёсткую границу, чтобы отказ БЫЛ ИЗМЕРЕН, а не съел прогон.
SLOW = {"gemini-3.1-pro-preview": 300}
# ⚠ ПОДМНОЖЕСТВО ЕДИНИЦ ДЛЯ МЕДЛЕННОЙ МОДЕЛИ, объявлено ДО покупки: первые 8 по стабильному
# порядку `units_zh`. Основание — замер, а не удобство: при наблюдённой скорости 16 клеток
# недостижимы, а 8 дают парную статистику с честно объявленной мощностью.
SLOW_UNITS = 8
def client(model: str) -> OpenAI:
base, env, *_ = CANDIDATES[model]
return OpenAI(api_key=os.environ[env], base_url=base,
timeout=SLOW.get(model, 900), max_retries=0)
RESERVE = "glm-5" # ратифицированный резерв D39.22 = арм B эксп-21
def panel() -> list[str]:
"""Состав панели Ф3 по ОБЪЯВЛЕННОМУ (во фризе Ф3, ДО покупок) правилу.
R0 боевой редактор — база всех контрастов;
R1 обязательный кандидат промта (гипотеза эксп-04 «gemini-pro — аутлаер прозы»);
R2 резерв эксп-21 `glm-5` — ПРЕЕМСТВЕННОСТЬ: только он позволяет пере-снять контраст
`A_law/B` на неконтаминированном материале, а без него эксп-22 не связан с эксп-21 ничем;
R3 самый ДЕШЁВЫЙ прошедший редактор — отвечает «хватает ли дешёвого»;
R4 самый ДОРОГОЙ прошедший — отвечает «покупает ли дорогой качество».
Порядок фиксирован: имена армов R0..R4 впечатываются в ключ судейского прохода, и сдвиг
порядка после выпуска заданий сделал бы семейство неповторимым.
"""
res = P2.screen_result()
out = [BATTLE, MANDATORY, RESERVE]
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"])
for m in ([ok[0]] if ok else []) + ([ok[-1]] if len(ok) > 1 else []):
if m not in out:
out.append(m)
return out[: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:
"""Текст клетки Ф3. ⚠ R0 (боевой редактор) НЕ покупается заново: его клетка над якорным
черновиком уже куплена в Ф2 (`tp2-edit`), и повторная покупка была бы вторым замером той же
вещи. Промт это и предписывает: «клетка dspro-над-flash покупается в Ф2 ОДИН раз»."""
if arm == "F":
return base_text(uid)
model = panel()[int(arm[1:])]
if model == BATTLE:
return P2.edit_text(P2.ANCHOR, uid)
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")
# ⚠ У модели, чьи редакторские клетки скрина не состоялись (503 провайдера), цены НЕТ.
# Подставлять сюда общий дефолт нельзя: `gemini-3.1-pro-preview` дороже него в семь раз,
# и смета фазы вышла бы заниженной втрое. Берём оценку кассы по ЭТОЙ модели.
est_price = price if price is not None else MONEY.model_expect(m)
free = " (куплено в Ф2, переиспользуется)" if m == BATTLE else ""
if m != BATTLE:
est += est_price * len(us)
print(f" R{i} {m:26s} скрин={r.get('verdict', 'не скринился'):12s} "
f"$/ед={est_price:.5f}{'' if price is not None else ' (оценка кассы)'}{free}")
print(f"\nсмета ${est:.4f} при потолке ${MONEY.CEILINGS['Ф3']:.2f}")
if est > MONEY.CEILINGS["Ф3"]:
print("СМЕТА НЕ ВЛЕЗАЕТ — сократить панель или пинг владельцу ДО покупок")
def cmd_run(argv: list[str] | None = None) -> None:
"""`--only=<модель>` разводит процессы по клеткам. Без него параллельные процессы идут по
панели в ОДНОМ порядке и упираются в один замок: денег это не тратит, но и не ускоряет."""
argv = argv or []
only = [a.split("=", 1)[1] for a in argv if a.startswith("--only=")]
led = MONEY.Guarded("Ф3", default_expect=0.03)
for m in (x for x in panel() if not only or x in only):
if m == BATTLE:
print(f" {m}: клетка куплена в Ф2 (tp2-edit) — не перекупается")
continue
us = M.units_zh()[:SLOW_UNITS] if m in SLOW else M.units_zh()
for u in us:
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": lambda: cmd_run(a),
"--score": cmd_score}.get(a[0])
fn() if fn else print(__doc__)