mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 12:06:29 +03:00
184 lines
5.6 KiB
Python
184 lines
5.6 KiB
Python
"""Eval runner CLI — loads cases, dispatches to an adapter, writes manifests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
from .adapter import HarnessAdapter
|
|
from .cli_adapter import CliSubprocessAdapter
|
|
from .fake_adapter import FakeAdapter
|
|
from .manifest import build_manifest, write_manifest
|
|
from .models import AdapterInput, EvalCase
|
|
from .path_safety import contained_path
|
|
|
|
|
|
def load_cases(manifest_path: Path) -> list[EvalCase]:
|
|
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
cases: list[EvalCase] = []
|
|
for entry in data.get("evals", []):
|
|
cases.append(
|
|
EvalCase(
|
|
id=entry["id"],
|
|
prompt=entry["prompt"],
|
|
expected_output=entry["expected_output"],
|
|
assertions=entry.get("assertions", []),
|
|
files=entry.get("files", []),
|
|
case_set=entry.get("case_set", "dev"),
|
|
)
|
|
)
|
|
return cases
|
|
|
|
|
|
def resolve_skill_path(manifest_path: Path) -> Path:
|
|
return manifest_path.parent.parent
|
|
|
|
|
|
def build_adapter(args: argparse.Namespace) -> HarnessAdapter:
|
|
if args.adapter == "fake":
|
|
return FakeAdapter()
|
|
if args.adapter == "cli":
|
|
if not args.command:
|
|
print("error: --command required for cli adapter", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
import shlex
|
|
|
|
command = shlex.split(args.command)
|
|
return CliSubprocessAdapter(
|
|
command,
|
|
prompt_mode=args.prompt_mode,
|
|
prompt_flag=args.prompt_flag,
|
|
timeout_seconds=args.timeout,
|
|
extra_args=args.extra_args.split() if args.extra_args else [],
|
|
)
|
|
print(f"error: unknown adapter '{args.adapter}'", file=sys.stderr)
|
|
raise SystemExit(2)
|
|
|
|
|
|
def run_trial(
|
|
adapter: HarnessAdapter,
|
|
case: EvalCase,
|
|
skill_path: Path,
|
|
output_dir: Path,
|
|
model: str,
|
|
) -> Path:
|
|
work_dir = contained_path(output_dir, "work", case.id)
|
|
case_output_dir = contained_path(output_dir, "trials", case.id)
|
|
|
|
adapter_input = AdapterInput(
|
|
skill_path=skill_path,
|
|
case=case,
|
|
work_dir=work_dir,
|
|
output_dir=case_output_dir,
|
|
model=model,
|
|
limits={"timeout_seconds": 120, "network_policy": "unspecified"},
|
|
)
|
|
|
|
started_at = datetime.now(timezone.utc)
|
|
result = adapter.execute(adapter_input)
|
|
finished_at = datetime.now(timezone.utc)
|
|
|
|
manifest = build_manifest(
|
|
adapter_name=adapter.name,
|
|
adapter_version=adapter.version,
|
|
harness_name=adapter.name,
|
|
harness_version=adapter.version,
|
|
model_provider="unspecified" if not model else model.split("/")[0],
|
|
model_id=model or "unspecified",
|
|
adapter_input=adapter_input,
|
|
adapter_output=result,
|
|
started_at=started_at,
|
|
finished_at=finished_at,
|
|
)
|
|
|
|
return write_manifest(manifest, contained_path(output_dir, "manifests"))
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(
|
|
prog="eval-runner",
|
|
description="Run eval cases against a candidate skill via a harness adapter.",
|
|
)
|
|
parser.add_argument(
|
|
"manifest",
|
|
type=Path,
|
|
help="path to an evals.json manifest",
|
|
)
|
|
parser.add_argument(
|
|
"--adapter",
|
|
choices=["fake", "cli"],
|
|
default="fake",
|
|
help="adapter to use (default: fake)",
|
|
)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
default=Path("eval-output"),
|
|
help="directory for run artifacts (default: eval-output)",
|
|
)
|
|
parser.add_argument(
|
|
"--model",
|
|
default="",
|
|
help="model identifier (e.g. anthropic/claude-sonnet-4-20250514)",
|
|
)
|
|
parser.add_argument(
|
|
"--case",
|
|
dest="case_id",
|
|
default=None,
|
|
help="run only this case ID",
|
|
)
|
|
parser.add_argument("--command", default=None, help="CLI command for cli adapter")
|
|
parser.add_argument("--prompt-mode", default="stdin", choices=["stdin", "arg"])
|
|
parser.add_argument("--prompt-flag", default="--prompt")
|
|
parser.add_argument("--timeout", type=int, default=120)
|
|
parser.add_argument("--extra-args", default=None)
|
|
args = parser.parse_args()
|
|
|
|
manifest_path = args.manifest.resolve()
|
|
if not manifest_path.is_file():
|
|
print(f"error: manifest not found: {manifest_path}", file=sys.stderr)
|
|
return 2
|
|
|
|
cases = load_cases(manifest_path)
|
|
if not cases:
|
|
print(f"error: no cases found in {manifest_path}", file=sys.stderr)
|
|
return 2
|
|
|
|
if args.case_id:
|
|
cases = [c for c in cases if c.id == args.case_id]
|
|
if not cases:
|
|
print(f"error: case '{args.case_id}' not found", file=sys.stderr)
|
|
return 2
|
|
|
|
skill_path = resolve_skill_path(manifest_path)
|
|
adapter = build_adapter(args)
|
|
output_dir = args.output_dir.resolve()
|
|
|
|
print(f"adapter: {adapter.name} v{adapter.version}")
|
|
print(f"skill: {skill_path.name}")
|
|
print(f"cases: {len(cases)}")
|
|
print(f"output: {output_dir}")
|
|
print()
|
|
|
|
failures = 0
|
|
for case in cases:
|
|
print(f" [{case.id}] running...", end=" ", flush=True)
|
|
manifest_file = run_trial(adapter, case, skill_path, output_dir, args.model)
|
|
status = json.loads(manifest_file.read_text(encoding="utf-8"))["status"]
|
|
if status == "completed":
|
|
print(f"ok -> {manifest_file.name}")
|
|
else:
|
|
print(f"{status} -> {manifest_file.name}")
|
|
failures += 1
|
|
|
|
print()
|
|
print(f"done: {len(cases)} trial(s), {failures} failure(s)")
|
|
return 1 if failures else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|