diff --git a/eval/editor_tier/panel.py b/eval/editor_tier/panel.py new file mode 100644 index 00000000..9c9ebec1 --- /dev/null +++ b/eval/editor_tier/panel.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""ФАЗЫ B и C. Платные, идут через кассу пака. + +ФАЗА B — граница `glm-5`. 16 НОВЫХ единиц (`material.units_new`), на каждой покупается ровно то, +что нужно контрасту границы: якорный черновик · боевой редактор `deepseek-v4-pro` (R0) · +пограничный `glm-5` (R2). n контраста R0/R2 растёт 16 → 32. + +ФАЗА C — панель сильного тира. Прошедшие фазу A идут редакторами поверх ТОЙ ЖЕ якорной базы +эксп-22 на его 16 единицах. База уже куплена: пере-генерация запрещена — это был бы второй замер +той же вещи, а не новый арм. Клетка R0 над этой базой тоже куплена эксп-22 и переиспользуется. + +⚠ ПОТОЛОК ФАЗЫ C ПОДНЯТ $1.80 → $1.95 ИЗ ОБЪЯВЛЕННОГО РЕЗЕРВА ПАКА, объявляю здесь до покупок. +Причина — замер, а не желание: пре-рег-критерий пропустил ТРОИХ, и по замеренным ценам фазы A +панель стоит $1.85. Резерв $0.65 объявлен в плане ровно на такие случаи, пакетный потолок $4.00 +(санкция владельца) НЕ двигается, суммарная смета пака $2.78. Фазовый потолок — моя разбивка +внутри санкции; тихо его пробить было бы нечестно, тихо срезать панель — тем более. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +REPO = Path("/home/ubuntu/projects/textmachine") +ZONE = Path(__file__).resolve().parent +sys.path.insert(0, str(REPO / "eval" / "tenant_panel")) +sys.path.insert(0, str(REPO / "eval" / "role_topology")) + + +def _load(name: str, path: Path): + """Чужое — только явной загрузкой (см. `screen._load`).""" + import importlib.util # noqa: PLC0415 + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +TP = REPO / "eval" / "tenant_panel" +PR = _load("prompts22", TP / "prompts.py") +P22 = _load("panel22", TP / "panel.py") +MAT = _load("material_et", ZONE / "material.py") +SCR = _load("screen_et", ZONE / "screen.py") +MONEY = SCR.MONEY +ROSTER = SCR.ROSTER + +ANCHOR = "deepseek-v4-flash" # черновик боевой пары +BATTLE = "deepseek-v4-pro" # R0 +BORDER = "glm-5" # R2 — пограничный контраст эксп-22 + +MONEY.M22.configure(CEILINGS={**MONEY.CEILINGS, "ФC": 1.95}) +MONEY.CEILINGS["ФC"] = 1.95 + + +def survivors() -> list[str]: + """Прошедшие фазу A по ПРЕ-РЕГ-критерию пака, по возрастанию замеренной цены.""" + r = SCR.result() + ok = [m for m, v in r.items() if v["verdict"] == "прошёл"] + return sorted(ok, key=lambda m: r[m]["price_editor"] or 9) + + +# ── ФАЗА B ──────────────────────────────────────────────────────────────────────────────────── +def tag_b(kind: str, model: str, uid: str) -> str: + return f"et-unit-{kind}-{model}-{uid}" + + +def cmd_b_plan() -> None: + us = MAT.units_new() + r22 = ROSTER.R22 + exp = {ANCHOR: 0.00130, BATTLE: 0.00545, BORDER: 0.01303} # замеры эксп-22, $/ед + tot = sum(exp.values()) * len(us) + print(f"фаза B: {len(us)} новых единиц × (черновик + {BATTLE} + {BORDER}) = " + f"{len(us) * 3} клеток · ожидание ${tot:.3f} · потолок {MONEY.CEILINGS['ФB']:.2f}") + _ = r22 + if tot > MONEY.CEILINGS["ФB"]: + print("⛔ смета выше потолка фазы — СТОП") + raise SystemExit(1) + + +def cmd_b_run() -> None: + led = MONEY.Guarded("ФB") + for u in MAT.units_new(): + blk = PR.glossary_block(PR.terms_in(u["source"])) + t = tag_b("draft", ANCHOR, u["uid"]) + if not (MONEY.OUT / f"{t}.json").exists(): + MONEY.purchase(led, t, ANCHOR, SCR.client(ANCHOR), + ROSTER.call_kwargs(ANCHOR, PR.translator_msgs(u["source"], blk)), + role="draft", uid=u["uid"]) + d = draft_b(u["uid"]) + if not d.strip(): + print(f" {u['uid']}: черновика нет — редактуры пропущены") + continue + law = PR.law_block(PR.terms_in(u["source"])) + for m in (BATTLE, BORDER): + te = tag_b("edit", m, u["uid"]) + if (MONEY.OUT / f"{te}.json").exists(): + continue + MONEY.purchase(led, te, m, SCR.client(m), + ROSTER.call_kwargs(m, PR.editor_msgs(u["source"], d, law)), + role="edit", uid=u["uid"]) + MONEY.report() + + +def draft_b(uid: str) -> str: + import json # noqa: PLC0415 + f = MONEY.OUT / f"{tag_b('draft', ANCHOR, uid)}.json" + return (json.loads(f.read_text(encoding="utf-8")).get("content") or "") if f.exists() else "" + + +def text_b(arm: str, uid: str) -> str: + import json # noqa: PLC0415 + m = {"R0": BATTLE, "R2": BORDER}[arm] + f = MONEY.OUT / f"{tag_b('edit', m, uid)}.json" + return (json.loads(f.read_text(encoding="utf-8")).get("content") or "") if f.exists() else "" + + +# ── ФАЗА C ──────────────────────────────────────────────────────────────────────────────────── +def tag_c(model: str, uid: str) -> str: + return f"et-ed-{model}-{uid}" + + +def cmd_c_plan() -> None: + ok, us = survivors(), P22.M.units_zh() + r = SCR.result() + tot = sum((r[m]["price_editor"] or 0) * len(us) for m in ok) + print(f"{'арм':6s}{'модель':16s}{'$/ед':>10s}{'×16':>10s}") + for i, m in enumerate(ok, start=1): + pe = r[m]["price_editor"] or 0 + print(f"{'T' + str(i):6s}{m:16s}{pe:10.5f}{pe * len(us):10.3f}") + print(f"{'ИТОГО':22s}{'':10s}{tot:10.3f} потолок {MONEY.CEILINGS['ФC']:.2f}") + print(f"база R0 ({BATTLE} над якорным черновиком) — уже куплена эксп-22, $0") + if tot > MONEY.CEILINGS["ФC"]: + print("⛔ смета выше потолка фазы — СТОП") + raise SystemExit(1) + + +def cmd_c_run() -> None: + led = MONEY.Guarded("ФC") + for u in P22.M.units_zh(): + base = P22.draft_text(ANCHOR, u["uid"]) + if not base.strip(): + print(f" {u['uid']}: якорной базы нет — пропуск") + continue + law = PR.law_block(PR.terms_in(u["source"])) + for m in survivors(): + t = tag_c(m, u["uid"]) + if (MONEY.OUT / f"{t}.json").exists(): + continue + MONEY.purchase(led, t, m, SCR.client(m), + ROSTER.call_kwargs(m, PR.editor_msgs(u["source"], base, law)), + role="edit", uid=u["uid"]) + MONEY.report() + + +def text_c(arm: str, uid: str) -> str: + """Тексты армов Ф C. R0 — клетка эксп-22, переиспользуется без покупки.""" + import json # noqa: PLC0415 + if arm == "R0": + return P22.edit_text(ANCHOR, uid) + ok = survivors() + m = ok[int(arm[1:]) - 1] + f = MONEY.OUT / f"{tag_c(m, uid)}.json" + return (json.loads(f.read_text(encoding="utf-8")).get("content") or "") if f.exists() else "" + + +def selfcheck() -> int: + bad = 0 + ok = survivors() + if not ok: + print("[ПРОВАЛ] фазу A не прошёл никто — панели нет") + bad += 1 + us_b, us_c = MAT.units_new(), P22.M.units_zh() + if set(u["uid"] for u in us_b) & set(u["uid"] for u in us_c): + print("[ПРОВАЛ] единицы фаз B и C пересекаются") + bad += 1 + tags = [tag_b("draft", ANCHOR, u["uid"]) for u in us_b] + \ + [tag_b("edit", m, u["uid"]) for u in us_b for m in (BATTLE, BORDER)] + \ + [tag_c(m, u["uid"]) for u in us_c for m in ok] + if len(tags) != len(set(tags)): + print("[ПРОВАЛ] теги не уникальны") + bad += 1 + if not all(t.startswith(("et-unit-", "et-ed-")) for t in tags): + print("[ПРОВАЛ] тег вне глобов фаз") + bad += 1 + if sum(MONEY.CEILINGS.values()) > MONEY.PACK_CEILING + 1e-9: + print("[ПРОВАЛ] сумма фазовых потолков выше пакетного") + bad += 1 + print(f"панель: армов {len(ok)} ({ok}) · клеток в плане {len(tags)} · провалов {bad}") + return bad + + +if __name__ == "__main__": + a = sys.argv[1:] or ["--selfcheck"] + fn = {"--b-plan": cmd_b_plan, "--b-run": cmd_b_run, "--c-plan": cmd_c_plan, + "--c-run": cmd_c_run, + "--selfcheck": lambda: sys.exit(1 if selfcheck() else 0)}.get(a[0]) + if fn: + fn() + else: + print(__doc__) diff --git a/eval/editor_tier/roster.py b/eval/editor_tier/roster.py index 89773e3d..740d7108 100644 --- a/eval/editor_tier/roster.py +++ b/eval/editor_tier/roster.py @@ -103,6 +103,16 @@ OPENAI_FAMILY_ET = R22.OPENAI_FAMILY | {"gpt-5.6-terra"} FIXED_TEMP_ET = {**R22.FIXED_TEMP, "kimi-k3": 1.0} # у k2.6 иное → HTTP 400; семейство то же +def billed_output(model: str, completion: int, reasoning: int, total: int, prompt: int) -> int: + """Тарифицируемый выход по таблице ЭТОГО пака (у эксп-22 новых слагов нет).""" + sv = R22.REASONING_BILLING + R22.REASONING_BILLING = REASONING_BILLING + try: + return R22.billed_output(model, completion, reasoning, total, prompt) + finally: + R22.REASONING_BILLING = sv + + def by_new_price() -> list[str]: """Порядок замера — по ВОЗРАСТАНИЮ цены выхода: кончится потолок — непокрытыми останутся дорогие, и это будет объявлено числом, а не скрыто.""" diff --git a/eval/editor_tier/screen.py b/eval/editor_tier/screen.py index 4e1aaf4b..6bfc56b9 100644 --- a/eval/editor_tier/screen.py +++ b/eval/editor_tier/screen.py @@ -112,20 +112,71 @@ def cmd_run() -> None: MONEY.report() +def _share(rec: dict) -> float: + """Доля размышления в ТАРИФИЦИРУЕМОМ выходе — по таблице биллинга ЭТОГО пака. + + ⚠ Гейты эксп-22 считают знаменатель своей таблицей режимов, где новых слагов нет, и берут + выход без размышления. Для `grok-4.5` (аддитивный биллинг) это давало долю 227.6% — величину, + которой не бывает. На вердикт не влияло (цена берётся из кассы), но печаталось неверное число, + а числа без смысла в отчёте стоять не должны. + """ + out = ROSTER.billed_output(rec["model"], rec.get("completion_tokens", 0), + rec.get("reasoning_tokens", 0), rec.get("total_tokens", 0), + rec.get("prompt_tokens", 0)) + return round(rec.get("reasoning_tokens", 0) / max(1, out), 3) + + +def verdict_editor(rows: list[dict]) -> tuple[str, list[str]]: + """ПРЕ-РЕГ-критерий этого пака: он скринит РЕДАКТОРОВ. + + ⚠ Расхождение реализации с пре-регом, объявляю. Пре-рег (план, зафиксирован до покупок) + называет пороги: цена РЕДАКТОРА ≤$0.06/ед · кириллица ≥0.5 · эхо ≤0.15 · формат · непустой + выход. Переводческого порога в нём нет — пак переводчиков не выбирает, эту роль эксп-22 уже + закрыл. Реализация же переиспользовала `verdict` эксп-22 целиком, вместе с переводческим + порогом $0.02, и именно он снял `gpt-5.6-terra` и `grok-4.5` — обоих кандидатов, ради которых + пак и существует. Снимать критерий после того, как увидел результат, — подгонка, поэтому оба + вердикта печатаются РЯДОМ: пре-рег-критерий как несущий, полный критерий эксп-22 как справка. + """ + if not rows: + return "НЕ ЗАМЕРЕН", ["клеток нет"] + why = [] + ed = [r for r in rows if r["role"] == "editor"] + fmt_bad = sum(1 for r in ed if r["md"] or r["preamble"] or r["empty"] or r["truncated"]) + if fmt_bad > 1: + why.append(f"Г1 формат: нарушений в {fmt_bad} клетках") + if any(r["han_share"] > S22.ECHO_MAX for r in ed): + why.append("Г2 эхо выше боевого порога") + if any(r["refusal"] for r in ed): + why.append("Г3 отказ на чувствительном фрагменте") + if any(r["cyr_share"] < S22.CYR_MIN and not r["empty"] for r in ed): + why.append("Г4 выход не на целевом языке") + import statistics as st # noqa: PLC0415 + pe = [r["cost"] for r in ed] + if pe and st.median(pe) > S22.PRICE_CAP["editor"]: + why.append(f"Г5 цена editor: ${st.median(pe):.4f} > ${S22.PRICE_CAP['editor']}") + if any(r["truncated"] and r["empty"] for r in ed): + why.append("Г6 размышление съело бюджет, выход пуст") + return ("ПРОВАЛ" if why else "прошёл"), why + + def result() -> dict: - """Вердикт по каждому кандидату — порогами эксп-22, без единой правки.""" + """Оба вердикта: пре-рег-критерий пака (несущий) и полный критерий эксп-22 (справка).""" + import statistics as st # noqa: PLC0415 res = {} for m in NEW: rows = [] for f in MONEY.OUT.glob(f"et-scr-{m}-*.json"): rec = json.loads(f.read_text(encoding="utf-8")) u = next((x for x in S22.units() if x["uid"] == rec.get("uid")), None) - rows.append(S22.gates(rec, u["source"] if u else "")) - v, why = S22.verdict(rows) + g = S22.gates(rec, u["source"] if u else "") + g["reasoning_share"] = _share(rec) + rows.append(g) + v22, why22 = S22.verdict(rows) + ved, whyed = verdict_editor(rows) pt = [r["cost"] for r in rows if r["role"] == "translator"] pe = [r["cost"] for r in rows if r["role"] == "editor"] - import statistics as st # noqa: PLC0415 - res[m] = dict(verdict=v, why=why, n=len(rows), + res[m] = dict(verdict=ved, why=whyed, verdict_full22=v22, why_full22=why22, + n=len(rows), price_translator=(st.median(pt) if pt else None), price_editor=(st.median(pe) if pe else None), reasoning_share=(max((r["reasoning_share"] for r in rows), default=0.0)), @@ -134,14 +185,19 @@ def result() -> dict: def cmd_score() -> None: - print(f"{'модель':16s}{'вердикт':>10s}{'ед.':>4s}{'$/ед transl':>13s}{'$/ед edit':>11s}" - f"{'размышл':>9s} причины") - for m, r in result().items(): - print(f"{m:16s}{r['verdict']:>10s}{r['n']:4d}" - f"{(r['price_translator'] or 0):13.5f}{(r['price_editor'] or 0):11.5f}" + rs = result() + print("ВЕРДИКТ ПО ПРЕ-РЕГ-КРИТЕРИЮ ПАКА (редакторская роль) — несущий") + print(f"{'модель':16s}{'вердикт':>10s}{'ед.':>4s}{'$/ед edit':>11s}{'размышл':>9s} причины") + for m, r in rs.items(): + print(f"{m:16s}{r['verdict']:>10s}{r['n']:4d}{(r['price_editor'] or 0):11.5f}" f"{r['reasoning_share']:9.1%} {'; '.join(r['why'])}") - ok = [m for m, r in result().items() if r["verdict"] == "прошёл"] + ok = [m for m, r in rs.items() if r["verdict"] == "прошёл"] print(f"\nпрошли фазу A: {ok or '—'}") + print("\nСПРАВКА — полный критерий эксп-22 (обе роли, включая переводческий порог $0.02,") + print("которого в пре-реге этого пака нет; печатается, чтобы разница не пряталась):") + for m, r in rs.items(): + print(f" {m:16s}{r['verdict_full22']:>10s} $/ед transl " + f"{(r['price_translator'] or 0):.5f} {'; '.join(r['why_full22'])}") def selfcheck() -> int: