Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
861 lines
38 KiB
Python
861 lines
38 KiB
Python
#!/usr/bin/env python3
|
|
"""Run Experiment 10-2 on a real illustrated, code-heavy technical book.
|
|
|
|
The tiny four-file fixture remains useful for a cheap tutorial. This is the
|
|
acceptance campaign: it translates Chapters 1 and 2 of the English edition of
|
|
this book (more than 240 KB, with real figures and fenced code), compares the
|
|
four-role Manager workflow with one accumulating Agent conversation, and saves
|
|
quality, wall-clock, context, token, and provenance evidence.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from dotenv import load_dotenv
|
|
from openai import OpenAI
|
|
|
|
HERE = Path(__file__).parent
|
|
REPO = HERE.parents[1]
|
|
DEFAULT_SOURCES = (REPO / "book-en" / "chapter1.md", REPO / "book-en" / "chapter2.md")
|
|
DIMENSIONS = ("accuracy", "fluency", "terminology", "markdown_code_fidelity")
|
|
|
|
|
|
def sha256(path: Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def sha256_text(text: str) -> str:
|
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def extract_title(text: str, fallback: str) -> str:
|
|
for line in text.splitlines():
|
|
if line.startswith("# "):
|
|
return line[2:].strip()
|
|
return fallback
|
|
|
|
|
|
def load_source_book(paths: list[Path]) -> tuple[dict[str, str], dict[str, str]]:
|
|
chapters: dict[str, str] = {}
|
|
title_to_path: dict[str, str] = {}
|
|
for path in paths:
|
|
text = path.read_text(encoding="utf-8")
|
|
title = extract_title(text, path.stem)
|
|
if title in chapters:
|
|
raise ValueError(f"duplicate source title: {title}")
|
|
chapters[title] = text
|
|
title_to_path[title] = str(path.relative_to(REPO))
|
|
return chapters, title_to_path
|
|
|
|
|
|
def markdown_blocks(text: str) -> list[str]:
|
|
"""Split at blank lines without ever cutting through a fenced code block."""
|
|
blocks: list[str] = []
|
|
current: list[str] = []
|
|
in_fence = False
|
|
for line in text.splitlines(keepends=True):
|
|
if line.lstrip().startswith("```"):
|
|
in_fence = not in_fence
|
|
current.append(line)
|
|
if not in_fence and not line.strip():
|
|
blocks.append("".join(current))
|
|
current = []
|
|
if current:
|
|
blocks.append("".join(current))
|
|
return blocks
|
|
|
|
|
|
def split_translation_units(
|
|
chapters: dict[str, str], max_characters: int = 36_000
|
|
) -> tuple[dict[str, str], dict[str, list[str]]]:
|
|
"""Create bounded chapter parts while retaining an exact reassembly map."""
|
|
units: dict[str, str] = {}
|
|
chapter_units: dict[str, list[str]] = {}
|
|
for title, text in chapters.items():
|
|
parts: list[str] = []
|
|
current = ""
|
|
for block in markdown_blocks(text):
|
|
if current and len(current) + len(block) > max_characters:
|
|
parts.append(current)
|
|
current = ""
|
|
if len(block) > max_characters:
|
|
# A pathological prose block may be larger than the target.
|
|
# Split at line boundaries; fenced code is one block and is
|
|
# deliberately allowed to exceed the target rather than cut.
|
|
if block.lstrip().startswith("```"):
|
|
if current:
|
|
parts.append(current)
|
|
current = ""
|
|
parts.append(block)
|
|
continue
|
|
for line in block.splitlines(keepends=True):
|
|
if current and len(current) + len(line) > max_characters:
|
|
parts.append(current)
|
|
current = ""
|
|
current += line
|
|
else:
|
|
current += block
|
|
if current:
|
|
parts.append(current)
|
|
names = []
|
|
for index, part in enumerate(parts, start=1):
|
|
name = f"{title} [Part {index}/{len(parts)}]"
|
|
units[name] = part
|
|
names.append(name)
|
|
chapter_units[title] = names
|
|
if "".join(parts) != text:
|
|
raise AssertionError(f"translation-unit split changed source bytes for {title}")
|
|
return units, chapter_units
|
|
|
|
|
|
def reassemble_translations(
|
|
translations: dict[str, str], chapter_units: dict[str, list[str]]
|
|
) -> dict[str, str]:
|
|
return {
|
|
chapter: "\n\n".join(translations[unit].rstrip() for unit in units).rstrip() + "\n"
|
|
for chapter, units in chapter_units.items()
|
|
}
|
|
|
|
|
|
def fenced_code_payloads(text: str) -> list[str]:
|
|
return re.findall(r"^```[^\n]*\n(.*?)^```[ \t]*$", text, flags=re.MULTILINE | re.DOTALL)
|
|
|
|
|
|
def image_targets(text: str) -> list[str]:
|
|
return re.findall(r"!\[[^\]]*\]\(([^\s)]+)(?:\s+[^)]*)?\)", text)
|
|
|
|
|
|
def link_targets(text: str) -> list[str]:
|
|
return re.findall(r"(?<!!)\[[^\]]+\]\(([^\s)]+)(?:\s+[^)]*)?\)", text)
|
|
|
|
|
|
def markdown_fidelity(source: str, translation: str) -> dict[str, Any]:
|
|
source_code = fenced_code_payloads(source)
|
|
translated_code = fenced_code_payloads(translation)
|
|
source_images = image_targets(source)
|
|
translated_images = image_targets(translation)
|
|
source_links = link_targets(source)
|
|
translated_links = link_targets(translation)
|
|
source_headings = len(re.findall(r"^#{1,6}\s+", source, flags=re.MULTILINE))
|
|
translated_headings = len(re.findall(r"^#{1,6}\s+", translation, flags=re.MULTILINE))
|
|
return {
|
|
"source_sha256": sha256_text(source),
|
|
"translation_sha256": sha256_text(translation),
|
|
"nonempty_translation": bool(translation.strip()),
|
|
"character_ratio": len(translation) / len(source) if source else 0.0,
|
|
"fenced_code": {
|
|
"source_count": len(source_code),
|
|
"translation_count": len(translated_code),
|
|
"exact_payload_sequence_preserved": source_code == translated_code,
|
|
},
|
|
"images": {
|
|
"source_count": len(source_images),
|
|
"translation_count": len(translated_images),
|
|
"exact_target_sequence_preserved": source_images == translated_images,
|
|
},
|
|
"links": {
|
|
"source_count": len(source_links),
|
|
"translation_count": len(translated_links),
|
|
"exact_target_sequence_preserved": source_links == translated_links,
|
|
},
|
|
"headings": {
|
|
"source_count": source_headings,
|
|
"translation_count": translated_headings,
|
|
"count_preserved": source_headings == translated_headings,
|
|
},
|
|
}
|
|
|
|
|
|
def validate_judge_response(payload: dict[str, Any]) -> dict[str, Any]:
|
|
payload = dict(payload)
|
|
variants = payload.get("variants")
|
|
repairs: list[str] = []
|
|
if isinstance(variants, dict) and {"X", "Y"}.issubset(variants):
|
|
extras = set(variants) - {"X", "Y"}
|
|
# ARK occasionally duplicates the two preference fields one level too
|
|
# deep while still returning complete X/Y rubrics. This is a purely
|
|
# structural, lossless repair; arbitrary extra keys and incomplete
|
|
# rubrics remain hard failures.
|
|
if extras and extras.issubset({"preferred", "preference_evidence"}):
|
|
for key in extras:
|
|
if key not in payload:
|
|
payload[key] = variants[key]
|
|
variants = {alias: variants[alias] for alias in ("X", "Y")}
|
|
repairs.append("lifted duplicated preference fields out of variants")
|
|
if not isinstance(variants, dict) or set(variants) != {"X", "Y"}:
|
|
raise ValueError("judge variants must contain exactly X and Y")
|
|
normalized: dict[str, Any] = {"variants": {}}
|
|
for alias in ("X", "Y"):
|
|
variant = variants[alias]
|
|
if not isinstance(variant, dict) or set(variant) != set(DIMENSIONS):
|
|
raise ValueError(f"judge variant {alias} must contain all rubric dimensions")
|
|
normalized["variants"][alias] = {}
|
|
for dimension in DIMENSIONS:
|
|
item = variant[dimension]
|
|
if not isinstance(item, dict):
|
|
raise ValueError(f"{alias}.{dimension} must be an object")
|
|
score, evidence = item.get("score"), item.get("evidence")
|
|
if isinstance(score, bool) or not isinstance(score, int) or not 1 <= score <= 5:
|
|
raise ValueError(f"{alias}.{dimension}.score must be an integer from 1 to 5")
|
|
if not isinstance(evidence, str) or not evidence.strip():
|
|
raise ValueError(f"{alias}.{dimension}.evidence must be non-empty")
|
|
normalized["variants"][alias][dimension] = {
|
|
"score": score, "evidence": evidence.strip(),
|
|
}
|
|
preferred = payload.get("preferred")
|
|
if preferred not in ("X", "Y", "tie"):
|
|
raise ValueError("judge preferred must be X, Y, or tie")
|
|
reason = payload.get("preference_evidence")
|
|
if not isinstance(reason, str) or not reason.strip():
|
|
raise ValueError("judge preference_evidence must be non-empty")
|
|
normalized.update(preferred=preferred, preference_evidence=reason.strip())
|
|
if repairs:
|
|
normalized["schema_repairs"] = repairs
|
|
return normalized
|
|
|
|
|
|
def _parse_json(text: str) -> dict[str, Any]:
|
|
value = (text or "").strip()
|
|
if value.startswith("```"):
|
|
lines = value.splitlines()[1:]
|
|
if lines and lines[-1].strip() == "```":
|
|
lines.pop()
|
|
value = "\n".join(lines)
|
|
payload = json.loads(value)
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("judge returned non-object JSON")
|
|
return payload
|
|
|
|
|
|
def make_judge() -> tuple[OpenAI, str, str]:
|
|
if os.getenv("ARK_API_KEY"):
|
|
return (
|
|
OpenAI(api_key=os.environ["ARK_API_KEY"], base_url="https://ark.cn-beijing.volces.com/api/v3"),
|
|
os.getenv("ARK_MODEL", "doubao-seed-1-6-250615"),
|
|
"Volcengine ARK",
|
|
)
|
|
if os.getenv("MISTRAL_API_KEY"):
|
|
return (
|
|
OpenAI(api_key=os.environ["MISTRAL_API_KEY"], base_url="https://api.mistral.ai/v1"),
|
|
"mistral-medium-latest",
|
|
"Mistral API",
|
|
)
|
|
raise RuntimeError("Official translation quality judging requires ARK_API_KEY or MISTRAL_API_KEY")
|
|
|
|
|
|
def judge_chapter(
|
|
client: OpenAI,
|
|
model: str,
|
|
source: str,
|
|
x_translation: str,
|
|
y_translation: str,
|
|
disable_thinking: bool = False,
|
|
receipt_path: Path | None = None,
|
|
max_attempts: int = 4,
|
|
) -> tuple[dict[str, Any], dict[str, int]]:
|
|
prompt = (
|
|
"You are an exacting bilingual technical-book translation evaluator. Compare two anonymous "
|
|
"Chinese translations against the complete English Markdown source. Score both X and Y from "
|
|
"1 to 5 on exactly: accuracy (no omissions, inventions, or changed claims); fluency; "
|
|
"terminology (consistent and technically correct); markdown_code_fidelity (figures, links, "
|
|
"headings, equations, and fenced code preserved). Each score needs concrete quoted or located "
|
|
"evidence. Prefer one only when evidence supports it. Return JSON only: "
|
|
'{"variants":{"X":{"accuracy":{"score":1,"evidence":"..."},"fluency":'
|
|
'{"score":1,"evidence":"..."},"terminology":{"score":1,"evidence":"..."},'
|
|
'"markdown_code_fidelity":{"score":1,"evidence":"..."}},"Y":{"accuracy":'
|
|
'{"score":1,"evidence":"..."},"fluency":{"score":1,"evidence":"..."},'
|
|
'"terminology":{"score":1,"evidence":"..."},"markdown_code_fidelity":'
|
|
'{"score":1,"evidence":"..."}}},"preferred":"X|Y|tie",'
|
|
'"preference_evidence":"..."}.\n\n'
|
|
f"COMPLETE ENGLISH SOURCE:\n{source}\n\nANONYMOUS CHINESE X:\n{x_translation}"
|
|
f"\n\nANONYMOUS CHINESE Y:\n{y_translation}"
|
|
)
|
|
kwargs: dict[str, Any] = {
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"temperature": 0,
|
|
"response_format": {"type": "json_object"},
|
|
}
|
|
if disable_thinking:
|
|
kwargs["extra_body"] = {"thinking": {"type": "disabled"}}
|
|
def repair_prompt(content: str, error: Exception) -> str:
|
|
return (
|
|
"This is a formatting repair, not a new evaluation. Reshape the JSON below into the "
|
|
"exact requested schema while preserving every substantive score, evidence statement, "
|
|
"preference, and preference explanation. The top level must contain variants, preferred, "
|
|
"and preference_evidence. variants must contain exactly X and Y. Each of X and Y must "
|
|
"contain exactly accuracy, fluency, terminology, and markdown_code_fidelity, and every "
|
|
"dimension must contain score and evidence. Do not re-evaluate, rename fields, nest Y "
|
|
f"inside X, or add keys. Previous validation error: {error}. Return JSON only.\n\n"
|
|
f"JSON TO REPAIR:\n{content}"
|
|
)
|
|
|
|
attempts: list[dict[str, Any]] = []
|
|
repair_message: str | None = None
|
|
if receipt_path is not None and receipt_path.exists():
|
|
saved = json.loads(receipt_path.read_text(encoding="utf-8"))
|
|
attempts = saved.get("attempts", [])
|
|
if attempts:
|
|
previous = attempts[-1]
|
|
previous_content = previous.get("response", {}).get("content", "")
|
|
try:
|
|
recovered = validate_judge_response(_parse_json(previous_content))
|
|
except (json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
repair_message = repair_prompt(previous_content, exc)
|
|
else:
|
|
previous["resume_validation"] = {
|
|
"valid": True,
|
|
"schema_repairs": recovered.get("schema_repairs", []),
|
|
}
|
|
write_json_atomic(receipt_path, {
|
|
"schema_version": 1,
|
|
"credential_free": True,
|
|
"attempts": attempts,
|
|
})
|
|
return recovered, {
|
|
"prompt_tokens": sum(
|
|
row["response"]["usage"]["prompt_tokens"] for row in attempts
|
|
),
|
|
"completion_tokens": sum(
|
|
row["response"]["usage"]["completion_tokens"] for row in attempts
|
|
),
|
|
"latency_milliseconds": sum(
|
|
row["latency_milliseconds"] for row in attempts
|
|
),
|
|
"attempt_count": len(attempts),
|
|
}
|
|
prior_attempt_count = len(attempts)
|
|
for retry_number in range(1, max_attempts + 1):
|
|
attempt_number = prior_attempt_count + retry_number
|
|
request = dict(kwargs)
|
|
request["messages"] = (
|
|
[{"role": "user", "content": repair_message}]
|
|
if repair_message is not None else kwargs["messages"]
|
|
)
|
|
started = time.perf_counter()
|
|
try:
|
|
response = client.chat.completions.create(**request)
|
|
except Exception as exc:
|
|
if "temperature" not in str(exc).lower() or "temperature" not in kwargs:
|
|
raise
|
|
kwargs.pop("temperature")
|
|
request.pop("temperature", None)
|
|
response = client.chat.completions.create(**request)
|
|
latency = time.perf_counter() - started
|
|
content = response.choices[0].message.content or ""
|
|
usage = response.usage
|
|
attempt = {
|
|
"attempt": attempt_number,
|
|
"request_kind": "schema_repair" if repair_message is not None else "quality_judgment",
|
|
"request": request,
|
|
"response": {
|
|
"id": getattr(response, "id", None),
|
|
"model": getattr(response, "model", None),
|
|
"created": getattr(response, "created", None),
|
|
"content": content,
|
|
"usage": {
|
|
"prompt_tokens": usage.prompt_tokens,
|
|
"completion_tokens": usage.completion_tokens,
|
|
"total_tokens": getattr(
|
|
usage, "total_tokens", usage.prompt_tokens + usage.completion_tokens
|
|
),
|
|
},
|
|
},
|
|
"latency_milliseconds": round(latency * 1000),
|
|
}
|
|
try:
|
|
result = validate_judge_response(_parse_json(content))
|
|
except (json.JSONDecodeError, TypeError, ValueError) as exc:
|
|
attempt["validation"] = {
|
|
"valid": False,
|
|
"error_type": type(exc).__name__,
|
|
"error": str(exc),
|
|
}
|
|
attempts.append(attempt)
|
|
if receipt_path is not None:
|
|
write_json_atomic(receipt_path, {
|
|
"schema_version": 1,
|
|
"credential_free": True,
|
|
"attempts": attempts,
|
|
})
|
|
if retry_number == max_attempts:
|
|
raise RuntimeError(
|
|
f"judge response failed schema validation after {attempt_number} total attempts: {exc}"
|
|
) from exc
|
|
repair_message = repair_prompt(content, exc)
|
|
continue
|
|
attempt["validation"] = {"valid": True}
|
|
attempts.append(attempt)
|
|
if receipt_path is not None:
|
|
write_json_atomic(receipt_path, {
|
|
"schema_version": 1,
|
|
"credential_free": True,
|
|
"attempts": attempts,
|
|
})
|
|
return result, {
|
|
"prompt_tokens": sum(row["response"]["usage"]["prompt_tokens"] for row in attempts),
|
|
"completion_tokens": sum(
|
|
row["response"]["usage"]["completion_tokens"] for row in attempts
|
|
),
|
|
"latency_milliseconds": sum(row["latency_milliseconds"] for row in attempts),
|
|
"attempt_count": len(attempts),
|
|
}
|
|
raise AssertionError("unreachable judge retry loop")
|
|
|
|
|
|
def aggregate_judges(chapter_judges: list[dict[str, Any]]) -> dict[str, Any]:
|
|
scores = {
|
|
mode: {dimension: [] for dimension in DIMENSIONS}
|
|
for mode in ("orchestration", "single_agent")
|
|
}
|
|
preferences = {"orchestration": 0, "single_agent": 0, "tie": 0}
|
|
for row in chapter_judges:
|
|
mapping = row["alias_to_mode"]
|
|
result = row["result"]
|
|
for alias, dimensions in result["variants"].items():
|
|
mode = mapping[alias]
|
|
for dimension, item in dimensions.items():
|
|
scores[mode][dimension].append(item["score"])
|
|
preferred = result["preferred"]
|
|
preferences["tie" if preferred == "tie" else mapping[preferred]] += 1
|
|
modes = {}
|
|
for mode, dimensions in scores.items():
|
|
means = {key: sum(values) / len(values) for key, values in dimensions.items()}
|
|
modes[mode] = {"dimension_means": means, "overall_mean": sum(means.values()) / len(means)}
|
|
return {"modes": modes, "chapter_preferences": preferences}
|
|
|
|
|
|
def source_statistics(chapters: dict[str, str]) -> dict[str, Any]:
|
|
return {
|
|
"chapter_count": len(chapters),
|
|
"bytes": sum(len(text.encode("utf-8")) for text in chapters.values()),
|
|
"lines": sum(len(text.splitlines()) for text in chapters.values()),
|
|
"image_references": sum(len(image_targets(text)) for text in chapters.values()),
|
|
"fenced_code_blocks": sum(len(fenced_code_payloads(text)) for text in chapters.values()),
|
|
"link_references": sum(len(link_targets(text)) for text in chapters.values()),
|
|
}
|
|
|
|
|
|
def tracker_receipt(tracker) -> dict[str, Any]:
|
|
return {
|
|
"calls": tracker.calls,
|
|
"by_agent": tracker.by_agent(),
|
|
"total_tokens": tracker.total_tokens(),
|
|
}
|
|
|
|
|
|
def write_json_atomic(path: Path, value: Any) -> None:
|
|
"""Persist a restart checkpoint without exposing half-written JSON."""
|
|
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
temporary.replace(path)
|
|
|
|
|
|
def serialize_arm(result: dict[str, Any]) -> dict[str, Any]:
|
|
"""Convert an agents.py arm result into credential-free checkpoint JSON."""
|
|
return {
|
|
**{key: value for key, value in result.items() if key != "tracker"},
|
|
"tracker_calls": result["tracker"].calls,
|
|
}
|
|
|
|
|
|
def restore_arm(payload: dict[str, Any], agents_module: Any) -> dict[str, Any]:
|
|
value = dict(payload)
|
|
calls = value.pop("tracker_calls")
|
|
tracker = agents_module.TokenTracker()
|
|
tracker.calls = calls
|
|
value["tracker"] = tracker
|
|
return value
|
|
|
|
|
|
def campaign_fingerprint(
|
|
chapters: dict[str, str], translation_units: dict[str, str], provider: str, model: str
|
|
) -> str:
|
|
contract = {
|
|
"chapters": {title: sha256_text(text) for title, text in chapters.items()},
|
|
"translation_units": {
|
|
title: sha256_text(text) for title, text in translation_units.items()
|
|
},
|
|
"provider": provider,
|
|
"model": model,
|
|
"thinking": "disabled" if provider in ("ark", "Volcengine ARK") else "provider_default",
|
|
}
|
|
return sha256_text(json.dumps(contract, ensure_ascii=False, sort_keys=True))
|
|
|
|
|
|
def load_checkpoint(path: Path, fingerprint: str) -> Any | None:
|
|
if not path.exists():
|
|
return None
|
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
if payload.get("campaign_fingerprint") != fingerprint:
|
|
raise RuntimeError(f"checkpoint does not match this campaign: {path}")
|
|
return payload["value"]
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Official full-scope Experiment 10-2 campaign")
|
|
parser.add_argument("--source", action="append", help="Markdown chapter; repeat (default: book-en ch1/ch2)")
|
|
parser.add_argument("--provider", choices=("mistral", "ark", "openai", "openrouter"), default="mistral")
|
|
parser.add_argument("--model", help="translation model (default chosen for provider)")
|
|
parser.add_argument(
|
|
"--max-unit-characters", type=int, default=20_000,
|
|
help="Markdown-safe translation unit size (default: 20000)",
|
|
)
|
|
parser.add_argument("--output-dir", help="validation directory (default timestamped)")
|
|
args = parser.parse_args()
|
|
load_dotenv(HERE / ".env")
|
|
os.environ["LLM_PROVIDER"] = args.provider
|
|
if args.model:
|
|
os.environ["OPENAI_MODEL"] = args.model
|
|
|
|
# Import only after provider/model selection because agents reads its configuration at import time.
|
|
import agents
|
|
import consistency
|
|
|
|
paths = [Path(item).resolve() for item in args.source] if args.source else list(DEFAULT_SOURCES)
|
|
chapters, source_paths = load_source_book(paths)
|
|
translation_units, chapter_units = split_translation_units(
|
|
chapters, max_characters=args.max_unit_characters
|
|
)
|
|
stats = source_statistics(chapters)
|
|
stats["translation_unit_count"] = len(translation_units)
|
|
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
output = Path(args.output_dir).resolve() if args.output_dir else HERE / "validation" / f"real_{timestamp}"
|
|
output.mkdir(parents=True, exist_ok=True)
|
|
fingerprint = campaign_fingerprint(
|
|
chapters, translation_units, agents.ACTIVE_PROVIDER or args.provider, agents.MODEL
|
|
)
|
|
|
|
started = time.perf_counter()
|
|
orch_started = time.perf_counter()
|
|
orchestration_checkpoint = output / "orchestration_checkpoint.json"
|
|
saved_orchestration = load_checkpoint(orchestration_checkpoint, fingerprint)
|
|
if saved_orchestration is None:
|
|
orchestration = agents.run_orchestration(
|
|
translation_units, str(output / "orchestration_parts"),
|
|
source_lang="英文", target_lang="中文"
|
|
)
|
|
orchestration_elapsed = time.perf_counter() - orch_started
|
|
write_json_atomic(orchestration_checkpoint, {
|
|
"campaign_fingerprint": fingerprint,
|
|
"value": {
|
|
"result": serialize_arm(orchestration),
|
|
"elapsed_seconds": orchestration_elapsed,
|
|
},
|
|
})
|
|
else:
|
|
orchestration = restore_arm(saved_orchestration["result"], agents)
|
|
orchestration_elapsed = saved_orchestration["elapsed_seconds"]
|
|
single_started = time.perf_counter()
|
|
single_checkpoint = output / "single_agent_checkpoint.json"
|
|
saved_single = load_checkpoint(single_checkpoint, fingerprint)
|
|
if saved_single is None:
|
|
single = agents.run_single_agent(
|
|
translation_units, str(output / "single_agent_parts"),
|
|
source_lang="英文", target_lang="中文"
|
|
)
|
|
single_elapsed = time.perf_counter() - single_started
|
|
write_json_atomic(single_checkpoint, {
|
|
"campaign_fingerprint": fingerprint,
|
|
"value": {
|
|
"result": serialize_arm(single),
|
|
"elapsed_seconds": single_elapsed,
|
|
},
|
|
})
|
|
else:
|
|
single = restore_arm(saved_single["result"], agents)
|
|
single_elapsed = saved_single["elapsed_seconds"]
|
|
|
|
orchestration_complete = reassemble_translations(orchestration["translations"], chapter_units)
|
|
single_complete = reassemble_translations(single["translations"], chapter_units)
|
|
for mode, complete in (
|
|
("orchestration", orchestration_complete), ("single_agent", single_complete)
|
|
):
|
|
destination = output / mode
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
for index, (title, text) in enumerate(complete.items(), start=1):
|
|
(destination / f"chapter{index}_zh.md").write_text(text, encoding="utf-8")
|
|
|
|
fidelity = {"orchestration": {}, "single_agent": {}}
|
|
for title, source in chapters.items():
|
|
fidelity["orchestration"][title] = markdown_fidelity(source, orchestration_complete[title])
|
|
fidelity["single_agent"][title] = markdown_fidelity(source, single_complete[title])
|
|
|
|
judge_client, judge_model, judge_provider = make_judge()
|
|
judge_checkpoint = output / "judge_checkpoint.json"
|
|
judge_receipt_dir = output / "judge_receipts"
|
|
judge_receipt_dir.mkdir(parents=True, exist_ok=True)
|
|
judge_rows = load_checkpoint(judge_checkpoint, fingerprint) or []
|
|
expected_titles = list(translation_units)
|
|
if [row.get("chapter") for row in judge_rows] != expected_titles[:len(judge_rows)]:
|
|
raise RuntimeError("judge checkpoint order does not match translation units")
|
|
for index, (title, source) in enumerate(translation_units.items()):
|
|
if index < len(judge_rows):
|
|
continue
|
|
alias_to_mode = (
|
|
{"X": "orchestration", "Y": "single_agent"}
|
|
if index % 2 == 0 else {"X": "single_agent", "Y": "orchestration"}
|
|
)
|
|
translations = {
|
|
"orchestration": orchestration["translations"][title],
|
|
"single_agent": single["translations"][title],
|
|
}
|
|
result, usage = judge_chapter(
|
|
judge_client, judge_model, source,
|
|
translations[alias_to_mode["X"]], translations[alias_to_mode["Y"]],
|
|
disable_thinking=judge_provider == "Volcengine ARK",
|
|
receipt_path=judge_receipt_dir / f"unit-{index + 1:02d}.json",
|
|
)
|
|
receipt = judge_receipt_dir / f"unit-{index + 1:02d}.json"
|
|
judge_rows.append({
|
|
"chapter": title,
|
|
"alias_to_mode": alias_to_mode,
|
|
"result": result,
|
|
"usage": usage,
|
|
"receipt": str(receipt.relative_to(output)),
|
|
"receipt_sha256": sha256(receipt),
|
|
})
|
|
write_json_atomic(judge_checkpoint, {
|
|
"campaign_fingerprint": fingerprint,
|
|
"value": judge_rows,
|
|
})
|
|
|
|
orch_consistency = consistency.analyze(orchestration_complete)
|
|
single_consistency = consistency.analyze(single_complete)
|
|
orch_adherence = consistency.check_adherence(orchestration_complete)
|
|
single_adherence = consistency.check_adherence(single_complete)
|
|
all_agent_types = set(orchestration["tracker"].by_agent())
|
|
translation_calls = orchestration["tracker"].calls + single["tracker"].calls
|
|
translation_fingerprints = {
|
|
(call.get("provider"), call.get("model"), call.get("thinking"))
|
|
for call in translation_calls
|
|
}
|
|
translation_provider, translation_model, translation_thinking = (
|
|
next(iter(translation_fingerprints))
|
|
if len(translation_fingerprints) == 1 else (None, None, None)
|
|
)
|
|
|
|
current_source_paths = [HERE / "run_official_experiment.py", HERE / "agents.py", HERE / "consistency.py"]
|
|
checkpoint_paths = [orchestration_checkpoint, single_checkpoint, judge_checkpoint]
|
|
translation_output_paths = [
|
|
output / mode / f"chapter{index}_zh.md"
|
|
for mode in ("orchestration", "single_agent")
|
|
for index in range(1, len(chapters) + 1)
|
|
]
|
|
prior_failure = output / "prior_judge_failure.json"
|
|
|
|
def repo_hash_map(files: list[Path]) -> dict[str, str]:
|
|
return {
|
|
str(path.resolve().relative_to(REPO)): sha256(path)
|
|
for path in files if path.is_file()
|
|
}
|
|
|
|
provenance = {
|
|
"campaign_fingerprint": fingerprint,
|
|
"current_acceptance_sources_sha256": repo_hash_map(current_source_paths),
|
|
"arm_and_judge_checkpoints_sha256": repo_hash_map(checkpoint_paths),
|
|
"reassembled_translation_outputs_sha256": repo_hash_map(translation_output_paths),
|
|
"raw_judge_receipts_sha256": {
|
|
str((output / row["receipt"]).resolve().relative_to(REPO)): row["receipt_sha256"]
|
|
for row in judge_rows
|
|
},
|
|
"negative_provenance_sha256": repo_hash_map([prior_failure]),
|
|
"resume_note": (
|
|
"The long campaign resumed from fingerprint-bound arm and judge checkpoints. "
|
|
"Current acceptance-source hashes bind the final validator/evidence builder; immutable "
|
|
"raw judge receipts retain every schema failure and repair call."
|
|
),
|
|
}
|
|
declared_provenance_hashes = {
|
|
key: digest
|
|
for field in (
|
|
"current_acceptance_sources_sha256",
|
|
"arm_and_judge_checkpoints_sha256",
|
|
"reassembled_translation_outputs_sha256",
|
|
"raw_judge_receipts_sha256",
|
|
"negative_provenance_sha256",
|
|
)
|
|
for key, digest in provenance[field].items()
|
|
}
|
|
receipt_payloads = [
|
|
json.loads((output / row["receipt"]).read_text(encoding="utf-8"))
|
|
for row in judge_rows
|
|
]
|
|
judge_attempt_count = sum(len(item.get("attempts", [])) for item in receipt_payloads)
|
|
rejected_judge_attempt_count = sum(
|
|
not attempt.get("validation", {}).get("valid", False)
|
|
for item in receipt_payloads for attempt in item.get("attempts", [])
|
|
)
|
|
gates = {
|
|
"real_illustrated_code_heavy_technical_book": (
|
|
stats["chapter_count"] >= 2 and stats["bytes"] >= 200_000
|
|
and stats["image_references"] >= 10 and stats["fenced_code_blocks"] >= 5
|
|
),
|
|
"four_agent_roles_executed": {"Glossary", "Translation", "Proofreading", "Manager"}.issubset(all_agent_types),
|
|
"both_modes_translated_every_chapter": all(
|
|
orchestration_complete.get(title, "").strip()
|
|
and single_complete.get(title, "").strip()
|
|
for title in chapters
|
|
),
|
|
"real_usage_recorded_for_every_call": all(
|
|
call.get("prompt_tokens", 0) > 0 and call.get("provider") and call.get("model")
|
|
for call in translation_calls
|
|
),
|
|
"uniform_translation_api_fingerprint": (
|
|
len(translation_fingerprints) == 1
|
|
and all((translation_provider, translation_model, translation_thinking))
|
|
),
|
|
"manager_context_excludes_translation_bodies": all(
|
|
text not in json.dumps(orchestration["manager_context_final"], ensure_ascii=False)
|
|
for text in orchestration["translations"].values()
|
|
),
|
|
"quality_compared_for_every_translation_unit": len(judge_rows) == len(translation_units),
|
|
"raw_judge_receipts_hashed": len(judge_rows) == len(translation_units) and all(
|
|
(output / row.get("receipt", "missing")).is_file()
|
|
and sha256(output / row["receipt"]) == row.get("receipt_sha256")
|
|
for row in judge_rows
|
|
),
|
|
"raw_judge_response_ids_and_usage_recorded": all(
|
|
item.get("attempts") and all(
|
|
attempt.get("response", {}).get("id")
|
|
and attempt.get("response", {}).get("usage", {}).get("prompt_tokens", 0) > 0
|
|
and attempt.get("response", {}).get("usage", {}).get("completion_tokens", 0) > 0
|
|
for attempt in item["attempts"]
|
|
)
|
|
for item in receipt_payloads
|
|
),
|
|
"checkpoint_fingerprints_match": all(
|
|
json.loads(path.read_text(encoding="utf-8")).get("campaign_fingerprint") == fingerprint
|
|
for path in checkpoint_paths
|
|
),
|
|
"all_declared_provenance_hashes_match": all(
|
|
(REPO / relative).is_file() and sha256(REPO / relative) == digest
|
|
for relative, digest in declared_provenance_hashes.items()
|
|
),
|
|
"efficiency_and_resources_compared": (
|
|
orchestration_elapsed > 0 and single_elapsed > 0
|
|
and orchestration["tracker"].total_tokens() > 0 and single["tracker"].total_tokens() > 0
|
|
),
|
|
}
|
|
artifact = {
|
|
"schema_version": 1,
|
|
"experiment": "10-3",
|
|
"timestamp_utc": datetime.now(timezone.utc).isoformat(),
|
|
"source_book": {
|
|
"identity": "AI Agents in Depth, English edition, Chapters 1-2",
|
|
"paths": source_paths,
|
|
"sha256": {title: sha256(paths[index]) for index, title in enumerate(chapters)},
|
|
"statistics": stats,
|
|
"max_translation_unit_characters_requested": args.max_unit_characters,
|
|
"translation_unit_sha256": {
|
|
title: sha256_text(text) for title, text in translation_units.items()
|
|
},
|
|
"chapter_translation_units": chapter_units,
|
|
},
|
|
"translation_api": {
|
|
"provider": translation_provider,
|
|
"model": translation_model,
|
|
"thinking": translation_thinking,
|
|
},
|
|
"quality_judge_api": {
|
|
"provider": judge_provider,
|
|
"model": judge_model,
|
|
"thinking": "disabled" if judge_provider == "Volcengine ARK" else "provider_default",
|
|
"raw_receipted_calls": judge_attempt_count,
|
|
"known_pre_receipt_failures": 1 if prior_failure.is_file() else 0,
|
|
"known_total_calls": judge_attempt_count + (1 if prior_failure.is_file() else 0),
|
|
"rejected_receipted_schema_attempts": rejected_judge_attempt_count,
|
|
"lossless_local_schema_normalizations": sum(
|
|
bool(row["result"].get("schema_repairs")) for row in judge_rows
|
|
),
|
|
"schema_formatting_repair_api_calls": sum(
|
|
attempt.get("request_kind") == "schema_repair"
|
|
for item in receipt_payloads for attempt in item.get("attempts", [])
|
|
),
|
|
"prompt_tokens": sum(row["usage"]["prompt_tokens"] for row in judge_rows),
|
|
"completion_tokens": sum(row["usage"]["completion_tokens"] for row in judge_rows),
|
|
"latency_milliseconds": sum(
|
|
row["usage"]["latency_milliseconds"] for row in judge_rows
|
|
),
|
|
},
|
|
"modes": {
|
|
"orchestration": {
|
|
"elapsed_seconds": orchestration_elapsed,
|
|
"manager_context_peak": orchestration["manager_context_peak"],
|
|
"tracker": tracker_receipt(orchestration["tracker"]),
|
|
"terminology_consistency": orch_consistency,
|
|
"mandated_terminology_adherence": orch_adherence,
|
|
},
|
|
"single_agent": {
|
|
"elapsed_seconds": single_elapsed,
|
|
"main_context_peak": single["main_context_peak"],
|
|
"tracker": tracker_receipt(single["tracker"]),
|
|
"terminology_consistency": single_consistency,
|
|
"mandated_terminology_adherence": single_adherence,
|
|
},
|
|
},
|
|
"markdown_fidelity": fidelity,
|
|
"blinded_quality_judges": judge_rows,
|
|
"quality_aggregate": aggregate_judges(judge_rows),
|
|
"comparison": {
|
|
"context_peak": {
|
|
"orchestration_manager": orchestration["manager_context_peak"],
|
|
"single_agent": single["main_context_peak"],
|
|
},
|
|
"wall_clock_seconds": {
|
|
"orchestration": orchestration_elapsed,
|
|
"single_agent": single_elapsed,
|
|
},
|
|
"total_tokens": {
|
|
"orchestration": orchestration["tracker"].total_tokens(),
|
|
"single_agent": single["tracker"].total_tokens(),
|
|
},
|
|
},
|
|
"provenance": provenance,
|
|
"acceptance_gates": gates,
|
|
"experiment_execution_complete": all(gates.values()),
|
|
"total_campaign_active_seconds": (
|
|
orchestration_elapsed + single_elapsed
|
|
+ sum(row["usage"]["latency_milliseconds"] for row in judge_rows) / 1000
|
|
),
|
|
"finalization_session_seconds": time.perf_counter() - started,
|
|
"interpretation_rule": (
|
|
"Completion means the full comparison ran with real APIs and all required metrics; "
|
|
"it does not require the Manager workflow to win every metric."
|
|
),
|
|
}
|
|
evidence = output / "evidence.json"
|
|
evidence.write_text(json.dumps(artifact, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
latest = HERE / "validation" / "latest.json"
|
|
latest.write_text(json.dumps({
|
|
"experiment": "10-3",
|
|
"status": "complete" if artifact["experiment_execution_complete"] else "incomplete",
|
|
"evidence": str(evidence.relative_to(HERE)),
|
|
"evidence_sha256": sha256(evidence),
|
|
"acceptance_gates": gates,
|
|
"comparison": artifact["comparison"],
|
|
"quality_aggregate": artifact["quality_aggregate"],
|
|
}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps({
|
|
"evidence": str(evidence),
|
|
"complete": artifact["experiment_execution_complete"],
|
|
"source_statistics": stats,
|
|
"comparison": artifact["comparison"],
|
|
"quality": artifact["quality_aggregate"],
|
|
}, ensure_ascii=False, indent=2))
|
|
return 0 if artifact["experiment_execution_complete"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|