Download src/deepagents_rl/harness/swe_docker_eval.py from vanishingradient/deepagents-rl-coding: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/vanishingradient/deepagents-rl-coding/resolve/main/src/deepagents_rl/harness/swe_docker_eval.py
- Command line
-
hf download hf://vanishingradient/deepagents-rl-coding/src/deepagents_rl/harness/swe_docker_eval.py
-
curl -L -o swe_docker_eval.py https://huggingface.co/vanishingradient/deepagents-rl-coding/resolve/main/src/deepagents_rl/harness/swe_docker_eval.py
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)) | |