Files
ai-contract-platform/yuqei-infra/scripts/multiscene_eval_plan.py
T

199 lines
9.7 KiB
Python

"""Build and validate the immutable W4 multiscene evaluation plan."""
from __future__ import annotations
from datetime import UTC, datetime
from hashlib import sha256
import json
from pathlib import Path
from typing import Any
from yuqei_contract_api.contract_templates import template_document_kind
from yuqei_contract_api.drafting_workspace import (
DraftingCandidateRequest, DraftingCreateRequest, DraftingEvaluationCreateRequest,
DraftingWorkspaceStore,
)
from yuqei_contract_api.in_memory_repository import InMemoryContractRepository
from yuqei_sdk.contract_drafting import ContractDraftRequest, draft_digest
from multiscene_eval_cases import build_cases
from multiscene_supplemental_cases import build_supplemental_cases
ROOT = Path(__file__).resolve().parents[2]
SCHEMA_VERSION = "multiscene-evaluation-plan.v1"
EXPECTED_OPERATIONS = {
"draft": 120, "semantic_review": 30, "repair_draft": 8,
"repair_review": 8, "specialist_review": 106, "intake": 8,
}
EXPECTED_BUDGET = 280
PIN_ROOTS = (
"yuqei-ai-sdk-python/src/yuqei_sdk",
"yuqei-ai-platform/services/ai-platform-api/src/yuqei_ai_platform_api",
"yuqei-contract-platform/services/contract-api/src/yuqei_contract_api",
)
PIN_FILES = (
"yuqei-infra/scripts/multiscene_eval_cases.py",
"yuqei-infra/scripts/multiscene_supplemental_cases.py",
"yuqei-infra/scripts/multiscene_call_ledger.py",
"yuqei-infra/scripts/multiscene_eval_plan.py",
"yuqei-infra/scripts/multiscene_eval_runner.py",
"yuqei-infra/scripts/run-core-drafting-smoke.py",
"yuqei-contract-platform/services/contract-api/tests/test_semantic_review_cross_service_flow.py",
"yuqei-ai-sdk-python/pyproject.toml",
"yuqei-ai-platform/services/ai-platform-api/pyproject.toml",
"yuqei-contract-platform/services/contract-api/pyproject.toml",
)
PIN_SUFFIXES = {".py", ".json", ".md", ".toml", ".yaml", ".yml"}
def _hash_bytes(value: bytes) -> str:
return sha256(value).hexdigest()
def _canonical(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(",", ":"), allow_nan=False)
def draft_request_projection(request: ContractDraftRequest | dict[str, Any]) -> dict[str, Any]:
"""Return stable model facts and template identities; omit workspace/lifecycle data."""
request = ContractDraftRequest.model_validate(request)
data = request.model_dump(mode="json")
snapshot = data["template_snapshot"]
return {
"document_kind": data["document_kind"], "title": data["contract_title"],
"contract_type": data["contract_type"], "industry": data["industry"],
"party_position": data["party_position"], "jurisdiction": data["jurisdiction"],
"transaction_stage": data["transaction_stage"], "facts": data["facts"],
"missing_facts": data["missing_facts"], "base_document": data["base_document"],
"instructions": data["drafting_instructions"],
"template_content_sha256": snapshot.get("content_sha256"),
"template_schema_sha256": snapshot.get("schema_sha256"),
}
def _source_pins(runner) -> dict[str, str]:
paths: set[Path] = set()
for relative in PIN_ROOTS:
root = ROOT / relative
paths.update(path for path in root.rglob("*") if path.is_file()
and path.suffix.lower() in PIN_SUFFIXES
and "__pycache__" not in path.parts)
paths.update(ROOT / relative for relative in PIN_FILES)
runner_file = Path(getattr(runner, "__file__", ""))
if runner_file.is_file():
paths.add(runner_file.resolve())
cross = getattr(runner, "CROSS", None)
cross_file = Path(getattr(cross, "__file__", ""))
if cross_file.is_file():
paths.add(cross_file.resolve())
missing = [str(path.relative_to(ROOT)) for path in paths if not path.is_file()]
if missing:
raise ValueError(f"source pin file missing: {missing}")
return {path.relative_to(ROOT).as_posix(): _hash_bytes(path.read_bytes())
for path in sorted(paths)}
def _prepared_projection(case: dict[str, Any], repository: InMemoryContractRepository) -> tuple[dict[str, Any], Any]:
# Base cases need the real contract policy (including auto repair); scene
# release-gate cases must remain in the isolated evaluation entry point.
evaluation = case["mode"] == "scene_evaluation"
store = DraftingWorkspaceStore(repository, mode="scene_evaluation" if evaluation else "contract")
template, _ = (store.evaluation_source(case["template_id"]) if evaluation
else repository.get_template_source(case["template_id"]))
request_type = DraftingEvaluationCreateRequest if evaluation else DraftingCreateRequest
context = case.get("scene_request_context", {})
created = request_type(
template_id=case["template_id"], expected_template_content_sha256=template.content_sha256,
title=case["title"], values=case["values"], industry=context.get("industry", "未指定"),
party_position=context.get("party_position", "中立"),
jurisdiction=context.get("jurisdiction", "中国大陆"),
transaction_stage=context.get("transaction_stage", "drafting"),
instructions=case["instructions"], **({"synthetic_only": True} if evaluation else {}),
)
record = store.create(created, tenant="w4-isolated", owner="w4-builder",
key=f"create-{case['case_id']}")
policy = {"track_changes": True, "semantic_review": not evaluation,
"auto_repair": case["auto_repair"]}
_, candidate = store.prepare(record.id, DraftingCandidateRequest(
expected_revision=record.revision, idempotency_key=f"candidate-{draft_digest(case['case_id'])}",
**policy), tenant="w4-isolated", owner="w4-builder")
projection = draft_request_projection(candidate.model_request)
if projection["document_kind"] != case["document_kind"]:
raise ValueError(f"document kind drift: {case['case_id']}")
actual_missing = [item["field"] for item in projection["missing_facts"]]
expected_missing = case["expectation_profile"]["expected_missing_fields"]
actual_status = "incomplete" if actual_missing else "candidate_ready"
if actual_missing != expected_missing or actual_status != case["expectation_profile"]["expected_status"]:
raise ValueError(f"prepared missing/status drift: {case['case_id']}")
return projection, template
def _normalized_cases() -> list[dict[str, Any]]:
repository = InMemoryContractRepository(contracts=[])
repository.allow_internal_trial_templates = True
drafting = []
for case in build_cases():
projection, template = _prepared_projection(case, repository)
policy = {"track_changes": True, "semantic_review": case["mode"] == "contract",
"auto_repair": case["auto_repair"]}
drafting.append({**case, "candidate_policy": policy,
"template_content_sha256": projection["template_content_sha256"],
"template_input_schema_sha256": projection["template_schema_sha256"],
"values_sha256": draft_digest(case["values"]),
"draft_request_projection": projection})
supplemental = []
for case in build_supplemental_cases():
supplemental.append({
"case_id": case["case_id"], "source_case_id": case["case_id"],
"kind": case["operation"], "mode": case["operation"], "operation": case["operation"],
"operations": [case["operation"]], "provider_call_budget": 1,
"request": case["request"].model_dump(mode="json"),
"request_sha256": case["request_sha256"], "expectations": case["expectations"],
**({"module": case["module"], "party_position": case["party_position"]}
if case["operation"] == "specialist_review" else {}),
})
return [*drafting, *supplemental]
def _operation_counts(cases: list[dict[str, Any]]) -> dict[str, int]:
result: dict[str, int] = {}
for case in cases:
for operation in case["operations"]:
result[operation] = result.get(operation, 0) + 1
return result
def build_plan(runner) -> dict[str, Any]:
cases = _normalized_cases()
counts = _operation_counts(cases)
if len(cases) != 144 or counts != EXPECTED_OPERATIONS:
raise ValueError(f"case or operation inventory mismatch: {len(cases)}, {counts}")
if sum(case["provider_call_budget"] for case in cases) != EXPECTED_BUDGET:
raise ValueError("global provider call budget mismatch")
return {
"schema_version": SCHEMA_VERSION, "prepared_at": datetime.now(UTC).isoformat(),
"model": "gpt-5.6-sol", "reasoning_effort": "medium", "timeout_seconds": 300,
"case_count": 144, "drafting_case_count": 120, "supplemental_case_count": 24,
"operation_limits": counts, "total_provider_call_budget": EXPECTED_BUDGET,
"source_pins": _source_pins(runner), "cases": cases,
"limitations": ["合成隔离评测,不是生产发布或法律批准。",
"deterministic projection equality does not establish semantic or legal correctness."],
}
def validate_plan(runner, path: Path, expected_sha: str | None = None) -> tuple[dict[str, Any], str]:
raw = Path(path).read_bytes()
actual = _hash_bytes(raw)
if expected_sha is not None and actual != expected_sha:
raise ValueError("plan raw SHA-256 mismatch")
plan = json.loads(raw)
expected = build_plan(runner)
if plan.get("schema_version") != SCHEMA_VERSION:
raise ValueError("unsupported multiscene plan")
left = {key: value for key, value in plan.items() if key != "prepared_at"}
right = {key: value for key, value in expected.items() if key != "prepared_at"}
if _canonical(left) != _canonical(right):
raise ValueError("multiscene plan content or source pins drift")
return plan, actual