-
Notifications
You must be signed in to change notification settings - Fork 802
Expand file tree
/
Copy pathlocal.py
More file actions
92 lines (80 loc) · 3.53 KB
/
Copy pathlocal.py
File metadata and controls
92 lines (80 loc) · 3.53 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
import os
import platform
import signal
import subprocess
from typing import Any
from pydantic import BaseModel
from minisweagent.exceptions import Submitted
from minisweagent.utils.serialize import recursive_merge
class LocalEnvironmentConfig(BaseModel):
cwd: str = ""
env: dict[str, str] = {}
timeout: int = 30
class LocalEnvironment:
def __init__(self, *, config_class: type = LocalEnvironmentConfig, **kwargs):
"""This class executes bash commands directly on the local machine."""
self.config = config_class(**kwargs)
def execute(self, action: dict, cwd: str = "", *, timeout: int | None = None) -> dict[str, Any]:
"""Execute a command in the local environment and return the result as a dict."""
command = action.get("command", "")
cwd = cwd or self.config.cwd or os.getcwd()
try:
result = _run(command, cwd, os.environ | self.config.env, timeout or self.config.timeout)
output = {"output": result.stdout, "returncode": result.returncode, "exception_info": ""}
except Exception as e:
raw_output = getattr(e, "output", None)
raw_output = (
raw_output.decode("utf-8", errors="replace") if isinstance(raw_output, bytes) else (raw_output or "")
)
output = {
"output": raw_output,
"returncode": -1,
"exception_info": f"An error occurred while executing the command: {e}",
"extra": {"exception_type": type(e).__name__, "exception": str(e)},
}
self._check_finished(output)
return output
def _check_finished(self, output: dict):
"""Raises Submitted if the output indicates task completion."""
lines = output.get("output", "").lstrip().splitlines(keepends=True)
if lines and lines[0].strip() == "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT" and output["returncode"] == 0:
submission = "".join(lines[1:])
raise Submitted(
{
"role": "exit",
"content": submission,
"extra": {"exit_status": "Submitted", "submission": submission},
}
)
def get_template_vars(self, **kwargs) -> dict[str, Any]:
return recursive_merge(self.config.model_dump(), platform.uname()._asdict(), os.environ, kwargs)
def serialize(self) -> dict:
return {
"info": {
"config": {
"environment": self.config.model_dump(mode="json"),
"environment_type": f"{self.__class__.__module__}.{self.__class__.__name__}",
}
}
}
def _run(command: str, cwd: str, env: dict[str, str], timeout: int) -> subprocess.CompletedProcess[str]:
"""Like subprocess.run, but kills the whole process group on timeout so no children are orphaned."""
process = subprocess.Popen(
command,
shell=True,
text=True,
cwd=cwd,
env=env,
encoding="utf-8",
errors="replace",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
start_new_session=os.name == "posix",
)
try:
stdout, _ = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL) if os.name == "posix" else process.kill()
stdout, _ = process.communicate()
raise subprocess.TimeoutExpired(command, timeout, output=stdout)
return subprocess.CompletedProcess(command, process.returncode, stdout=stdout)