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.
Files changed (73) hide show
  1. swe_lab/__init__.py +14 -0
  2. swe_lab/__main__.py +5 -0
  3. swe_lab/cli/__init__.py +29 -0
  4. swe_lab/cli/eval.py +95 -0
  5. swe_lab/cli/rollout.py +173 -0
  6. swe_lab/cli/verify.py +506 -0
  7. swe_lab/conversation/__init__.py +39 -0
  8. swe_lab/conversation/model.py +100 -0
  9. swe_lab/conversation/observer.py +95 -0
  10. swe_lab/datasets/__init__.py +21 -0
  11. swe_lab/datasets/loader.py +165 -0
  12. swe_lab/datasets/swebench_pro/__init__.py +19 -0
  13. swe_lab/datasets/swebench_pro/constants.py +59 -0
  14. swe_lab/datasets/swebench_pro/execution.py +73 -0
  15. swe_lab/datasets/swebench_pro/patches.py +79 -0
  16. swe_lab/datasets/swebench_pro/record.py +148 -0
  17. swe_lab/datasets/swebench_pro/unit_test.py +267 -0
  18. swe_lab/evaluation/__init__.py +10 -0
  19. swe_lab/evaluation/methods/__init__.py +5 -0
  20. swe_lab/evaluation/methods/unit_test/__init__.py +5 -0
  21. swe_lab/evaluation/methods/unit_test/run.py +98 -0
  22. swe_lab/evaluation/verdict.py +70 -0
  23. swe_lab/harnesses/__init__.py +9 -0
  24. swe_lab/harnesses/base.py +40 -0
  25. swe_lab/harnesses/claude_code/__init__.py +23 -0
  26. swe_lab/harnesses/claude_code/binary.py +145 -0
  27. swe_lab/harnesses/claude_code/capture.py +19 -0
  28. swe_lab/harnesses/claude_code/constants.py +46 -0
  29. swe_lab/harnesses/claude_code/convert.py +245 -0
  30. swe_lab/harnesses/claude_code/errors.py +117 -0
  31. swe_lab/harnesses/claude_code/harness.py +167 -0
  32. swe_lab/harnesses/claude_code/proxy.py +164 -0
  33. swe_lab/harnesses/claude_code/trace.py +305 -0
  34. swe_lab/paths.py +67 -0
  35. swe_lab/pipelines/__init__.py +1 -0
  36. swe_lab/pipelines/related_files/README.md +79 -0
  37. swe_lab/pipelines/related_files/__init__.py +44 -0
  38. swe_lab/pipelines/related_files/__main__.py +74 -0
  39. swe_lab/pipelines/related_files/agent_run.py +543 -0
  40. swe_lab/pipelines/related_files/agent_validator.py +191 -0
  41. swe_lab/pipelines/related_files/aggregator.py +148 -0
  42. swe_lab/pipelines/related_files/annotation_prompt.py +131 -0
  43. swe_lab/pipelines/related_files/annotator.py +84 -0
  44. swe_lab/pipelines/related_files/combine.py +167 -0
  45. swe_lab/pipelines/related_files/pipeline.py +215 -0
  46. swe_lab/pipelines/related_files/schema.py +141 -0
  47. swe_lab/pipelines/related_files/storage.py +136 -0
  48. swe_lab/pipelines/related_files/traces.py +484 -0
  49. swe_lab/pipelines/related_files/workspace.py +92 -0
  50. swe_lab/repo/__init__.py +17 -0
  51. swe_lab/repo/provider.py +231 -0
  52. swe_lab/sandbox/__init__.py +49 -0
  53. swe_lab/sandbox/backend.py +142 -0
  54. swe_lab/sandbox/backends/__init__.py +62 -0
  55. swe_lab/sandbox/backends/ghjob.py +184 -0
  56. swe_lab/sandbox/backends/host.py +275 -0
  57. swe_lab/sandbox/errors.py +7 -0
  58. swe_lab/sandbox/manager.py +294 -0
  59. swe_lab/sandbox/mounts.py +65 -0
  60. swe_lab/sandbox/observer.py +114 -0
  61. swe_lab/sandbox/observers/__init__.py +5 -0
  62. swe_lab/sandbox/observers/diff_extract.py +93 -0
  63. swe_lab/sandbox/patch.py +187 -0
  64. swe_lab/sandbox/resources.py +105 -0
  65. swe_lab/sandbox/result.py +88 -0
  66. swe_lab/sandbox/spec.py +28 -0
  67. swe_lab/sandbox/testing.py +168 -0
  68. swe_lab/solve.py +180 -0
  69. swe_lab-0.1.0.dist-info/METADATA +92 -0
  70. swe_lab-0.1.0.dist-info/RECORD +73 -0
  71. swe_lab-0.1.0.dist-info/WHEEL +4 -0
  72. swe_lab-0.1.0.dist-info/entry_points.txt +2 -0
  73. 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
@@ -0,0 +1,5 @@
1
+ """``python -m swe_lab`` entry point."""
2
+
3
+ from .cli import app
4
+
5
+ app()
@@ -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