mini-swe-agent 1.16.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.
- mini_swe_agent-1.16.0.dist-info/METADATA +314 -0
- mini_swe_agent-1.16.0.dist-info/RECORD +62 -0
- mini_swe_agent-1.16.0.dist-info/WHEEL +5 -0
- mini_swe_agent-1.16.0.dist-info/entry_points.txt +5 -0
- mini_swe_agent-1.16.0.dist-info/licenses/LICENSE.md +21 -0
- mini_swe_agent-1.16.0.dist-info/top_level.txt +1 -0
- minisweagent/__init__.py +83 -0
- minisweagent/__main__.py +7 -0
- minisweagent/agents/__init__.py +1 -0
- minisweagent/agents/default.py +131 -0
- minisweagent/agents/interactive.py +153 -0
- minisweagent/agents/interactive_textual.py +450 -0
- minisweagent/config/README.md +10 -0
- minisweagent/config/__init__.py +27 -0
- minisweagent/config/default.yaml +157 -0
- minisweagent/config/extra/__init__.py +1 -0
- minisweagent/config/extra/swebench.yaml +230 -0
- minisweagent/config/extra/swebench_roulette.yaml +233 -0
- minisweagent/config/extra/swebench_xml.yaml +215 -0
- minisweagent/config/github_issue.yaml +146 -0
- minisweagent/config/mini.tcss +86 -0
- minisweagent/config/mini.yaml +158 -0
- minisweagent/config/mini_no_temp.yaml +158 -0
- minisweagent/environments/__init__.py +31 -0
- minisweagent/environments/docker.py +114 -0
- minisweagent/environments/extra/__init__.py +0 -0
- minisweagent/environments/extra/bubblewrap.py +112 -0
- minisweagent/environments/extra/swerex_docker.py +47 -0
- minisweagent/environments/local.py +38 -0
- minisweagent/environments/singularity.py +97 -0
- minisweagent/models/__init__.py +114 -0
- minisweagent/models/anthropic.py +35 -0
- minisweagent/models/extra/__init__.py +0 -0
- minisweagent/models/extra/roulette.py +61 -0
- minisweagent/models/litellm_model.py +100 -0
- minisweagent/models/litellm_response_api_model.py +80 -0
- minisweagent/models/openrouter_model.py +125 -0
- minisweagent/models/portkey_model.py +154 -0
- minisweagent/models/portkey_response_api_model.py +74 -0
- minisweagent/models/requesty_model.py +119 -0
- minisweagent/models/test_models.py +42 -0
- minisweagent/models/utils/__init__.py +0 -0
- minisweagent/models/utils/cache_control.py +54 -0
- minisweagent/models/utils/key_per_thread.py +20 -0
- minisweagent/models/utils/openai_utils.py +41 -0
- minisweagent/py.typed +0 -0
- minisweagent/run/__init__.py +1 -0
- minisweagent/run/extra/__init__.py +0 -0
- minisweagent/run/extra/config.py +114 -0
- minisweagent/run/extra/swebench.py +266 -0
- minisweagent/run/extra/swebench_single.py +79 -0
- minisweagent/run/extra/utils/__init__.py +0 -0
- minisweagent/run/extra/utils/batch_progress.py +178 -0
- minisweagent/run/github_issue.py +87 -0
- minisweagent/run/hello_world.py +36 -0
- minisweagent/run/inspector.py +212 -0
- minisweagent/run/mini.py +108 -0
- minisweagent/run/mini_extra.py +44 -0
- minisweagent/run/utils/__init__.py +0 -0
- minisweagent/run/utils/save.py +78 -0
- minisweagent/utils/__init__.py +0 -0
- minisweagent/utils/log.py +36 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Identical config file to mini.yaml, but without temperature=0.0
|
|
2
|
+
agent:
|
|
3
|
+
system_template: |
|
|
4
|
+
You are a helpful assistant that can interact with a computer.
|
|
5
|
+
|
|
6
|
+
Your response must contain exactly ONE bash code block with ONE command (or commands connected with && or ||).
|
|
7
|
+
Include a THOUGHT section before your command where you explain your reasoning process.
|
|
8
|
+
Format your response as shown in <format_example>.
|
|
9
|
+
|
|
10
|
+
<format_example>
|
|
11
|
+
Your reasoning and analysis here. Explain why you want to perform the action.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
your_command_here
|
|
15
|
+
```
|
|
16
|
+
</format_example>
|
|
17
|
+
|
|
18
|
+
Failure to follow these rules will cause your response to be rejected.
|
|
19
|
+
instance_template: |
|
|
20
|
+
Please solve this issue: {{task}}
|
|
21
|
+
|
|
22
|
+
You can execute bash commands and edit files to implement the necessary changes.
|
|
23
|
+
|
|
24
|
+
## Recommended Workflow
|
|
25
|
+
|
|
26
|
+
This workflows should be done step-by-step so that you can iterate on your changes and any possible problems.
|
|
27
|
+
|
|
28
|
+
1. Analyze the codebase by finding and reading relevant files
|
|
29
|
+
2. Create a script to reproduce the issue
|
|
30
|
+
3. Edit the source code to resolve the issue
|
|
31
|
+
4. Verify your fix works by running your script again
|
|
32
|
+
5. Test edge cases to ensure your fix is robust
|
|
33
|
+
6. Submit your changes and finish your work by issuing the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`.
|
|
34
|
+
Do not combine it with any other command. <important>After this command, you cannot continue working on this task.</important>
|
|
35
|
+
|
|
36
|
+
## Important Rules
|
|
37
|
+
|
|
38
|
+
1. Every response must contain exactly one action
|
|
39
|
+
2. The action must be enclosed in triple backticks
|
|
40
|
+
3. Directory or environment variable changes are not persistent. Every action is executed in a new subshell.
|
|
41
|
+
However, you can prefix any action with `MY_ENV_VAR=MY_VALUE cd /path/to/working/dir && ...` or write/load environment variables from files
|
|
42
|
+
|
|
43
|
+
<system_information>
|
|
44
|
+
{{system}} {{release}} {{version}} {{machine}}
|
|
45
|
+
</system_information>
|
|
46
|
+
|
|
47
|
+
## Formatting your response
|
|
48
|
+
|
|
49
|
+
Here is an example of a correct response:
|
|
50
|
+
|
|
51
|
+
<example_response>
|
|
52
|
+
THOUGHT: I need to understand the structure of the repository first. Let me check what files are in the current directory to get a better understanding of the codebase.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
ls -la
|
|
56
|
+
```
|
|
57
|
+
</example_response>
|
|
58
|
+
|
|
59
|
+
## Useful command examples
|
|
60
|
+
|
|
61
|
+
### Create a new file:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cat <<'EOF' > newfile.py
|
|
65
|
+
import numpy as np
|
|
66
|
+
hello = "world"
|
|
67
|
+
print(hello)
|
|
68
|
+
EOF
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Edit files with sed:
|
|
72
|
+
|
|
73
|
+
{%- if system == "Darwin" -%}
|
|
74
|
+
<important>
|
|
75
|
+
You are on MacOS. For all the below examples, you need to use `sed -i ''` instead of `sed -i`.
|
|
76
|
+
</important>
|
|
77
|
+
{%- endif -%}
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Replace all occurrences
|
|
81
|
+
sed -i 's/old_string/new_string/g' filename.py
|
|
82
|
+
|
|
83
|
+
# Replace only first occurrence
|
|
84
|
+
sed -i 's/old_string/new_string/' filename.py
|
|
85
|
+
|
|
86
|
+
# Replace first occurrence on line 1
|
|
87
|
+
sed -i '1s/old_string/new_string/' filename.py
|
|
88
|
+
|
|
89
|
+
# Replace all occurrences in lines 1-10
|
|
90
|
+
sed -i '1,10s/old_string/new_string/g' filename.py
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### View file content:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# View specific lines with numbers
|
|
97
|
+
nl -ba filename.py | sed -n '10,20p'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Any other command you want to run
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
anything
|
|
104
|
+
```
|
|
105
|
+
action_observation_template: |
|
|
106
|
+
<returncode>{{output.returncode}}</returncode>
|
|
107
|
+
{% if output.output | length < 10000 -%}
|
|
108
|
+
<output>
|
|
109
|
+
{{ output.output -}}
|
|
110
|
+
</output>
|
|
111
|
+
{%- else -%}
|
|
112
|
+
<warning>
|
|
113
|
+
The output of your last command was too long.
|
|
114
|
+
Please try a different command that produces less output.
|
|
115
|
+
If you're looking at a file you can try use head, tail or sed to view a smaller number of lines selectively.
|
|
116
|
+
If you're using grep or find and it produced too much output, you can use a more selective search pattern.
|
|
117
|
+
If you really need to see something from the full command's output, you can redirect output to a file and then search in that file.
|
|
118
|
+
</warning>
|
|
119
|
+
{%- set elided_chars = output.output | length - 10000 -%}
|
|
120
|
+
<output_head>
|
|
121
|
+
{{ output.output[:5000] }}
|
|
122
|
+
</output_head>
|
|
123
|
+
<elided_chars>
|
|
124
|
+
{{ elided_chars }} characters elided
|
|
125
|
+
</elided_chars>
|
|
126
|
+
<output_tail>
|
|
127
|
+
{{ output.output[-5000:] }}
|
|
128
|
+
</output_tail>
|
|
129
|
+
{%- endif -%}
|
|
130
|
+
format_error_template: |
|
|
131
|
+
Please always provide EXACTLY ONE action in triple backticks, found {{actions|length}} actions.
|
|
132
|
+
If you want to end the task, please issue the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`
|
|
133
|
+
without any other command.
|
|
134
|
+
Else, please format your response exactly as follows:
|
|
135
|
+
|
|
136
|
+
<response_example>
|
|
137
|
+
Here are some thoughts about why you want to perform the action.
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
<action>
|
|
141
|
+
```
|
|
142
|
+
</response_example>
|
|
143
|
+
|
|
144
|
+
Note: In rare cases, if you need to reference a similar format in your command, you might have
|
|
145
|
+
to proceed in two steps, first writing TRIPLEBACKTICKSBASH, then replacing them with ```bash.
|
|
146
|
+
step_limit: 0.
|
|
147
|
+
cost_limit: 3.
|
|
148
|
+
mode: confirm
|
|
149
|
+
environment:
|
|
150
|
+
env:
|
|
151
|
+
PAGER: cat
|
|
152
|
+
MANPAGER: cat
|
|
153
|
+
LESS: -R
|
|
154
|
+
PIP_PROGRESS_BAR: 'off'
|
|
155
|
+
TQDM_DISABLE: '1'
|
|
156
|
+
model:
|
|
157
|
+
model_kwargs:
|
|
158
|
+
drop_params: true
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Environment implementations for mini-SWE-agent."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import importlib
|
|
5
|
+
|
|
6
|
+
from minisweagent import Environment
|
|
7
|
+
|
|
8
|
+
_ENVIRONMENT_MAPPING = {
|
|
9
|
+
"docker": "minisweagent.environments.docker.DockerEnvironment",
|
|
10
|
+
"singularity": "minisweagent.environments.singularity.SingularityEnvironment",
|
|
11
|
+
"local": "minisweagent.environments.local.LocalEnvironment",
|
|
12
|
+
"swerex_docker": "minisweagent.environments.extra.swerex_docker.SwerexDockerEnvironment",
|
|
13
|
+
"bubblewrap": "minisweagent.environments.extra.bubblewrap.BubblewrapEnvironment",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_environment_class(spec: str) -> type[Environment]:
|
|
18
|
+
full_path = _ENVIRONMENT_MAPPING.get(spec, spec)
|
|
19
|
+
try:
|
|
20
|
+
module_name, class_name = full_path.rsplit(".", 1)
|
|
21
|
+
module = importlib.import_module(module_name)
|
|
22
|
+
return getattr(module, class_name)
|
|
23
|
+
except (ValueError, ImportError, AttributeError):
|
|
24
|
+
msg = f"Unknown environment type: {spec} (resolved to {full_path}, available: {_ENVIRONMENT_MAPPING})"
|
|
25
|
+
raise ValueError(msg)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_environment(config: dict, *, default_type: str = "") -> Environment:
|
|
29
|
+
config = copy.deepcopy(config)
|
|
30
|
+
environment_class = config.pop("environment_class", default_type)
|
|
31
|
+
return get_environment_class(environment_class)(**config)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
import uuid
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class DockerEnvironmentConfig:
|
|
12
|
+
image: str
|
|
13
|
+
cwd: str = "/"
|
|
14
|
+
"""Working directory in which to execute commands."""
|
|
15
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
16
|
+
"""Environment variables to set in the container."""
|
|
17
|
+
forward_env: list[str] = field(default_factory=list)
|
|
18
|
+
"""Environment variables to forward to the container.
|
|
19
|
+
Variables are only forwarded if they are set in the host environment.
|
|
20
|
+
In case of conflict with `env`, the `env` variables take precedence.
|
|
21
|
+
"""
|
|
22
|
+
timeout: int = 30
|
|
23
|
+
"""Timeout for executing commands in the container."""
|
|
24
|
+
executable: str = os.getenv("MSWEA_DOCKER_EXECUTABLE", "docker")
|
|
25
|
+
"""Path to the docker/container executable."""
|
|
26
|
+
run_args: list[str] = field(default_factory=lambda: ["--rm"])
|
|
27
|
+
"""Additional arguments to pass to the docker/container executable.
|
|
28
|
+
Default is ["--rm"], which removes the container after it exits.
|
|
29
|
+
"""
|
|
30
|
+
container_timeout: str = "2h"
|
|
31
|
+
"""Max duration to keep container running. Uses the same format as the sleep command."""
|
|
32
|
+
pull_timeout: int = 120
|
|
33
|
+
"""Timeout in seconds for pulling images."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DockerEnvironment:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
config_class: type = DockerEnvironmentConfig,
|
|
41
|
+
logger: logging.Logger | None = None,
|
|
42
|
+
**kwargs,
|
|
43
|
+
):
|
|
44
|
+
"""This class executes bash commands in a Docker container using direct docker commands.
|
|
45
|
+
See `DockerEnvironmentConfig` for keyword arguments.
|
|
46
|
+
"""
|
|
47
|
+
self.logger = logger or logging.getLogger("minisweagent.environment")
|
|
48
|
+
self.container_id: str | None = None
|
|
49
|
+
self.config = config_class(**kwargs)
|
|
50
|
+
self._start_container()
|
|
51
|
+
|
|
52
|
+
def get_template_vars(self) -> dict[str, Any]:
|
|
53
|
+
return asdict(self.config)
|
|
54
|
+
|
|
55
|
+
def _start_container(self):
|
|
56
|
+
"""Start the Docker container and return the container ID."""
|
|
57
|
+
container_name = f"minisweagent-{uuid.uuid4().hex[:8]}"
|
|
58
|
+
cmd = [
|
|
59
|
+
self.config.executable,
|
|
60
|
+
"run",
|
|
61
|
+
"-d",
|
|
62
|
+
"--name",
|
|
63
|
+
container_name,
|
|
64
|
+
"-w",
|
|
65
|
+
self.config.cwd,
|
|
66
|
+
*self.config.run_args,
|
|
67
|
+
self.config.image,
|
|
68
|
+
"sleep",
|
|
69
|
+
self.config.container_timeout,
|
|
70
|
+
]
|
|
71
|
+
self.logger.debug(f"Starting container with command: {shlex.join(cmd)}")
|
|
72
|
+
result = subprocess.run(
|
|
73
|
+
cmd,
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
timeout=self.config.pull_timeout, # docker pull might take a while
|
|
77
|
+
check=True,
|
|
78
|
+
)
|
|
79
|
+
self.logger.info(f"Started container {container_name} with ID {result.stdout.strip()}")
|
|
80
|
+
self.container_id = result.stdout.strip()
|
|
81
|
+
|
|
82
|
+
def execute(self, command: str, cwd: str = "", *, timeout: int | None = None) -> dict[str, Any]:
|
|
83
|
+
"""Execute a command in the Docker container and return the result as a dict."""
|
|
84
|
+
cwd = cwd or self.config.cwd
|
|
85
|
+
assert self.container_id, "Container not started"
|
|
86
|
+
|
|
87
|
+
cmd = [self.config.executable, "exec", "-w", cwd]
|
|
88
|
+
for key in self.config.forward_env:
|
|
89
|
+
if (value := os.getenv(key)) is not None:
|
|
90
|
+
cmd.extend(["-e", f"{key}={value}"])
|
|
91
|
+
for key, value in self.config.env.items():
|
|
92
|
+
cmd.extend(["-e", f"{key}={value}"])
|
|
93
|
+
cmd.extend([self.container_id, "bash", "-lc", command])
|
|
94
|
+
|
|
95
|
+
result = subprocess.run(
|
|
96
|
+
cmd,
|
|
97
|
+
text=True,
|
|
98
|
+
timeout=timeout or self.config.timeout,
|
|
99
|
+
encoding="utf-8",
|
|
100
|
+
errors="replace",
|
|
101
|
+
stdout=subprocess.PIPE,
|
|
102
|
+
stderr=subprocess.STDOUT,
|
|
103
|
+
)
|
|
104
|
+
return {"output": result.stdout, "returncode": result.returncode}
|
|
105
|
+
|
|
106
|
+
def cleanup(self):
|
|
107
|
+
"""Stop and remove the Docker container."""
|
|
108
|
+
if getattr(self, "container_id", None) is not None: # if init fails early, container_id might not be set
|
|
109
|
+
cmd = f"(timeout 60 {self.config.executable} stop {self.container_id} || {self.config.executable} rm -f {self.container_id}) >/dev/null 2>&1 &"
|
|
110
|
+
subprocess.Popen(cmd, shell=True)
|
|
111
|
+
|
|
112
|
+
def __del__(self):
|
|
113
|
+
"""Cleanup container when object is destroyed."""
|
|
114
|
+
self.cleanup()
|
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
[Bubblewrap](https://github.com/containers/bubblewrap) is a low-level, unprivileged sandboxing tool for Linux that enables running applications
|
|
3
|
+
in isolated environments with restricted access to the operating system and user data.
|
|
4
|
+
This environment uses bubblewrap to execute commands in a sandboxed environment.
|
|
5
|
+
|
|
6
|
+
!!! warning
|
|
7
|
+
This environment is experimental.
|
|
8
|
+
|
|
9
|
+
!!! warning
|
|
10
|
+
This environment is not supported on Windows.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import platform
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import tempfile
|
|
19
|
+
import uuid
|
|
20
|
+
from dataclasses import asdict, dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class BubblewrapEnvironmentConfig:
|
|
27
|
+
cwd: str = ""
|
|
28
|
+
"""Working directory for the sandbox."""
|
|
29
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
30
|
+
"""Dictionary of environment variables to set in the sandbox."""
|
|
31
|
+
timeout: int = 30
|
|
32
|
+
"""Timeout for the command in seconds."""
|
|
33
|
+
executable: str = os.getenv("MSWEA_BUBBLEWRAP_EXECUTABLE", "bwrap")
|
|
34
|
+
"""Path to the bubblewrap executable."""
|
|
35
|
+
wrapper_args: list[str] = field(
|
|
36
|
+
default_factory=lambda: [
|
|
37
|
+
"--unshare-user-try",
|
|
38
|
+
"--ro-bind",
|
|
39
|
+
"/usr",
|
|
40
|
+
"/usr",
|
|
41
|
+
"--ro-bind",
|
|
42
|
+
"/bin",
|
|
43
|
+
"/bin",
|
|
44
|
+
"--ro-bind",
|
|
45
|
+
"/lib",
|
|
46
|
+
"/lib",
|
|
47
|
+
"--ro-bind",
|
|
48
|
+
"/lib64",
|
|
49
|
+
"/lib64",
|
|
50
|
+
"--ro-bind",
|
|
51
|
+
"/etc",
|
|
52
|
+
"/etc",
|
|
53
|
+
"--tmpfs",
|
|
54
|
+
"/tmp",
|
|
55
|
+
"--proc",
|
|
56
|
+
"/proc",
|
|
57
|
+
"--dev",
|
|
58
|
+
"/dev",
|
|
59
|
+
"--new-session",
|
|
60
|
+
"--setenv",
|
|
61
|
+
"PATH",
|
|
62
|
+
"/usr/local/bin:/usr/sbin:/usr/bin:/bin",
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
"""Arguments to pass to the bubblewrap executable."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BubblewrapEnvironment:
|
|
69
|
+
def __init__(
|
|
70
|
+
self, *, config_class: type = BubblewrapEnvironmentConfig, logger: logging.Logger | None = None, **kwargs
|
|
71
|
+
):
|
|
72
|
+
"""This class executes bash commands in a bubblewrap environment and a separate working
|
|
73
|
+
directory for each environment. See `BubblewrapEnvironmentConfig` for kwargs.
|
|
74
|
+
"""
|
|
75
|
+
self.logger = logger or logging.getLogger("minisweagent.environment")
|
|
76
|
+
self.config = config_class(**kwargs)
|
|
77
|
+
self.working_dir = Path(tempfile.gettempdir()) / f"minisweagent-{uuid.uuid4().hex[:8]}"
|
|
78
|
+
self.working_dir.mkdir(parents=True)
|
|
79
|
+
|
|
80
|
+
def execute(self, command: str, cwd: str = "", *, timeout: int | None = None) -> dict[str, Any]:
|
|
81
|
+
"""Execute a command in the bubblewrap environment and return the result as a dict."""
|
|
82
|
+
cwd = cwd or self.config.cwd or str(self.working_dir)
|
|
83
|
+
|
|
84
|
+
cmd = [self.config.executable] + self.config.wrapper_args + ["--bind", cwd, cwd, "--chdir", cwd]
|
|
85
|
+
|
|
86
|
+
# Add environment variables
|
|
87
|
+
for key, value in self.config.env.items():
|
|
88
|
+
cmd.extend(["--setenv", key, value])
|
|
89
|
+
|
|
90
|
+
cmd.extend(["bash", "-c", command])
|
|
91
|
+
|
|
92
|
+
result = subprocess.run(
|
|
93
|
+
cmd,
|
|
94
|
+
text=True,
|
|
95
|
+
timeout=timeout or self.config.timeout,
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
errors="replace",
|
|
98
|
+
stdout=subprocess.PIPE,
|
|
99
|
+
stderr=subprocess.STDOUT,
|
|
100
|
+
)
|
|
101
|
+
return {"output": result.stdout, "returncode": result.returncode}
|
|
102
|
+
|
|
103
|
+
def cleanup(self):
|
|
104
|
+
if self.working_dir.exists():
|
|
105
|
+
shutil.rmtree(self.working_dir)
|
|
106
|
+
|
|
107
|
+
def __del__(self):
|
|
108
|
+
"""Cleanup working_dir when object is destroyed."""
|
|
109
|
+
self.cleanup()
|
|
110
|
+
|
|
111
|
+
def get_template_vars(self) -> dict[str, Any]:
|
|
112
|
+
return asdict(self.config) | platform.uname()._asdict()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from dataclasses import asdict, dataclass, field
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from swerex.deployment.docker import DockerDeployment
|
|
6
|
+
from swerex.runtime.abstract import Command as RexCommand
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SwerexDockerEnvironmentConfig:
|
|
11
|
+
image: str
|
|
12
|
+
cwd: str = "/"
|
|
13
|
+
"""Working directory in which to execute commands."""
|
|
14
|
+
timeout: int = 30
|
|
15
|
+
"""Timeout for executing commands in the container."""
|
|
16
|
+
deployment_extra_kwargs: dict[str, Any] = field(default_factory=dict)
|
|
17
|
+
"""Extra kwargs to pass to DockerDeployment."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SwerexDockerEnvironment:
|
|
21
|
+
def __init__(self, **kwargs):
|
|
22
|
+
"""This class executes bash commands in a Docker container using SWE-ReX for sandboxing."""
|
|
23
|
+
self.config = SwerexDockerEnvironmentConfig(**kwargs)
|
|
24
|
+
self.deployment = DockerDeployment(image=self.config.image, **self.config.deployment_extra_kwargs)
|
|
25
|
+
asyncio.run(self.deployment.start())
|
|
26
|
+
|
|
27
|
+
def execute(self, command: str, cwd: str = "", *, timeout: int | None = None) -> dict[str, Any]:
|
|
28
|
+
"""Execute a command in the environment and return the raw output."""
|
|
29
|
+
output = asyncio.run(
|
|
30
|
+
self.deployment.runtime.execute(
|
|
31
|
+
RexCommand(
|
|
32
|
+
command=command,
|
|
33
|
+
shell=True,
|
|
34
|
+
check=False,
|
|
35
|
+
cwd=cwd or self.config.cwd,
|
|
36
|
+
timeout=timeout or self.config.timeout,
|
|
37
|
+
merge_output_streams=True,
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
return {
|
|
42
|
+
"output": output.stdout,
|
|
43
|
+
"returncode": output.exit_code,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def get_template_vars(self) -> dict[str, Any]:
|
|
47
|
+
return asdict(self.config)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import platform
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class LocalEnvironmentConfig:
|
|
10
|
+
cwd: str = ""
|
|
11
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
12
|
+
timeout: int = 30
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LocalEnvironment:
|
|
16
|
+
def __init__(self, *, config_class: type = LocalEnvironmentConfig, **kwargs):
|
|
17
|
+
"""This class executes bash commands directly on the local machine."""
|
|
18
|
+
self.config = config_class(**kwargs)
|
|
19
|
+
|
|
20
|
+
def execute(self, command: str, cwd: str = "", *, timeout: int | None = None):
|
|
21
|
+
"""Execute a command in the local environment and return the result as a dict."""
|
|
22
|
+
cwd = cwd or self.config.cwd or os.getcwd()
|
|
23
|
+
result = subprocess.run(
|
|
24
|
+
command,
|
|
25
|
+
shell=True,
|
|
26
|
+
text=True,
|
|
27
|
+
cwd=cwd,
|
|
28
|
+
env=os.environ | self.config.env,
|
|
29
|
+
timeout=timeout or self.config.timeout,
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
errors="replace",
|
|
32
|
+
stdout=subprocess.PIPE,
|
|
33
|
+
stderr=subprocess.STDOUT,
|
|
34
|
+
)
|
|
35
|
+
return {"output": result.stdout, "returncode": result.returncode}
|
|
36
|
+
|
|
37
|
+
def get_template_vars(self) -> dict[str, Any]:
|
|
38
|
+
return asdict(self.config) | platform.uname()._asdict() | os.environ
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
import uuid
|
|
9
|
+
from dataclasses import asdict, dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SingularityEnvironmentConfig:
|
|
16
|
+
image: str
|
|
17
|
+
cwd: str = "/"
|
|
18
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
19
|
+
"""Environment variables to set in the container."""
|
|
20
|
+
forward_env: list[str] = field(default_factory=list)
|
|
21
|
+
"""Environment variables to forward to the container."""
|
|
22
|
+
timeout: int = 30
|
|
23
|
+
"""Timeout for executing commands in the container."""
|
|
24
|
+
executable: str = os.getenv("MSWEA_SINGULARITY_EXECUTABLE", "singularity")
|
|
25
|
+
"""Path to the singularity executable."""
|
|
26
|
+
sandbox_build_retries: int = 3
|
|
27
|
+
"""Number of retries for building the sandbox if an error occurs."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SingularityEnvironment:
|
|
31
|
+
def __init__(
|
|
32
|
+
self, *, config_class: type = SingularityEnvironmentConfig, logger: logging.Logger | None = None, **kwargs
|
|
33
|
+
):
|
|
34
|
+
"""Singularity environment. See `SingularityEnvironmentConfig` for kwargs."""
|
|
35
|
+
self.logger = logger or logging.getLogger("minisweagent.environment")
|
|
36
|
+
self.config = config_class(**kwargs)
|
|
37
|
+
self.sandbox_dir = self._build_sandbox()
|
|
38
|
+
|
|
39
|
+
def _build_sandbox(self) -> Path:
|
|
40
|
+
# Building the sandbox can fail (very rarely), so we retry it
|
|
41
|
+
max_retries = self.config.sandbox_build_retries
|
|
42
|
+
for attempt in range(max_retries):
|
|
43
|
+
sandbox_dir = Path(tempfile.gettempdir()) / f"minisweagent-{uuid.uuid4().hex[:8]}"
|
|
44
|
+
try:
|
|
45
|
+
subprocess.run(
|
|
46
|
+
[self.config.executable, "build", "--sandbox", sandbox_dir, self.config.image],
|
|
47
|
+
check=True,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
)
|
|
50
|
+
break
|
|
51
|
+
except subprocess.CalledProcessError as e:
|
|
52
|
+
shutil.rmtree(sandbox_dir, ignore_errors=True)
|
|
53
|
+
self.logger.error(
|
|
54
|
+
f"Error building image {self.config.image}, stdout: {e.stdout}, stderr: {e.stderr} (attempt {attempt + 1}/{max_retries})"
|
|
55
|
+
)
|
|
56
|
+
if attempt == max_retries - 1:
|
|
57
|
+
raise
|
|
58
|
+
return sandbox_dir
|
|
59
|
+
|
|
60
|
+
def get_template_vars(self) -> dict[str, Any]:
|
|
61
|
+
return asdict(self.config)
|
|
62
|
+
|
|
63
|
+
def execute(self, command: str, cwd: str = "", *, timeout: int | None = None) -> dict[str, Any]:
|
|
64
|
+
"""Execute a command in a Singularity container and return the result as a dict."""
|
|
65
|
+
cmd = [self.config.executable, "exec"]
|
|
66
|
+
|
|
67
|
+
# Do not inherit directories and env vars from host
|
|
68
|
+
cmd.extend(["--contain", "--cleanenv"])
|
|
69
|
+
|
|
70
|
+
work_dir = cwd or self.config.cwd
|
|
71
|
+
if work_dir and work_dir != "/":
|
|
72
|
+
cmd.extend(["--pwd", work_dir])
|
|
73
|
+
|
|
74
|
+
for key in self.config.forward_env:
|
|
75
|
+
if (value := os.getenv(key)) is not None:
|
|
76
|
+
cmd.extend(["--env", f"{key}={value}"])
|
|
77
|
+
for key, value in self.config.env.items():
|
|
78
|
+
cmd.extend(["--env", f"{key}={value}"])
|
|
79
|
+
|
|
80
|
+
cmd.extend(["--writable", str(self.sandbox_dir), "bash", "-c", command])
|
|
81
|
+
result = subprocess.run(
|
|
82
|
+
cmd,
|
|
83
|
+
text=True,
|
|
84
|
+
timeout=timeout or self.config.timeout,
|
|
85
|
+
encoding="utf-8",
|
|
86
|
+
errors="replace",
|
|
87
|
+
stdout=subprocess.PIPE,
|
|
88
|
+
stderr=subprocess.STDOUT,
|
|
89
|
+
)
|
|
90
|
+
return {"output": result.stdout, "returncode": result.returncode}
|
|
91
|
+
|
|
92
|
+
def cleanup(self):
|
|
93
|
+
shutil.rmtree(self.sandbox_dir, ignore_errors=True)
|
|
94
|
+
|
|
95
|
+
def __del__(self):
|
|
96
|
+
"""Cleanup sandbox when object is destroyed."""
|
|
97
|
+
self.cleanup()
|