Image-Text-to-Text
Transformers
English
Chinese
eviscene
multimodal
image-factuality
agent
sft
distillation
Instructions to use ZiAnwang/EviScene with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ZiAnwang/EviScene with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="ZiAnwang/EviScene")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("ZiAnwang/EviScene", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use ZiAnwang/EviScene with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "ZiAnwang/EviScene" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/ZiAnwang/EviScene
- SGLang
How to use ZiAnwang/EviScene with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "ZiAnwang/EviScene" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "ZiAnwang/EviScene" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use ZiAnwang/EviScene with Docker Model Runner:
docker model run hf.co/ZiAnwang/EviScene
Download eviscene/eval/run_eval.py from ZiAnwang/EviScene: direct link, hf CLI and curl.
- Browser
- Download file 44.1 kB
-
https://huggingface.co/ZiAnwang/EviScene/resolve/main/eviscene/eval/run_eval.py
- Command line
-
hf download hf://ZiAnwang/EviScene/eviscene/eval/run_eval.py
-
curl -L -o run_eval.py https://huggingface.co/ZiAnwang/EviScene/resolve/main/eviscene/eval/run_eval.py
44.1 kB
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import hashlib | |
| import json | |
| import os | |
| import statistics | |
| import subprocess | |
| from dataclasses import asdict | |
| from collections import Counter | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Any, Dict, Iterable, List | |
| from eviscene.workflow import ( | |
| AGENT_DECISION_POLICY_VERSION, | |
| VerificationWorkflow, | |
| WorkflowConfig, | |
| ) | |
| from eviscene.provider_profiles import PROFILE_IDS | |
| from eviscene.storage import default_eval_root | |
| from eviscene.eval.release_adapter import ( | |
| image_only_case_from_runtime_row, | |
| load_runtime_release, | |
| resolve_runtime_image_path, | |
| ) | |
| from eviscene.eval.gemini_run_guard import GeminiRunGuard | |
| from eviscene.eval.result_records import classification_prediction | |
| from eviscene.eval.run_artifacts import private_index | |
| from eviscene.eval.scoring_release_adapter import ( | |
| SCORING_PACKAGE_SCHEMA_VERSION, | |
| ScoringRuntimeRelease, | |
| adapt_scoring_gold_for_process, | |
| load_scoring_release, | |
| resolve_scoring_image_path, | |
| ) | |
| from eviscene.orchestrator.runtime_case import verify_case_image | |
| from eviscene.orchestrator.source_access import SourceAccessPolicy | |
| from eviscene.redaction import sanitize_for_persistence | |
| from eviscene.trajectory.exporter import ( | |
| export_trajectory_sft_example, | |
| trajectory_policy_step_ids, | |
| ) | |
| from eviscene.trajectory.perception_exporter import export_perception_example | |
| from eviscene.trajectory.reference_chain import score_reference_chain_trace | |
| from eviscene.trajectory.scoring import score_process_trace | |
| from scripts.audit_real_trace import audit_trace | |
| RUN_SCHEMA_VERSION = "ifv-eval-run-v2" | |
| ROLLOUT_MEMBER_SCHEMA_VERSION = "ifv-rollout-group-member-v1" | |
| ACTIVE_POLICY_STAGES = ( | |
| "UNIFIED_REACT", | |
| "UNIFIED_JUDGMENT", | |
| ) | |
| def _parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description="Run EviScene benchmark evaluation." | |
| ) | |
| parser.add_argument( | |
| "--benchmark", | |
| required=True, | |
| help=( | |
| "Path to runtime_input/cases.jsonl in a v0.3 benchmark or " | |
| "reviewed ifv-scoring-gold-v1 package." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--profile", | |
| choices=PROFILE_IDS, | |
| default=None, | |
| help="Explicit teacher/student provider profile.", | |
| ) | |
| parser.add_argument( | |
| "--provider", | |
| default=None, | |
| help="LLM provider for the agent.", | |
| ) | |
| parser.add_argument( | |
| "--model", | |
| default=None, | |
| help="Model name for the agent.", | |
| ) | |
| parser.add_argument( | |
| "--vlm-provider", | |
| default=None, | |
| help="Optional VLM provider override.", | |
| ) | |
| parser.add_argument( | |
| "--vlm-model", | |
| default=None, | |
| help="Optional VLM model override.", | |
| ) | |
| parser.add_argument( | |
| "--image-access-mode", | |
| choices=["direct_multimodal", "separate_vlm"], | |
| default="direct_multimodal", | |
| help=( | |
| "Whether the main LLM receives the image directly or only receives " | |
| "structured VLM observations." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--agent-decision-policy-version", | |
| choices=["unified-react-v1"], | |
| default=AGENT_DECISION_POLICY_VERSION, | |
| help=( | |
| "Agent orchestration policy. The current runtime uses unified-react-v1." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--llm-wire-api", | |
| default=None, | |
| choices=["interactions", "responses", "chat_completions"], | |
| help="Optional wire API override. Gemini accepts interactions only.", | |
| ) | |
| parser.add_argument( | |
| "--vlm-wire-api", | |
| default=None, | |
| choices=["interactions", "responses", "chat_completions"], | |
| help="Optional VLM wire API override. Gemini production runs require interactions.", | |
| ) | |
| parser.add_argument( | |
| "--output-dir", | |
| default=None, | |
| help="Directory for traces, predictions, and summary files.", | |
| ) | |
| parser.add_argument( | |
| "--concurrency", | |
| type=int, | |
| default=1, | |
| help="Concurrent complete rollout episodes.", | |
| ) | |
| parser.add_argument( | |
| "--rollouts-per-case", | |
| type=int, | |
| default=1, | |
| help="Independent complete episodes per case (recommended for RL: 4).", | |
| ) | |
| parser.add_argument( | |
| "--base-sampling-seed", | |
| type=int, | |
| default=1729, | |
| help="Base seed for reproducible per-episode Qwen sampling seeds.", | |
| ) | |
| parser.add_argument( | |
| "--timeout", | |
| type=float, | |
| default=1800.0, | |
| help="Per-image timeout budget in seconds.", | |
| ) | |
| parser.add_argument( | |
| "--limit", | |
| type=int, | |
| default=None, | |
| help="Optional sample limit for debugging.", | |
| ) | |
| parser.add_argument( | |
| "--case-id", | |
| action="append", | |
| default=None, | |
| help=( | |
| "Run only this case_id. Repeat to select an explicit ordered canary " | |
| "set without exposing labels to the Agent." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--shard-count", | |
| type=int, | |
| default=1, | |
| help="Deterministically split sorted case IDs into this many shards.", | |
| ) | |
| parser.add_argument( | |
| "--shard-index", | |
| type=int, | |
| default=0, | |
| help="Zero-based shard index selected after explicit case filtering.", | |
| ) | |
| parser.add_argument( | |
| "--training-prohibited", | |
| action="store_true", | |
| help=( | |
| "Mark every derived trajectory as ineligible for training; required " | |
| "for frozen external evaluation releases." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--source-access-policy", | |
| default=None, | |
| help=( | |
| "Evaluation-only JSON policy for blocking benchmark-origin sources. " | |
| "The policy is never added to model context or traces." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--disable-web-search", | |
| action="store_true", | |
| help="Ablation: remove text_search from the formal ReAct runtime.", | |
| ) | |
| parser.add_argument( | |
| "--disable-image-retrieval", | |
| action="store_true", | |
| help=( | |
| "Ablation: remove text_image_search and reverse_image_search." | |
| ), | |
| ) | |
| parser.add_argument( | |
| "--disable-evidence-inspection", | |
| action="store_true", | |
| help=( | |
| "Ablation: remove visit and compare_with_reference." | |
| ), | |
| ) | |
| return parser.parse_args() | |
| def _load_benchmark(path: Path) -> List[Dict[str, Any]]: | |
| if path.suffix.lower() == ".jsonl": | |
| rows = [] | |
| for line in path.read_text(encoding="utf-8").splitlines(): | |
| if line.strip(): | |
| rows.append(json.loads(line)) | |
| return rows | |
| payload = json.loads(path.read_text(encoding="utf-8")) | |
| if isinstance(payload, list): | |
| return payload | |
| raise TypeError("Benchmark must be a JSON list or JSONL file.") | |
| def _safe_mean(values: List[float]) -> float: | |
| if not values: | |
| return 0.0 | |
| return float(statistics.mean(values)) | |
| def _positive_int(value: Any, *, name: str) -> int: | |
| try: | |
| parsed = int(value) | |
| except (TypeError, ValueError) as exc: | |
| raise ValueError(f"{name} must be an integer") from exc | |
| if parsed < 1: | |
| raise ValueError(f"{name} must be at least 1") | |
| return parsed | |
| def _qwen_stage_thinking_config( | |
| *, | |
| provider: str, | |
| model_name: str, | |
| ) -> Dict[str, str]: | |
| """Record the actual Qwen thinking switch used by the runtime. | |
| Gemini stage settings are irrelevant for local Qwen serving. For the | |
| active unified stages, Qwen3.5 and Qwen3-VL both default to visible | |
| thinking in ``Orchestrator._stage_generation_config``; an explicit | |
| environment override remains authoritative. | |
| """ | |
| active_provider = str(provider).strip().lower() | |
| local_qwen = active_provider in {"qwen_local", "lmdeploy"} | |
| result: Dict[str, str] = {} | |
| for stage in ACTIVE_POLICY_STAGES: | |
| raw = os.getenv( | |
| f"QWEN_{stage}_ENABLE_THINKING", | |
| "true" if local_qwen else "false", | |
| ).strip().lower() | |
| if raw not in {"true", "false"}: | |
| raise ValueError( | |
| f"QWEN_{stage}_ENABLE_THINKING must be true or false." | |
| ) | |
| result[stage.lower()] = raw | |
| return result | |
| def _now_iso() -> str: | |
| return datetime.now().astimezone().isoformat(timespec="seconds") | |
| def _sha256(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as handle: | |
| for chunk in iter(lambda: handle.read(1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def _git_commit() -> str: | |
| configured = os.getenv("GIT_COMMIT", "").strip() | |
| try: | |
| completed = subprocess.run( | |
| ["git", "rev-parse", "HEAD"], | |
| cwd=Path(__file__).resolve().parents[2], | |
| check=True, | |
| capture_output=True, | |
| text=True, | |
| timeout=5, | |
| ) | |
| except (OSError, subprocess.SubprocessError): | |
| return configured | |
| actual = completed.stdout.strip() | |
| if configured and actual and configured != actual: | |
| raise RuntimeError( | |
| "GIT_COMMIT does not match the runtime checkout: " | |
| f"configured={configured}, actual={actual}" | |
| ) | |
| return actual or configured | |
| def _write_json(path: Path, payload: Dict[str, Any]) -> None: | |
| serialized = json.dumps( | |
| sanitize_for_persistence(payload), | |
| ensure_ascii=False, | |
| indent=2, | |
| ) | |
| _write_text(path, serialized + "\n") | |
| def _write_jsonl(path: Path, rows: Iterable[Dict[str, Any]]) -> None: | |
| lines = [ | |
| json.dumps( | |
| sanitize_for_persistence(row), | |
| ensure_ascii=False, | |
| separators=(",", ":"), | |
| ) | |
| for row in rows | |
| ] | |
| _write_text(path, "\n".join(lines) + ("\n" if lines else "")) | |
| def _write_text(path: Path, text: str) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| pending = path.with_name(f".{path.name}.tmp") | |
| pending.write_text(text, encoding="utf-8") | |
| pending.replace(path) | |
| def _run_result_record( | |
| sample: Dict[str, Any], | |
| result: Dict[str, Any], | |
| ) -> Dict[str, Any]: | |
| state = result.get("state") or {} | |
| judgment = result.get("judgment") | |
| verdict_basis = result.get("verdict_basis") | |
| if verdict_basis is None and isinstance(judgment, dict): | |
| verdict_basis = judgment.get("verdict_basis") | |
| return { | |
| "case_id": sample.get("case_id"), | |
| "verdict": result.get("verdict"), | |
| "confidence": result.get("confidence"), | |
| "verdict_basis": verdict_basis, | |
| "termination": result.get("termination"), | |
| "time_taken": result.get("time_taken"), | |
| "total_tool_calls": result.get("total_tool_calls"), | |
| "llm_api_calls": result.get("llm_api_calls"), | |
| "token_usage": result.get("token_usage"), | |
| "error": result.get("error"), | |
| "stage_timings": state.get("stage_timings", {}), | |
| "trace_path": None, | |
| } | |
| def _classification_prediction( | |
| sample: Dict[str, Any], | |
| result: Dict[str, Any], | |
| ) -> Dict[str, Any] | None: | |
| """Return a scorer row only for a completed factual judgment.""" | |
| return classification_prediction(sample, result) | |
| def _compute_summary(run_results: List[Dict[str, Any]]) -> Dict[str, Any]: | |
| total = len(run_results) | |
| verdict_counter: Counter[str] = Counter() | |
| times: List[float] = [] | |
| tool_calls: List[float] = [] | |
| llm_calls: List[float] = [] | |
| errors = 0 | |
| for row in run_results: | |
| pred = str(row.get("verdict", "")) | |
| verdict_counter[pred] += 1 | |
| if ( | |
| pred == "error" | |
| or str(row.get("termination") or "") == "error" | |
| or bool(str(row.get("error") or "").strip()) | |
| ): | |
| errors += 1 | |
| if isinstance(row.get("time_taken"), (int, float)): | |
| times.append(float(row["time_taken"])) | |
| if isinstance(row.get("total_tool_calls"), (int, float)): | |
| tool_calls.append(float(row["total_tool_calls"])) | |
| if isinstance(row.get("llm_api_calls"), (int, float)): | |
| llm_calls.append(float(row["llm_api_calls"])) | |
| return { | |
| "num_samples": total, | |
| "num_errors": errors, | |
| "predicted_verdict_distribution": dict(verdict_counter), | |
| "avg_time_taken_sec": round(_safe_mean(times), 3), | |
| "avg_tool_calls": round(_safe_mean(tool_calls), 3), | |
| "avg_llm_api_calls": round(_safe_mean(llm_calls), 3), | |
| } | |
| def _private_index( | |
| rows: List[Dict[str, Any]], | |
| *, | |
| key: str, | |
| name: str, | |
| ) -> Dict[str, Dict[str, Any]]: | |
| return private_index(rows, key=key, name=name) | |
| def _safe_trace_filename(identifier: str) -> str: | |
| safe_id = "".join( | |
| character if character.isalnum() or character in "-_." | |
| else "_" | |
| for character in str(identifier) | |
| ) | |
| return f"{safe_id}.json" | |
| def _select_samples( | |
| samples: List[Dict[str, Any]], | |
| *, | |
| requested_case_ids: List[str] | None, | |
| limit: int | None, | |
| shard_count: int = 1, | |
| shard_index: int = 0, | |
| ) -> List[Dict[str, Any]]: | |
| indexed: Dict[str, Dict[str, Any]] = {} | |
| for sample in samples: | |
| case_id = str(sample.get("case_id", "")).strip() | |
| if not case_id: | |
| raise ValueError("runtime input row lacks case_id") | |
| if case_id in indexed: | |
| raise ValueError( | |
| f"image-only release contains duplicate case_id {case_id}" | |
| ) | |
| indexed[case_id] = sample | |
| if requested_case_ids: | |
| requested = [str(item).strip() for item in requested_case_ids] | |
| if any(not item for item in requested): | |
| raise ValueError("--case-id values must be non-empty") | |
| if len(requested) != len(set(requested)): | |
| raise ValueError("--case-id values must be unique") | |
| missing = [case_id for case_id in requested if case_id not in indexed] | |
| if missing: | |
| raise ValueError( | |
| "requested case_id values are absent from release: " | |
| + ", ".join(missing) | |
| ) | |
| samples = [indexed[case_id] for case_id in requested] | |
| if shard_count < 1: | |
| raise ValueError("--shard-count must be at least 1") | |
| if shard_index < 0 or shard_index >= shard_count: | |
| raise ValueError("--shard-index must be in [0, shard-count)") | |
| if shard_count > 1: | |
| samples = [ | |
| sample | |
| for index, sample in enumerate( | |
| sorted(samples, key=lambda item: str(item["case_id"])) | |
| ) | |
| if index % shard_count == shard_index | |
| ] | |
| if limit is not None: | |
| samples = samples[:limit] | |
| return samples | |
| def _file_descriptor(path: Path | None) -> Dict[str, Any] | None: | |
| if path is None or not path.is_file(): | |
| return None | |
| return {"path": str(path.resolve()), "sha256": _sha256(path)} | |
| def _rollout_specs( | |
| samples: List[Dict[str, Any]], | |
| runtime_cases: List[Any], | |
| *, | |
| rollouts_per_case: int, | |
| base_sampling_seed: int, | |
| policy_revision: str, | |
| model: str, | |
| ) -> List[Dict[str, Any]]: | |
| """Expand public cases into isolated, reproducible rollout episodes.""" | |
| if rollouts_per_case < 1: | |
| raise ValueError("--rollouts-per-case must be at least 1") | |
| specs: List[Dict[str, Any]] = [] | |
| for sample, runtime_case in zip(samples, runtime_cases): | |
| case_id = str(runtime_case.case_id) | |
| group_material = { | |
| "case_id": case_id, | |
| "policy_revision": policy_revision, | |
| "model": model, | |
| "base_sampling_seed": int(base_sampling_seed), | |
| "group_size": rollouts_per_case, | |
| } | |
| prompt_group_id = "pg-" + hashlib.sha256( | |
| json.dumps( | |
| group_material, | |
| ensure_ascii=False, | |
| sort_keys=True, | |
| separators=(",", ":"), | |
| ).encode("utf-8") | |
| ).hexdigest()[:24] | |
| for rollout_index in range(rollouts_per_case): | |
| seed_material = ( | |
| f"{base_sampling_seed}:{case_id}:{rollout_index}" | |
| ).encode("utf-8") | |
| sampling_seed = int.from_bytes( | |
| hashlib.sha256(seed_material).digest()[:4], "big" | |
| ) & 0x7FFFFFFF | |
| episode_id = case_id | |
| if rollouts_per_case > 1: | |
| episode_id = ( | |
| f"{case_id[:150]}--{prompt_group_id[3:11]}" | |
| f"--r{rollout_index:03d}" | |
| ) | |
| specs.append( | |
| { | |
| "sample": sample, | |
| "runtime_case": runtime_case, | |
| "case_id": case_id, | |
| "prompt_group_id": prompt_group_id, | |
| "episode_id": episode_id, | |
| "rollout_index": rollout_index, | |
| "group_size": rollouts_per_case, | |
| "sampling_seed": sampling_seed, | |
| } | |
| ) | |
| return specs | |
| async def _run_eval(args: argparse.Namespace) -> Dict[str, Any]: | |
| benchmark_path = Path(args.benchmark).expanduser().resolve() | |
| manifest_probe = json.loads( | |
| (benchmark_path.parent.parent / "manifest.json").read_text( | |
| encoding="utf-8" | |
| ) | |
| ) | |
| is_scoring_release = ( | |
| str(manifest_probe.get("schema_version") or "") | |
| == SCORING_PACKAGE_SCHEMA_VERSION | |
| ) | |
| release = ( | |
| load_scoring_release(benchmark_path) | |
| if is_scoring_release | |
| else load_runtime_release(benchmark_path) | |
| ) | |
| samples = _load_benchmark(benchmark_path) | |
| samples = _select_samples( | |
| samples, | |
| requested_case_ids=getattr(args, "case_id", None), | |
| limit=args.limit, | |
| shard_count=int(getattr(args, "shard_count", 1)), | |
| shard_index=int(getattr(args, "shard_index", 0)), | |
| ) | |
| samples = [ | |
| ( | |
| resolve_scoring_image_path(sample, release) | |
| if isinstance(release, ScoringRuntimeRelease) | |
| else resolve_runtime_image_path(sample, benchmark_path) | |
| ) | |
| for sample in samples | |
| ] | |
| runtime_cases = [ | |
| image_only_case_from_runtime_row(sample) for sample in samples | |
| ] | |
| case_ids = [case.case_id for case in runtime_cases] | |
| if len(case_ids) != len(set(case_ids)): | |
| raise ValueError("image-only release contains duplicate case_id values") | |
| for runtime_case in runtime_cases: | |
| verify_case_image(runtime_case, runtime_case.image_path) | |
| runtime_commit = _git_commit() | |
| rollouts_per_case = _positive_int( | |
| getattr(args, "rollouts_per_case", 1), | |
| name="rollouts per case", | |
| ) | |
| base_sampling_seed = int(getattr(args, "base_sampling_seed", 1729)) | |
| config = WorkflowConfig( | |
| profile_id=getattr(args, "profile", None), | |
| provider=getattr(args, "provider", None), | |
| model_name=getattr(args, "model", None), | |
| vlm_provider=getattr(args, "vlm_provider", None), | |
| vlm_model=getattr(args, "vlm_model", None), | |
| image_access_mode=getattr( | |
| args, | |
| "image_access_mode", | |
| "direct_multimodal", | |
| ), | |
| llm_wire_api=getattr(args, "llm_wire_api", None), | |
| vlm_wire_api=getattr(args, "vlm_wire_api", None), | |
| timeout=args.timeout, | |
| enable_web_search=not bool( | |
| getattr(args, "disable_web_search", False) | |
| ), | |
| enable_image_retrieval=not bool( | |
| getattr(args, "disable_image_retrieval", False) | |
| ), | |
| enable_evidence_inspection=not bool( | |
| getattr(args, "disable_evidence_inspection", False) | |
| ), | |
| save_traces=True, | |
| decision_policy_version=getattr( | |
| args, | |
| "agent_decision_policy_version", | |
| AGENT_DECISION_POLICY_VERSION, | |
| ), | |
| ) | |
| rollout_specs = _rollout_specs( | |
| samples, | |
| runtime_cases, | |
| rollouts_per_case=rollouts_per_case, | |
| base_sampling_seed=base_sampling_seed, | |
| policy_revision=runtime_commit, | |
| model=str(config.model_name), | |
| ) | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| run_dir = ( | |
| Path(args.output_dir) | |
| if args.output_dir | |
| else default_eval_root() | |
| / ( | |
| f"{timestamp}_{config.provider}_" | |
| f"{str(config.model_name).replace('/', '_')}" | |
| ) | |
| ) | |
| if run_dir.exists() and any(run_dir.iterdir()): | |
| raise FileExistsError( | |
| f"Evaluation output directory must be new or empty: {run_dir}" | |
| ) | |
| stage_thinking_levels = { | |
| stage.lower(): os.getenv( | |
| f"GEMINI_{stage}_THINKING_LEVEL", | |
| os.getenv( | |
| "GEMINI_AGENT_THINKING_LEVEL", | |
| "high" if stage.lower() == "unified_react" else "low", | |
| ), | |
| ).strip().lower() | |
| for stage in ACTIVE_POLICY_STAGES | |
| } | |
| qwen_stage_thinking = _qwen_stage_thinking_config( | |
| provider=str(config.provider), | |
| model_name=str(config.model_name), | |
| ) | |
| explicit_policy = ( | |
| SourceAccessPolicy.load(args.source_access_policy) | |
| if args.source_access_policy | |
| else SourceAccessPolicy.load(release.artifacts.source_access_policy) | |
| if release.artifacts.source_access_policy is not None | |
| else None | |
| ) | |
| config.source_access_policy = explicit_policy | |
| trace_dir = run_dir / "traces" | |
| trace_dir.mkdir(parents=True, exist_ok=True) | |
| manifest_path = run_dir / "run_manifest.json" | |
| manifest: Dict[str, Any] = { | |
| "schema_version": RUN_SCHEMA_VERSION, | |
| "run_id": run_dir.name, | |
| "status": "running", | |
| "started_at": _now_iso(), | |
| "completed_at": None, | |
| "git_commit": runtime_commit, | |
| "benchmark": { | |
| "path": str(benchmark_path), | |
| "sha256": _sha256(benchmark_path), | |
| "sample_count": len(samples), | |
| "episode_count": len(rollout_specs), | |
| "limit": args.limit, | |
| "shard_count": int(getattr(args, "shard_count", 1)), | |
| "shard_index": int(getattr(args, "shard_index", 0)), | |
| "training_prohibited": bool( | |
| getattr(args, "training_prohibited", False) | |
| ), | |
| "runtime_release": True, | |
| "release_id": release.release_id, | |
| "release_stage": release.release_stage, | |
| "release_manifest": _file_descriptor(release.manifest_path), | |
| "runtime_contract_version": release.runtime_contract_version, | |
| "input_mode": release.input_mode, | |
| "decision_policy_version": release.decision_policy_version, | |
| "classification_protocol": ( | |
| _file_descriptor(release.artifacts.classification_protocol) | |
| if release.artifacts.classification_protocol is not None | |
| else None | |
| ), | |
| "process_reference_protocol": ( | |
| _file_descriptor(release.artifacts.process_reference_protocol) | |
| if release.artifacts.process_reference_protocol is not None | |
| else None | |
| ), | |
| "evaluation_gold": None, | |
| }, | |
| "agent": { | |
| "profile_id": config.profile_id, | |
| "decision_policy_version": config.decision_policy_version, | |
| "provider": config.provider, | |
| "model": config.model_name, | |
| "vlm_provider": config.vlm_provider, | |
| "vlm_model": config.vlm_model, | |
| "image_access_mode": config.image_access_mode, | |
| "base_url": config.llm_base_url, | |
| "vlm_base_url": config.vlm_base_url, | |
| "llm_wire_api": config.llm_wire_api, | |
| "vlm_wire_api": config.vlm_wire_api, | |
| "timeout_seconds": args.timeout, | |
| "max_tool_actions": 24, | |
| "stage_thinking_levels": stage_thinking_levels, | |
| "qwen_stage_enable_thinking": qwen_stage_thinking, | |
| "concurrency": max(1, args.concurrency), | |
| "gemini_max_inflight_requests": ( | |
| int( | |
| os.getenv( | |
| "GEMINI_MAX_INFLIGHT_REQUESTS", | |
| str(max(1, args.concurrency)), | |
| ) | |
| ) | |
| if str(config.provider).lower() == "gemini" | |
| else None | |
| ), | |
| "gemini_eval_max_concurrency": ( | |
| int( | |
| os.getenv( | |
| "GEMINI_EVAL_MAX_CONCURRENCY", | |
| str(max(1, args.concurrency)), | |
| ) | |
| ) | |
| if str(config.provider).lower() == "gemini" | |
| else None | |
| ), | |
| "perception_cache_enabled": os.getenv( | |
| "PERCEPTION_CACHE_ENABLED", | |
| "1", | |
| ).strip().lower() | |
| in {"1", "true", "yes"}, | |
| "web_cache_enabled": os.getenv( | |
| "TOOL_CACHE_ENABLED", | |
| "0", | |
| ).strip().lower() | |
| in {"1", "true", "yes"}, | |
| "tool_cache_ttl_seconds": os.getenv( | |
| "TOOL_CACHE_TTL_SECONDS", | |
| "3600", | |
| ), | |
| "perception_cache_version": os.getenv( | |
| "PERCEPTION_CACHE_VERSION", | |
| "perception-v1", | |
| ), | |
| "ocr_cache_version": os.getenv( | |
| "OCR_CACHE_VERSION", | |
| f"{os.getenv('OCR_BACKEND', 'baidu').strip().lower()}-v1", | |
| ), | |
| "rollouts_per_case": rollouts_per_case, | |
| "base_sampling_seed": base_sampling_seed, | |
| "tool_families": config.react_tool_family_config.to_manifest(), | |
| "sampling_seed_derivation": ( | |
| "sha256(base_seed:case_id:rollout_index)-31bit-v1" | |
| ), | |
| }, | |
| "source_access_policy": { | |
| "active": bool(explicit_policy and explicit_policy.active), | |
| "policy_id": explicit_policy.policy_id if explicit_policy else None, | |
| "cache_partition": explicit_policy.cache_partition if explicit_policy else None, | |
| }, | |
| "artifacts": { | |
| "predictions": "predictions.jsonl", | |
| "episode_predictions": "episode_predictions.jsonl", | |
| "run_results": "run_results.jsonl", | |
| "process_metrics": "process_metrics.jsonl", | |
| "reference_chain_metrics": "reference_chain_metrics.jsonl", | |
| "trajectory_scores": "trajectory_scores.jsonl", | |
| "trajectory_sft": "trajectory_sft.jsonl", | |
| "perception_trajectories": "perception_trajectories.jsonl", | |
| "rollout_groups": "rollout_groups.jsonl", | |
| "post_rollout_rewards": "post_rollout_rewards.jsonl", | |
| "summary": "summary.json", | |
| "traces": "traces/", | |
| }, | |
| } | |
| gemini_guard = GeminiRunGuard.acquire( | |
| provider=config.provider, | |
| concurrency=max(1, args.concurrency), | |
| run_id=manifest["run_id"], | |
| ) | |
| try: | |
| _write_json(manifest_path, manifest) | |
| config.output_dir = str(trace_dir) | |
| workflow = VerificationWorkflow(config) | |
| image_paths = [ | |
| str(spec["sample"]["image_path"]) for spec in rollout_specs | |
| ] | |
| image_ids = [str(spec["episode_id"]) for spec in rollout_specs] | |
| results = await workflow.run_batch( | |
| image_paths=image_paths, | |
| image_ids=image_ids, | |
| runtime_cases=[spec["runtime_case"] for spec in rollout_specs], | |
| sampling_seeds=[spec["sampling_seed"] for spec in rollout_specs], | |
| concurrency=max(1, args.concurrency), | |
| ) | |
| evaluation_gold_index = _private_index( | |
| _load_benchmark(release.artifacts.evaluation_gold), | |
| key="case_id", | |
| name="evaluation-gold", | |
| ) | |
| missing_gold = [ | |
| case_id | |
| for case_id in case_ids | |
| if case_id not in evaluation_gold_index | |
| ] | |
| if missing_gold: | |
| raise RuntimeError( | |
| "evaluation-gold rows missing runtime case IDs: " | |
| + ", ".join(missing_gold[:10]) | |
| ) | |
| manifest["benchmark"]["evaluation_gold"] = _file_descriptor( | |
| release.artifacts.evaluation_gold | |
| ) | |
| predictions: List[Dict[str, Any]] = [] | |
| episode_predictions: List[Dict[str, Any]] = [] | |
| run_results: List[Dict[str, Any]] = [] | |
| process_metrics: List[Dict[str, Any]] = [] | |
| reference_chain_metrics: List[Dict[str, Any]] = [] | |
| trajectory_scores: List[Dict[str, Any]] = [] | |
| trajectory_sft: List[Dict[str, Any]] = [] | |
| perception_trajectories: List[Dict[str, Any]] = [] | |
| rollout_members: List[Dict[str, Any]] = [] | |
| post_rollout_rewards: List[Dict[str, Any]] = [] | |
| for spec, result in zip(rollout_specs, results): | |
| sample = spec["sample"] | |
| identity = str(spec["case_id"]) | |
| episode_id = str(spec["episode_id"]) | |
| record = _run_result_record(sample, result) | |
| record.update( | |
| { | |
| "prompt_group_id": spec["prompt_group_id"], | |
| "episode_id": episode_id, | |
| "rollout_index": spec["rollout_index"], | |
| "sampling_seed": spec["sampling_seed"], | |
| } | |
| ) | |
| trace_path = trace_dir / _safe_trace_filename(episode_id) | |
| member = { | |
| "schema_version": ROLLOUT_MEMBER_SCHEMA_VERSION, | |
| "prompt_group_id": spec["prompt_group_id"], | |
| "case_id": identity, | |
| "episode_id": episode_id, | |
| "rollout_index": spec["rollout_index"], | |
| "group_size": spec["group_size"], | |
| "sampling_seed": spec["sampling_seed"], | |
| "policy_revision": manifest["git_commit"], | |
| "trace_path": None, | |
| "training_prohibited": ( | |
| bool(getattr(args, "training_prohibited", False)) | |
| or release.release_stage == "development_subset" | |
| ), | |
| "training_eligible": False, | |
| "sft_structured_eligibility_required": isinstance( | |
| release, ScoringRuntimeRelease | |
| ), | |
| } | |
| if trace_path.exists(): | |
| record["trace_path"] = trace_path.relative_to(run_dir).as_posix() | |
| member["trace_path"] = record["trace_path"] | |
| trace = json.loads(trace_path.read_text(encoding="utf-8")) | |
| trace_state = trace.get("state") | |
| if isinstance(trace_state, dict) and isinstance( | |
| trace_state.get("perception"), dict | |
| ): | |
| perception_trajectories.append( | |
| export_perception_example( | |
| trace, | |
| source_metadata={ | |
| "source_run_id": manifest["run_id"], | |
| "runtime_commit": manifest["git_commit"], | |
| "release_id": release.release_id, | |
| "runtime_contract_version": ( | |
| release.runtime_contract_version | |
| ), | |
| }, | |
| ).model_dump(mode="json") | |
| ) | |
| private_gold = evaluation_gold_index[identity] | |
| process_gold = ( | |
| adapt_scoring_gold_for_process(private_gold) | |
| if isinstance(release, ScoringRuntimeRelease) | |
| else private_gold | |
| ) | |
| process_metadata = { | |
| "evaluation_gold": _file_descriptor( | |
| release.artifacts.evaluation_gold | |
| ), | |
| } | |
| if release.artifacts.process_reference_protocol is not None: | |
| process_metadata["process_reference_protocol"] = ( | |
| _file_descriptor( | |
| release.artifacts.process_reference_protocol | |
| ) | |
| ) | |
| metrics, teacher_score = score_process_trace( | |
| trace, | |
| process_gold, | |
| score_metadata=process_metadata, | |
| ) | |
| metrics["episode_id"] = episode_id | |
| metrics["prompt_group_id"] = spec["prompt_group_id"] | |
| teacher_score["episode_id"] = episode_id | |
| teacher_score["prompt_group_id"] = spec["prompt_group_id"] | |
| audit_report = audit_trace( | |
| trace_path, | |
| enforce_source_access_policy=bool( | |
| explicit_policy and explicit_policy.active | |
| ), | |
| source_access_policy=explicit_policy, | |
| ) | |
| strict_failures = audit_report.failures(strict_scheduler=True) | |
| hard_failures = audit_report.failures(strict_scheduler=False) | |
| strict_trace_audit_pass = not strict_failures | |
| hard_trace_audit_pass = not hard_failures | |
| metrics["strict_trace_audit_pass"] = strict_trace_audit_pass | |
| metrics["hard_trace_audit_pass"] = hard_trace_audit_pass | |
| metrics["strict_trace_audit_failures"] = [ | |
| asdict(item) for item in strict_failures | |
| ] | |
| process_metrics.append(metrics) | |
| if isinstance(release, ScoringRuntimeRelease): | |
| reference_score = { | |
| "schema_version": "ifv-scoring-reference-chain-v1", | |
| "case_id": identity, | |
| "applicable": False, | |
| "reason": ( | |
| "ifv-scoring-gold-v1 uses a structured relation " | |
| "target gate instead of the v0.3 reference protocol" | |
| ), | |
| } | |
| else: | |
| reference_score = await score_reference_chain_trace( | |
| trace, | |
| private_gold, | |
| score_metadata=process_metadata, | |
| ) | |
| reference_score["episode_id"] = episode_id | |
| reference_score["prompt_group_id"] = spec["prompt_group_id"] | |
| reference_chain_metrics.append(reference_score) | |
| trajectory_scores.append(teacher_score) | |
| try: | |
| exported_trajectory = export_trajectory_sft_example( | |
| trace, | |
| source_metadata={ | |
| "source_run_id": manifest["run_id"], | |
| "runtime_commit": manifest["git_commit"], | |
| "release_id": release.release_id, | |
| "runtime_contract_version": ( | |
| release.runtime_contract_version | |
| ), | |
| "process_reference_protocol_version": ( | |
| release.manifest.get( | |
| "process_reference_protocol_version", | |
| "", | |
| ) | |
| ), | |
| }, | |
| ) | |
| except ValueError as exc: | |
| metrics["training_eligible"] = False | |
| reasons = list( | |
| metrics.get("training_exclusion_reasons", []) or [] | |
| ) | |
| reasons.append(f"trajectory_sft_export_rejected: {exc}") | |
| metrics["training_exclusion_reasons"] = list( | |
| dict.fromkeys(reasons) | |
| ) | |
| teacher_score["training_eligible"] = False | |
| teacher_score["training_exclusion_reasons"] = list( | |
| metrics["training_exclusion_reasons"] | |
| ) | |
| exported_trajectory = None | |
| if exported_trajectory is not None: | |
| trajectory_sft.append( | |
| exported_trajectory.model_dump(mode="json") | |
| ) | |
| policy_step_ids = trajectory_policy_step_ids( | |
| trace, | |
| episode_id=episode_id, | |
| ) | |
| member["sft_training_eligible"] = bool( | |
| strict_trace_audit_pass | |
| and bool(metrics.get("training_eligible", False)) | |
| and exported_trajectory is not None | |
| and not member["training_prohibited"] | |
| and not isinstance(release, ScoringRuntimeRelease) | |
| ) | |
| member["rl_reward_eligible"] = bool( | |
| not metrics.get("engineering_error", False) | |
| and bool(policy_step_ids) | |
| and not member["training_prohibited"] | |
| ) | |
| member["training_eligible"] = member["sft_training_eligible"] | |
| post_rollout_rewards.append( | |
| { | |
| "schema_version": "ifv-post-rollout-deterministic-v1", | |
| "prompt_group_id": spec["prompt_group_id"], | |
| "case_id": identity, | |
| "episode_id": episode_id, | |
| "classification_correct": bool( | |
| metrics.get("result_correct", False) | |
| ), | |
| "fatal_engineering_error": bool( | |
| metrics.get("engineering_error", False) | |
| ), | |
| "strict_trace_audit_pass": bool( | |
| strict_trace_audit_pass | |
| ), | |
| "hard_trace_audit_pass": bool(hard_trace_audit_pass), | |
| "strict_trace_audit_failure_codes": [ | |
| str(item.get("code", "")) | |
| for item in metrics.get("strict_trace_audit_failures", []) | |
| if str(item.get("code", "")) | |
| ], | |
| "training_prohibited": member["training_prohibited"], | |
| "rl_reward_eligible": member["rl_reward_eligible"], | |
| "step_ids": policy_step_ids, | |
| "process_components": dict( | |
| teacher_score.get("components") or {} | |
| ), | |
| } | |
| ) | |
| else: | |
| process_metrics.append( | |
| { | |
| "schema_version": "ifv-process-metrics-v2", | |
| "case_id": identity, | |
| "engineering_error": True, | |
| "runtime_verdict": result.get("verdict"), | |
| "result_correct": False, | |
| "first_error": { | |
| "error": "canonical trace is missing", | |
| }, | |
| } | |
| ) | |
| reference_chain_metrics.append( | |
| { | |
| "schema_version": "ifv-reference-chain-metrics-v2", | |
| "case_id": identity, | |
| "engineering_error": True, | |
| "error": "canonical trace is missing", | |
| "metrics": { | |
| "fact_recovery_recall": 0.0, | |
| "chain_recovery_recall": 0.0, | |
| "evidence_recovery_recall": 0.0, | |
| "basis_reference_precision": 0.0, | |
| }, | |
| } | |
| ) | |
| trajectory_scores.append( | |
| { | |
| "schema_version": "ifv-trajectory-score-v2", | |
| "case_id": identity, | |
| "components": { | |
| "result_reward": 0.0, | |
| "grounded_finding_reward": 0.0, | |
| "gap_coverage_reward": 0.0, | |
| "bridge_reward": 0.0, | |
| "stop_calibration_reward": 0.0, | |
| "duplicate_action_penalty": 0.0, | |
| "invalid_task_penalty": 0.0, | |
| "normalized_cost_penalty": 0.0, | |
| }, | |
| "total": 0.0, | |
| } | |
| ) | |
| post_rollout_rewards.append( | |
| { | |
| "schema_version": "ifv-post-rollout-deterministic-v1", | |
| "prompt_group_id": spec["prompt_group_id"], | |
| "case_id": identity, | |
| "episode_id": episode_id, | |
| "classification_correct": False, | |
| "fatal_engineering_error": True, | |
| "strict_trace_audit_pass": False, | |
| "training_prohibited": member["training_prohibited"], | |
| "step_ids": [], | |
| "process_components": {}, | |
| } | |
| ) | |
| run_results.append(record) | |
| rollout_members.append(member) | |
| prediction = _classification_prediction(sample, result) | |
| if prediction is not None: | |
| episode_predictions.append( | |
| { | |
| **prediction, | |
| "episode_id": episode_id, | |
| "prompt_group_id": spec["prompt_group_id"], | |
| "rollout_index": spec["rollout_index"], | |
| } | |
| ) | |
| # predictions.jsonl stays consumable by the data-owned | |
| # classification scorer: one row per public case. Multi-rollout | |
| # analysis belongs in episode_predictions.jsonl. | |
| if spec["rollout_index"] == 0: | |
| predictions.append(prediction) | |
| summary = _compute_summary(run_results) | |
| summary["run_id"] = manifest["run_id"] | |
| _write_jsonl(run_dir / "predictions.jsonl", predictions) | |
| _write_jsonl( | |
| run_dir / "episode_predictions.jsonl", | |
| episode_predictions, | |
| ) | |
| _write_jsonl(run_dir / "run_results.jsonl", run_results) | |
| _write_jsonl(run_dir / "process_metrics.jsonl", process_metrics) | |
| _write_jsonl( | |
| run_dir / "reference_chain_metrics.jsonl", | |
| reference_chain_metrics, | |
| ) | |
| _write_jsonl(run_dir / "trajectory_scores.jsonl", trajectory_scores) | |
| _write_jsonl(run_dir / "trajectory_sft.jsonl", trajectory_sft) | |
| _write_jsonl( | |
| run_dir / "perception_trajectories.jsonl", | |
| perception_trajectories, | |
| ) | |
| _write_jsonl(run_dir / "rollout_groups.jsonl", rollout_members) | |
| _write_jsonl( | |
| run_dir / "post_rollout_rewards.jsonl", | |
| post_rollout_rewards, | |
| ) | |
| _write_json(run_dir / "summary.json", summary) | |
| manifest["status"] = ( | |
| "completed_with_errors" if summary["num_errors"] else "completed" | |
| ) | |
| manifest["completed_at"] = _now_iso() | |
| manifest["result"] = { | |
| "num_samples": len(samples), | |
| "num_episodes": summary["num_samples"], | |
| "num_errors": summary["num_errors"], | |
| } | |
| _write_json(manifest_path, manifest) | |
| return summary | |
| except BaseException as exc: | |
| manifest["status"] = "failed" | |
| manifest["completed_at"] = _now_iso() | |
| manifest["error"] = f"{type(exc).__name__}: {exc}" | |
| _write_json(manifest_path, manifest) | |
| raise | |
| finally: | |
| gemini_guard.release() | |
| def main() -> None: | |
| args = _parse_args() | |
| summary = asyncio.run(_run_eval(args)) | |
| print(json.dumps(summary, ensure_ascii=False, indent=2)) | |
| if int(summary.get("num_errors", 0) or 0) > 0: | |
| raise SystemExit(1) | |
| if __name__ == "__main__": | |
| main() | |