Spaces:
Running
Running
| """ | |
| scripts/local_eval.py (v2) | |
| ββββββββββββββββββββββββββββ | |
| Real SWE-bench evaluation WITHOUT Docker. | |
| Key improvements over v1: | |
| - Clones each repo ONCE and reuses via git stash/checkout | |
| - Properly fetches specific commits | |
| - Handles both patch formats (string & dict) | |
| - Saves disk by not re-cloning for every issue | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| from datetime import datetime, timezone | |
| # ββ Config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| EVAL_INPUT = Path(os.environ.get("EVAL_INPUT", "results/eval/patches_to_eval.jsonl")) | |
| RESULTS_OUT = Path("results/eval/local_eval_results.jsonl") | |
| REPO_CACHE = Path("results/eval/repo_cache") # repos cloned here once | |
| MAX_ISSUES = int(os.environ.get("MAX_EVAL", "16")) | |
| RESULTS_OUT.parent.mkdir(parents=True, exist_ok=True) | |
| REPO_CACHE.mkdir(parents=True, exist_ok=True) | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run(cmd, cwd=None, timeout=600, input_bytes=None): | |
| return subprocess.run(cmd, cwd=cwd, capture_output=True, | |
| timeout=timeout, input=input_bytes) | |
| def ensure_repo(repo: str) -> Path: | |
| """Clone repo once into REPO_CACHE. Re-uses if already there.""" | |
| repo_dir = REPO_CACHE / repo.replace("/", "__") | |
| if not repo_dir.exists(): | |
| url = f"https://github.com/{repo}.git" | |
| print(f" Cloning {repo} (one-time, ~1-2 min)...", flush=True) | |
| run(["git", "clone", url, str(repo_dir)], timeout=900) | |
| return repo_dir | |
| def checkout_commit(repo_dir: Path, commit: str) -> bool: | |
| """Fetch + checkout a specific commit.""" | |
| # Reset any changes first | |
| run(["git", "checkout", "."], cwd=repo_dir, timeout=30) | |
| run(["git", "clean", "-fd"], cwd=repo_dir, timeout=30) | |
| # Try direct checkout | |
| r = run(["git", "checkout", commit], cwd=repo_dir, timeout=30) | |
| if r.returncode == 0: | |
| return True | |
| # Fetch and retry | |
| print(f" Fetching commit {commit[:8]}...", flush=True) | |
| run(["git", "fetch", "origin", commit], cwd=repo_dir, timeout=120) | |
| run(["git", "fetch", "--all"], cwd=repo_dir, timeout=300) | |
| r = run(["git", "checkout", commit], cwd=repo_dir, timeout=30) | |
| return r.returncode == 0 | |
| def install_deps(repo_dir: Path): | |
| run([sys.executable, "-m", "pip", "install", "-e", ".", | |
| "--quiet", "--no-deps", "--no-build-isolation"], | |
| cwd=repo_dir, timeout=180) | |
| def extract_patch(patch_field) -> str: | |
| """Handle both string and dict patch formats.""" | |
| if isinstance(patch_field, dict): | |
| return patch_field.get("patch", "") or "" | |
| return patch_field or "" | |
| def apply_patch(patch_text: str, repo_dir: Path) -> bool: | |
| if not patch_text.strip(): | |
| return False | |
| r = run(["git", "apply", "--whitespace=fix", "-"], | |
| cwd=repo_dir, input_bytes=patch_text.encode(), timeout=30) | |
| if r.returncode != 0: | |
| # Try with --reject (partial apply info) | |
| r2 = run(["git", "apply", "--check", "-"], | |
| cwd=repo_dir, input_bytes=patch_text.encode(), timeout=30) | |
| print(f" Patch check stderr: {r2.stderr.decode()[:200]}", flush=True) | |
| return r.returncode == 0 | |
| def run_tests(repo_dir: Path, test_ids: list[str], timeout=90) -> tuple[bool, str]: | |
| if not test_ids: | |
| return True, "(no tests)" | |
| cmd = [sys.executable, "-m", "pytest", "-v", "--tb=short", | |
| "--no-header", "-x", "--timeout=30"] + test_ids | |
| try: | |
| r = subprocess.run(cmd, capture_output=True, text=True, | |
| cwd=repo_dir, timeout=timeout) | |
| out = r.stdout + r.stderr | |
| return r.returncode == 0, out[-2000:] | |
| except subprocess.TimeoutExpired: | |
| return False, "TIMEOUT" | |
| except Exception as e: | |
| return False, str(e) | |
| def load_instance(instance_id: str) -> dict | None: | |
| """Load SWE-bench instance from HF dataset.""" | |
| try: | |
| from datasets import load_dataset | |
| ds = load_dataset("princeton-nlp/SWE-bench_Lite", split="test", | |
| trust_remote_code=True) | |
| for row in ds: | |
| if row["instance_id"] == instance_id: | |
| return dict(row) | |
| except Exception as e: | |
| print(f" datasets error: {e}", flush=True) | |
| return None | |
| # ββ Main evaluator ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def evaluate(patch_rec: dict) -> dict: | |
| iid = patch_rec["instance_id"] | |
| print(f"\n{'β'*60}") | |
| print(f"βΊ {iid}", flush=True) | |
| patch_text = extract_patch(patch_rec.get("patch", "")) | |
| if not patch_text.strip(): | |
| print(" β Empty patch β skipping") | |
| return {**patch_rec, "eval_resolved": False, "eval_error": "empty patch"} | |
| # Fetch ground truth | |
| print(" Loading SWE-bench instance...", flush=True) | |
| instance = load_instance(iid) | |
| if not instance: | |
| return {**patch_rec, "eval_resolved": False, "eval_error": "HF dataset load failed"} | |
| base_commit = instance["base_commit"] | |
| fail_to_pass = json.loads(instance.get("FAIL_TO_PASS", "[]")) | |
| pass_to_pass = json.loads(instance.get("PASS_TO_PASS", "[]")) | |
| # Limit pass_to_pass to 3 to save time | |
| test_ids = fail_to_pass + pass_to_pass[:3] | |
| print(f" Commit: {base_commit[:10]} | " | |
| f"F2P: {len(fail_to_pass)} | P2P: {len(pass_to_pass[:3])}", flush=True) | |
| # Get/reuse cached repo | |
| repo_dir = ensure_repo(patch_rec["repo"]) | |
| # Checkout exact commit | |
| print(f" Checking out {base_commit[:8]}...", flush=True) | |
| if not checkout_commit(repo_dir, base_commit): | |
| return {**patch_rec, "eval_resolved": False, | |
| "eval_error": f"checkout {base_commit[:8]} failed"} | |
| # Install at this commit | |
| install_deps(repo_dir) | |
| # Apply patch | |
| print(" Applying patch...", flush=True) | |
| if not apply_patch(patch_text, repo_dir): | |
| # Reset and report | |
| run(["git", "checkout", "."], cwd=repo_dir, timeout=30) | |
| return {**patch_rec, "eval_resolved": False, | |
| "eval_error": "patch did not apply", "eval_base_commit": base_commit} | |
| # Run tests | |
| print(f" Running {len(test_ids)} tests...", flush=True) | |
| passed, output = run_tests(repo_dir, test_ids) | |
| # Reset repo | |
| run(["git", "checkout", "."], cwd=repo_dir, timeout=30) | |
| run(["git", "clean", "-fd"], cwd=repo_dir, timeout=30) | |
| status = "β RESOLVED" if passed else "β NOT RESOLVED" | |
| print(f" β {status}") | |
| return { | |
| **patch_rec, | |
| "eval_resolved": passed, | |
| "eval_base_commit": base_commit, | |
| "eval_patch_ok": True, | |
| "eval_fail_to_pass": fail_to_pass, | |
| "eval_output": output[-400:], | |
| "eval_ts": datetime.now(timezone.utc).isoformat(), | |
| } | |
| def main(): | |
| print("π¬ SWE-bench Local Evaluation (v2 β repo cache)") | |
| print(f" Input: {EVAL_INPUT}") | |
| print(f" Output: {RESULTS_OUT}") | |
| print(f" Limit: {MAX_ISSUES} issues\n") | |
| with open(EVAL_INPUT) as f: | |
| patches = [json.loads(l) for l in f if l.strip()][:MAX_ISSUES] | |
| # Load already done | |
| done_ids, eval_results = set(), [] | |
| if RESULTS_OUT.exists(): | |
| with open(RESULTS_OUT) as f: | |
| for line in f: | |
| if line.strip(): | |
| r = json.loads(line) | |
| done_ids.add(r["instance_id"]) | |
| eval_results.append(r) | |
| print(f"Resuming: {len(done_ids)} done, {len(patches)-len(done_ids)} to go\n") | |
| # Install datasets if needed | |
| try: | |
| import datasets # noqa | |
| except ImportError: | |
| subprocess.run([sys.executable, "-m", "pip", "install", "datasets", "-q"]) | |
| resolved = sum(1 for r in eval_results if r.get("eval_resolved")) | |
| for i, patch in enumerate(patches): | |
| iid = patch["instance_id"] | |
| if iid in done_ids: | |
| print(f"[{i+1}/{len(patches)}] SKIP {iid}") | |
| continue | |
| print(f"\n[{i+1}/{len(patches)}]", end="", flush=True) | |
| result = evaluate(patch) | |
| eval_results.append(result) | |
| done_ids.add(iid) | |
| if result.get("eval_resolved"): | |
| resolved += 1 | |
| with open(RESULTS_OUT, "a") as f: | |
| f.write(json.dumps(result) + "\n") | |
| done_count = len([r for r in eval_results if "eval_resolved" in r]) | |
| print(f" Score so far: {resolved}/{done_count} " | |
| f"({100*resolved//max(done_count,1)}%)\n", flush=True) | |
| # Summary | |
| evaluated = [r for r in eval_results if "eval_resolved" in r] | |
| resolved_list = [r for r in evaluated if r.get("eval_resolved")] | |
| print("\n" + "="*60) | |
| print("FINAL RESULTS") | |
| print("="*60) | |
| for r in evaluated: | |
| s = "β " if r.get("eval_resolved") else "β" | |
| err = f" [{r.get('eval_error','')}]" if r.get("eval_error") else "" | |
| print(f" {s} {r['instance_id']}{err}") | |
| print() | |
| print(f"Resolved: {len(resolved_list)}/{len(evaluated)} " | |
| f"({100*len(resolved_list)//max(len(evaluated),1)}%)") | |
| print(f"Saved to: {RESULTS_OUT}") | |
| if __name__ == "__main__": | |
| main() | |