textmachine/eval/editor_harness/selfcheck.py

118 lines
6.4 KiB
Python
Raw Permalink 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
"""Селф-ревью рига эксп-20 ДО платных вызовов (требование владельца 05.08).
Проверяется исполнением, а не глазами:
С1 ключи всех заявленных провайдеров присутствуют в окружении (значения НЕ печатаются);
С2 все четыре контракта рендерятся без незаполненных плейсхолдеров, и блок закона одинаков;
С3 `classify_ops` воспроизводит счёт отказов настоящего аппликатора на УЖЕ ОПЛАЧЕННОМ сырье
эксп-19 — иначе раунд починки вернёт модели не те операции;
С4 контракты действительно разные на проводе (иначе меряем один и тот же промпт дважды);
С5 гардрейл: попытка погасить мышление у DeepSeek обязана падать, а не тихо проходить.
"""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
HERE = Path(__file__).resolve().parent
# ⚠ ПОРЯДОК ВАЖЕН (селф-ревью 05.08): в `eval/editor_contract/` лежит СВОЙ `probe.py`, и при
# обратном порядке вставок он затеняет наш — `import probe` подтягивал чужой модуль.
# HERE вставляется ПОСЛЕДНИМ, поэтому стоит первым в sys.path и выигрывает.
sys.path.insert(0, str(REPO / "eval" / "editor_contract"))
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
sys.path.insert(0, str(HERE))
import probe as Q # noqa: E402
import editor_wire_probe as P # noqa: E402
from apply import apply_ops # noqa: E402
from inject_probe import pick_windows # noqa: E402
OLD = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
FAIL = 0
def check(name: str, ok: bool, note: str = "") -> None:
global FAIL
if not ok:
FAIL += 1
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + (f"{note}" if note and not ok else ""))
def main() -> None:
# С1 — ключи (значения наружу не идут)
for m, spec in Q.MODELS.items():
check(f"С1 ключ {spec[1]} для {m}", bool(os.environ.get(spec[1])), "не найден в окружении")
wins = pick_windows()
_, buf, terms = wins[0]
draft = P.draft_of(0)
blk = P.editor_block(terms, P.HEADER_LAW_CLAUSE, None)
# С2 — рендер
sysmsgs = {}
for c in Q.CONTRACTS:
msgs = Q.messages(c, buf, draft, blk)
holes = set()
for mm in msgs:
holes |= set(re.findall(r"\{\{(\w+)\}\}", mm["content"]))
check(f"С2 {c}: плейсхолдеров нет", not holes, ", ".join(sorted(holes)))
check(f"С2 {c}: блок закона на месте", any("КАНОНИЧЕСКИЕ" in mm["content"] for mm in msgs))
sysmsgs[c] = msgs[0]["content"]
# С4 — контракты различны
uniq = len(set(sysmsgs.values()))
check("С4 все четыре контракта различны на проводе", uniq == len(Q.CONTRACTS),
f"уникальных системных промптов {uniq} из {len(Q.CONTRACTS)}")
for c, t in sysmsgs.items():
print(f" {c:13s} системный промпт {len(t):5d} зн.")
# С6 — секция FEWSHOT обязана дропаться (дефект 05.08, пойман в ходе прогона: арм `full`
# уехал с примерами перевёрстки, то есть база сравнения не была боевым контрактом).
fullsys = sysmsgs["full"]
check("С6 full: блок FEWSHOT дропнут, как в проде",
"---FEWSHOT---" not in fullsys and "Он вышел за дверь" not in fullsys)
check("С6 full: блок ДИСКУРС-ПЕРЕВЁРСТКИ на месте", "ДИСКУРС-ПЕРЕВЁРСТКА" in fullsys)
check("С6 full: длина совпала с проводом эксп-19 (3071 зн.)", len(fullsys) == 3071,
f"сейчас {len(fullsys)}")
# С3 — классификатор против настоящего аппликатора на оплаченном сырье
tot = mism = seen = 0
for k in range(6):
for r in (1, 2, 3):
f = OLD / f"ec-D1-w{k}-r{r}.json"
if not f.exists():
continue
reply = json.loads(f.read_text(encoding="utf-8"))["content"]
d = P.draft_of(k)
res = apply_ops(d, reply)
mine = Q.classify_ops(d, reply)
seen += 1
# аппликатор считает отказы notfound+ambiguous; мой классификатор — те же классы
ref = res.rejected_notfound + res.rejected_ambiguous
got = sum(1 for _, _, st in mine if st in ("reject_notfound", "reject_ambiguous"))
tot += ref
if ref != got:
mism += 1
print(f"{f.stem}: аппликатор {ref}, классификатор {got}")
if len(mine) != res.ops_total and not res.malformed:
mism += 1
print(f"{f.stem}: операций аппликатор {res.ops_total}, классификатор {len(mine)}")
check(f"С3 классификатор совпал с аппликатором на {seen} вызовах эксп-19 (отказов в сырье {tot})",
mism == 0, f"расхождений {mism}")
# С5 — гардрейл
try:
Q.call("deepseek-v4-pro", [{"role": "user", "content": "x"}], "selfcheck-guard", True)
check("С5 гашение мышления у DeepSeek запрещено", False, "вызов ПРОШЁЛ — гардрейл дырявый")
except SystemExit:
check("С5 гашение мышления у DeepSeek запрещено", True, "падает, как и должно")
print(f"\n{'РИГ ГОТОВ' if not FAIL else f'ПРОВАЛОВ: {FAIL}'}")
sys.exit(1 if FAIL else 0)
if __name__ == "__main__":
main()