项目文件夹

文件
wehub-resource-sync 8a21a212f8
Deploy Documentation / deploy (push) Has been cancelled
Canary / build-cli (push) Has been skipped
Canary / Upload Install Script (push) Has been skipped
Canary / bundle-desktop (push) Has been skipped
Canary / bundle-desktop-intel (push) Has been skipped
Canary / bundle-desktop-linux (push) Has been skipped
Canary / bundle-desktop-windows (push) Has been skipped
Canary / bundle-desktop-windows-cuda (push) Has been skipped
Canary / Release (push) Has been skipped
Cargo Deny / deny (push) Has been skipped
Unused Dependencies / machete (push) Has been skipped
Canary / Prepare Version (push) Failing after 1s
Live Provider Tests / check-fork (push) Failing after 0s
Create Minor Release PR / check-version-bump-pr (push) Has been skipped
Publish Ask AI Bot Docker Image / docker (push) Failing after 1s
Live Provider Tests / changes (push) Has been skipped
Scorecard supply-chain security / Scorecard analysis (push) Has been skipped
Publish Docker Image / docker (push) Failing after 1s
CI / changes (push) Failing after 8s
Create Minor Release PR / release (push) Has been skipped
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:04:08 +08:00

226 行
7.2 KiB
Python

"""Build the harbor config and launch a benchmark job."""
from __future__ import annotations
import argparse
import json
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
import yaml
from agent import PROVIDER_SECRETS
HARBOR_DIR = Path(__file__).resolve().parent
RUNS_DIR = HARBOR_DIR / "runs"
CONFIG_TEMPLATE_PATH = HARBOR_DIR / "config_template.yaml"
DEFAULT_DATASET = "terminal-bench/terminal-bench-2"
DEFAULT_MODEL = "anthropic/claude-sonnet-4-6"
DEFAULT_EXTENSIONS = ["developer", "todo"]
DEFAULT_CONCURRENCY = 4
DEFAULT_MAX_TURNS = 100
def find_dotenv() -> Path | None:
cwd_env = Path.cwd() / ".env"
if cwd_env.is_file():
return cwd_env
script_env = HARBOR_DIR / ".env"
if script_env.is_file():
return script_env
return None
def load_dotenv() -> None:
env_path = find_dotenv()
if env_path is None:
return
for line in env_path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
os.environ.setdefault(key, value)
def render_goose_config(extensions: list[str]) -> tuple[str, list[dict[str, str]]]:
"""Render config.yaml from the template, enabling the given extensions.
Returns (config_yaml_text, recipe_extension_entries).
Raises ValueError for any extension not found in the template.
"""
if not CONFIG_TEMPLATE_PATH.is_file():
raise FileNotFoundError(f"Missing template: {CONFIG_TEMPLATE_PATH}")
template = yaml.safe_load(CONFIG_TEMPLATE_PATH.read_text())
available = template.get("extensions") or {}
unknown = [name for name in extensions if name not in available]
if unknown:
raise ValueError(
f"Unknown extensions: {', '.join(unknown)}. "
f"Known: {', '.join(sorted(available))}"
)
for name, entry in available.items():
entry["enabled"] = name in extensions
recipe_entries = [
{"type": available[name]["type"], "name": name} for name in extensions
]
return yaml.dump(template, sort_keys=False), recipe_entries
def default_job_name(model: str, dataset: str) -> str:
safe_model = re.sub(r"[^A-Za-z0-9._-]+", "-", model).strip("-")
safe_dataset = re.sub(r"[^A-Za-z0-9._-]+", "-", dataset).strip("-")
timestamp = datetime.now().strftime("%Y-%m-%d__%H-%M-%S")
return f"goose-{safe_dataset}-{safe_model}-{timestamp}"
def validate_job_name(job_name: str) -> str:
if not re.match(r"^[A-Za-z0-9][A-Za-z0-9._-]*$", job_name):
raise ValueError(
"Job name must start with a letter or number and contain only "
"letters, numbers, dots, underscores, and hyphens"
)
return job_name
def parse_csv(value: str) -> list[str]:
return [item.strip() for item in value.split(",") if item.strip()]
PACKAGE_INDEX_ENV_VARS = ("UV_DEFAULT_INDEX", "PIP_INDEX_URL", "UV_INDEX_URL")
def package_index_env() -> dict[str, str]:
index_url = next(
(os.environ[key] for key in PACKAGE_INDEX_ENV_VARS if os.environ.get(key)),
None,
)
if index_url is None:
return {}
return {key: index_url for key in PACKAGE_INDEX_ENV_VARS}
def dataset_config(dataset_ref: str, tasks: list[str]) -> dict[str, Any]:
name, sep, ref = dataset_ref.rpartition("@")
dataset_name = name if sep else dataset_ref
dataset: dict[str, Any] = {"name": dataset_name}
if sep:
dataset["ref" if "/" in name else "version"] = ref
if tasks:
dataset["task_names"] = tasks
return dataset
def build_harbor_config(args: argparse.Namespace) -> dict[str, Any]:
if "/" not in args.model:
raise ValueError("--model must be in provider/model form, e.g. anthropic/claude-sonnet-4-6")
if args.trials < 1:
raise ValueError("--trials must be at least 1")
if args.concurrency < 1:
raise ValueError("--concurrency must be at least 1")
if args.timeout_multiplier <= 0:
raise ValueError("--timeout-multiplier must be positive")
goose_binary = args.goose_binary.expanduser().resolve()
if not goose_binary.is_file():
raise ValueError(f"--goose-binary does not exist or is not a file: {args.goose_binary}")
config_yaml, extension_entries = render_goose_config(args.extensions)
provider = args.model.split("/", 1)[0]
missing_secrets = [
key for key in PROVIDER_SECRETS.get(provider, []) if not os.environ.get(key)
]
if missing_secrets:
raise ValueError(
f"Missing env vars for provider '{provider}': {', '.join(missing_secrets)}. "
f"Set them in a .env file (cwd or {HARBOR_DIR}) or your shell."
)
agent_kwargs: dict[str, Any] = {
"goose_binary": str(goose_binary),
"config_yaml": config_yaml,
"extension_entries": extension_entries,
"install_goose_runtime_deps": args.install_goose_runtime_deps,
}
if args.max_turns is not None:
agent_kwargs["max_turns"] = args.max_turns
job_name = (
validate_job_name(args.job_name)
if args.job_name
else default_job_name(args.model, args.dataset)
)
index_env = package_index_env()
container_env_passthrough = [
f"{key}=${{{key}}}"
for key in PROVIDER_SECRETS.get(provider, [])
if os.environ.get(key)
] + [f"{key}={value}" for key, value in index_env.items()]
config: dict[str, Any] = {
"job_name": job_name,
"jobs_dir": str(RUNS_DIR),
"n_attempts": args.trials,
"n_concurrent_trials": args.concurrency,
"environment": {
"type": "docker",
"force_build": False,
"delete": True,
"env": container_env_passthrough,
},
"agents": [
{
"import_path": "agent:GooseBinaryAgent",
"model_name": args.model,
"kwargs": agent_kwargs,
}
],
"datasets": [dataset_config(args.dataset, args.tasks)],
}
if index_env:
config["verifier"] = {"env": index_env}
if args.timeout_multiplier != 1.0:
config["timeout_multiplier"] = args.timeout_multiplier
return config
def cmd_run(args: argparse.Namespace) -> int:
load_dotenv()
try:
config = build_harbor_config(args)
except Exception as error:
print(f"error: {error}", file=sys.stderr)
return 2
RUNS_DIR.mkdir(parents=True, exist_ok=True)
job_dir = RUNS_DIR / config["job_name"]
job_dir.mkdir(parents=True, exist_ok=True)
config_path = job_dir / "_generated_config.json"
config_path.write_text(json.dumps(config, indent=2) + "\n")
command = ["harbor", "run", "-c", str(config_path)]
print(f"Job: {config['job_name']}")
print(f"Config: {config_path}")
print(f"Runs: {RUNS_DIR}")
if args.dry_run:
return 0
env = os.environ.copy()
env["PYTHONPATH"] = f"{HARBOR_DIR}{os.pathsep}{env.get('PYTHONPATH', '')}".rstrip(os.pathsep)
completed = subprocess.run(command, env=env, check=False)
return completed.returncode