475 lines
18 KiB
Python
475 lines
18 KiB
Python
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
import argparse
|
|
import io
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
import tarfile
|
|
import traceback
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from docker import DockerClient, from_env
|
|
from docker.errors import ImageNotFound
|
|
from docker.models.containers import Container
|
|
from tqdm import tqdm
|
|
|
|
from .utils import BENCHMARK_CONFIG, docker_exec
|
|
|
|
|
|
class BenchmarkEvaluation:
|
|
"""
|
|
Main class for running experiments and evaluations.
|
|
Handles Docker image management, environment preparation, patch generation, and evaluation.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
benchmark: str,
|
|
working_dir: str,
|
|
trae_config_file_name: str,
|
|
dataset: str = "SWE-bench_Verified",
|
|
docker_env_config: str = "",
|
|
benchmark_harness_path: str = "",
|
|
run_id: str = "trae-agent",
|
|
max_workers: int = 4,
|
|
instance_ids: list[str] | None = None,
|
|
):
|
|
"""
|
|
Initialize the BenchmarkEvaluation class.
|
|
|
|
Args:
|
|
benchmark: Benchmark name.
|
|
working_dir: Path for workspace (used for temp files and artifacts).
|
|
trae_config_file_name: Path to Trae config file.
|
|
dataset: Dataset name.
|
|
docker_env_config: Path to Docker environment config file.
|
|
benchmark_harness_path: Path to benchmark harness (for evaluation).
|
|
run_id: Unique run identifier.
|
|
max_workers: Maximum number of parallel workers.
|
|
instance_ids: List of instance IDs to run (optional).
|
|
"""
|
|
assert benchmark in BENCHMARK_CONFIG, f"Invalid benchmark name: {benchmark}"
|
|
self.config = BENCHMARK_CONFIG[benchmark]
|
|
self.dataset_name = dataset
|
|
assert self.dataset_name in self.config.valid_datasets, (
|
|
f"Invalid dataset name: {self.dataset_name}"
|
|
)
|
|
|
|
self.benchmark = benchmark
|
|
self.dataset = self.config.load_dataset(self.dataset_name)
|
|
self.docker_client: DockerClient = from_env()
|
|
self.image_status: dict[Any, Any] = {}
|
|
|
|
self.working_dir = Path(working_dir)
|
|
self.benchmark_harness_path = benchmark_harness_path
|
|
self.run_id = run_id
|
|
self.max_workers = max_workers
|
|
if instance_ids is None:
|
|
instance_ids = [instance["instance_id"] for instance in self.dataset]
|
|
else:
|
|
self.instance_ids = instance_ids
|
|
|
|
if docker_env_config != "":
|
|
with open(docker_env_config, "r") as f:
|
|
self.docker_env_config: dict[str, dict[str, str]] = json.load(f)
|
|
else:
|
|
self.docker_env_config = {}
|
|
|
|
self.working_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.trae_config_file_name = trae_config_file_name
|
|
shutil.copyfile(self.trae_config_file_name, self.working_dir / "trae_config.yaml")
|
|
|
|
self.results_dir = Path("results")
|
|
self.task_id = f"{self.benchmark}_{self.dataset_name}_{self.run_id}".replace("/", "_")
|
|
self.task_results_dir = self.results_dir / self.task_id
|
|
self.task_results_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.pull_images()
|
|
|
|
def _image_name(self, instance_id: str) -> str:
|
|
"""
|
|
Get the Docker image name for a given instance ID.
|
|
|
|
Args:
|
|
instance_id: Instance identifier.
|
|
|
|
Returns:
|
|
Docker image name string.
|
|
"""
|
|
return self.config.image_name(instance_id)
|
|
|
|
def _check_images(self):
|
|
"""
|
|
Check existence of required Docker images for all instances.
|
|
Updates self.image_status dict.
|
|
"""
|
|
for item in tqdm(self.dataset, desc="Checking image status"):
|
|
instance_id: str = item["instance_id"]
|
|
image_name = self._image_name(instance_id)
|
|
try:
|
|
_ = self.docker_client.images.get(image_name)
|
|
self.image_status[instance_id] = True
|
|
except ImageNotFound:
|
|
self.image_status[instance_id] = False
|
|
|
|
try:
|
|
_ = self.docker_client.images.get("ubuntu:22.04")
|
|
except Exception:
|
|
self.docker_client.images.pull("ubuntu:22.04")
|
|
|
|
def pull_images(self):
|
|
"""
|
|
Pull missing Docker images required for all instances.
|
|
"""
|
|
self._check_images()
|
|
ids = self.instance_ids if self.instance_ids else list(self.image_status.keys())
|
|
print(f"Total number of images: {len(ids)}")
|
|
instance_ids = [instance_id for instance_id in ids if not self.image_status[instance_id]]
|
|
print(f"Number of images to download: {len(instance_ids)}")
|
|
if len(instance_ids) == 0:
|
|
return
|
|
for instance_id in tqdm(instance_ids, desc="Downloading images"):
|
|
image_name = self._image_name(instance_id)
|
|
self.docker_client.images.pull(image_name)
|
|
|
|
def prepare_trae_agent(self):
|
|
"""
|
|
Build Trae Agent and UV inside a base Ubuntu container.
|
|
Save built artifacts to workspace for later use in experiment containers.
|
|
"""
|
|
tars = ["trae-agent.tar", "uv.tar", "uv_shared.tar"]
|
|
all_exist = all((self.working_dir / tar).exists() for tar in tars)
|
|
if all_exist:
|
|
print("Found built trae-agent and uv artifacts. Skipping building.")
|
|
return
|
|
|
|
try:
|
|
image = self.docker_client.images.get("ubuntu:22.04")
|
|
except Exception:
|
|
image = self.docker_client.images.pull("ubuntu:22.04")
|
|
|
|
repo_root_path = Path(__file__).parent.parent
|
|
assert (repo_root_path / "trae_agent" / "__init__.py").is_file()
|
|
|
|
container = self.docker_client.containers.run(
|
|
image=image,
|
|
command="bash",
|
|
detach=True,
|
|
tty=True,
|
|
stdin_open=True,
|
|
volumes={
|
|
self.working_dir.absolute().as_posix(): {"bind": "/trae-workspace", "mode": "rw"},
|
|
repo_root_path.absolute().as_posix(): {"bind": "/trae-src", "mode": "ro"},
|
|
},
|
|
environment=self.docker_env_config.get("preparation_env", None),
|
|
)
|
|
|
|
build_commands = [
|
|
"apt-get update",
|
|
"apt-get install -y curl",
|
|
"curl -LsSf https://astral.sh/uv/install.sh | sh",
|
|
"rm -rf /trae-workspace/trae-agent && mkdir /trae-workspace/trae-agent",
|
|
"cp -r -t /trae-workspace/trae-agent/ /trae-src/trae_agent /trae-src/.python-version /trae-src/pyproject.toml /trae-src/uv.lock /trae-src/README.md",
|
|
"cd /trae-workspace/trae-agent && source $HOME/.local/bin/env && uv sync",
|
|
]
|
|
|
|
for command in tqdm(
|
|
build_commands, desc="Building trae-agent inside base Docker container"
|
|
):
|
|
try:
|
|
new_command = f'/bin/bash -c "{command}"'
|
|
return_code, output = docker_exec(container, new_command)
|
|
except Exception:
|
|
print(f"{command} failed.")
|
|
print(traceback.format_exc())
|
|
break
|
|
if return_code is not None and return_code != 0:
|
|
print("Docker exec error. Error message: {}".format(output))
|
|
container.stop()
|
|
container.remove()
|
|
exit(-1)
|
|
|
|
for tar_name, src_path in [
|
|
("trae-agent.tar", "/trae-workspace/trae-agent"),
|
|
("uv.tar", "/root/.local/bin/uv"),
|
|
("uv_shared.tar", "/root/.local/share/uv"),
|
|
]:
|
|
try:
|
|
with open(self.working_dir / tar_name, "wb") as f:
|
|
bits, _ = container.get_archive(src_path)
|
|
for chunk in bits:
|
|
f.write(chunk)
|
|
except Exception:
|
|
print(f"Failed to save {tar_name} from container.")
|
|
|
|
container.stop()
|
|
container.remove()
|
|
|
|
def prepare_experiment_container(self, instance: dict[str, str]) -> Container:
|
|
"""
|
|
Prepare experiment Docker container for a given instance.
|
|
The container mounts the results directory for this instance,
|
|
so all outputs are directly accessible on the host.
|
|
Args:
|
|
instance: Instance dictionary.
|
|
Returns:
|
|
Docker container object.
|
|
"""
|
|
|
|
image_name = self._image_name(instance["instance_id"])
|
|
instance_result_dir = self.task_results_dir / instance["instance_id"]
|
|
instance_result_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
self.config.problem_statement(instance, instance_result_dir)
|
|
|
|
container: Container = self.docker_client.containers.run(
|
|
image_name,
|
|
command="/bin/bash",
|
|
detach=True,
|
|
tty=True,
|
|
stdin_open=True,
|
|
volumes={
|
|
instance_result_dir.absolute().as_posix(): {"bind": "/instance-data", "mode": "rw"},
|
|
},
|
|
working_dir="/trae-workspace",
|
|
environment=self.docker_env_config.get("experiment_env", None),
|
|
stream=True,
|
|
)
|
|
|
|
for fname in ["trae-agent.tar", "uv.tar", "uv_shared.tar", "trae_config.yaml"]:
|
|
tar_stream = io.BytesIO()
|
|
with tarfile.open(fileobj=tar_stream, mode="w") as tar:
|
|
tar.add(self.working_dir / fname, arcname=fname)
|
|
tar_stream.seek(0)
|
|
container.put_archive("/trae-workspace", tar_stream.getvalue())
|
|
|
|
setup_commands = [
|
|
"tar xf trae-agent.tar",
|
|
"tar xf uv.tar",
|
|
"mkdir -p /root/.local/bin",
|
|
"mv uv /root/.local/bin/",
|
|
"tar xf uv_shared.tar",
|
|
"mkdir -p /root/.local/share",
|
|
"mv uv /root/.local/share/",
|
|
]
|
|
for command in setup_commands:
|
|
try:
|
|
new_command = f'/bin/bash -c "{command}"'
|
|
return_code, output = docker_exec(container, new_command)
|
|
if return_code is not None and return_code != 0:
|
|
print("Docker exec error. Error message: {}".format(output))
|
|
except Exception:
|
|
print(f"{command} failed.")
|
|
print(traceback.format_exc())
|
|
break
|
|
return container
|
|
|
|
def run_one_instance(self, instance_id: str):
|
|
"""
|
|
Run patch generation for a single instance.
|
|
All outputs are written directly to the mounted results directory.
|
|
Args:
|
|
instance_id: Instance identifier.
|
|
"""
|
|
instance = next((inst for inst in self.dataset if inst["instance_id"] == instance_id), None)
|
|
if instance is None:
|
|
print(f"Instance {instance_id} not found.")
|
|
return
|
|
|
|
working_dir = self.config.working_dir(instance_id)
|
|
|
|
container_problem_statement_path = "/instance-data/problem_statement.txt"
|
|
container_patch_file_path = f"/instance-data/{instance_id}.patch"
|
|
container_traj_path = f"/instance-data/{instance_id}.json"
|
|
|
|
container = self.prepare_experiment_container(instance)
|
|
command = (
|
|
f"source trae-agent/.venv/bin/activate && "
|
|
f"trae-cli run --file {container_problem_statement_path} "
|
|
f'--working-dir="{working_dir}" '
|
|
f"--config-file trae_config.yaml --must-patch "
|
|
f"--patch-path {container_patch_file_path} --trajectory-file {container_traj_path}"
|
|
)
|
|
new_command = f"/bin/bash -c '{command}'"
|
|
try:
|
|
return_code, output = docker_exec(container, new_command)
|
|
if return_code is not None and return_code != 0:
|
|
print("Docker exec error. Error message: {}".format(output))
|
|
except Exception:
|
|
print(f"{command} failed.")
|
|
print(traceback.format_exc())
|
|
|
|
container.stop()
|
|
container.remove()
|
|
|
|
def run_all(self):
|
|
"""
|
|
Run patch generation for all instances in the dataset, with parallelism controlled by max_workers.
|
|
"""
|
|
instance_ids = [instance["instance_id"] for instance in self.dataset]
|
|
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
futures = {
|
|
executor.submit(self.run_one_instance, instance_id): instance_id
|
|
for instance_id in instance_ids
|
|
}
|
|
for future in tqdm(
|
|
as_completed(futures), total=len(futures), desc="Running all instances"
|
|
):
|
|
instance_id = futures[future]
|
|
try:
|
|
future.result()
|
|
except Exception as e:
|
|
print(f"Instance {instance_id} failed: {e}")
|
|
|
|
def run_eval(self):
|
|
"""
|
|
Run evaluation using the benchmark harness.
|
|
Evaluation results and predictions.json are stored in the task results directory.
|
|
"""
|
|
self.config.evaluate_harness_before(
|
|
self.task_results_dir, self.dataset_name, self.max_workers
|
|
)
|
|
|
|
benchmark_harness_path = Path(self.benchmark_harness_path)
|
|
cmd = self.config.evaluate_harness(
|
|
self.dataset_name, self.task_results_dir, self.task_id, self.max_workers
|
|
)
|
|
process = subprocess.run(cmd, capture_output=True, cwd=benchmark_harness_path.as_posix())
|
|
print(process.stdout.decode())
|
|
print(process.stderr.decode())
|
|
|
|
result_filename = "results.json"
|
|
result_path = self.task_results_dir / result_filename
|
|
print(f"Evaluation completed and file saved to {result_path}")
|
|
|
|
self.config.evaluate_harness_after(self.benchmark_harness_path, self.task_id)
|
|
|
|
def get_all_preds(self, instance_ids: list[str] | None = None):
|
|
"""
|
|
Collect all generated patches and write predictions.json to results directory.
|
|
|
|
Args:
|
|
instance_ids: List of instance IDs to collect (optional).
|
|
"""
|
|
preds: list[dict[str, str]] = []
|
|
if not instance_ids:
|
|
instance_ids = [instance["instance_id"] for instance in self.dataset]
|
|
for instance_id in instance_ids:
|
|
patch_path = self.task_results_dir / instance_id / f"{instance_id}.patch"
|
|
if not patch_path.exists():
|
|
continue
|
|
with open(patch_path, "r") as f:
|
|
patch = f.read()
|
|
preds.append(
|
|
{
|
|
"instance_id": instance_id,
|
|
"model_name_or_path": "trae-agent",
|
|
"model_patch": patch,
|
|
}
|
|
)
|
|
with open(self.task_results_dir / "predictions.json", "w") as f:
|
|
json.dump(preds, f)
|
|
|
|
|
|
def main():
|
|
"""
|
|
Main entry point for benchmark evaluation script.
|
|
Parses command-line arguments and runs patch generation and/or evaluation.
|
|
"""
|
|
argument_parser = argparse.ArgumentParser()
|
|
argument_parser.add_argument(
|
|
"--benchmark", type=str, default="SWE-bench", help="Benchmark name."
|
|
)
|
|
argument_parser.add_argument(
|
|
"--dataset", type=str, default="SWE-bench_Verified", help="Dataset name."
|
|
)
|
|
argument_parser.add_argument(
|
|
"--working-dir", type=str, default="./trae-workspace", help="Workspace directory."
|
|
)
|
|
argument_parser.add_argument(
|
|
"--config-file", type=str, default="trae_config.yaml", help="Trae agent config file path."
|
|
)
|
|
argument_parser.add_argument(
|
|
"--docker-env-config", type=str, default="", required=False, help="Docker env config file."
|
|
)
|
|
argument_parser.add_argument(
|
|
"--instance_ids",
|
|
nargs="+",
|
|
type=str,
|
|
help="Instance IDs to run (space separated).",
|
|
)
|
|
argument_parser.add_argument(
|
|
"--benchmark-harness-path",
|
|
type=str,
|
|
default="",
|
|
required=False,
|
|
help="Path to benchmark harness (for evaluation).",
|
|
)
|
|
argument_parser.add_argument(
|
|
"--run-id",
|
|
type=str,
|
|
required=False,
|
|
default="trae-agent",
|
|
help="Run ID for benchmark evaluation.",
|
|
)
|
|
argument_parser.add_argument(
|
|
"--mode",
|
|
type=str,
|
|
choices=["e2e", "expr", "eval"],
|
|
default="e2e",
|
|
help="e2e: both patch generation and evaluation; expr: only patch generation; eval: only evaluation.",
|
|
)
|
|
argument_parser.add_argument(
|
|
"--max_workers", type=int, default=4, help="Maximum number of parallel workers."
|
|
)
|
|
|
|
args = argument_parser.parse_args()
|
|
evaluation = BenchmarkEvaluation(
|
|
args.benchmark,
|
|
args.working_dir,
|
|
args.config_file,
|
|
args.dataset,
|
|
args.docker_env_config,
|
|
args.benchmark_harness_path,
|
|
args.run_id,
|
|
args.max_workers,
|
|
args.instance_ids,
|
|
)
|
|
|
|
evaluation.prepare_trae_agent()
|
|
|
|
# Patch generation (expr/e2e mode)
|
|
if args.mode in ("e2e", "expr"):
|
|
if args.instance_ids:
|
|
print(f"Running specified instances: {args.instance_ids}")
|
|
with ThreadPoolExecutor(max_workers=args.max_workers) as executor:
|
|
futures = {
|
|
executor.submit(evaluation.run_one_instance, instance_id): instance_id
|
|
for instance_id in args.instance_ids
|
|
}
|
|
for future in tqdm(
|
|
as_completed(futures), total=len(futures), desc="Running instances"
|
|
):
|
|
instance_id = futures[future]
|
|
try:
|
|
future.result()
|
|
except Exception as e:
|
|
print(f"Instance {instance_id} failed: {e}")
|
|
else:
|
|
print("Running all instances in dataset.")
|
|
evaluation.run_all()
|
|
|
|
# Evaluation (eval/e2e mode)
|
|
if args.mode in ("e2e", "eval"):
|
|
evaluation.get_all_preds(args.instance_ids)
|
|
evaluation.run_eval()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|