-
Notifications
You must be signed in to change notification settings - Fork 54
Expand file tree
/
Copy pathswe_bench_pro_eval.py
More file actions
575 lines (483 loc) · 22 KB
/
swe_bench_pro_eval.py
File metadata and controls
575 lines (483 loc) · 22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
"""
The script is used to evaluate the performance of the SWEAP Pro agent with Modal.
This evaluation script:
1. Takes a CSV file containing test cases and a JSON file containing patches
2. Runs each patch in a Modal sandbox environment using Docker Hub images
3. Executes the tests using local run scripts and collects results
4. Calculates overall accuracy based on test pass/fail status
Usage:
python sweap_pro_eval_modal.py \
--raw_sample_path=data.csv \
--patch_path={OUTPUT}/gold_patches.json \
--output_dir={OUTPUT}/ \
--scripts_dir=run_scripts \
--num_workers=100 \
--dockerhub_username=your-username
It expects:
- Local run scripts in run_scripts/{instance_id}/run_script.sh
- Local parser scripts in run_scripts/{instance_id}/parser.py
- CSV file with columns: instance_id, before_repo_set_cmd, selected_test_files_to_run,
base_commit, base_dockerfile, instance_dockerfile, FAIL_TO_PASS, PASS_TO_PASS
And the generated patch file (gold_patches.json) should have the following format:
[
{
"instance_id": "unique_id",
"patch": "git patch content",
"prefix": "optional_prefix"
},
...
]
"""
import argparse
import concurrent.futures
import json
import os
import platform as py_platform
import re
try:
import modal # Lazy/optional: only required when not using --use_local_docker
except Exception:
modal = None
try:
import docker # Optional: used when --use_local_docker is set
except Exception:
docker = None
import pandas as pd
from tqdm import tqdm
from helper_code.image_uri import get_dockerhub_image_uri
# Credit: prabhuteja12
def load_base_docker(iid):
with open(f"dockerfiles/base_dockerfile/{iid}/Dockerfile") as fp:
return fp.read()
def instance_docker(iid):
with open(f"dockerfiles/instance_dockerfile/{iid}/Dockerfile") as fp:
return fp.read()
def load_local_script(scripts_dir, instance_id, script_name):
"""Load a script file from local scripts directory."""
script_path = os.path.join(scripts_dir, instance_id, script_name)
if not os.path.exists(script_path):
raise FileNotFoundError(f"Script not found: {script_path}")
with open(script_path, 'r') as f:
return f.read()
def strip_binary_hunks(patch: str) -> str:
"""Remove binary diff sections from a git patch."""
if not patch:
return patch
sections = re.split(r'(?=^diff --git )', patch, flags=re.MULTILINE)
kept: list[str] = []
for section in sections:
if not section.strip():
continue
if re.search(r'^Binary files .* differ$', section, re.MULTILINE):
continue
if re.search(r'^GIT binary patch$', section, re.MULTILINE):
continue
kept.append(section)
return "".join(kept)
def create_entryscript(sample):
before_repo_set_cmd = sample["before_repo_set_cmd"].strip().split("\n")[-1]
selected_test_files_to_run = ",".join(eval(sample["selected_test_files_to_run"]))
base_commit = sample["base_commit"]
base_dockerfile = load_base_docker(sample["instance_id"])
instance_dockerfile = instance_docker(sample["instance_id"])
# Extract ENV commands from dockerfiles
env_cmds = []
for dockerfile_content in [base_dockerfile, instance_dockerfile]:
for line in dockerfile_content.split("\n"):
line = line.strip()
if line.startswith("ENV"):
# Convert ENV commands to export statements
env_cmd = line.replace("ENV", "export", 1)
env_cmds.append(env_cmd)
env_cmds = "\n".join(env_cmds)
entry_script = f"""
{env_cmds}
# apply patch
cd /app
git reset --hard {base_commit}
git checkout {base_commit}
git apply -v /workspace/patch.diff
{before_repo_set_cmd}
# run test and save stdout and stderr to separate files
bash /workspace/run_script.sh {selected_test_files_to_run} > /workspace/stdout.log 2> /workspace/stderr.log
# run parsing script
python /workspace/parser.py /workspace/stdout.log /workspace/stderr.log /workspace/output.json
"""
return entry_script
def create_dockerhub_tag(uid, repo_name=""):
"""
Convert instance_id and repo name to Docker Hub compatible tag format.
This must match the format used in the upload script.
Args:
uid (str): The instance_id (e.g., "django__django-12345")
repo_name (str): The repository name from ECR (e.g., "sweap-images/nodebb.nodebb")
Returns:
str: Docker Hub compatible tag (e.g., "nodebb-nodebb-12345")
"""
if repo_name:
# For "NodeBB/NodeBB" -> repo_base="nodebb", repo_name="nodebb"
# Format: {repo_base}.{repo_name}-{OriginalCase}__{OriginalCase}-{hash}-{version}
# Example: nodebb.nodebb-NodeBB__NodeBB-7b8bffd763e2155cf88f3ebc258fa68ebe18188d-vf2cf3cbd463b7ad942381f1c6d077626485a1e9e
repo_base, repo_name_only = repo_name.lower().split("/")
# Keep original case for the instance_id part (after removing "instance_" prefix)
hsh = uid.replace("instance_", "")
return f"{repo_base}.{repo_name_only}-{hsh}"
else:
image_name = "default"
# Extract the tag part from the instance ID
# For UIDs that start with a pattern like "django__django-", extract everything after position 9
if "__" in uid and len(uid) > 9:
tag_part = uid[9:] # Skip the first 9 characters (e.g., "django__")
else:
tag_part = uid
return f"{image_name}-{tag_part}"
def prepare_run(uid, output_dir, prefix, redo):
uid_dir = os.path.join(output_dir, uid)
os.makedirs(uid_dir, exist_ok=True)
output_path = os.path.join(uid_dir, f"{prefix}_output.json")
if not redo and os.path.exists(output_path):
print(f"Skipping {uid} - output already exists")
with open(output_path, "r") as f:
return json.load(f), output_path, os.path.join(uid_dir, "workspace")
workspace_dir = os.path.join(uid_dir, "workspace")
os.makedirs(workspace_dir, exist_ok=True)
return None, output_path, workspace_dir
def write_patch_snapshot(output_dir, uid, prefix, patch):
with open(os.path.join(output_dir, uid, f"{prefix}_patch.diff"), "w") as f:
f.write(patch)
def assemble_workspace_files(uid, scripts_dir, patch, sample):
run_script = load_local_script(scripts_dir, uid, "run_script.sh")
parser_script = load_local_script(scripts_dir, uid, "parser.py")
entryscript_content = create_entryscript(sample)
cleaned_patch = strip_binary_hunks(patch)
if cleaned_patch != patch:
print(f"Stripped binary diff hunks from patch for {uid}")
files = {
"patch.diff": cleaned_patch,
"run_script.sh": run_script,
"parser.py": parser_script,
"entryscript.sh": entryscript_content,
}
return files, entryscript_content
def write_files_modal(sandbox, files):
for rel_path, content in files.items():
with sandbox.open(f"/workspace/{rel_path}", "w") as f:
f.write(content)
def write_files_local(workspace_dir, files):
for rel_path, content in files.items():
dst = os.path.join(workspace_dir, rel_path)
with open(dst, "w") as f:
f.write(content)
def save_entryscript_copy(output_dir, uid, prefix, entryscript_content):
with open(os.path.join(output_dir, uid, f"{prefix}_entryscript.sh"), "w") as f:
f.write(entryscript_content if entryscript_content is not None else "")
def collect_outputs_modal(sandbox, output_dir, uid, prefix):
# Save logs first (best-effort)
try:
with sandbox.open("/workspace/stdout.log", "r") as f_in:
with open(os.path.join(output_dir, uid, f"{prefix}_stdout.log"), "w") as f:
stdout_content = f_in.read()
f.write(stdout_content if stdout_content is not None else "")
except FileNotFoundError:
pass
try:
with sandbox.open("/workspace/stderr.log", "r") as f_in:
with open(os.path.join(output_dir, uid, f"{prefix}_stderr.log"), "w") as f:
stderr_content = f_in.read()
f.write(stderr_content if stderr_content is not None else "")
except FileNotFoundError:
pass
# Then try to read output.json
try:
with sandbox.open("/workspace/output.json", "r") as f_in:
output = json.load(f_in)
with open(os.path.join(output_dir, uid, f"{prefix}_output.json"), "w") as f:
json.dump(output, f)
return output
except FileNotFoundError:
print(
f"Warning: output.json not found for {uid}. Check {prefix}_stdout.log and {prefix}_stderr.log for details"
)
return None
def collect_outputs_local(workspace_dir, output_dir, uid, prefix):
def _copy_safe(src_name, dest_name):
src_path = os.path.join(workspace_dir, src_name)
dest_path = os.path.join(output_dir, uid, dest_name)
try:
with open(src_path, "r") as f_in:
content = f_in.read()
except FileNotFoundError:
content = ""
with open(dest_path, "w") as f_out:
f_out.write(content if content is not None else "")
_copy_safe("stdout.log", f"{prefix}_stdout.log")
_copy_safe("stderr.log", f"{prefix}_stderr.log")
# Then try to read output.json
try:
with open(os.path.join(workspace_dir, "output.json"), "r") as f_in:
output = json.load(f_in)
with open(os.path.join(output_dir, uid, f"{prefix}_output.json"), "w") as f:
json.dump(output, f)
return output
except FileNotFoundError:
print(
f"Warning: output.json not found for {uid}. Check {prefix}_stdout.log and {prefix}_stderr.log for details"
)
return None
def eval_with_modal(patch, sample, output_dir, dockerhub_username, scripts_dir, prefix="", redo=False, block_network=False, docker_platform=None):
if modal is None:
raise RuntimeError("modal is not installed. Install it or run with --use_local_docker")
uid = sample["instance_id"]
existing_output, output_path, workspace_dir = prepare_run(uid, output_dir, prefix, redo)
if existing_output is not None:
return existing_output
sandbox = None
print(f"Running evaluation for {uid}")
try:
write_patch_snapshot(output_dir, uid, prefix, patch)
try:
files, entryscript_content = assemble_workspace_files(uid, scripts_dir, patch, sample)
except FileNotFoundError as e:
print(f"Error loading scripts for {uid}: {e}")
return None
app = modal.App.lookup(name="swe-bench-pro-eval", create_if_missing=True)
# Use Docker Hub image instead of ECR
dockerhub_image_uri = get_dockerhub_image_uri(uid, dockerhub_username, sample.get("repo", ""))
print(f"Using Docker Hub image: {dockerhub_image_uri}")
image = modal.Image.from_registry(
dockerhub_image_uri
)
sandbox = modal.Sandbox.create(
image=image,
app=app,
timeout=60 * 60,
cpu=(1, 4),
memory=(5 * 1024, 30 * 1024),
block_network=block_network,
)
process = sandbox.exec("mkdir", "-p", "/workspace")
process.wait()
write_files_modal(sandbox, files)
process = sandbox.exec("bash", "/workspace/entryscript.sh")
process.wait()
# Check if the process was successful
if process.returncode != 0:
print(f"Entryscript failed for {uid} with return code: {process.returncode}")
# Get stderr from the process directly (note: this may not work with all Modal versions)
try:
stderr_content = getattr(process, 'stderr', None)
if stderr_content and hasattr(stderr_content, 'read'):
error_details = stderr_content.read()
if error_details:
print(f"Error details for {uid}:")
print(error_details[:1000]) # Print first 1000 chars
except Exception as e:
print(f"Failed to read stderr for {uid}: {e}")
output = collect_outputs_modal(sandbox, output_dir, uid, prefix)
if output is None:
return None
save_entryscript_copy(output_dir, uid, prefix, entryscript_content)
return output
except Exception as e:
print(f"Error in eval_with_modal for {uid}: {repr(e)}")
print(f"Error type: {type(e)}")
return None
finally:
if sandbox:
try:
sandbox.terminate()
except Exception:
pass
def eval_with_docker(patch, sample, output_dir, dockerhub_username, scripts_dir, prefix="", redo=False, block_network=False, docker_platform=None):
if docker is None:
raise RuntimeError("docker SDK is not installed. Install via 'pip install docker' or run without --use_local_docker")
uid = sample["instance_id"]
existing_output, output_path, workspace_dir = prepare_run(uid, output_dir, prefix, redo)
if existing_output is not None:
return existing_output
print(f"Running local-docker evaluation for {uid}")
try:
try:
files, entryscript_content = assemble_workspace_files(uid, scripts_dir, patch, sample)
except FileNotFoundError as e:
print(f"Error loading scripts for {uid}: {e}")
return None
write_files_local(workspace_dir, files)
write_patch_snapshot(output_dir, uid, prefix, patch)
# Run container via Docker SDK
dockerhub_image_uri = get_dockerhub_image_uri(uid, dockerhub_username, sample.get("repo", ""))
print(f"Using Docker Hub image: {dockerhub_image_uri}")
client = docker.from_env()
try:
if docker_platform:
client.images.pull(dockerhub_image_uri, platform=docker_platform)
else:
client.images.pull(dockerhub_image_uri)
except Exception as pull_err:
# If pull fails, fall back to a local image if present; otherwise, fail this run
try:
client.images.get(dockerhub_image_uri)
print(f"Using locally available image: {dockerhub_image_uri}")
except Exception:
print(f"Failed to pull or find image locally for {uid}: {pull_err}")
return None
abs_workspace_dir = os.path.abspath(workspace_dir)
volumes = {abs_workspace_dir: {"bind": "/workspace", "mode": "rw"}}
run_kwargs = {
"volumes": volumes,
"detach": True,
"remove": True,
"entrypoint": "/bin/bash", # Override image entrypoint
"command": ["-c", "bash /workspace/entryscript.sh"],
}
if block_network:
run_kwargs["network_mode"] = "none"
# Optional platform override (useful on Apple Silicon)
if docker_platform:
run_kwargs["platform"] = docker_platform
container = client.containers.run(
dockerhub_image_uri,
**run_kwargs,
)
result = container.wait()
status_code = result.get("StatusCode", 1) if isinstance(result, dict) else 1
if status_code != 0:
print(f"Entryscript failed for {uid} with return code: {status_code}")
# Collect outputs and logs, and save entryscript for reference
output = collect_outputs_local(workspace_dir, output_dir, uid, prefix)
if output is None:
return None
save_entryscript_copy(output_dir, uid, prefix, entryscript_content)
return output
except Exception as e:
print(f"Error in eval_with_docker for {uid}: {repr(e)}")
print(f"Error type: {type(e)}")
return None
def parse_args():
parser = argparse.ArgumentParser(description="Run SWEAP Pro evaluations using Modal or local Docker with Docker Hub images and local scripts")
parser.add_argument("--raw_sample_path", required=True, help="Path to the raw sample CSV file")
parser.add_argument(
"--patch_path", required=True, help="Path to the JSON file containing patches"
)
parser.add_argument("--output_dir", required=True, help="Directory to store evaluation outputs")
parser.add_argument(
"--dockerhub_username", required=True, help="Docker Hub username where sweap-images repository is located"
)
parser.add_argument(
"--scripts_dir", required=True, help="Directory containing local run scripts (e.g., scripts/run_scripts)"
)
parser.add_argument(
"--use_local_docker", action="store_true", help="Run locally with Docker instead of Modal"
)
parser.add_argument(
"--docker_platform",
default=None,
help="Docker platform override, e.g., linux/amd64; defaults to auto-detect",
)
parser.add_argument(
"--redo", action="store_true", help="Redo evaluations even if output exists"
)
parser.add_argument(
"--num_workers",
type=int,
default=50,
help="Number of workers to run evaluations in parallel",
)
parser.add_argument(
"--block_network", action="store_true", help="Block network access inside container"
)
return parser.parse_args()
def main():
args = parse_args()
# Support both JSONL and CSV input files
if args.raw_sample_path.endswith(".jsonl"):
raw_sample_df = pd.read_json(args.raw_sample_path, lines=True)
else:
raw_sample_df = pd.read_csv(args.raw_sample_path)
# Replace nulls with empty strings
raw_sample_df = raw_sample_df.fillna("")
# use instance_id as index
raw_sample_df = raw_sample_df.set_index("instance_id", drop=False)
# each patch sample is a dict with keys: instance_id, patch, prefix
with open(args.patch_path, "r") as f:
patches_to_run = json.load(f)
eval_results = {}
# Filter patches to only include those with matching instance_ids in the raw sample data
valid_patches = []
missing_instances = []
for patch_sample in patches_to_run:
instance_id = patch_sample["instance_id"]
if instance_id in raw_sample_df.index:
valid_patches.append(patch_sample)
else:
missing_instances.append(instance_id)
if missing_instances:
print(f"Warning: Found {len(missing_instances)} patch instances not in raw sample data:")
for missing_id in missing_instances[:5]: # Show first 5
print(f" - {missing_id}")
if len(missing_instances) > 5:
print(f" ... and {len(missing_instances) - 5} more")
print(f"Proceeding with {len(valid_patches)} valid patches out of {len(patches_to_run)} total patches")
# Select runtime
# Auto-detect default platform if not provided: prefer linux/amd64 on Apple Silicon
detected_platform = None
if args.use_local_docker and args.docker_platform is None:
try:
if py_platform.machine().lower() in {"arm64", "aarch64"}:
detected_platform = "linux/amd64"
except Exception:
detected_platform = None
eval_fn = eval_with_docker if args.use_local_docker else eval_with_modal
# Use ThreadPoolExecutor to run evaluations in parallel
with concurrent.futures.ThreadPoolExecutor(max_workers=args.num_workers) as executor:
# Create a dictionary mapping futures to their patch samples for progress tracking
future_to_patch = {
executor.submit(
eval_fn,
patch_sample.get("model_patch", patch_sample.get("patch", "")),
raw_sample_df.loc[patch_sample["instance_id"]],
args.output_dir,
args.dockerhub_username,
args.scripts_dir,
prefix=patch_sample.get("prefix", ""),
redo=args.redo,
block_network=args.block_network,
docker_platform=(args.docker_platform or detected_platform) if args.use_local_docker else None,
): patch_sample
for patch_sample in valid_patches
}
# Track progress with tqdm and show running accuracy
pbar = tqdm(concurrent.futures.as_completed(future_to_patch), total=len(valid_patches))
for future in pbar:
patch_sample = future_to_patch[future]
try:
# Get the result (if any error occurred, it will be raised here)
output = future.result()
if output is None:
print(f'Evaluation for {patch_sample["instance_id"]} returned None')
eval_results[patch_sample["instance_id"]] = False
else:
instance_id = patch_sample["instance_id"]
if instance_id not in raw_sample_df.index:
print(f'Warning: Instance {instance_id} not found in raw sample data, skipping')
eval_results[instance_id] = False
else:
raw_sample = raw_sample_df.loc[instance_id]
passed_tests = {x["name"] for x in output["tests"] if x["status"] == "PASSED"}
f2p = set(eval(raw_sample["fail_to_pass"]))
p2p = set(eval(raw_sample["pass_to_pass"]))
result = (f2p | p2p) <= passed_tests
eval_results[instance_id] = result
current_accuracy = sum(eval_results.values()) / len(eval_results)
pbar.set_description(f"Accuracy: {current_accuracy:.2%}")
except Exception as exc:
print(f'Evaluation for {patch_sample["instance_id"]} generated an exception: {exc}')
eval_results[patch_sample["instance_id"]] = False
# Update progress bar description with current accuracy
current_accuracy = sum(eval_results.values()) / len(eval_results)
pbar.set_description(f"Accuracy: {current_accuracy:.2%}")
with open(os.path.join(args.output_dir, "eval_results.json"), "w") as f:
json.dump(eval_results, f)
print("Overall accuracy: ", sum(eval_results.values()) / len(eval_results))
if __name__ == "__main__":
main()