textmachine/eval/editor_harness/repair_probe.py

112 lines
6.9 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
"""Арм SCOPED REPAIR: починка по УЖЕ НАЙДЕННОМУ спану — механизм, которого у нас нет.
`research/22:176` фиксирует раскол дословно: «Checker→typed-error→scoped repair | AiNiee
`ProofreadTask.py:250`; LinguaGacha retrans-by-keyword | у нас НЕТ (флагаем, не чиним)».
Что проверяется. Мои же замеры дали, что цену редакторского вызова держит РЕШЕНИЕ, а не формат:
арм `locate` (только найди, ничего не чини) стоил 2.14× от полного переписывания на deepseek-v4-pro.
Если так — починка по указанному месту обязана быть на порядок дешевле: модели не нужно ни искать,
ни держать в голове весь текст, ей дают одно предложение и тип ошибки.
Материал — те же 12 посаженных дефектов пробы 18 (§0.5): 6 несуществующих слов (к1) и 6 смысловых
инверсий (к2). Эталон известен посимвольно, поэтому починка скорится детерминированно, без судьи,
теми же регексами `fix_rx`/`bad_rx`, что валидированы ревью посылки (12/12 корректны).
⚠ Объявляется заранее: арм меряет починку при ИДЕАЛЬНОЙ детекции. Он НЕ отвечает, найдут ли гейты
эти дефекты сами — гейта на «несуществующее слово» и на смысловую инверсию у нас нет. Поэтому
результат читается как ВЕРХНЯЯ ГРАНИЦА схемы, а не как её цена в проде.
"""
from __future__ import annotations
import json
import re
import sys
import time
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
HERE = Path(__file__).resolve().parent
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 inject_probe import pick_windows # noqa: E402
RAW = Q.RAW
TYPE = {"k1": "несуществующее слово (модель выдумала слово, которого в русском языке нет)",
"k2": "смысловое искажение против исходника (сказано обратное или иное, чем в оригинале)"}
def sentence_with(text: str, needle: str) -> str | None:
"""Предложение, содержащее посаженный фрагмент. Границы — по .!?… и переносу строки."""
i = text.find(needle)
if i < 0:
return None
starts = [m.end() for m in re.finditer(r"[.!?…]\s+|\n", text[:i])]
s = starts[-1] if starts else 0
m = re.search(r"[.!?…](\s|$)|\n", text[i + len(needle):])
e = i + len(needle) + (m.end() if m else 0)
return text[s:e].strip()
def plan() -> list[tuple]:
out = []
for k, (_, buf, _) in enumerate(pick_windows()):
planted, _ = P.planted_draft(k, P.draft_of(k))
for cls, old, new, bad_rx, fix_rx in P.DEFECTS[k]:
sent = sentence_with(planted, new)
if not sent:
print(f" ⚠ w{k}/{cls}: не выделил предложение вокруг посадки — пропуск")
continue
# ⚠ Селф-ревью 05.08: у части посадок дефект ДОБАВЛЯЕТ слово, не заменяя ключевое
# («мреязное СОКРОВИЩЕ природы»), поэтому `fix_rx` совпадает с испорченным предложением
# ЕЩЁ ДО починки. В таких клетках «восстановлен» не несёт информации — помечаем их и
# держим вне строгого знаменателя, иначе бездействие модели засчиталось бы как успех.
degenerate = bool(re.search(fix_rx, sent))
out.append((k, cls, sent, buf, bad_rx, fix_rx, old, degenerate))
return out
def main(models: list[str]) -> None:
pl = plan()
print(f"починок в плане: {len(pl)} (ожидается 12)")
tpl = HERE / "prompts" / "repair.md"
for m in models:
short = {"deepseek-v4-pro": "dp", "glm-5": "gl", "gpt-5.6-luna": "lu",
"grok-4.3": "gr"}[m]
nothink = Q.MODELS[m][5] in ("disable", "effort_none")
rows, spent = [], 0.0
for k, cls, sent, buf, bad_rx, fix_rx, old, degen in pl:
flagged = f"{TYPE[cls]}\n\nПроблемное предложение перевода:\n{sent}"
sys_msg, user = Q.render(tpl, buf, flagged)
rec = Q.call(m, [{"role": "system", "content": sys_msg},
{"role": "user", "content": user}],
f"rp-{short}-w{k}-{cls}", nothink)
spent += rec["cost_usd"]
got = rec["content"].strip()
rows.append(dict(window=k, cls=cls, degenerate=degen,
still=bool(re.search(bad_rx, got)),
restored=bool(re.search(fix_rx, got)),
cost=rec["cost_usd"], out_tokens=rec["completion_tokens"],
reasoning=rec["reasoning_chars"], chars=len(got)))
time.sleep(0.2)
inf = [r for r in rows if not r["degenerate"]]
snyat = sum(1 for r in rows if not r["still"])
strict = sum(1 for r in inf if not r["still"] and r["restored"])
print(f"\n{m}: СНЯТ дефект {snyat}/{len(rows)} · "
f"ВОССТАНОВЛЕН строго {strict}/{len(inf)} (клеток с информативным эталоном {len(inf)} "
f"из {len(rows)}) · ${spent:.6f}, ${spent / max(len(rows), 1):.6f} за починку")
for cls in ("k1", "k2"):
sub = [r for r in rows if r["cls"] == cls]
si = [r for r in sub if not r["degenerate"]]
if sub:
print(f" {cls}: снят {sum(1 for r in sub if not r['still'])}/{len(sub)}"
+ (f" · восстановлен {sum(1 for r in si if not r['still'] and r['restored'])}/{len(si)}" if si else ""))
(RAW / f"repair-{short}.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1),
encoding="utf-8")
if __name__ == "__main__":
main(sys.argv[1:] or ["deepseek-v4-pro"])