mini-swe-agent 1.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 (47) hide show
  1. mini_swe_agent-1.1.0.dist-info/METADATA +288 -0
  2. mini_swe_agent-1.1.0.dist-info/RECORD +47 -0
  3. mini_swe_agent-1.1.0.dist-info/WHEEL +5 -0
  4. mini_swe_agent-1.1.0.dist-info/entry_points.txt +5 -0
  5. mini_swe_agent-1.1.0.dist-info/licenses/LICENSE.md +21 -0
  6. mini_swe_agent-1.1.0.dist-info/top_level.txt +1 -0
  7. minisweagent/__init__.py +67 -0
  8. minisweagent/__main__.py +7 -0
  9. minisweagent/agents/__init__.py +1 -0
  10. minisweagent/agents/default.py +129 -0
  11. minisweagent/agents/interactive.py +148 -0
  12. minisweagent/agents/interactive_textual.py +324 -0
  13. minisweagent/config/README.md +9 -0
  14. minisweagent/config/__init__.py +24 -0
  15. minisweagent/config/__pycache__/__init__.cpython-313.pyc +0 -0
  16. minisweagent/config/default.yaml +143 -0
  17. minisweagent/config/extra/__init__.py +1 -0
  18. minisweagent/config/extra/swebench.yaml +229 -0
  19. minisweagent/config/github_issue.yaml +146 -0
  20. minisweagent/config/local.yaml +154 -0
  21. minisweagent/config/local2.tcss +128 -0
  22. minisweagent/environments/__init__.py +1 -0
  23. minisweagent/environments/docker.py +98 -0
  24. minisweagent/environments/extra/__init__.py +0 -0
  25. minisweagent/environments/extra/swerex_docker.py +39 -0
  26. minisweagent/environments/local.py +33 -0
  27. minisweagent/environments/singularity.py +52 -0
  28. minisweagent/models/__init__.py +81 -0
  29. minisweagent/models/anthropic.py +19 -0
  30. minisweagent/models/litellm_model.py +64 -0
  31. minisweagent/models/test_models.py +38 -0
  32. minisweagent/models/utils/cache_control.py +42 -0
  33. minisweagent/models/utils/key_per_thread.py +18 -0
  34. minisweagent/py.typed +0 -0
  35. minisweagent/run/__init__.py +1 -0
  36. minisweagent/run/extra/__init__.py +0 -0
  37. minisweagent/run/extra/config.py +100 -0
  38. minisweagent/run/extra/swebench.py +235 -0
  39. minisweagent/run/extra/swebench_single.py +53 -0
  40. minisweagent/run/extra/utils/batch_progress.py +164 -0
  41. minisweagent/run/github_issue.py +80 -0
  42. minisweagent/run/hello_world.py +36 -0
  43. minisweagent/run/inspector.py +212 -0
  44. minisweagent/run/mini.py +118 -0
  45. minisweagent/run/mini_extra.py +44 -0
  46. minisweagent/run/utils/__init__.py +0 -0
  47. minisweagent/run/utils/save.py +35 -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" / "local2.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()
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Run mini-SWE-agent in your local environment.
4
+
5
+ [not dim]
6
+ There are two different user interfaces:
7
+
8
+ [bold green]mini[/bold green] Simple REPL-style interface
9
+ [bold green]mini -v[/bold green] Pager-style interface (Textual)
10
+
11
+ More information about the usage: [bold green]https://mini-swe-agent.com/latest/usage/mini/[/bold green]
12
+ [/not dim]
13
+ """
14
+
15
+ import os
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import typer
20
+ import yaml
21
+ from prompt_toolkit.formatted_text import HTML
22
+ from prompt_toolkit.history import FileHistory
23
+ from prompt_toolkit.shortcuts import PromptSession
24
+ from rich.console import Console
25
+
26
+ from minisweagent import Environment, Model, global_config_dir
27
+ from minisweagent.agents.interactive import InteractiveAgent
28
+ from minisweagent.agents.interactive_textual import AgentApp
29
+ from minisweagent.config import builtin_config_dir, get_config_path
30
+ from minisweagent.environments.local import LocalEnvironment
31
+ from minisweagent.models import get_model
32
+ from minisweagent.run.extra.config import configure_if_first_time
33
+ from minisweagent.run.utils.save import save_traj
34
+
35
+ DEFAULT_CONFIG = Path(os.getenv("MSWEA_LOCAL_CONFIG_PATH", builtin_config_dir / "local.yaml"))
36
+ console = Console(highlight=False)
37
+ app = typer.Typer(rich_markup_mode="rich")
38
+ prompt_session = PromptSession(history=FileHistory(global_config_dir / "mini_task_history.txt"))
39
+
40
+
41
+ def run_interactive(model: Model, env: Environment, agent_config: dict, task: str, output: Path | None = None) -> Any:
42
+ agent = InteractiveAgent(
43
+ model,
44
+ env,
45
+ **agent_config,
46
+ )
47
+
48
+ exit_status, result = None, None
49
+ try:
50
+ exit_status, result = agent.run(task)
51
+ except KeyboardInterrupt:
52
+ console.print("\n[bold red]KeyboardInterrupt -- goodbye[/bold red]")
53
+ finally:
54
+ if output:
55
+ save_traj(agent, output, exit_status=exit_status, result=result)
56
+ return agent
57
+
58
+
59
+ def run_textual(model: Model, env: Environment, agent_config: dict, task: str, output: Path | None = None) -> Any:
60
+ agent_app = AgentApp(
61
+ model,
62
+ env,
63
+ task,
64
+ **agent_config,
65
+ )
66
+ try:
67
+ agent_app.run()
68
+ except KeyboardInterrupt:
69
+ typer.echo("\nKeyboardInterrupt -- goodbye")
70
+ finally:
71
+ save_traj(agent_app.agent, Path("traj.json"), exit_status=agent_app.exit_status, result=agent_app.result)
72
+
73
+
74
+ @app.command(help=__doc__)
75
+ def main(
76
+ visual: bool = typer.Option(False, "-v", "--visual", help="Use visual (pager-style) UI (Textual)"),
77
+ model_name: str | None = typer.Option(
78
+ None,
79
+ "-m",
80
+ "--model",
81
+ help="Model to use",
82
+ ),
83
+ task: str | None = typer.Option(None, "-t", "--task", help="Task/problem statement", show_default=False),
84
+ yolo: bool = typer.Option(False, "-y", "--yolo", help="Run without confirmation"),
85
+ cost_limit: float | None = typer.Option(None, "-l", "--cost-limit", help="Cost limit. Set to 0 to disable."),
86
+ config_spec: Path = typer.Option(DEFAULT_CONFIG, "-c", "--config", help="Path to config file"),
87
+ output: Path | None = typer.Option(None, "-o", "--output", help="Output file"),
88
+ ) -> Any:
89
+ configure_if_first_time()
90
+ config = yaml.safe_load(get_config_path(config_spec).read_text())
91
+
92
+ if not task:
93
+ console.print("[bold yellow]What do you want to do?")
94
+ task = prompt_session.prompt(
95
+ "",
96
+ multiline=True,
97
+ bottom_toolbar=HTML(
98
+ "Submit task: <b fg='yellow' bg='black'>Esc+Enter</b> | "
99
+ "Navigate history: <b fg='yellow' bg='black'>Arrow Up/Down</b> | "
100
+ "Search history: <b fg='yellow' bg='black'>Ctrl+R</b>"
101
+ ),
102
+ )
103
+ console.print("[bold green]Got that, thanks![/bold green]")
104
+
105
+ config["agent"]["mode"] = "confirm" if not yolo else "yolo"
106
+ if cost_limit:
107
+ config["agent"]["cost_limit"] = cost_limit
108
+ model = get_model(model_name, config.get("model", {}))
109
+ env = LocalEnvironment(**config.get("env", {}))
110
+
111
+ if visual:
112
+ return run_textual(model, env, config["agent"], task, output) # type: ignore[arg-type]
113
+ else:
114
+ return run_interactive(model, env, config["agent"], task, output) # type: ignore[arg-type]
115
+
116
+
117
+ if __name__ == "__main__":
118
+ 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,35 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from minisweagent import Agent
5
+
6
+
7
+ def save_traj(
8
+ agent: Agent | None,
9
+ path: Path,
10
+ *,
11
+ exit_status: str | None = None,
12
+ result: str | None = None,
13
+ extra_info: dict | None = None,
14
+ **kwargs,
15
+ ):
16
+ data = {
17
+ "info": {
18
+ "exit_status": exit_status,
19
+ "submission": result,
20
+ "model_stats": {
21
+ "instance_cost": 0.0,
22
+ "api_calls": 0,
23
+ },
24
+ },
25
+ "messages": [],
26
+ } | kwargs
27
+ if agent is not None:
28
+ data["info"]["model_stats"]["instance_cost"] = agent.model.cost
29
+ data["info"]["model_stats"]["api_calls"] = agent.model.n_calls
30
+ data["messages"] = agent.messages
31
+ if extra_info:
32
+ data["info"].update(extra_info)
33
+
34
+ path.parent.mkdir(parents=True, exist_ok=True)
35
+ path.write_text(json.dumps(data, indent=2))