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,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Simple trajectory inspector for browsing agent conversation trajectories.
|
|
4
|
+
|
|
5
|
+
[not dim]
|
|
6
|
+
More information about the usage: [bold green]https://mini-swe-agent.com/latest/usage/inspector/[/bold green]
|
|
7
|
+
[/not dim]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
from textual.app import App, ComposeResult
|
|
17
|
+
from textual.binding import Binding
|
|
18
|
+
from textual.containers import Container, Vertical, VerticalScroll
|
|
19
|
+
from textual.widgets import Footer, Header, Static
|
|
20
|
+
|
|
21
|
+
from minisweagent.agents.interactive_textual import _messages_to_steps
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(rich_markup_mode="rich", add_completion=False)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TrajectoryInspector(App):
|
|
27
|
+
BINDINGS = [
|
|
28
|
+
Binding("right,l", "next_step", "Step++"),
|
|
29
|
+
Binding("left,h", "previous_step", "Step--"),
|
|
30
|
+
Binding("0", "first_step", "Step=0"),
|
|
31
|
+
Binding("$", "last_step", "Step=-1"),
|
|
32
|
+
Binding("j,down", "scroll_down", "Scroll down"),
|
|
33
|
+
Binding("k,up", "scroll_up", "Scroll up"),
|
|
34
|
+
Binding("L", "next_trajectory", "Next trajectory"),
|
|
35
|
+
Binding("H", "previous_trajectory", "Previous trajectory"),
|
|
36
|
+
Binding("q", "quit", "Quit"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def __init__(self, trajectory_files: list[Path]):
|
|
40
|
+
css_path = os.environ.get(
|
|
41
|
+
"MSWEA_INSPECTOR_STYLE_PATH", str(Path(__file__).parent.parent / "config" / "mini.tcss")
|
|
42
|
+
)
|
|
43
|
+
self.__class__.CSS = Path(css_path).read_text()
|
|
44
|
+
|
|
45
|
+
super().__init__()
|
|
46
|
+
self.trajectory_files = trajectory_files
|
|
47
|
+
self._i_trajectory = 0
|
|
48
|
+
self._i_step = 0
|
|
49
|
+
self.messages = []
|
|
50
|
+
self.steps = []
|
|
51
|
+
|
|
52
|
+
if trajectory_files:
|
|
53
|
+
self._load_current_trajectory()
|
|
54
|
+
|
|
55
|
+
# --- Basics ---
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def i_step(self) -> int:
|
|
59
|
+
"""Current step index."""
|
|
60
|
+
return self._i_step
|
|
61
|
+
|
|
62
|
+
@i_step.setter
|
|
63
|
+
def i_step(self, value: int) -> None:
|
|
64
|
+
"""Set current step index, automatically clamping to valid bounds."""
|
|
65
|
+
if value != self._i_step and self.n_steps > 0:
|
|
66
|
+
self._i_step = max(0, min(value, self.n_steps - 1))
|
|
67
|
+
self.query_one(VerticalScroll).scroll_to(y=0, animate=False)
|
|
68
|
+
self.update_content()
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def n_steps(self) -> int:
|
|
72
|
+
"""Number of steps in current trajectory."""
|
|
73
|
+
return len(self.steps)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def i_trajectory(self) -> int:
|
|
77
|
+
"""Current trajectory index."""
|
|
78
|
+
return self._i_trajectory
|
|
79
|
+
|
|
80
|
+
@i_trajectory.setter
|
|
81
|
+
def i_trajectory(self, value: int) -> None:
|
|
82
|
+
"""Set current trajectory index, automatically clamping to valid bounds."""
|
|
83
|
+
if value != self._i_trajectory and self.n_trajectories > 0:
|
|
84
|
+
self._i_trajectory = max(0, min(value, self.n_trajectories - 1))
|
|
85
|
+
self._load_current_trajectory()
|
|
86
|
+
self.query_one(VerticalScroll).scroll_to(y=0, animate=False)
|
|
87
|
+
self.update_content()
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def n_trajectories(self) -> int:
|
|
91
|
+
"""Number of trajectory files."""
|
|
92
|
+
return len(self.trajectory_files)
|
|
93
|
+
|
|
94
|
+
def _load_current_trajectory(self) -> None:
|
|
95
|
+
"""Load the currently selected trajectory file."""
|
|
96
|
+
if not self.trajectory_files:
|
|
97
|
+
self.messages = []
|
|
98
|
+
self.steps = []
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
trajectory_file = self.trajectory_files[self.i_trajectory]
|
|
102
|
+
try:
|
|
103
|
+
data = json.loads(trajectory_file.read_text())
|
|
104
|
+
|
|
105
|
+
if isinstance(data, list):
|
|
106
|
+
self.messages = data
|
|
107
|
+
elif isinstance(data, dict) and "messages" in data:
|
|
108
|
+
self.messages = data["messages"]
|
|
109
|
+
else:
|
|
110
|
+
raise ValueError("Unrecognized trajectory format")
|
|
111
|
+
|
|
112
|
+
self.steps = _messages_to_steps(self.messages)
|
|
113
|
+
self._i_step = 0
|
|
114
|
+
except (json.JSONDecodeError, FileNotFoundError, ValueError) as e:
|
|
115
|
+
self.messages = []
|
|
116
|
+
self.steps = []
|
|
117
|
+
self.notify(f"Error loading {trajectory_file.name}: {e}", severity="error")
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def current_trajectory_name(self) -> str:
|
|
121
|
+
"""Get the name of the current trajectory file."""
|
|
122
|
+
if not self.trajectory_files:
|
|
123
|
+
return "No trajectories"
|
|
124
|
+
return self.trajectory_files[self.i_trajectory].name
|
|
125
|
+
|
|
126
|
+
def compose(self) -> ComposeResult:
|
|
127
|
+
yield Header()
|
|
128
|
+
with Container(id="main"):
|
|
129
|
+
with VerticalScroll():
|
|
130
|
+
yield Vertical(id="content")
|
|
131
|
+
yield Footer()
|
|
132
|
+
|
|
133
|
+
def on_mount(self) -> None:
|
|
134
|
+
self.update_content()
|
|
135
|
+
|
|
136
|
+
def update_content(self) -> None:
|
|
137
|
+
"""Update the displayed content."""
|
|
138
|
+
container = self.query_one("#content", Vertical)
|
|
139
|
+
container.remove_children()
|
|
140
|
+
|
|
141
|
+
if not self.steps:
|
|
142
|
+
container.mount(Static("No trajectory loaded or empty trajectory"))
|
|
143
|
+
self.title = "Trajectory Inspector - No Data"
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
for message in self.steps[self.i_step]:
|
|
147
|
+
if isinstance(message["content"], list):
|
|
148
|
+
content_str = "\n".join([item["text"] for item in message["content"]])
|
|
149
|
+
else:
|
|
150
|
+
content_str = str(message["content"])
|
|
151
|
+
message_container = Vertical(classes="message-container")
|
|
152
|
+
container.mount(message_container)
|
|
153
|
+
role = message["role"].replace("assistant", "mini-swe-agent")
|
|
154
|
+
message_container.mount(Static(role.upper(), classes="message-header"))
|
|
155
|
+
message_container.mount(Static(Text(content_str, no_wrap=False), classes="message-content"))
|
|
156
|
+
|
|
157
|
+
self.title = (
|
|
158
|
+
f"Trajectory {self.i_trajectory + 1}/{self.n_trajectories} - "
|
|
159
|
+
f"{self.current_trajectory_name} - "
|
|
160
|
+
f"Step {self.i_step + 1}/{self.n_steps}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# --- Navigation actions ---
|
|
164
|
+
|
|
165
|
+
def action_next_step(self) -> None:
|
|
166
|
+
self.i_step += 1
|
|
167
|
+
|
|
168
|
+
def action_previous_step(self) -> None:
|
|
169
|
+
self.i_step -= 1
|
|
170
|
+
|
|
171
|
+
def action_first_step(self) -> None:
|
|
172
|
+
self.i_step = 0
|
|
173
|
+
|
|
174
|
+
def action_last_step(self) -> None:
|
|
175
|
+
self.i_step = self.n_steps - 1
|
|
176
|
+
|
|
177
|
+
def action_next_trajectory(self) -> None:
|
|
178
|
+
self.i_trajectory += 1
|
|
179
|
+
|
|
180
|
+
def action_previous_trajectory(self) -> None:
|
|
181
|
+
self.i_trajectory -= 1
|
|
182
|
+
|
|
183
|
+
def action_scroll_down(self) -> None:
|
|
184
|
+
vs = self.query_one(VerticalScroll)
|
|
185
|
+
vs.scroll_to(y=vs.scroll_target_y + 15)
|
|
186
|
+
|
|
187
|
+
def action_scroll_up(self) -> None:
|
|
188
|
+
vs = self.query_one(VerticalScroll)
|
|
189
|
+
vs.scroll_to(y=vs.scroll_target_y - 15)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@app.command(help=__doc__)
|
|
193
|
+
def main(
|
|
194
|
+
path: str = typer.Argument(".", help="Directory to search for trajectory files or specific trajectory file"),
|
|
195
|
+
) -> None:
|
|
196
|
+
path_obj = Path(path)
|
|
197
|
+
|
|
198
|
+
if path_obj.is_file():
|
|
199
|
+
trajectory_files = [path_obj]
|
|
200
|
+
elif path_obj.is_dir():
|
|
201
|
+
trajectory_files = sorted(path_obj.rglob("*.traj.json"))
|
|
202
|
+
if not trajectory_files:
|
|
203
|
+
raise typer.BadParameter(f"No trajectory files found in '{path}'")
|
|
204
|
+
else:
|
|
205
|
+
raise typer.BadParameter(f"Error: Path '{path}' does not exist")
|
|
206
|
+
|
|
207
|
+
inspector = TrajectoryInspector(trajectory_files)
|
|
208
|
+
inspector.run()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
if __name__ == "__main__":
|
|
212
|
+
app()
|
minisweagent/run/mini.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Run mini-SWE-agent in your local environment. This is the default executable `mini`."""
|
|
4
|
+
# Read this first: https://mini-swe-agent.com/latest/usage/mini/ (usage)
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import traceback
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
import yaml
|
|
13
|
+
from prompt_toolkit.formatted_text import HTML
|
|
14
|
+
from prompt_toolkit.history import FileHistory
|
|
15
|
+
from prompt_toolkit.shortcuts import PromptSession
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
from minisweagent import global_config_dir
|
|
19
|
+
from minisweagent.agents.interactive import InteractiveAgent
|
|
20
|
+
from minisweagent.agents.interactive_textual import TextualAgent
|
|
21
|
+
from minisweagent.config import builtin_config_dir, get_config_path
|
|
22
|
+
from minisweagent.environments.local import LocalEnvironment
|
|
23
|
+
from minisweagent.models import get_model
|
|
24
|
+
from minisweagent.run.extra.config import configure_if_first_time
|
|
25
|
+
from minisweagent.run.utils.save import save_traj
|
|
26
|
+
from minisweagent.utils.log import logger
|
|
27
|
+
|
|
28
|
+
DEFAULT_CONFIG = Path(os.getenv("MSWEA_MINI_CONFIG_PATH", builtin_config_dir / "mini.yaml"))
|
|
29
|
+
DEFAULT_OUTPUT = global_config_dir / "last_mini_run.traj.json"
|
|
30
|
+
console = Console(highlight=False)
|
|
31
|
+
app = typer.Typer(rich_markup_mode="rich")
|
|
32
|
+
prompt_session = PromptSession(history=FileHistory(global_config_dir / "mini_task_history.txt"))
|
|
33
|
+
_HELP_TEXT = """Run mini-SWE-agent in your local environment.
|
|
34
|
+
|
|
35
|
+
[not dim]
|
|
36
|
+
There are two different user interfaces:
|
|
37
|
+
|
|
38
|
+
[bold green]mini[/bold green] Simple REPL-style interface
|
|
39
|
+
[bold green]mini -v[/bold green] Pager-style interface (Textual)
|
|
40
|
+
|
|
41
|
+
More information about the usage: [bold green]https://mini-swe-agent.com/latest/usage/mini/[/bold green]
|
|
42
|
+
[/not dim]
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# fmt: off
|
|
47
|
+
@app.command(help=_HELP_TEXT)
|
|
48
|
+
def main(
|
|
49
|
+
visual: bool = typer.Option(False, "-v", "--visual", help="Toggle (pager-style) UI (Textual) depending on the MSWEA_VISUAL_MODE_DEFAULT environment setting",),
|
|
50
|
+
model_name: str | None = typer.Option( None, "-m", "--model", help="Model to use",),
|
|
51
|
+
model_class: str | None = typer.Option(None, "--model-class", help="Model class to use (e.g., 'anthropic' or 'minisweagent.models.anthropic.AnthropicModel')", rich_help_panel="Advanced"),
|
|
52
|
+
task: str | None = typer.Option(None, "-t", "--task", help="Task/problem statement", show_default=False),
|
|
53
|
+
yolo: bool = typer.Option(False, "-y", "--yolo", help="Run without confirmation"),
|
|
54
|
+
cost_limit: float | None = typer.Option(None, "-l", "--cost-limit", help="Cost limit. Set to 0 to disable."),
|
|
55
|
+
config_spec: Path = typer.Option(DEFAULT_CONFIG, "-c", "--config", help="Path to config file"),
|
|
56
|
+
output: Path | None = typer.Option(DEFAULT_OUTPUT, "-o", "--output", help="Output trajectory file"),
|
|
57
|
+
exit_immediately: bool = typer.Option( False, "--exit-immediately", help="Exit immediately when the agent wants to finish instead of prompting.", rich_help_panel="Advanced"),
|
|
58
|
+
) -> Any:
|
|
59
|
+
# fmt: on
|
|
60
|
+
configure_if_first_time()
|
|
61
|
+
config_path = get_config_path(config_spec)
|
|
62
|
+
console.print(f"Loading agent config from [bold green]'{config_path}'[/bold green]")
|
|
63
|
+
config = yaml.safe_load(config_path.read_text())
|
|
64
|
+
|
|
65
|
+
if not task:
|
|
66
|
+
console.print("[bold yellow]What do you want to do?")
|
|
67
|
+
task = prompt_session.prompt(
|
|
68
|
+
"",
|
|
69
|
+
multiline=True,
|
|
70
|
+
bottom_toolbar=HTML(
|
|
71
|
+
"Submit task: <b fg='yellow' bg='black'>Esc+Enter</b> | "
|
|
72
|
+
"Navigate history: <b fg='yellow' bg='black'>Arrow Up/Down</b> | "
|
|
73
|
+
"Search history: <b fg='yellow' bg='black'>Ctrl+R</b>"
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
console.print("[bold green]Got that, thanks![/bold green]")
|
|
77
|
+
|
|
78
|
+
if yolo:
|
|
79
|
+
config.setdefault("agent", {})["mode"] = "yolo"
|
|
80
|
+
if cost_limit is not None:
|
|
81
|
+
config.setdefault("agent", {})["cost_limit"] = cost_limit
|
|
82
|
+
if exit_immediately:
|
|
83
|
+
config.setdefault("agent", {})["confirm_exit"] = False
|
|
84
|
+
if model_class is not None:
|
|
85
|
+
config.setdefault("model", {})["model_class"] = model_class
|
|
86
|
+
model = get_model(model_name, config.get("model", {}))
|
|
87
|
+
env = LocalEnvironment(**config.get("env", {}))
|
|
88
|
+
|
|
89
|
+
# Both visual flag and the MSWEA_VISUAL_MODE_DEFAULT flip the mode, so it's essentially a XOR
|
|
90
|
+
agent_class = InteractiveAgent
|
|
91
|
+
if visual == (os.getenv("MSWEA_VISUAL_MODE_DEFAULT", "false") == "false"):
|
|
92
|
+
agent_class = TextualAgent
|
|
93
|
+
|
|
94
|
+
agent = agent_class(model, env, **config.get("agent", {}))
|
|
95
|
+
exit_status, result, extra_info = None, None, None
|
|
96
|
+
try:
|
|
97
|
+
exit_status, result = agent.run(task) # type: ignore[arg-type]
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"Error running agent: {e}", exc_info=True)
|
|
100
|
+
exit_status, result = type(e).__name__, str(e)
|
|
101
|
+
extra_info = {"traceback": traceback.format_exc()}
|
|
102
|
+
finally:
|
|
103
|
+
save_traj(agent, output, exit_status=exit_status, result=result, extra_info=extra_info) # type: ignore[arg-type]
|
|
104
|
+
return agent
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
app()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from importlib import import_module
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
subcommands = [
|
|
9
|
+
("minisweagent.run.extra.config", ["config"], "Manage the global config file"),
|
|
10
|
+
("minisweagent.run.inspector", ["inspect", "i", "inspector"], "Run inspector (browse trajectories)"),
|
|
11
|
+
("minisweagent.run.github_issue", ["github-issue", "gh"], "Run on a GitHub issue"),
|
|
12
|
+
("minisweagent.run.extra.swebench", ["swebench"], "Evaluate on SWE-bench (batch mode)"),
|
|
13
|
+
("minisweagent.run.extra.swebench_single", ["swebench-single"], "Evaluate on SWE-bench (single instance)"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_docstring() -> str:
|
|
18
|
+
lines = [
|
|
19
|
+
"This is the [yellow]central entry point for all extra commands[/yellow] from mini-swe-agent.",
|
|
20
|
+
"",
|
|
21
|
+
"Available sub-commands:",
|
|
22
|
+
"",
|
|
23
|
+
]
|
|
24
|
+
for _, aliases, description in subcommands:
|
|
25
|
+
alias_text = " or ".join(f"[bold green]{alias}[/bold green]" for alias in aliases)
|
|
26
|
+
lines.append(f" {alias_text}: {description}")
|
|
27
|
+
return "\n".join(lines)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main():
|
|
31
|
+
args = sys.argv[1:]
|
|
32
|
+
|
|
33
|
+
if len(args) == 0 or len(args) == 1 and args[0] in ["-h", "--help"]:
|
|
34
|
+
return Console().print(get_docstring())
|
|
35
|
+
|
|
36
|
+
for module_path, aliases, _ in subcommands:
|
|
37
|
+
if args[0] in aliases:
|
|
38
|
+
return import_module(module_path).app(args[1:], prog_name=f"mini-extra {aliases[0]}")
|
|
39
|
+
|
|
40
|
+
return Console().print(get_docstring())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import json
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from minisweagent import Agent, __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_class_name_with_module(obj: Any) -> str:
|
|
11
|
+
"""Get the full class name with module path."""
|
|
12
|
+
return f"{obj.__class__.__module__}.{obj.__class__.__name__}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _asdict(obj: Any) -> dict:
|
|
16
|
+
"""Convert config objects to dicts."""
|
|
17
|
+
if dataclasses.is_dataclass(obj):
|
|
18
|
+
return dataclasses.asdict(obj) # type: ignore[arg-type]
|
|
19
|
+
return obj # let's try our luck
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def save_traj(
|
|
23
|
+
agent: Agent | None,
|
|
24
|
+
path: Path | None,
|
|
25
|
+
*,
|
|
26
|
+
print_path: bool = True,
|
|
27
|
+
exit_status: str | None = None,
|
|
28
|
+
result: str | None = None,
|
|
29
|
+
extra_info: dict | None = None,
|
|
30
|
+
print_fct: Callable = print,
|
|
31
|
+
**kwargs,
|
|
32
|
+
):
|
|
33
|
+
"""Save the trajectory of the agent to a file.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
agent: The agent to save the trajectory of.
|
|
37
|
+
path: The path to save the trajectory to.
|
|
38
|
+
print_path: Whether to print confirmation of path to the terminal.
|
|
39
|
+
exit_status: The exit status of the agent.
|
|
40
|
+
result: The result/submission of the agent.
|
|
41
|
+
extra_info: Extra information to save (will be merged into the info dict).
|
|
42
|
+
**kwargs: Additional information to save (will be merged into top level)
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
if path is None:
|
|
46
|
+
return
|
|
47
|
+
data = {
|
|
48
|
+
"info": {
|
|
49
|
+
"exit_status": exit_status,
|
|
50
|
+
"submission": result,
|
|
51
|
+
"model_stats": {
|
|
52
|
+
"instance_cost": 0.0,
|
|
53
|
+
"api_calls": 0,
|
|
54
|
+
},
|
|
55
|
+
"mini_version": __version__,
|
|
56
|
+
},
|
|
57
|
+
"messages": [],
|
|
58
|
+
"trajectory_format": "mini-swe-agent-1",
|
|
59
|
+
} | kwargs
|
|
60
|
+
if agent is not None:
|
|
61
|
+
data["info"]["model_stats"]["instance_cost"] = agent.model.cost
|
|
62
|
+
data["info"]["model_stats"]["api_calls"] = agent.model.n_calls
|
|
63
|
+
data["messages"] = agent.messages
|
|
64
|
+
data["info"]["config"] = {
|
|
65
|
+
"agent": _asdict(agent.config),
|
|
66
|
+
"model": _asdict(agent.model.config),
|
|
67
|
+
"environment": _asdict(agent.env.config),
|
|
68
|
+
"agent_type": _get_class_name_with_module(agent),
|
|
69
|
+
"model_type": _get_class_name_with_module(agent.model),
|
|
70
|
+
"environment_type": _get_class_name_with_module(agent.env),
|
|
71
|
+
}
|
|
72
|
+
if extra_info:
|
|
73
|
+
data["info"].update(extra_info)
|
|
74
|
+
|
|
75
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
path.write_text(json.dumps(data, indent=2))
|
|
77
|
+
if print_path:
|
|
78
|
+
print_fct(f"Saved trajectory to '{path}'")
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from rich.logging import RichHandler
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _setup_root_logger() -> None:
|
|
8
|
+
logger = logging.getLogger("minisweagent")
|
|
9
|
+
logger.setLevel(logging.DEBUG)
|
|
10
|
+
_handler = RichHandler(
|
|
11
|
+
show_path=False,
|
|
12
|
+
show_time=False,
|
|
13
|
+
show_level=False,
|
|
14
|
+
markup=True,
|
|
15
|
+
)
|
|
16
|
+
_formatter = logging.Formatter("%(name)s: %(levelname)s: %(message)s")
|
|
17
|
+
_handler.setFormatter(_formatter)
|
|
18
|
+
logger.addHandler(_handler)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def add_file_handler(path: Path | str, level: int = logging.DEBUG, *, print_path: bool = True) -> None:
|
|
22
|
+
logger = logging.getLogger("minisweagent")
|
|
23
|
+
handler = logging.FileHandler(path)
|
|
24
|
+
handler.setLevel(level)
|
|
25
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
26
|
+
handler.setFormatter(formatter)
|
|
27
|
+
logger.addHandler(handler)
|
|
28
|
+
if print_path:
|
|
29
|
+
print(f"Logging to '{path}'")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_setup_root_logger()
|
|
33
|
+
logger = logging.getLogger("minisweagent")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["logger"]
|