swe-lab 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swe_lab/__init__.py +14 -0
- swe_lab/__main__.py +5 -0
- swe_lab/cli/__init__.py +29 -0
- swe_lab/cli/eval.py +95 -0
- swe_lab/cli/rollout.py +173 -0
- swe_lab/cli/verify.py +506 -0
- swe_lab/conversation/__init__.py +39 -0
- swe_lab/conversation/model.py +100 -0
- swe_lab/conversation/observer.py +95 -0
- swe_lab/datasets/__init__.py +21 -0
- swe_lab/datasets/loader.py +165 -0
- swe_lab/datasets/swebench_pro/__init__.py +19 -0
- swe_lab/datasets/swebench_pro/constants.py +59 -0
- swe_lab/datasets/swebench_pro/execution.py +73 -0
- swe_lab/datasets/swebench_pro/patches.py +79 -0
- swe_lab/datasets/swebench_pro/record.py +148 -0
- swe_lab/datasets/swebench_pro/unit_test.py +267 -0
- swe_lab/evaluation/__init__.py +10 -0
- swe_lab/evaluation/methods/__init__.py +5 -0
- swe_lab/evaluation/methods/unit_test/__init__.py +5 -0
- swe_lab/evaluation/methods/unit_test/run.py +98 -0
- swe_lab/evaluation/verdict.py +70 -0
- swe_lab/harnesses/__init__.py +9 -0
- swe_lab/harnesses/base.py +40 -0
- swe_lab/harnesses/claude_code/__init__.py +23 -0
- swe_lab/harnesses/claude_code/binary.py +145 -0
- swe_lab/harnesses/claude_code/capture.py +19 -0
- swe_lab/harnesses/claude_code/constants.py +46 -0
- swe_lab/harnesses/claude_code/convert.py +245 -0
- swe_lab/harnesses/claude_code/errors.py +117 -0
- swe_lab/harnesses/claude_code/harness.py +167 -0
- swe_lab/harnesses/claude_code/proxy.py +164 -0
- swe_lab/harnesses/claude_code/trace.py +305 -0
- swe_lab/paths.py +67 -0
- swe_lab/pipelines/__init__.py +1 -0
- swe_lab/pipelines/related_files/README.md +79 -0
- swe_lab/pipelines/related_files/__init__.py +44 -0
- swe_lab/pipelines/related_files/__main__.py +74 -0
- swe_lab/pipelines/related_files/agent_run.py +543 -0
- swe_lab/pipelines/related_files/agent_validator.py +191 -0
- swe_lab/pipelines/related_files/aggregator.py +148 -0
- swe_lab/pipelines/related_files/annotation_prompt.py +131 -0
- swe_lab/pipelines/related_files/annotator.py +84 -0
- swe_lab/pipelines/related_files/combine.py +167 -0
- swe_lab/pipelines/related_files/pipeline.py +215 -0
- swe_lab/pipelines/related_files/schema.py +141 -0
- swe_lab/pipelines/related_files/storage.py +136 -0
- swe_lab/pipelines/related_files/traces.py +484 -0
- swe_lab/pipelines/related_files/workspace.py +92 -0
- swe_lab/repo/__init__.py +17 -0
- swe_lab/repo/provider.py +231 -0
- swe_lab/sandbox/__init__.py +49 -0
- swe_lab/sandbox/backend.py +142 -0
- swe_lab/sandbox/backends/__init__.py +62 -0
- swe_lab/sandbox/backends/ghjob.py +184 -0
- swe_lab/sandbox/backends/host.py +275 -0
- swe_lab/sandbox/errors.py +7 -0
- swe_lab/sandbox/manager.py +294 -0
- swe_lab/sandbox/mounts.py +65 -0
- swe_lab/sandbox/observer.py +114 -0
- swe_lab/sandbox/observers/__init__.py +5 -0
- swe_lab/sandbox/observers/diff_extract.py +93 -0
- swe_lab/sandbox/patch.py +187 -0
- swe_lab/sandbox/resources.py +105 -0
- swe_lab/sandbox/result.py +88 -0
- swe_lab/sandbox/spec.py +28 -0
- swe_lab/sandbox/testing.py +168 -0
- swe_lab/solve.py +180 -0
- swe_lab-0.1.0.dist-info/METADATA +92 -0
- swe_lab-0.1.0.dist-info/RECORD +73 -0
- swe_lab-0.1.0.dist-info/WHEEL +4 -0
- swe_lab-0.1.0.dist-info/entry_points.txt +2 -0
- swe_lab-0.1.0.dist-info/licenses/LICENSE +202 -0
swe_lab/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Annotation tooling for SWE-Bench related files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .datasets import Dataset, load_dataset, SweBenchProInstance
|
|
6
|
+
from .repo import GitCheckoutProvider, RepoProvider
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Dataset",
|
|
10
|
+
"GitCheckoutProvider",
|
|
11
|
+
"RepoProvider",
|
|
12
|
+
"SweBenchProInstance",
|
|
13
|
+
"load_dataset",
|
|
14
|
+
]
|
swe_lab/__main__.py
ADDED
swe_lab/cli/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The ``swe_lab`` command-line interface.
|
|
2
|
+
|
|
3
|
+
One Typer app; each subcommand is a typed function in its own module (the
|
|
4
|
+
dispatcher stays a thin table so it never grows into one giant file). Run it as
|
|
5
|
+
``python -m swe_lab <subcommand>``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from .eval import eval_cmd
|
|
11
|
+
from .rollout import rollout_in_docker
|
|
12
|
+
from .verify import verify_cmd
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(add_completion=False, no_args_is_help=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.callback()
|
|
18
|
+
def root() -> None:
|
|
19
|
+
"""swe-lab: build, run, and evaluate SWE-agent evaluation data."""
|
|
20
|
+
# A top-level callback keeps this a multi-command group: subcommands are
|
|
21
|
+
# required even while `eval` is the only one registered (Typer otherwise
|
|
22
|
+
# collapses a single-command app into that command).
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_ = app.command("eval")(eval_cmd)
|
|
26
|
+
_ = app.command("rollout")(rollout_in_docker)
|
|
27
|
+
_ = app.command("verify")(verify_cmd)
|
|
28
|
+
|
|
29
|
+
__all__ = ["app"]
|
swe_lab/cli/eval.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""The ``eval`` subcommand: grade one instance's patch in a container."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from swe_lab.datasets.loader import load_dataset
|
|
13
|
+
from swe_lab.datasets.swebench_pro import SweBenchProInstance
|
|
14
|
+
from swe_lab.datasets.swebench_pro.unit_test import compile_unit_test
|
|
15
|
+
from swe_lab.evaluation.methods.unit_test import run_unit_test
|
|
16
|
+
from swe_lab.paths import cache_root, find_repo_root
|
|
17
|
+
from swe_lab.sandbox import BackendKind, build_backend
|
|
18
|
+
|
|
19
|
+
_WORKSPACES_SUBDIR = "eval_workspaces"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def eval_cmd(
|
|
23
|
+
instance_id: str,
|
|
24
|
+
dataset: str = "swebench_pro",
|
|
25
|
+
gold: Annotated[
|
|
26
|
+
bool, typer.Option(help="Grade the instance's own gold patch.")
|
|
27
|
+
] = False,
|
|
28
|
+
patch_file: Annotated[
|
|
29
|
+
Path | None, typer.Option(help="Path to a candidate .diff to grade.")
|
|
30
|
+
] = None,
|
|
31
|
+
timeout: Annotated[
|
|
32
|
+
float, typer.Option(help="Seconds before the eval run is killed.")
|
|
33
|
+
] = 1800.0,
|
|
34
|
+
network: Annotated[
|
|
35
|
+
bool, typer.Option(help="Give the container network access.")
|
|
36
|
+
] = True,
|
|
37
|
+
pull: Annotated[
|
|
38
|
+
bool, typer.Option(help="Pull the image before running.")
|
|
39
|
+
] = True,
|
|
40
|
+
backend: Annotated[
|
|
41
|
+
BackendKind,
|
|
42
|
+
typer.Option(help="Sandbox backend (host Docker, or the GH job)."),
|
|
43
|
+
] = BackendKind.HOST,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Grade one instance by running its tests in its container.
|
|
46
|
+
|
|
47
|
+
Applies the patch (``--gold`` for the instance's own gold patch, or a
|
|
48
|
+
candidate via ``--patch-file``), runs the instance's test suite, and reports
|
|
49
|
+
the verdict. Exit code is 0 iff the patch resolves the instance.
|
|
50
|
+
"""
|
|
51
|
+
if gold == (patch_file is not None):
|
|
52
|
+
raise typer.BadParameter("pass exactly one of --gold / --patch-file")
|
|
53
|
+
|
|
54
|
+
instance = load_dataset(dataset).require(instance_id)
|
|
55
|
+
if not isinstance(instance, SweBenchProInstance):
|
|
56
|
+
raise typer.BadParameter(f"dataset {dataset!r} is not wired for eval yet")
|
|
57
|
+
|
|
58
|
+
if gold:
|
|
59
|
+
patch = instance.patch
|
|
60
|
+
else:
|
|
61
|
+
assert patch_file is not None # guaranteed by the exactly-one check above
|
|
62
|
+
patch = patch_file.read_text()
|
|
63
|
+
|
|
64
|
+
root = find_repo_root()
|
|
65
|
+
sandbox_spec, unit_spec = compile_unit_test(
|
|
66
|
+
instance, patch=patch, repo_root=root
|
|
67
|
+
)
|
|
68
|
+
workspace = cache_root(root) / _WORKSPACES_SUBDIR / instance.instance_id
|
|
69
|
+
# The manager refuses a non-empty workspace; a fresh grade starts clean.
|
|
70
|
+
shutil.rmtree(workspace, ignore_errors=True)
|
|
71
|
+
|
|
72
|
+
result, verdict = run_unit_test(
|
|
73
|
+
sandbox_spec,
|
|
74
|
+
unit_spec,
|
|
75
|
+
backend=build_backend(backend, network=network, pull=pull),
|
|
76
|
+
workspace=workspace,
|
|
77
|
+
timeout=timeout,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
summary: dict[str, object] = {
|
|
81
|
+
"instance_id": instance.instance_id,
|
|
82
|
+
"status": result.status.value,
|
|
83
|
+
"resolved": bool(verdict and verdict.resolved),
|
|
84
|
+
}
|
|
85
|
+
if verdict is not None:
|
|
86
|
+
summary |= {
|
|
87
|
+
"score": verdict.score,
|
|
88
|
+
"output_state": verdict.output_state.value,
|
|
89
|
+
"passed": sorted(verdict.passed),
|
|
90
|
+
"missing": sorted(verdict.missing),
|
|
91
|
+
}
|
|
92
|
+
if result.error is not None:
|
|
93
|
+
summary["error"] = repr(result.error)
|
|
94
|
+
print(json.dumps(summary, indent=2))
|
|
95
|
+
raise typer.Exit(0 if summary["resolved"] else 1)
|
swe_lab/cli/rollout.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""The ``rollout`` subcommand: solve one instance, optionally graded."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import shutil
|
|
9
|
+
from typing import Annotated
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from swe_lab.datasets.loader import load_dataset
|
|
14
|
+
from swe_lab.datasets.swebench_pro import SweBenchProInstance
|
|
15
|
+
from swe_lab.datasets.swebench_pro.unit_test import (
|
|
16
|
+
compile_sandbox_spec,
|
|
17
|
+
compile_solve_prompt,
|
|
18
|
+
compile_unit_test,
|
|
19
|
+
)
|
|
20
|
+
from swe_lab.evaluation.methods.unit_test import run_unit_test
|
|
21
|
+
from swe_lab.harnesses.claude_code import Capture
|
|
22
|
+
from swe_lab.harnesses.claude_code.constants import (
|
|
23
|
+
DEFAULT_MODEL,
|
|
24
|
+
OAUTH_TOKEN_ENV,
|
|
25
|
+
)
|
|
26
|
+
from swe_lab.paths import cache_root, find_repo_root
|
|
27
|
+
from swe_lab.sandbox import BackendKind, build_backend
|
|
28
|
+
from swe_lab.solve import RolloutOutcome, run_rollout
|
|
29
|
+
|
|
30
|
+
_ROLLOUT_SUBDIR = "rollout_workspaces"
|
|
31
|
+
_EVAL_SUBDIR = "eval_workspaces"
|
|
32
|
+
_DEFAULT_TIMEOUT_S = 1800.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rollout_in_docker(
|
|
36
|
+
instance_id: Annotated[
|
|
37
|
+
str, typer.Argument(help="The instance to solve (e.g. acme__widget-1).")
|
|
38
|
+
],
|
|
39
|
+
dataset: Annotated[
|
|
40
|
+
str, typer.Option(help="Dataset the instance belongs to.")
|
|
41
|
+
] = "swebench_pro",
|
|
42
|
+
model: Annotated[
|
|
43
|
+
str, typer.Option(help="Model alias or id the agent runs as.")
|
|
44
|
+
] = DEFAULT_MODEL,
|
|
45
|
+
grade: Annotated[
|
|
46
|
+
bool, typer.Option(help="Grade the produced patch afterwards.")
|
|
47
|
+
] = False,
|
|
48
|
+
timeout: Annotated[
|
|
49
|
+
float, typer.Option(help="Seconds before the agent run is killed.")
|
|
50
|
+
] = _DEFAULT_TIMEOUT_S,
|
|
51
|
+
pull: Annotated[
|
|
52
|
+
bool, typer.Option(help="Pull the image before running.")
|
|
53
|
+
] = True,
|
|
54
|
+
capture: Annotated[
|
|
55
|
+
Capture, typer.Option(help="Agent-trace capture strategy.")
|
|
56
|
+
] = Capture.STREAM,
|
|
57
|
+
backend: Annotated[
|
|
58
|
+
BackendKind,
|
|
59
|
+
typer.Option(help="Sandbox backend (host Docker, or the GH job)."),
|
|
60
|
+
] = BackendKind.HOST,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Run a headless agent to solve one instance in its container.
|
|
63
|
+
|
|
64
|
+
The agent edits the repo; its patch is the worktree diff vs the base commit.
|
|
65
|
+
With ``--grade`` the patch is then run through the instance's tests. An empty
|
|
66
|
+
patch is never graded as a pass. Exit code is 0 unless a graded run fails.
|
|
67
|
+
"""
|
|
68
|
+
if not os.environ.get(OAUTH_TOKEN_ENV):
|
|
69
|
+
raise typer.BadParameter(
|
|
70
|
+
f"{OAUTH_TOKEN_ENV} is not set; the agent cannot authenticate."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
instance = load_dataset(dataset).require(instance_id)
|
|
74
|
+
if not isinstance(instance, SweBenchProInstance):
|
|
75
|
+
raise typer.BadParameter(
|
|
76
|
+
f"dataset {dataset!r} is not wired for rollout yet"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
root = find_repo_root()
|
|
80
|
+
spec = compile_sandbox_spec(instance)
|
|
81
|
+
prompt = compile_solve_prompt(instance)
|
|
82
|
+
workspace = cache_root(root) / _ROLLOUT_SUBDIR / instance.instance_id
|
|
83
|
+
shutil.rmtree(workspace, ignore_errors=True)
|
|
84
|
+
|
|
85
|
+
run_backend = build_backend(
|
|
86
|
+
backend, network=True, pull=pull, pass_env=[OAUTH_TOKEN_ENV]
|
|
87
|
+
)
|
|
88
|
+
outcome = run_rollout(
|
|
89
|
+
spec,
|
|
90
|
+
prompt=prompt,
|
|
91
|
+
model=model,
|
|
92
|
+
backend=run_backend,
|
|
93
|
+
workspace=workspace,
|
|
94
|
+
timeout=timeout,
|
|
95
|
+
capture=capture,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
summary: dict[str, object] = {
|
|
99
|
+
"instance_id": outcome.instance_id,
|
|
100
|
+
"status": outcome.status.value,
|
|
101
|
+
"agent_complete": outcome.complete,
|
|
102
|
+
"is_empty_patch": outcome.is_empty,
|
|
103
|
+
"binary_stripped": outcome.binary_stripped,
|
|
104
|
+
"patch_file": str(outcome.workspace / "patch.diff"),
|
|
105
|
+
"workspace": str(outcome.workspace),
|
|
106
|
+
}
|
|
107
|
+
resolved = _finish(
|
|
108
|
+
summary, instance, outcome, grade, root, pull, timeout, backend
|
|
109
|
+
)
|
|
110
|
+
print(json.dumps(summary, indent=2))
|
|
111
|
+
raise typer.Exit(0 if (not grade or resolved) else 1)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _finish(
|
|
115
|
+
summary: dict[str, object],
|
|
116
|
+
instance: SweBenchProInstance,
|
|
117
|
+
outcome: RolloutOutcome,
|
|
118
|
+
grade: bool,
|
|
119
|
+
root: Path,
|
|
120
|
+
pull: bool,
|
|
121
|
+
timeout: float,
|
|
122
|
+
backend: BackendKind,
|
|
123
|
+
) -> bool:
|
|
124
|
+
"""Record the run's ``outcome`` string (and grade), returning ``resolved``.
|
|
125
|
+
|
|
126
|
+
An explicit outcome makes an unresolved run's *reason* readable, never
|
|
127
|
+
guessed: ``empty_patch`` (no edits — grading skipped) is distinct from
|
|
128
|
+
``unresolved_tests_failed`` (a real patch that graded false).
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
summary: The summary dict to record ``outcome``/``grade`` into.
|
|
132
|
+
instance: The instance (for compiling the grade run).
|
|
133
|
+
outcome: The rollout outcome (its patch is graded).
|
|
134
|
+
grade: Whether to grade at all.
|
|
135
|
+
root: The repo root (for cache/workspace paths).
|
|
136
|
+
pull: Whether to pull the image for the grade run.
|
|
137
|
+
timeout: Seconds before the grade run is killed.
|
|
138
|
+
backend: Which sandbox backend to grade on.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Whether the patch resolved the instance (always ``False`` when not graded).
|
|
142
|
+
"""
|
|
143
|
+
if not grade:
|
|
144
|
+
summary["outcome"] = "solved_not_graded"
|
|
145
|
+
return False
|
|
146
|
+
if outcome.is_empty:
|
|
147
|
+
summary["outcome"] = "empty_patch"
|
|
148
|
+
summary["grade"] = {"resolved": False, "reason": "empty_patch"}
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
sandbox_spec, unit_spec = compile_unit_test(
|
|
152
|
+
instance, patch=outcome.patch, repo_root=root
|
|
153
|
+
)
|
|
154
|
+
eval_ws = cache_root(root) / _EVAL_SUBDIR / instance.instance_id
|
|
155
|
+
shutil.rmtree(eval_ws, ignore_errors=True)
|
|
156
|
+
_, verdict = run_unit_test(
|
|
157
|
+
sandbox_spec,
|
|
158
|
+
unit_spec,
|
|
159
|
+
backend=build_backend(backend, network=False, pull=pull),
|
|
160
|
+
workspace=eval_ws,
|
|
161
|
+
timeout=timeout,
|
|
162
|
+
)
|
|
163
|
+
resolved = bool(verdict and verdict.resolved)
|
|
164
|
+
summary["outcome"] = "resolved" if resolved else "unresolved_tests_failed"
|
|
165
|
+
if verdict is not None:
|
|
166
|
+
summary["grade"] = {
|
|
167
|
+
"resolved": verdict.resolved,
|
|
168
|
+
"score": verdict.score,
|
|
169
|
+
"output_state": verdict.output_state.value,
|
|
170
|
+
"passed": sorted(verdict.passed),
|
|
171
|
+
"missing": sorted(verdict.missing),
|
|
172
|
+
}
|
|
173
|
+
return resolved
|