textmachine/eval/dovodka/predpolet_proverka.py

249 lines
16 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
# -*- coding: utf-8 -*-
"""predpolet_proverka.py — ассерты пред-полёта. Исполняемые, а не «посмотрела глазами».
Промт §5.3 требует от пред-полёта пять вещей: сигнатуру Δprompt_tokens, пять разных снапшотов,
кириллицу ≥0.99, эхо 0 и гейты референса. Гарды прибора при n=1 их ПЕЧАТАЮТ, но не ОСТАНАВЛИВАЮТ:
`chetyre.py` рассчитан на пачку. Поэтому стоп-критерии пачки исполняются здесь, и каждый — с кодом.
⛔ Ни одного платного вызова: только чтение баз и логов.
"""
import json
import math
import pathlib
import re
import sqlite3
import sys
B = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else pathlib.Path.home() / "books/gu-zhenren/probe-4axes")
PIN = json.loads((pathlib.Path(__file__).parent / "prereg-4axes/prereg.json").read_text())
COEF = PIN["коэффициент_восстановления_размышления"]
LOW_THRESHOLD = PIN["деньги"]["порогены_клетки_low"]
ARMS = ["p7-off", "p7-low", "p9-off", "p9-low", "glm-off"]
CROSS = ARMS[:4]
fails, warns = [], []
def ro(db, q, a=()):
con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
con.row_factory = sqlite3.Row
r = con.execute(q, a).fetchall()
con.close()
return r
def cjk_count(t):
return sum(1 for c in (t or "") if 0x4E00 <= ord(c) <= 0x9FFF or 0x3400 <= ord(c) <= 0x4DBF)
def cyr_share(t):
import unicodedata
letters = [c for c in (t or "") if unicodedata.category(c).startswith("L")]
if not letters:
return 0.0
return sum(1 for c in letters if "CYRILLIC" in unicodedata.name(c, "")) / len(letters)
print("" * 92)
print("АССЕРТЫ ПРЕД-ПОЛЁТА — стоп-критерии пачки, исполняемые")
print("" * 92)
# ── 1. КЛЕТКИ: цена, попытка 0, доставка ────────────────────────────────────────────────────
print("\n## 1. КЛЕТКИ ЮНИТА")
cells = {}
for arm in ARMS:
db = B / "arms" / f"{arm}.db"
rows = ro(db, """
select rl.chapter, rl.chunk_idx, rl.prompt_tokens pt, rl.cached_tokens ct,
rl.completion_tokens cmpl, rl.cost_usd, rl.finish_reason fin, rl.model_requested model,
cp.attempt, cp.response_text resp
from request_log rl left join checkpoints cp on cp.request_hash = rl.request_hash
where rl.stage='edit' and rl.tm_hit=0 order by rl.id""")
if not rows:
fails.append(f"{arm}: НИ ОДНОЙ редакторской строки — клетка не куплена")
continue
a0 = next((r for r in rows if r["attempt"] == 0), None)
if a0 is None:
fails.append(f"{arm}: нет попытки 0 (реконструкция через checkpoints не нашла attempt=0)")
continue
st = ro(db, "select chapter, chunk_idx, disposition, flag_reason, cost_usd, final_hash, content_hash "
"from chunk_status where stage='edit'")
unit_cost = sum(r["cost_usd"] for r in st)
shipped = ""
if st and st[0]["final_hash"]:
sh = ro(db, "select response_text from checkpoints where request_hash=?", (st[0]["final_hash"],))
shipped = sh[0][0] if sh else ""
rh = None if arm == "glm-off" else a0["cmpl"] - COEF * len(a0["resp"] or "")
cells[arm] = {
"key": (a0["chapter"], a0["chunk_idx"]), "pt": a0["pt"], "ct": a0["ct"], "cmpl": a0["cmpl"],
"cost0": a0["cost_usd"], "fin": a0["fin"], "model": a0["model"], "resp": a0["resp"] or "",
"attempts": len(rows), "unit_cost": unit_cost, "shipped": shipped,
"content_hash": st[0]["content_hash"] if st else "", "disp": st[0]["disposition"] if st else "?",
"flag": st[0]["flag_reason"] if st else "", "rhat": rh,
}
c = cells[arm]
print(f" {arm:8s} юнит {c['key']} · попыток {c['attempts']} · pt {c['pt']} (кэш {c['ct']}) · "
f"cmpl {c['cmpl']} · finish {c['fin']} · клетка ${c['cost0']:.6f} · юнит ${c['unit_cost']:.6f}")
print(f" диспозиция {c['disp']}{'/' + c['flag'] if c['flag'] else ''} · сырой ответ "
f"{len(c['resp'])} зн · отгружено {len(c['shipped'])} зн · "
f"{('%.0f' % c['rhat']) if c['rhat'] is not None else '0 (по построению)'}")
if len(cells) != 5:
fails.append(f"клеток куплено {len(cells)} из 5")
# ── 2. ⛔ ОДИН И ТОТ ЖЕ ЮНИТ У ПЯТИ РУК ──────────────────────────────────────────────────────
print("\n## 2. БЛОКИРОВКА ТРУДНОСТИ: пять рук обязаны редактировать ОДИН юнит")
keys = {a: c["key"] for a, c in cells.items()}
print(f" ключи: {keys}")
if len(set(keys.values())) != 1:
fails.append(f"руки взяли РАЗНЫЕ юниты: {keys} — блокировка трудности сломана")
else:
print(" ✓ юнит один")
# ── 3. ⛔ ПОБАЙТНАЯ ИДЕНТИЧНОСТЬ ВХОДА ВНУТРИ ПРОМПТА ────────────────────────────────────────
print("\n## 3. content_hash — побайтная сигнатура отрендеренных сообщений")
ch = {a: c["content_hash"] for a, c in cells.items()}
for a, h in ch.items():
print(f" {a:8s} {h[:24] or '(пусто)'}")
# ⚠ только НЕПУСТЫЕ хеши: на пустом стенде оба множества были бы {None} и «совпадение»
# отрапортовалось бы ложно — ровно тот класс ложной находки, за который здесь ловят.
p7 = {h for h in (ch.get("p7-off"), ch.get("p7-low")) if h}
p9 = {h for h in (ch.get("p9-off"), ch.get("p9-low"), ch.get("glm-off")) if h}
if len(p7) != 1 and p7:
fails.append(f"content_hash РАЗНЫЙ внутри пары P7: {p7} — руки видят разный вход, а должны только разный ЭФФОРТ")
if len(p9) != 1 and p9:
fails.append(f"content_hash РАЗНЫЙ внутри группы P9: {p9}")
if p7 and p9 and (p7 & p9):
fails.append("content_hash СОВПАЛ между P7 и P9 — ось промпта не доехала до сообщений")
if len(p7) == 1 and len(p9) == 1 and not (p7 & p9):
print(" ✓ внутри P7 одинаков, внутри P9 одинаков, между P7 и P9 РАЗЛИЧЕН — ровно как должно быть")
# ── 4. СИГНАТУРА Δprompt_tokens ─────────────────────────────────────────────────────────────
print("\n## 4. СИГНАТУРА Δprompt_tokens (∅ low) — её пинит ФРИЗ №2")
sig = {}
for label, off, low in (("P7", "p7-off", "p7-low"), ("P9", "p9-off", "p9-low")):
if off in cells and low in cells:
sig[label] = cells[off]["pt"] - cells[low]["pt"]
print(f" {label}: {cells[off]['pt']} {cells[low]['pt']} = {sig[label]}")
if not sig:
fails.append("сигнатура не снята — нет пар попыток 0")
# ── 5. ⛔ КИРИЛЛИЦА И ЭХО — СТОП-КРИТЕРИИ ПАЧКИ ─────────────────────────────────────────────
print("\n## 5. КИРИЛЛИЦА ≥0.99 И ЭХО = 0 (по СЫРОМУ ответу: санитайзер эхо вырезает)")
for arm, c in cells.items():
raw_cjk, ship_cjk = cjk_count(c["resp"]), cjk_count(c["shipped"])
raw_cyr, ship_cyr = cyr_share(c["resp"]), cyr_share(c["shipped"])
print(f" {arm:8s} сырой: CJK {raw_cjk:4d} кириллица {raw_cyr:.4f} | отгружено: CJK {ship_cjk:4d} "
f"кириллица {ship_cyr:.4f}")
if arm in CROSS:
if raw_cjk > 0:
fails.append(f"{arm}: ЭХО в сыром ответе ({raw_cjk} иероглифов) — стоп-критерий пред-полёта")
if raw_cyr < 0.99:
fails.append(f"{arm}: кириллица {raw_cyr:.4f} < 0.99 — стоп-критерий пред-полёта")
else:
if raw_cjk > 0:
warns.append(f"референс glm-off: CJK {raw_cjk} в сыром ответе — ожидаемая зона эха "
f"(thinking выключен), в E5 идёт ОТДЕЛЬНОЙ строкой, пачку не блокирует")
# ── 6. ГЕЙТЫ РЕФЕРЕНСА ──────────────────────────────────────────────────────────────────────
print("\n## 6. ГЕЙТЫ РЕФЕРЕНСА glm-5")
if "glm-off" in cells:
g = cells["glm-off"]
resid = g["cmpl"] - COEF * len(g["resp"])
rel = abs(resid) / g["cmpl"] if g["cmpl"] else 1.0
print(f" остаток completion {COEF}·len(resp) = {g['cmpl']} {COEF * len(g['resp']):.0f} = {resid:+.0f}"
f" ({rel:.1%} от completion)")
print(" ⚠ «R̂ = 0 по построению» — определение. УЛИКА, что thinking действительно выключен, — "
"именно этот остаток.")
if rel > 0.15:
warns.append(f"референс: остаток {rel:.1%} > 15% — на модели с выключенным thinking ожидался "
f"близкий к нулю; либо тумблер не доехал, либо у glm другой токенизатор")
# ── 7. ⛔ ЦЕНА КЛЕТКИ low ПРОТИВ ПОРОГА, ОБЪЯВЛЕННОГО ДО ЧИСЕЛ ──────────────────────────────
print(f"\n## 7. ЦЕНА КЛЕТКИ low ПРОТИВ ПОРОГА ${LOW_THRESHOLD} (объявлен в пре-реге ДО покупки)")
lows = [cells[a]["cost0"] for a in ("p7-low", "p9-low") if a in cells]
offs = [cells[a]["cost0"] for a in ("p7-off", "p9-off") if a in cells]
if lows:
m = sum(lows) / len(lows)
print(f" low: {['$%.6f' % x for x in lows]} ⇒ среднее ${m:.6f}")
print(f" ∅ : {['$%.6f' % x for x in offs]}")
if m > LOW_THRESHOLD:
fails.append(f"СРЕДНЯЯ КЛЕТКА low ${m:.6f} > порога ${LOW_THRESHOLD} — СТОП И ВОПРОС ВЛАДЕЛЬЦУ "
f"до основной покупки (выше в замороженной смете сценария нет)")
else:
print(f" ✓ ${m:.6f} ≤ ${LOW_THRESHOLD} — иду дальше")
for a in ("p7-off", "p9-off"):
if a in cells and cells[a]["cost0"] > 0.20:
warns.append(f"{a}: клетка ${cells[a]['cost0']:.6f} > $0.20 — отметка (симметрия к порогу low), "
f"не остановка")
# ── 8. ПРОВОДНАЯ УЛИКА ИЗ СЫРЫХ ТЕЛ ─────────────────────────────────────────────────────────
print("\n## 8. ПРОВОДНАЯ УЛИКА — что реально уехало в теле запроса")
for arm in ARMS:
log = B / "logs" / f"pre-{arm}.log"
if not log.exists():
warns.append(f"{arm}: лога {log.name} нет — проводная улика не снята")
continue
txt = log.read_text(errors="replace")
n_ex = txt.count("llm exchange")
# ⚠ ТЕЛО ЛОГИРУЕТСЯ ЭКРАНИРОВАННЫМ: slog печатает request="{\"max_tokens\":16000,...}".
# Первая редакция грепа искала неэкранированные кавычки, ничего не находила и рапортовала
# «ключа нет» — то есть дефект ИНСТРУМЕНТА выглядел как находка о проводе. Поймано
# положительным контролем: `grep -c reasoning_effort` даёт 1 там, где регулярка давала 0.
def find(key, val=r'\\?"([\w.\-]+)\\?"'):
# экранированы ОБЕ пары кавычек: \"reasoning_effort\":\"low\"
return sorted(set(re.findall(r'\\?"' + key + r'\\?":\s*' + val, txt)))
eff = find("reasoning_effort")
think = sorted(set(re.findall(r'\\?"thinking\\?":\s*\{\\?"type\\?":\s*\\?"(\w+)', txt)))
mt = sorted(set(int(x) for x in find("max_tokens", r'(\d+)')))
tmp = sorted(set(find("temperature", r'([\d.]+)')))
mdl = find("model")
print(f" {arm:8s} обменов {n_ex} · reasoning_effort {eff or ''} · thinking {think or ''} · "
f"max_tokens {mt or ''} · temperature {tmp or ''} · model {mdl or ''}")
# ⛔ ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ НА ГРЕП: «ключа нет» обязано отличаться от «лога нет».
if n_ex == 0:
warns.append(f"{arm}: в логе НОЛЬ строк «llm exchange» — «ключа нет» здесь ничего не доказывает")
continue
want_eff = "low" if arm.endswith("low") else None
if want_eff and eff != [want_eff]:
fails.append(f"{arm}: в теле reasoning_effort {eff}, а должно быть ['{want_eff}']")
if not want_eff and arm != "glm-off" and eff:
fails.append(f"{arm}: в теле ЕСТЬ reasoning_effort {eff}, а у ∅-руки ключа быть не должно")
if arm == "glm-off" and think != ["disabled"]:
fails.append(f"{arm}: thinking в теле {think}, а должно быть ['disabled']")
# ── 9. ПОТОЛОК ВЫВОДА ОДИНАКОВ ВО ВСЕХ ЧЕТЫРЁХ РУКАХ КРЕСТА ─────────────────────────────────
print("\n## 9. ПОТОЛОК ВЫВОДА (требование §14: одинаков во всех руках креста)")
mts = {}
for arm in ARMS:
log = B / "logs" / f"pre-{arm}.log"
if log.exists():
m = re.findall(r'msg="calling model".*?max_tokens=(\d+)', log.read_text(errors="replace"))
edits = sorted(set(int(x) for x in m))
mts[arm] = edits
print(f" {arm:8s} max_tokens в вызовах: {edits}")
cross_mt = {a: [x for x in v if x >= 16000] for a, v in mts.items() if a in CROSS}
flat = {tuple(v) for v in cross_mt.values() if v}
if len(flat) > 1:
fails.append(f"потолок вывода РАЗЛИЧЕН между руками креста: {cross_mt} — конфаунд, убивший прошлый замер")
elif flat:
print(f"у четырёх рук креста потолок один: {sorted(flat)[0]}")
if "glm-off" in mts:
print(f"у референса свой потолок ({mts['glm-off']}): у glm-5 нет min_max_tokens, «равный потолок» "
f"на него НЕ распространяется — это печатается рядом с его числом")
# ── ИТОГ ────────────────────────────────────────────────────────────────────────────────────
print("\n" + "" * 92)
for w in warns:
print(f"{w}")
if fails:
print("\n⛔ ПРЕД-ПОЛЁТ НЕ ПРОШЁЛ:")
for f in fails:
print(f" · {f}")
print("\nПАЧКУ НЕ НАЧИНАТЬ. Это стоп-критерии §5.1, а не рекомендации.")
sys.exit(1)
print("✓ ПРЕД-ПОЛЁТ ПРОЙДЕН — все стоп-критерии §5.1 исполнены командой, а не осмотрены глазами.")
print(f" сигнатуру {sig} пинит ФРИЗ №2 — до пачки.")
sys.exit(0)