#!/usr/bin/env python3 """INDICATIVE in-context experiment for the adaptive-memory verdict (docs/research/14). Tests THE central BAR question of the "free win" (in-context demonstrations) strand: does adding the book's own DEMONSTRATIONS (prior translated passages) beat the deterministic baseline the bank ALREADY ships — soft-glossary injection — for keeping a recurring entity's rendering canonical and consistent on a REAL zh→ru literary chunk; and do the learning layer's failure modes actively HARM (Power-of-Noise near-miss distractor, 2401.14887; over-trust of a poisoned entry, 2510.00829)? Testbed: 魯迅《阿Q正传》ch5-9 (real PD zh) — a dense chunk where А-Q fantasizes the revolution and names 8 recurring characters at once. Canonical dst = В. Рогов's published translation (eval/data/samples/ru/luxun-ah-q-ru.txt); demonstrations are AUTHENTIC Rogov sentences. Metric = the bank's OWN deterministic post-check (canonical stem present in output), scored only over entities whose zh key is in the source chunk. Arms: A_bare no memory (raw model) B_glossary soft-glossary "src → dst" block <- THE deterministic baseline C_demos glossary + 3 authentic prior passages <- the learning layer (free win) D_noise glossary + 3 IRRELEVANT prior passages <- Power-of-Noise near-miss arm E_poison glossary with ONE wrong dst (赵太爷→"Ли") <- over-trust / silent-injection arm Model: deepseek-v4-flash (the real draft model; thinking ON by default → avoids the zh echo). N reps (temp 0.3) for stability. INDICATIVE: small N, hand-built accept-regexes (self-confirmation risk), one model, one chunk. Not the Phase-2.5 in-house eval — a probe. Usage: eval/.venv/bin/python eval/adaptive_incontext.py [--reps 3] [--provider deepseek] Output: stdout table + eval/data/adaptive_incontext.json """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import refusal_bench as rb # reuse call_provider + providers quirks + env load ROOT = Path(__file__).resolve().parent SRC = ROOT / "data" / "samples" / "zh" / "luxun-ah-q-ch5-9.txt" OUT = ROOT / "data" / "adaptive_incontext.json" # Dense multi-entity window (offset 6360, 650 chars): А-Q's revolution fantasy. CHUNK_OFF, CHUNK_LEN = 6360, 650 # Recurring entities: zh key -> (canonical Rogov dst shown in glossary, accept-regex on # the canonical STEM allowing Russian declension). Stem match = the bank's post-check (E1). ENTITIES = { "阿Q": ("А-кью", r"А[-\s]?кью"), "未庄": ("Вэйчжуан (деревня)", r"Вэйчжуан"), "赵太爷": ("почтенный Чжао", r"Чжао"), "王胡": ("Бородатый Ван", r"Ван\b"), "小D": ("Маленький Дэн", r"Дэн"), "假洋鬼子": ("Поддельный заморский чёрт", r"заморск\w+ чёрт|поддельн"), "吴妈": ("У-ма", r"У[-\s]?ма"), "邹七嫂": ("тётушка Цзоу Седьмая", r"Цзоу"), } # Arm C: AUTHENTIC prior translated passages (Rogov), each carrying a recurring entity in # natural inflected Russian — the "demonstration" the learning layer would retrieve. DEMOS_RELEVANT = [ "Он знал, что вместо него нанимают Маленького Дэна, этого худого и слабосильного бедняка.", "Сын почтенного Чжао добился, наконец, учёной степени сюцая, и об этом оповестили всю деревню.", "Жители деревни Вэйчжуан требовали от него только подённой работы да насмехались над ним.", ] # Arm D: IRRELEVANT prior passages (near-miss distractors) — plausible book prose, but # about material NOT in this chunk. Tests "better nothing than garbage" on the real wire. DEMOS_NOISE = [ "Стены храма Тудигун почернели от времени, и ветер свистел в щелях по ночам.", "На реке скрипели вёсла лодочников, а торговцы раскладывали на пристани сушёную рыбу.", "В тот год урожай риса выдался скудным, и цены на масло поднялись по всей округе.", ] SYSTEM = ("Ты профессиональный литературный переводчик. Переведи фрагмент художественного " "произведения на русский язык. Это перевод существующего текста, предоставленного " "правообладателем. Сохрани все реплики и детали без пропусков; стиль — живой " "литературный русский. Выведи ТОЛЬКО перевод, без комментариев.") def glossary_block(poison=False): lines = [] for zh, (dst, _) in ENTITIES.items(): d = "господин Ли (важный чиновник)" if (poison and zh == "赵太爷") else dst lines.append(f"{zh} → {d}") return "ГЛОССАРИЙ (используй эти утверждённые переводы имён/терминов последовательно):\n" + "\n".join(lines) def demos_block(demos): return ("РАНЕЕ ПЕРЕВЕДЁННЫЕ ФРАГМЕНТЫ ЭТОЙ КНИГИ (держи ту же терминологию и стиль):\n" + "\n".join(f"— {d}" for d in demos)) def build_user(chunk, arm): parts = [] if arm in ("B_glossary", "C_demos", "D_noise"): parts.append(glossary_block()) if arm == "E_poison": parts.append(glossary_block(poison=True)) if arm == "C_demos": parts.append(demos_block(DEMOS_RELEVANT)) if arm == "D_noise": parts.append(demos_block(DEMOS_NOISE)) parts.append("ФРАГМЕНТ ДЛЯ ПЕРЕВОДА:\n" + chunk) return "\n\n".join(parts) def cjk_ratio(s): cjk = len(re.findall(r"[㐀-鿿]", s)) return round(cjk / max(1, len(s)), 3) def score(output, present): """Bank post-check: fraction of source-present entities whose canonical stem is in output.""" matched = [zh for zh in present if re.search(ENTITIES[zh][1], output)] return round(len(matched) / max(1, len(present)), 3), matched def main(): ap = argparse.ArgumentParser() ap.add_argument("--reps", type=int, default=3) ap.add_argument("--provider", default="deepseek") ap.add_argument("--providers", default=str(ROOT / "providers.json")) args = ap.parse_args() prov = next(p for p in json.load(open(args.providers))["providers"] if p["name"] == args.provider) chunk = SRC.read_text(encoding="utf-8")[CHUNK_OFF:CHUNK_OFF + CHUNK_LEN] present = [zh for zh in ENTITIES if zh in chunk] print(f"provider={prov['model']} chunk={len(chunk)}ch present entities ({len(present)}): {present}\n") arms = ["A_bare", "B_glossary", "C_demos", "D_noise", "E_poison"] results = {"provider": prov["model"], "present": present, "reps": args.reps, "arms": {}} print(f"{'arm':<12} {'canon-consist':<14} {'cjk-echo':<9} {'poison-followed':<16} note") print("-" * 78) for arm in arms: user = build_user(chunk, arm) recs = [] for _ in range(args.reps): text, err, usage = rb.call_provider(prov, SYSTEM, user, timeout=180) if not text: recs.append({"err": err}); continue cons, matched = score(text, present) rec = {"consist": cons, "matched": matched, "cjk": cjk_ratio(text), "poison_followed": bool(re.search(r"господин Ли|\bЛи\b", text)) and not re.search(r"Чжао", text), "len": len(text), "sample": text[:0]} recs.append(rec) ok = [r for r in recs if "err" not in r] if not ok: print(f"{arm:<12} {'ALL-FAILED':<14} {'':<9} {'':<16} {[r['err'] for r in recs][:1]}") results["arms"][arm] = {"records": recs}; continue mc = round(sum(r["consist"] for r in ok) / len(ok), 3) me = round(sum(r["cjk"] for r in ok) / len(ok), 3) pf = sum(r["poison_followed"] for r in ok) echo = " ECHO!" if me > 0.15 else "" results["arms"][arm] = {"mean_consist": mc, "mean_cjk": me, "poison_followed": pf, "n_ok": len(ok), "records": recs} print(f"{arm:<12} {mc:<14} {me:<9} {str(pf)+'/'+str(len(ok)):<16} n={len(ok)}{echo}") OUT.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8") print(f"\nsaved -> {OUT}") print("Reading: B−A = does the deterministic soft-glossary baseline help; C−B = does the\n" "learning layer (demonstrations) beat that baseline; D vs C = Power-of-Noise harm;\n" "E poison-followed>0 = over-trust (why the learning layer must NOT touch approved names).") if __name__ == "__main__": main()