textmachine/eval/tenant_panel/prompts.py

159 lines
8.4 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
"""СБОРКА СООБЩЕНИЙ ЭКСП-22: боевые промты + бриф МОЕЙ книги + блоки банка. $0.
Почему свой модуль, а не `inject_probe.render_translator`. Тот прошит книгой 蛊真人: заголовок,
жанр и транскрипция лежат КОНСТАНТАМИ в коде. Для эксп-22 книга другая, и вся книго-специфика
обязана быть данными (§0.1 общности: «книжный термин в общем пар-слое = утечка»). Здесь брифы —
данные, промты — боевые файлы `backend/prompts/`, а рендер побайтно повторяет `render.go`.
Что берётся ДОСЛОВНО и почему:
* `translator.md` / `editor.md` — боевые, из `backend/prompts/zh-ru/` (сравнение идёт с тем, что
реально поедет в прод);
* `HEADER_LAW_PLAIN` — константа эксп-21, байт-идентичная движковому `editor_header`, БЕЗ
оговорки области: закон-блок пинится промтом эксп-22 (D39.104);
* блок ГЛОССАРИЯ переводчика (банкнота на переводческом проходе) — формат `injection.txt`;
* пара en идёт пар-пакетом `role_topology/prompts/en-ru/` — тем же, что мерил эксп-21.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
REPO = Path("/home/ubuntu/projects/textmachine")
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
import editor_wire_probe as P # noqa: E402
RT = REPO / "eval" / "role_topology"
BATTLE = REPO / "backend" / "prompts" / "zh-ru"
PAIR_EN = RT / "prompts" / "en-ru"
USER_SEP = "\n---USER---\n"
FEWSHOT_SEP = "\n---FEWSHOT---\n"
BANK_FILE = Path.home() / "books" / "tenant-panel" / "bank.json"
# Бриф КНИГИ СРЕЗА — данные, не код. Поля те же, что у движка (`render.go` Render).
BRIEF_ZH = {"source_lang": "zh", "target_lang": "ru", "genre": "вебновелла",
"audience": "взрослые читатели вебновелл", "title": "阴阳天帝诀", "venuti": "0.60",
"honorifics": "keep", "transcription": "palladius", "footnotes": "minimal"}
BRIEF_EN = {"source_lang": "en", "target_lang": "ru", "genre": "тёмное фэнтези",
"audience": "взрослые читатели фэнтези", "title": "Empire of the Dawn",
"venuti": "0.60", "honorifics": "keep", "transcription": "practical",
"footnotes": "minimal"}
GLOSSARY_HEADER = ("ГЛОССАРИЙ (используй эти утверждённые переводы имён и терминов последовательно; "
"строки с пометкой ⟨проверить⟩ — неподтверждённые кандидаты):")
def render(tpl: str, brief: dict, text: str, draft: str) -> str:
"""Зеркало `render.go` Render: ОДИН проход, подставленные значения не пере-сканируются."""
vals = dict(brief, text=text, draft=draft)
out, rest = [], tpl
while True:
i = rest.find("{{")
if i < 0:
out.append(rest)
return "".join(out)
j = rest.find("}}", i)
if j < 0:
out.append(rest)
return "".join(out)
out.append(rest[:i])
out.append(str(vals.get(rest[i + 2:j].strip(), rest[i:j + 2])))
rest = rest[j + 2:]
def load_template(path: Path) -> tuple[str, str, str]:
"""(системная часть без FEWSHOT, блок FEWSHOT, пользовательская часть).
FEWSHOT дропается: в проде `stages[edit].few_shot: false`, и эксп-19/20/21 его тоже дропали —
иначе база сравнения не боевая.
"""
canon = P.strip_comments(path.read_text(encoding="utf-8"))
sys_part, user = canon.split(USER_SEP, 1)
head = sys_part.split(FEWSHOT_SEP, 1)
return head[0].strip(), (head[1].strip() if len(head) == 2 else ""), user.strip()
def bank() -> dict:
"""Банк среза: {термин исходника: {"dst": канон, "rx": регекс формы}}. Пусто — банка нет."""
return json.loads(BANK_FILE.read_text(encoding="utf-8"))["terms"] if BANK_FILE.exists() else {}
def terms_in(src: str) -> list[str]:
"""Термы банка, реально встреченные В ЭТОЙ единице — так инъектирует боевой путь (D39.104)."""
return [t for t in bank() if t in src]
def glossary_block(terms: list[str]) -> str | None:
"""Банкнота переводческого прохода: формат `internal/lang/data/injection.txt`."""
b = bank()
if not terms:
return None
return GLOSSARY_HEADER + "\n" + "\n".join(f"{t}{b[t]['dst']}" for t in terms)
def law_block(terms: list[str]) -> str | None:
"""Закон-блок редактора: заголовок эксп-21 БЕЗ оговорки + строки `- src → «dst»`."""
b = bank()
if not terms:
return None
return P.HEADER_LAW_PLAIN + "\n" + "\n".join(f"- {t} → «{b[t]['dst']}»" for t in terms)
def translator_msgs(src: str, block: str | None, en: bool = False) -> list[dict]:
core, _few, user = load_template(PAIR_EN / "translator.md" if en else BATTLE / "translator.md")
brief = BRIEF_EN if en else BRIEF_ZH
m = [{"role": "system", "content": render(core, brief, src, "")}]
if block:
m.append({"role": "system", "content": block})
m.append({"role": "user", "content": render(user, brief, src, "")})
return m
def editor_msgs(src: str, draft: str, block: str | None, en: bool = False) -> list[dict]:
core, _few, user = load_template(PAIR_EN / "editor.md" if en else BATTLE / "editor.md")
brief = BRIEF_EN if en else BRIEF_ZH
m = [{"role": "system", "content": render(core, brief, src, draft)}]
if block:
m.append({"role": "system", "content": block})
m.append({"role": "user", "content": render(user, brief, src, draft)})
return m
def selfcheck() -> int:
"""Проверка исполнением: плейсхолдеров не осталось, книга подставлена, блоки различаются."""
bad = 0
def chk(n: str, ok: bool, d: str = "") -> None:
nonlocal bad
bad += not ok
print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {n}" + (f" {d}" if d else ""))
src, draft = "第一章 试炼\n少年抬头看天。", "Глава первая. Испытание\nЮноша поднял глаза к небу."
for en in (False, True):
for name, msgs in (("переводчик", translator_msgs(src, None, en)),
("редактор", editor_msgs(src, draft, None, en))):
joined = "\n".join(m["content"] for m in msgs)
chk(f"{name} {'en' if en else 'zh'}: плейсхолдеров не осталось", "{{" not in joined)
title = (BRIEF_EN if en else BRIEF_ZH)["title"]
chk(f"{name} {'en' if en else 'zh'}: подставлена книга среза", title in joined)
chk(f"{name} {'en' if en else 'zh'}: исходник в сообщении", src in joined)
chk("FEWSHOT не уехал в системный промт",
"FEWSHOT" not in editor_msgs(src, draft, None)[0]["content"])
b = bank()
if b:
t = list(b)[:2]
law, glo = law_block(t), glossary_block(t)
chk("закон-блок несёт заголовок эксп-21 без оговорки",
law.startswith(P.HEADER_LAW_PLAIN) and "глоссарий обязателен" not in law)
chk("блоки закона и глоссария различаются", law != glo)
chk("термы фильтруются по единице", terms_in("нет иероглифов") == [])
else:
print("[ПРОП ] банк ещё не собран — блоки не проверены")
print(f"\n{'СБОРКА ЧИСТА' if not bad else f'ПРОВАЛОВ: {bad}'}")
return bad
if __name__ == "__main__":
sys.exit(1 if selfcheck() else 0)