narcolepticchicken commited on
Commit
29c4a80
Β·
verified Β·
1 Parent(s): 90ed4c7

Upload batch_validate.py

Browse files
Files changed (1) hide show
  1. batch_validate.py +261 -0
batch_validate.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Batch Cascade Validation Script
3
+
4
+ Runs the cascade agent on MULTIPLE SWE-bench instances and
5
+ verifies each patch via conda environment + pytest.
6
+
7
+ This is the script that proves the cascade causally, not just
8
+ correlationally from trace simulation.
9
+
10
+ Usage:
11
+ python batch_validate.py --instances 3 --target cascade-only
12
+ python batch_validate.py --instances 5 --target all
13
+
14
+ Requirements: hf_jobs with a10g-largex2, 8h timeout
15
+ """
16
+
17
+ import json, os, re, subprocess, sys, tempfile, time, traceback
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+
21
+ # ============================================================
22
+ # INSTANCE LISTS
23
+ # ============================================================
24
+ CASCADE_ONLY = [
25
+ "astropy__astropy-14365", "astropy__astropy-14995",
26
+ "django__django-11815", "django__django-13089",
27
+ "django__django-13807", "django__django-14315",
28
+ "matplotlib__matplotlib-25224", "matplotlib__matplotlib-25311",
29
+ "sympy__sympy-19487", "sympy__sympy-20590",
30
+ ]
31
+
32
+ FRONTIER_ONLY = [
33
+ "django__django-12453", "django__django-14030",
34
+ "django__django-14349", "django__django-14855",
35
+ "django__django-15098", "django__django-16235",
36
+ "matplotlib__matplotlib-26020", "psf__requests-6028",
37
+ "pylint-dev__pylint-7080", "scikit-learn__scikit-learn-13439",
38
+ "scikit-learn__scikit-learn-14087", "sphinx-doc__sphinx-10323",
39
+ "sphinx-doc__sphinx-10466", "sphinx-doc__sphinx-10614",
40
+ ]
41
+
42
+ def sh(cmd, cwd=None, timeout=120):
43
+ r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout, shell=True)
44
+ return r.returncode, r.stdout, r.stderr
45
+
46
+ def ensure_conda():
47
+ for p in [os.path.expanduser("~/miniconda3/bin/conda"), "/opt/conda/bin/conda"]:
48
+ if os.path.exists(p): return p
49
+ print("πŸ“¦ Installing Miniconda...")
50
+ sh("wget -q https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && bash /tmp/miniconda.sh -b -p $HOME/miniconda3", timeout=300)
51
+ p = os.path.expanduser("~/miniconda3/bin/conda")
52
+ sh(f"{p} config --set always_yes yes --set changeps1 no && {p} tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main 2>/dev/null; true", timeout=30)
53
+ os.environ["PATH"] = os.path.expanduser("~/miniconda3/bin:") + os.environ.get("PATH", "")
54
+ return p
55
+
56
+ def is_valid_patch(text):
57
+ return bool(text) and len(text) > 10 and 'diff --git' in text and '@@' in text
58
+
59
+ def extract_patch(text):
60
+ m = re.search(r'<patch>\s*\n?(.*?)</patch>', text, re.DOTALL)
61
+ if m and is_valid_patch(m.group(1)): return m.group(1).strip()
62
+ m = re.search(r'```diff\s*\n(.*?)```', text, re.DOTALL)
63
+ if m and is_valid_patch(m.group(1)): return m.group(1).strip()
64
+ di = text.find('diff --git')
65
+ if di >= 0:
66
+ patch = text[di:].strip()[:2000]
67
+ if is_valid_patch(patch): return patch
68
+ return None
69
+
70
+ def call_model(client, messages, max_tokens=4096):
71
+ try:
72
+ c = client.chat.completions.create(model=client.model, messages=messages, max_tokens=max_tokens, temperature=0.2)
73
+ t = c.choices[0].message.content
74
+ it = c.usage.prompt_tokens if hasattr(c,'usage') and c.usage else 0
75
+ ot = c.usage.completion_tokens if hasattr(c,'usage') and c.usage else len(t)//4
76
+ return t, it, ot
77
+ except Exception as e: return f"[ERROR: {e}]", 0, 0
78
+
79
+ def run_cascade(instance, repo_dir, conda, env_name):
80
+ from huggingface_hub import InferenceClient
81
+ T1, T2 = "meta-llama/Llama-3.1-8B-Instruct", "meta-llama/Llama-3.3-70B-Instruct"
82
+
83
+ system = f"Fix bug in {instance['repo']}. Repo: {repo_dir}.\n<bash>cmd</bash>\n<patch>\ndiff --git a/path b/path\n--- a/path\n+++ b/path\n@@ -N,M +N,M @@\nchanges\n</patch>\n<submit>Done</submit>"
84
+ messages = [{"role":"system","content":system},{"role":"user","content":f"PROBLEM:\n{instance.get('problem_statement','')}\n\nExplore and fix."}]
85
+
86
+ for tier, mid, mt in [("T1",T1,30),("T2",T2,30)]:
87
+ print(f"\n[{tier}] {mid}")
88
+ client = InferenceClient(mid)
89
+ ti = to = 0
90
+ for turn in range(mt):
91
+ text, it, ot = call_model(client, messages, 4096)
92
+ ti += it; to += ot
93
+ messages.append({"role":"assistant","content":text})
94
+ print(f" T{turn+1}: {it}+{ot} tok")
95
+
96
+ patch = extract_patch(text)
97
+ if patch:
98
+ (Path(repo_dir)/"_c.patch").write_text(patch)
99
+ rc, out, err = sh(f"cd {repo_dir} && git apply --check _c.patch 2>&1", timeout=10)
100
+ if rc == 0:
101
+ print(f" βœ… VALID ({len(patch)}ch)")
102
+ return {"patch":patch,"tier":tier,"turns":turn+1,"input_tokens":ti,"output_tokens":to}
103
+ print(f" ❌ Invalid: {err[:100]}")
104
+ messages.append({"role":"user","content":f"Patch check failed: {err[:200]}\nUse git diff for valid unified diff."})
105
+ continue
106
+
107
+ for cmd in re.findall(r'<bash>(.*?)</bash>', text, re.DOTALL):
108
+ cmd = cmd.strip().replace("pytest", f"{conda} run -n {env_name} python -m pytest")
109
+ rc, out, err = sh(cmd, cwd=str(repo_dir), timeout=60)
110
+ o = f"<output>\n{(out+err)[:1500]}\n</output>"
111
+ if rc: o = o[:-9] + f" [EXIT:{rc}]\n</output>"
112
+ messages.append({"role":"user","content":o})
113
+
114
+ if "<submit>" in text: break
115
+ return {"patch":None,"tier":None,"turns":0,"input_tokens":0,"output_tokens":0}
116
+
117
+ def verify_patch(instance, patch, repo_dir, conda, env_name):
118
+ base = instance.get("base_commit","")
119
+ tp = instance.get("test_patch","")
120
+ f2p = instance.get("FAIL_TO_PASS",[])
121
+
122
+ sh(f"cd {repo_dir} && git checkout -f {base} && git clean -fd", timeout=30)
123
+
124
+ (Path(repo_dir)/"_aco.patch").write_text(patch)
125
+ rc, out, err = sh(f"cd {repo_dir} && git apply --check _aco.patch", timeout=10)
126
+ if rc: return {"resolved":False,"error":f"patch check: {err[:150]}"}
127
+ sh(f"cd {repo_dir} && git apply _aco.patch", timeout=10)
128
+
129
+ (Path(repo_dir)/"_t.patch").write_text(tp)
130
+ sh(f"cd {repo_dir} && (git apply _t.patch) || git apply --reject _t.patch 2>/dev/null; true", timeout=10)
131
+
132
+ cmd = f"cd {repo_dir} && {conda} run -n {env_name} python -m pytest -v --tb=short -x {' '.join(f2p[:10])}"
133
+ rc, out, err = sh(cmd, timeout=300)
134
+
135
+ if rc == 0:
136
+ p2p = instance.get("PASS_TO_PASS",[])
137
+ if p2p:
138
+ cmd2 = f"cd {repo_dir} && {conda} run -n {env_name} python -m pytest -v --tb=short -x {' '.join(p2p[:10])}"
139
+ rc2, out2, err2 = sh(cmd2, timeout=300)
140
+ if rc2: return {"resolved":False,"error":f"P2P: {(out2+err2)[:200]}"}
141
+ return {"resolved":True,"test_output":(out+err)[:500]}
142
+ return {"resolved":False,"error":f"{len(re.findall(r'FAILED', out+err))} F2P failures","test_output":(out+err)[:500]}
143
+
144
+ def setup_env(conda, instance, repo_dir, env_name):
145
+ ec = instance.get("environment_setup_commit","")
146
+ if ec:
147
+ sh(f"cd {repo_dir} && git fetch origin {ec} && git checkout {ec}", timeout=60)
148
+
149
+ eyml = None
150
+ for c in ["environment.yml","dev/environment.yml",".github/environment.yml","ci/environment.yml"]:
151
+ if (Path(repo_dir)/c).exists(): eyml = c; break
152
+
153
+ if eyml:
154
+ rc, out, err = sh(f"{conda} env create -f {repo_dir}/{eyml} -n {env_name} --quiet", timeout=600)
155
+ else:
156
+ rc, out, err = sh(f"{conda} create -n {env_name} python=3.10 pip -y", timeout=300)
157
+
158
+ if rc:
159
+ rc, out, err = sh(f"{conda} create -n {env_name} python=3.10 pip -y", timeout=300)
160
+ if rc: return False, f"conda: {err[:200]}"
161
+
162
+ base = instance["base_commit"]
163
+ sh(f"cd {repo_dir} && git fetch origin {base} && git checkout {base}", timeout=60)
164
+ sh(f"cd {repo_dir} && {conda} run -n {env_name} pip install -e . 2>&1 | tail -3", timeout=300)
165
+ sh(f"cd {repo_dir} && {conda} run -n {env_name} pip install . 2>&1 | tail -3", timeout=300)
166
+ return True, ""
167
+
168
+ def main():
169
+ import datasets
170
+ target = os.environ.get("INSTANCE_TARGET", "cascade-only")
171
+ max_instances = int(os.environ.get("MAX_INSTANCES", "3"))
172
+
173
+ print(f"πŸš€ BATCH CASCADE VALIDATION β€” target={target} max={max_instances}")
174
+ print(f" {datetime.now().isoformat()}")
175
+
176
+ conda = ensure_conda()
177
+ if not conda: print("❌ No conda"); sys.exit(1)
178
+
179
+ ds = datasets.load_dataset("princeton-nlp/SWE-bench_Verified", split="test")
180
+
181
+ if target == "cascade-only":
182
+ iids = CASCADE_ONLY[:max_instances]
183
+ elif target == "frontier-only":
184
+ iids = FRONTIER_ONLY[:max_instances]
185
+ else:
186
+ iids = [r["instance_id"] for r in ds][:max_instances]
187
+
188
+ instances = {}
189
+ for row in ds:
190
+ if row["instance_id"] in iids:
191
+ instances[row["instance_id"]] = dict(row)
192
+
193
+ print(f"Instances: {iids}\n")
194
+
195
+ results = []
196
+ for i, iid in enumerate(iids):
197
+ instance = instances.get(iid)
198
+ if not instance: continue
199
+
200
+ print(f"\n{'='*60}\n[{i+1}/{len(iids)}] {iid}\n{'='*60}")
201
+
202
+ with tempfile.TemporaryDirectory(prefix=f"aco_{i}_") as tmpdir:
203
+ repo_dir = Path(tmpdir) / "repo"
204
+ env_name = f"aco_{iid.replace('__','_').replace('-','_')[:30]}"
205
+
206
+ print(f"Clone...")
207
+ url = f"https://github.com/{instance['repo']}.git"
208
+ rc, out, err = sh(f"git clone --depth 100 {url} {repo_dir}", timeout=600)
209
+ if rc:
210
+ results.append({"instance_id":iid,"resolved":False,"error":f"Clone: {err[:200]}"})
211
+ continue
212
+
213
+ print(f"Env...")
214
+ ok, err = setup_env(conda, instance, repo_dir, env_name)
215
+ if not ok:
216
+ results.append({"instance_id":iid,"resolved":False,"error":f"Env: {err}"})
217
+ continue
218
+
219
+ print(f"Cascade...")
220
+ agent = run_cascade(instance, repo_dir, conda, env_name)
221
+ if not agent["patch"]:
222
+ results.append({"instance_id":iid,"resolved":False,"tier":None,"error":"No valid patch"})
223
+ sh(f"{conda} env remove -n {env_name} -y --quiet 2>/dev/null; true", timeout=30)
224
+ continue
225
+
226
+ print(f"Verify...")
227
+ verify = verify_patch(instance, agent["patch"], repo_dir, conda, env_name)
228
+
229
+ r = {
230
+ "instance_id":iid, "repo":instance["repo"],
231
+ "resolved":verify["resolved"], "tier":agent["tier"],
232
+ "turns":agent["turns"], "input_tokens":agent["input_tokens"],
233
+ "output_tokens":agent["output_tokens"],
234
+ "error":verify.get("error"),
235
+ "timestamp":datetime.now().isoformat()
236
+ }
237
+ results.append(r)
238
+
239
+ status = "βœ…" if verify["resolved"] else "❌"
240
+ print(f" {status} {agent['tier']} {agent['turns']}t")
241
+
242
+ sh(f"{conda} env remove -n {env_name} -y --quiet 2>/dev/null; true", timeout=30)
243
+
244
+ # Incremental save
245
+ with open("batch_results.jsonl","w") as f:
246
+ for r in results: f.write(json.dumps(r)+"\n")
247
+
248
+ resolved = [r for r in results if r["resolved"]]
249
+ t1 = [r for r in resolved if r.get("tier")=="T1"]
250
+ t2 = [r for r in resolved if r.get("tier")=="T2"]
251
+
252
+ print(f"\n{'='*60}\nRESULTS: {len(resolved)}/{len(results)} resolved")
253
+ print(f" T1: {len(t1)} T2: {len(t2)}")
254
+ for r in results:
255
+ s = "βœ…" if r["resolved"] else "❌"
256
+ print(f" {s} {r['instance_id']} [{r.get('tier','')}]")
257
+ print(f"Saved: batch_results.jsonl")
258
+
259
+ if __name__=="__main__":
260
+ try: sys.exit(main())
261
+ except Exception as e: print(f"πŸ’₯ {e}"); traceback.print_exc(); sys.exit(1)