vanishingradient's picture
Add deepagents_rl package sources
4d620d9 verified
Raw History Blame Contribute Delete
17.5 kB
"""Official-style SWE-bench evaluation via the instance Docker image.
Protocol (matches SWE-bench harness):
1. Start the prebuilt instance image (`task['image']`)
2. Apply the model's git patch into `/testbed`
3. Run the instance `eval_script` (applies test_patch, runs pytest)
4. Parse status between `>>>>> Start/End Test Output`
5. Resolve = all FAIL_TO_PASS pass AND all PASS_TO_PASS pass
Uses the Docker CLI (not the Python SDK) for reliability on Windows.
"""
from __future__ import annotations
import json
import shutil
import subprocess
import tempfile
import time
import uuid
from pathlib import Path
from typing import Any
from deepagents_rl.observability.logging import call, error, result, span, step, truncate
START_TEST_OUTPUT = ">>>>> Start Test Output"
END_TEST_OUTPUT = ">>>>> End Test Output"
GIT_APPLY_CMDS = [
"git apply --verbose",
"git apply --verbose --3way",
"git apply --verbose --reject",
]
def fix_eval_script_for_docker_desktop(script: str) -> str:
"""
Upstream eval_script uses `conda activate testbed`. On Docker Desktop the
image's *base* conda entrypoint is often broken (`conda` -> TypeError), while
`/opt/miniconda3/envs/testbed/bin/*` works. Prefetch that env onto PATH and
stub `conda activate` so the official test_patch + pytest body still runs.
"""
preamble = (
"#!/bin/bash\n"
"set -uxo pipefail\n"
'export PATH="/opt/miniconda3/envs/testbed/bin:/opt/miniconda3/bin:$PATH"\n'
"conda() {\n"
' if [ "$1" = "activate" ]; then\n'
' export PATH="/opt/miniconda3/envs/${2:-testbed}/bin:$PATH"\n'
" hash -r\n"
" return 0\n"
" fi\n"
" return 0\n"
"}\n"
)
body: list[str] = []
for line in script.replace("\r\n", "\n").splitlines():
if line.startswith("#!"):
continue
if line.strip().startswith("source /opt/miniconda3/bin/activate"):
continue
if "conda activate" in line:
continue
body.append(line)
return preamble + "\n".join(body) + "\n"
def parse_log_pytest(log: str) -> dict[str, str]:
"""SWE-bench pytest log parser (status token first on the line)."""
status_map: dict[str, str] = {}
statuses = ("PASSED", "FAILED", "ERROR", "SKIPPED", "XFAIL", "XPASS")
for line in log.splitlines():
if not any(line.startswith(s) for s in statuses):
continue
if line.startswith("FAILED"):
line = line.replace(" - ", " ")
parts = line.split()
if len(parts) <= 1:
continue
status_map[parts[1]] = parts[0]
return status_map
def _extract_test_region(log: str) -> str | None:
if START_TEST_OUTPUT not in log or END_TEST_OUTPUT not in log:
return None
return log.split(START_TEST_OUTPUT, 1)[1].split(END_TEST_OUTPUT, 1)[0]
def _test_passed(case: str, sm: dict[str, str]) -> bool:
return case in sm and sm[case] in ("PASSED", "XFAIL")
def _test_failed(case: str, sm: dict[str, str]) -> bool:
return case not in sm or sm[case] in ("FAILED", "ERROR", "SKIPPED")
def _test_maintained(case: str, sm: dict[str, str]) -> bool:
return _test_passed(case, sm) or (case in sm and sm[case] == "SKIPPED")
def grade_status_map(
status_map: dict[str, str],
fail_to_pass: list[str],
pass_to_pass: list[str],
) -> dict[str, Any]:
"""Official resolution: FULL iff all F2P pass and all P2P maintained."""
f2p_ok = [t for t in fail_to_pass if _test_passed(t, status_map)]
f2p_fail = [t for t in fail_to_pass if _test_failed(t, status_map)]
p2p_ok = [t for t in pass_to_pass if _test_maintained(t, status_map)]
p2p_fail = [t for t in pass_to_pass if not _test_maintained(t, status_map)]
f2p_frac = (len(f2p_ok) / len(fail_to_pass)) if fail_to_pass else 1.0
p2p_frac = (len(p2p_ok) / len(pass_to_pass)) if pass_to_pass else 1.0
if f2p_frac == 1.0 and p2p_frac == 1.0:
resolved = "FULL"
elif 0.0 < f2p_frac < 1.0 and p2p_frac == 1.0:
resolved = "PARTIAL"
else:
resolved = "NO"
return {
"resolved": resolved == "FULL",
"resolution": resolved,
"fail_to_pass": {"success": f2p_ok, "failure": f2p_fail, "frac": f2p_frac},
"pass_to_pass": {"success": p2p_ok, "failure": p2p_fail, "frac": p2p_frac},
"pass_frac": float(f2p_frac),
# P2P regressions discount reward (cannot claim FULL/PARTIAL)
"reward_total": float(f2p_frac if p2p_frac == 1.0 else f2p_frac * 0.5),
"status_map": status_map,
}
def _docker(
*args: str,
timeout: int | None = None,
merge_stderr: bool = False,
) -> subprocess.CompletedProcess[str]:
exe = shutil.which("docker")
if not exe:
raise RuntimeError("docker CLI not found on PATH")
cmdline = "docker " + " ".join(args)
call("docker", cmdline, timeout_s=timeout, merge_stderr=merge_stderr)
t0 = time.perf_counter()
try:
if merge_stderr:
proc = subprocess.run(
[exe, *args],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=timeout,
encoding="utf-8",
errors="replace",
)
else:
proc = subprocess.run(
[exe, *args],
capture_output=True,
text=True,
timeout=timeout,
encoding="utf-8",
errors="replace",
)
except subprocess.TimeoutExpired:
error("docker", f"timeout after {timeout}s", cmd=cmdline)
raise
dt = int((time.perf_counter() - t0) * 1000)
out = (proc.stdout or "") + ("" if merge_stderr else (proc.stderr or ""))
result(
"docker",
f"exit={proc.returncode} ({dt}ms)",
exit_code=proc.returncode,
duration_ms=dt,
output=truncate(out, 800),
)
return proc
def evaluate_model_patch(
task: dict[str, Any],
model_patch: str,
*,
timeout_s: int = 1800,
log_dir: Path | None = None,
keep_container: bool = False,
) -> dict[str, Any]:
"""
Grade one prediction with the official instance Docker image + eval_script.
`task` must include: instance_id, image, eval_script, fail_to_pass, pass_to_pass
"""
instance_id = task["instance_id"]
image = task.get("image")
eval_script = task.get("eval_script") or ""
fail_to_pass = list(task.get("fail_to_pass") or [])
pass_to_pass = list(task.get("pass_to_pass") or [])
if not image:
raise ValueError(f"{instance_id}: missing Docker image field from SWE-bench Lite")
if not eval_script.strip():
raise ValueError(f"{instance_id}: missing eval_script")
if log_dir is None:
log_dir = Path(tempfile.mkdtemp(prefix="swe_eval_"))
log_dir.mkdir(parents=True, exist_ok=True)
report: dict[str, Any] = {
"instance_id": instance_id,
"image": image,
"patch_exists": bool(model_patch and model_patch.strip()),
"patch_successfully_applied": False,
"resolved": False,
"resolution": "NO",
"pass_frac": 0.0,
"reward_total": 0.0,
"fail_to_pass": {"success": [], "failure": fail_to_pass, "frac": 0.0},
"pass_to_pass": {"success": [], "failure": [], "frac": 0.0},
"status_map": {},
"error": None,
"log_dir": str(log_dir),
}
step(
"eval.begin",
f"instance={instance_id}",
image=image,
patch_bytes=len((model_patch or "").encode("utf-8")),
n_f2p=len(fail_to_pass),
n_p2p=len(pass_to_pass),
log_dir=str(log_dir),
)
if not model_patch or not model_patch.strip():
step("eval.empty_patch", "no model_patch — unresolved")
report["error"] = "empty_model_patch"
(log_dir / "report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
return report
patch_path = log_dir / "model.patch"
eval_path = log_dir / "eval.sh"
eval_raw_path = log_dir / "eval.upstream.sh"
patch_path.write_text(model_patch.replace("\r\n", "\n"), encoding="utf-8", newline="\n")
eval_raw_path.write_text(eval_script.replace("\r\n", "\n"), encoding="utf-8", newline="\n")
eval_path.write_text(
fix_eval_script_for_docker_desktop(eval_script),
encoding="utf-8",
newline="\n",
)
step(
"eval.write_files",
"wrote model.patch + eval.sh",
patch_path=str(patch_path),
eval_path=str(eval_path),
patch_preview=truncate(model_patch, 400),
)
run_tag = uuid.uuid4().hex[:10]
container = f"darl-eval-{run_tag}"
try:
with span("eval.docker_lifecycle", container=container, image=image):
inspect = _docker("image", "inspect", image, timeout=60)
if inspect.returncode != 0:
step("eval.docker_pull", f"image missing locally, pulling {image}")
pull = _docker("pull", image, timeout=max(timeout_s, 3600))
if pull.returncode != 0:
report["error"] = f"docker_pull_failed: {(pull.stderr or pull.stdout)[-2000:]}"
error("eval.docker_pull", report["error"])
(log_dir / "report.json").write_text(
json.dumps(report, indent=2), encoding="utf-8"
)
return report
else:
step("eval.docker_image", "image already present")
_docker("rm", "-f", container, timeout=60)
create = _docker(
"create",
"--name",
container,
"--workdir",
"/testbed",
image,
"tail",
"-f",
"/dev/null",
timeout=120,
)
if create.returncode != 0:
report["error"] = (
f"docker_create_failed: {(create.stderr or create.stdout)[-2000:]}"
)
error("eval.docker_create", report["error"])
(log_dir / "report.json").write_text(
json.dumps(report, indent=2), encoding="utf-8"
)
return report
start = _docker("start", container, timeout=120)
if start.returncode != 0:
report["error"] = (
f"docker_start_failed: {(start.stderr or start.stdout)[-2000:]}"
)
error("eval.docker_start", report["error"])
(log_dir / "report.json").write_text(
json.dumps(report, indent=2), encoding="utf-8"
)
return report
step("eval.docker_cp", "copy patch + eval.sh into container")
cp1 = _docker("cp", str(patch_path), f"{container}:/tmp/model.patch", timeout=120)
cp2 = _docker("cp", str(eval_path), f"{container}:/eval.sh", timeout=120)
if cp1.returncode != 0 or cp2.returncode != 0:
report["error"] = (
f"docker_cp_failed: patch={(cp1.stderr or '')[-500:]} "
f"eval={(cp2.stderr or '')[-500:]}"
)
error("eval.docker_cp", report["error"])
(log_dir / "report.json").write_text(
json.dumps(report, indent=2), encoding="utf-8"
)
return report
_docker(
"exec",
"-w",
"/testbed",
container,
"git",
"config",
"--global",
"--add",
"safe.directory",
"/testbed",
timeout=60,
)
apply_log = []
applied = False
step("eval.apply_patch", "trying git apply cascade")
for cmd in GIT_APPLY_CMDS:
parts = cmd.split()
resp = _docker(
"exec",
"-w",
"/testbed",
container,
*parts,
"/tmp/model.patch",
timeout=120,
)
apply_log.append(
{
"cmd": f"{cmd} /tmp/model.patch",
"exit": resp.returncode,
"out": ((resp.stdout or "") + (resp.stderr or ""))[-2000:],
}
)
if resp.returncode == 0:
applied = True
step("eval.apply_patch_ok", cmd)
break
step("eval.apply_patch_retry", f"failed: {cmd}", exit_code=resp.returncode)
(log_dir / "apply_patch.json").write_text(
json.dumps(apply_log, indent=2), encoding="utf-8"
)
if not applied:
report["error"] = "patch_apply_failed"
report["apply_log"] = apply_log
error("eval.apply_patch", "all git apply strategies failed")
(log_dir / "test_output.txt").write_text(
json.dumps(apply_log, indent=2), encoding="utf-8"
)
(log_dir / "report.json").write_text(
json.dumps(report, indent=2), encoding="utf-8"
)
return report
report["patch_successfully_applied"] = True
step("eval.run_eval_script", "bash /eval.sh (official test_patch + pytest)")
t0 = time.time()
eval_proc = _docker(
"exec",
"-w",
"/testbed",
container,
"/bin/bash",
"-lc",
"/bin/bash /eval.sh 2>&1",
timeout=timeout_s,
merge_stderr=True,
)
elapsed = time.time() - t0
test_output = eval_proc.stdout or ""
(log_dir / "test_output.txt").write_text(test_output, encoding="utf-8")
report["eval_exit_code"] = eval_proc.returncode
report["eval_seconds"] = elapsed
step(
"eval.eval_script_done",
f"exit={eval_proc.returncode} seconds={elapsed:.1f}",
output_chars=len(test_output),
output_tail=truncate(test_output, 600),
)
region = _extract_test_region(test_output)
status_map = parse_log_pytest(region or "")
if not status_map:
step("eval.parse_fallback", "marked region empty — parsing full log")
status_map = parse_log_pytest(test_output)
if not status_map:
report["error"] = (
"missing_test_output_markers"
if region is None
else "no_parsed_test_statuses"
)
error("eval.parse", report["error"])
(log_dir / "report.json").write_text(
json.dumps(report, indent=2), encoding="utf-8"
)
return report
step(
"eval.parse_ok",
f"parsed {len(status_map)} test statuses",
sample=list(status_map.items())[:8],
)
graded = grade_status_map(status_map, fail_to_pass, pass_to_pass)
report.update(graded)
report["error"] = None
step(
"eval.grade",
f"resolution={graded['resolution']} f2p={graded['pass_frac']}",
resolved=graded["resolved"],
resolution=graded["resolution"],
pass_frac=graded["pass_frac"],
reward_total=graded["reward_total"],
f2p_success=graded["fail_to_pass"]["success"],
f2p_failure=graded["fail_to_pass"]["failure"],
p2p_frac=graded["pass_to_pass"]["frac"],
)
(log_dir / "report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
return report
except subprocess.TimeoutExpired as e:
report["error"] = f"timeout: {e}"
error("eval.timeout", report["error"])
(log_dir / "report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
return report
except Exception as e: # noqa: BLE001
report["error"] = f"{type(e).__name__}: {e}"
error("eval.exception", report["error"])
(log_dir / "report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
return report
finally:
if not keep_container:
try:
step("eval.cleanup", f"rm container {container}")
_docker("rm", "-f", container, timeout=120)
except Exception as e: # noqa: BLE001
error("eval.cleanup", str(e))