swebot-client 0.9.7__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.
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ swebot-client — Python client for the swebot autonomous coding agent.
4
+
5
+ Pure Python — zero dependencies beyond the standard library.
6
+
7
+ Install:
8
+ pip install swebot-client
9
+ uv add swebot-client
10
+
11
+ Usage:
12
+ from swebot_client import SwebotClient
13
+
14
+ client = SwebotClient("http://localhost:8080")
15
+ session = client.create_session()
16
+ for chunk in session.stream("explain goroutines"):
17
+ print(chunk, end="", flush=True)
18
+ """
19
+
20
+ __version__ = "0.9.7"
21
+ __all__ = ["SwebotClient", "Session", "ToolCall", "TodoItem", "Message", "Event"]
22
+
23
+ import json
24
+ import sys
25
+ import time
26
+ import urllib.error
27
+ import urllib.request
28
+ from dataclasses import dataclass, field
29
+ from typing import Callable, Dict, Iterator, List, Optional, Union
30
+
31
+ # ── ANSI colours ──────────────────────────────────────────────────────────────
32
+
33
+ RESET = "\033[0m"
34
+ BOLD = "\033[1m"
35
+ DIM = "\033[2m"
36
+ RED = "\033[91m"
37
+ GREEN = "\033[92m"
38
+ YELLOW = "\033[93m"
39
+ BLUE = "\033[94m"
40
+ MAGENTA = "\033[95m"
41
+ CYAN = "\033[96m"
42
+ WHITE = "\033[97m"
43
+
44
+ def _color(text: str, *codes: str) -> str:
45
+ if not sys.stdout.isatty():
46
+ return text
47
+ return "".join(codes) + text + RESET
48
+
49
+
50
+ # ── Data types ─────────────────────────────────────────────────────────────────
51
+
52
+ @dataclass
53
+ class ToolCall:
54
+ id: str
55
+ name: str
56
+ input: str = ""
57
+ output: str = ""
58
+ is_error: bool = False
59
+
60
+
61
+ @dataclass
62
+ class TodoItem:
63
+ id: str
64
+ title: str
65
+ description: str
66
+ status: str # pending | in_progress | done
67
+
68
+
69
+ @dataclass
70
+ class Message:
71
+ role: str
72
+ content: str
73
+ tool_calls: list = field(default_factory=list)
74
+
75
+
76
+ @dataclass
77
+ class Event:
78
+ kind: str
79
+ data: dict
80
+
81
+
82
+ # ── Client ────────────────────────────────────────────────────────────────────
83
+
84
+ class SwebotClient:
85
+ """HTTP client for the swebot backend API."""
86
+
87
+ def __init__(self, addr: str = "http://localhost:8080", token: str = ""):
88
+ self.addr = addr.rstrip("/")
89
+ self.token = token
90
+
91
+ # ── Helpers ───────────────────────────────────────────────────────────────
92
+
93
+ def _get(self, path: str) -> Dict:
94
+ return self._request("GET", path)
95
+
96
+ def _delete(self, path: str) -> None:
97
+ self._request("DELETE", path)
98
+
99
+ def _request(self, method: str, path: str, body=None) -> Dict:
100
+ url = f"{self.addr}{path}"
101
+ data = json.dumps(body).encode() if body is not None else None
102
+ req = urllib.request.Request(url, method=method, data=data)
103
+ req.add_header("Content-Type", "application/json")
104
+ if self.token:
105
+ req.add_header("Authorization", f"Bearer {self.token}")
106
+ try:
107
+ with urllib.request.urlopen(req) as resp:
108
+ body = resp.read()
109
+ return json.loads(body) if body else {}
110
+ except urllib.error.HTTPError as e:
111
+ raise RuntimeError(f"HTTP {e.code} {method} {path}: {e.read().decode()}") from e
112
+
113
+ def _stream(self, path: str, body: dict) -> Iterator[Event]:
114
+ url = f"{self.addr}{path}"
115
+ data = json.dumps(body).encode()
116
+ req = urllib.request.Request(url, method="POST", data=data)
117
+ req.add_header("Content-Type", "application/json")
118
+ if self.token:
119
+ req.add_header("Authorization", f"Bearer {self.token}")
120
+ event_type = ""
121
+ with urllib.request.urlopen(req) as resp:
122
+ for raw in resp:
123
+ line = raw.decode().rstrip("\n")
124
+ if line.startswith("event: "):
125
+ event_type = line[7:]
126
+ elif line.startswith("data: ") and event_type:
127
+ yield Event(kind=event_type, data=json.loads(line[6:]))
128
+ event_type = ""
129
+
130
+ # ── Health ────────────────────────────────────────────────────────────────
131
+
132
+ def health(self) -> bool:
133
+ try:
134
+ self._request("GET", "/health")
135
+ return True
136
+ except Exception:
137
+ return False
138
+
139
+ def wait_until_ready(self, timeout: float = 10.0) -> bool:
140
+ deadline = time.time() + timeout
141
+ while time.time() < deadline:
142
+ if self.health():
143
+ return True
144
+ time.sleep(0.2)
145
+ return False
146
+
147
+ # ── Sessions ──────────────────────────────────────────────────────────────
148
+
149
+ def create_session(self, agent: str = "default") -> "Session":
150
+ resp = self._request("POST", "/api/sessions", {"agent": agent})
151
+ return Session(self, resp["id"], agent)
152
+
153
+ def get_session(self, session_id: str) -> "Session":
154
+ resp = self._request("GET", f"/api/sessions/{session_id}")
155
+ return Session(self, resp["id"], resp.get("agent", "default"), resp)
156
+
157
+ def list_sessions(self) -> List[dict]:
158
+ resp = self._request("GET", "/api/sessions")
159
+ return resp if isinstance(resp, list) else resp.get("sessions", [])
160
+
161
+ def delete_session(self, session_id: str) -> None:
162
+ self._request("DELETE", f"/api/sessions/{session_id}")
163
+
164
+ # ── Agents / providers ────────────────────────────────────────────────────
165
+
166
+ def list_agents(self) -> List[dict]:
167
+ resp = self._request("GET", "/api/agents")
168
+ return resp if isinstance(resp, list) else resp.get("agents", [])
169
+
170
+ def list_providers(self) -> List[dict]:
171
+ resp = self._request("GET", "/api/providers")
172
+ return resp if isinstance(resp, list) else resp.get("providers", [])
173
+
174
+ # ── MCP management ────────────────────────────────────────────────────────
175
+
176
+ def list_mcp(self) -> List[dict]:
177
+ """Return all configured MCP servers with live connection state."""
178
+ resp = self._request("GET", "/api/mcp")
179
+ return resp if isinstance(resp, list) else resp.get("servers", [])
180
+
181
+ def add_mcp(
182
+ self,
183
+ name: str,
184
+ *,
185
+ type: str = "stdio",
186
+ command: str = "",
187
+ args: Optional[List[str]] = None,
188
+ url: str = "",
189
+ env: Optional[List[str]] = None,
190
+ headers: Optional[Dict[str, str]] = None,
191
+ tools: Optional[List[str]] = None,
192
+ disabled: bool = False,
193
+ ) -> Dict:
194
+ """Add a new MCP server and connect it immediately."""
195
+ payload: dict = {"name": name, "type": type}
196
+ if command:
197
+ payload["command"] = command
198
+ if args:
199
+ payload["args"] = args
200
+ if url:
201
+ payload["url"] = url
202
+ if env:
203
+ payload["env"] = env
204
+ if headers:
205
+ payload["headers"] = headers
206
+ if tools:
207
+ payload["tools"] = tools
208
+ if disabled:
209
+ payload["disabled"] = True
210
+ return self._request("POST", "/api/mcp", payload)
211
+
212
+ def update_mcp(self, name: str, **fields) -> Dict:
213
+ """Update an existing MCP server's config (reconnects automatically)."""
214
+ fields["name"] = name
215
+ return self._request("PUT", f"/api/mcp/{name}", fields)
216
+
217
+ def delete_mcp(self, name: str) -> Dict:
218
+ """Remove an MCP server from config and disconnect it."""
219
+ return self._request("DELETE", f"/api/mcp/{name}")
220
+
221
+ def enable_mcp(self, name: str) -> Dict:
222
+ """Enable a disabled MCP server and connect it."""
223
+ return self._request("POST", f"/api/mcp/{name}/enable")
224
+
225
+ def disable_mcp(self, name: str) -> Dict:
226
+ """Disable an MCP server (keeps config, stops connection)."""
227
+ return self._request("POST", f"/api/mcp/{name}/disable")
228
+
229
+ def reload_mcp(self) -> Dict:
230
+ """Disconnect all MCP servers and reconnect every enabled one."""
231
+ return self._request("POST", "/api/mcp/reload")
232
+
233
+ # ── Backend management ────────────────────────────────────────────────────
234
+
235
+ def stats(self) -> Dict:
236
+ """Return backend stats: mode, agents, prompts, tools, lines, mcp_count."""
237
+ return self._request("GET", "/api/stats")
238
+
239
+ def skills(self) -> list:
240
+ """Return the catalogue of all built-in skill agents with active status."""
241
+ return self._request("GET", "/api/skills")
242
+
243
+ def shutdown(self) -> Dict:
244
+ """Gracefully stop the backend. Returns the last session ID.
245
+
246
+ After this call the backend process exits. Resume with:
247
+ swebot --serve
248
+ swebot --tui (or --session <last_session_id>)
249
+ """
250
+ return self._request("POST", "/api/shutdown")
251
+
252
+
253
+ class Session:
254
+ """A single conversation session with the swebot agent."""
255
+
256
+ def __init__(self, client: SwebotClient, session_id: str,
257
+ agent: str = "default", data: dict = None):
258
+ self.client = client
259
+ self.id = session_id
260
+ self.agent = agent
261
+ self._data = data or {}
262
+
263
+ # ── Messaging ─────────────────────────────────────────────────────────────
264
+
265
+ def stream(self, message: str) -> Iterator[str]:
266
+ """Stream the assistant's reply token-by-token."""
267
+ for ev in self._stream_events(message):
268
+ if ev.kind == "content_delta":
269
+ yield ev.data.get("delta", "")
270
+
271
+ def send(self, message: str) -> str:
272
+ """Send a message and return the complete reply (non-streaming)."""
273
+ reply = ""
274
+ for ev in self._stream_events(message):
275
+ if ev.kind == "content_delta":
276
+ reply += ev.data.get("delta", "")
277
+ elif ev.kind == "message_complete":
278
+ reply = ev.data.get("content", reply)
279
+ return reply
280
+
281
+ def send_with_events(
282
+ self,
283
+ message: str,
284
+ on_token: Optional[Callable[[str], None]] = None,
285
+ on_tool_start: Optional[Callable[[ToolCall], None]] = None,
286
+ on_tool_result: Optional[Callable[[ToolCall], None]] = None,
287
+ ) -> str:
288
+ """Send a message, firing callbacks for tokens and tool calls."""
289
+ reply = ""
290
+ active_tools: dict[str, ToolCall] = {}
291
+
292
+ for ev in self._stream_events(message):
293
+ if ev.kind == "content_delta":
294
+ delta = ev.data.get("delta", "")
295
+ reply += delta
296
+ if on_token:
297
+ on_token(delta)
298
+
299
+ elif ev.kind == "tool_start":
300
+ tc = ToolCall(
301
+ id=ev.data.get("tool_id", ""),
302
+ name=ev.data.get("tool_name", ""),
303
+ )
304
+ active_tools[tc.id] = tc
305
+ if on_tool_start:
306
+ on_tool_start(tc)
307
+
308
+ elif ev.kind == "tool_result":
309
+ tid = ev.data.get("tool_id", "")
310
+ tc = active_tools.pop(tid, ToolCall(id=tid, name=ev.data.get("tool_name", "")))
311
+ tc.output = ev.data.get("content", "")
312
+ tc.is_error = ev.data.get("is_error", False)
313
+ if on_tool_result:
314
+ on_tool_result(tc)
315
+
316
+ elif ev.kind == "message_complete":
317
+ reply = ev.data.get("content", reply)
318
+
319
+ return reply
320
+
321
+ def _stream_events(self, message: str) -> Iterator[Event]:
322
+ return self.client._stream(
323
+ f"/api/sessions/{self.id}/messages",
324
+ {"content": message},
325
+ )
326
+
327
+ # ── Todos ─────────────────────────────────────────────────────────────────
328
+
329
+ def compact(self) -> Dict:
330
+ """Manually trigger context compaction for this session.
331
+
332
+ swebot auto-compacts at 85 % of the model context window. Call this
333
+ to compact earlier and free up tokens.
334
+
335
+ Returns a dict with ``{"compacted": true, "summary": "..."}`` on
336
+ success or ``{"compacted": false, "reason": "..."}`` when not needed.
337
+ """
338
+ return self.client._request("POST", f"/api/sessions/{self.id}/compact")
339
+
340
+ def clear(self) -> Dict:
341
+ """Clear all context (messages + knowledge) from this session.
342
+
343
+ Resets the session to a fresh state as if swebot just started, without
344
+ restarting the process. The session ID, agent, and model are preserved.
345
+
346
+ Useful for:
347
+ - SWE-bench harness: clean slate between tasks
348
+ - Switching to a different task without restarting
349
+
350
+ Returns ``{"status": "cleared"}`` on success.
351
+ """
352
+ return self.client._request("POST", f"/api/sessions/{self.id}/clear")
353
+
354
+ def token_usage(self) -> Dict:
355
+ """Return token usage for this session from the stats endpoint.
356
+
357
+ Returns::
358
+
359
+ {
360
+ "tokens_in": 14200,
361
+ "tokens_out": 3400,
362
+ "context_window": 200000,
363
+ "usage_pct": 8.8,
364
+ }
365
+ """
366
+ stats = self.client._request("GET", "/api/stats")
367
+ win = stats.get("context_window", 0)
368
+ tin = stats.get("tokens_in", 0)
369
+ tout = stats.get("tokens_out", 0)
370
+ pct = round((tin / win * 100), 1) if win else 0
371
+ return {
372
+ "tokens_in": tin,
373
+ "tokens_out": tout,
374
+ "context_window": win,
375
+ "usage_pct": pct,
376
+ }
377
+
378
+ def get_todos(self) -> List['TodoItem']:
379
+ resp = self.client._request("GET", f"/api/sessions/{self.id}/todos")
380
+ todos_list = resp if isinstance(resp, list) else resp.get("todos", [])
381
+ return [
382
+ TodoItem(
383
+ id=t["id"], title=t["title"],
384
+ description=t.get("description", ""),
385
+ status=t.get("status", "pending"),
386
+ )
387
+ for t in todos_list
388
+ ]
389
+
390
+ def update_todo(self, todo_id: str, status: str) -> None:
391
+ self.client._request(
392
+ "PATCH", f"/api/sessions/{self.id}/todos/{todo_id}",
393
+ {"status": status},
394
+ )
395
+
396
+ # ── Info ──────────────────────────────────────────────────────────────────
397
+
398
+ def refresh(self) -> "Session":
399
+ self._data = self.client._request("GET", f"/api/sessions/{self.id}")
400
+ return self
401
+
402
+ @property
403
+ def message_count(self) -> int:
404
+ return self._data.get("message_count", 0)
405
+
406
+ def __repr__(self) -> str:
407
+ return f"Session(id={self.id!r}, agent={self.agent!r})"
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: swebot-client
3
+ Version: 0.9.7
4
+ Summary: Python client for the swebot autonomous coding agent
5
+ Project-URL: Homepage, https://github.com/tickup-se/swebot
6
+ Project-URL: Repository, https://github.com/tickup-se/swebot
7
+ Project-URL: Issues, https://github.com/tickup-se/swebot/issues
8
+ Author: Tickup SE
9
+ License-Expression: MIT
10
+ Keywords: ai,automation,coding-agent,llm,swebot
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+
24
+ # swebot-client
25
+
26
+ Python client for the [swebot](https://github.com/tickup-se/swebot) autonomous coding agent.
27
+
28
+ Pure Python — zero dependencies beyond the standard library.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ # pip
34
+ pip install swebot-client
35
+
36
+ # uv
37
+ uv add swebot-client
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ from swebot_client import SwebotClient
44
+
45
+ client = SwebotClient("http://localhost:8080")
46
+ session = client.create_session()
47
+
48
+ # Stream tokens
49
+ for token in session.stream("explain goroutines"):
50
+ print(token, end="", flush=True)
51
+
52
+ # Or get the full reply at once
53
+ reply = session.send("what is 2+2?")
54
+ print(reply)
55
+ ```
56
+
57
+ ## Features
58
+
59
+ - **Streaming** — real-time token-by-token output via SSE
60
+ - **Tool callbacks** — observe tool invocations and results
61
+ - **Session management** — create, resume, list, delete sessions
62
+ - **Todos** — read and update agent todo items
63
+ - **MCP management** — configure MCP servers programmatically
64
+ - **Context control** — compact or clear session context
65
+ - **Skills** — list and use built-in skill agents
66
+
67
+ ## Examples
68
+
69
+ ```python
70
+ from swebot_client import SwebotClient, Session, ToolCall, TodoItem
71
+
72
+ client = SwebotClient("http://localhost:8080")
73
+
74
+ # Wait for backend to be ready
75
+ client.wait_until_ready(timeout=10)
76
+
77
+ # Create a session with a specific agent
78
+ session = client.create_session(agent="developer")
79
+
80
+ # Stream with tool call callbacks
81
+ def on_tool(tc: ToolCall):
82
+ print(f" ⚙ {tc.name}")
83
+
84
+ reply = session.send_with_events(
85
+ "create a hello.py file",
86
+ on_tool_start=on_tool,
87
+ )
88
+
89
+ # Read todos
90
+ for todo in session.get_todos():
91
+ print(f"[{todo.status}] {todo.title}")
92
+
93
+ # List MCP servers
94
+ for mcp in client.list_mcp():
95
+ print(f"{mcp['name']}: {'connected' if mcp.get('connected') else 'disconnected'}")
96
+
97
+ # Token usage
98
+ usage = session.token_usage()
99
+ print(f"Tokens: {usage['tokens_in']:,} in / {usage['tokens_out']:,} out ({usage['usage_pct']}%)")
100
+ ```
101
+
102
+ ## API reference
103
+
104
+ ### `SwebotClient(addr, token="")`
105
+
106
+ | Method | Description |
107
+ |--------|-------------|
108
+ | `health()` | Check if backend is reachable |
109
+ | `wait_until_ready(timeout)` | Block until backend responds |
110
+ | `create_session(agent)` | Create a new session → `Session` |
111
+ | `get_session(id)` | Resume an existing session → `Session` |
112
+ | `list_sessions()` | List all sessions |
113
+ | `delete_session(id)` | Delete a session |
114
+ | `list_agents()` | List available agents |
115
+ | `list_providers()` | List LLM providers |
116
+ | `stats()` | Backend stats (model, tokens, uptime) |
117
+ | `skills()` | List built-in skill agents |
118
+ | `list_mcp()` | List MCP servers |
119
+ | `add_mcp(...)` | Add an MCP server |
120
+ | `delete_mcp(name)` | Remove an MCP server |
121
+ | `shutdown()` | Gracefully stop the backend |
122
+
123
+ ### `Session`
124
+
125
+ | Method | Description |
126
+ |--------|-------------|
127
+ | `stream(message)` | Stream tokens → `Iterator[str]` |
128
+ | `send(message)` | Send and get full reply → `str` |
129
+ | `send_with_events(message, ...)` | Send with tool callbacks → `str` |
130
+ | `get_todos()` | List todo items → `list[TodoItem]` |
131
+ | `update_todo(id, status)` | Update a todo's status |
132
+ | `compact()` | Trigger context compaction |
133
+ | `clear()` | Clear all session context |
134
+ | `token_usage()` | Get token usage stats |
135
+
136
+ ## Requirements
137
+
138
+ - Python 3.9+
139
+ - A running swebot backend (`swebot --serve`)
140
+
141
+ ## License
142
+
143
+ MIT
@@ -0,0 +1,4 @@
1
+ swebot_client/__init__.py,sha256=vl07oOBr7YC1VR5c7vFIm6yu6bI0vwBa0rWFm0wPcl4,15328
2
+ swebot_client-0.9.7.dist-info/METADATA,sha256=5adnvwc1LQBoLUWY6bgavMHOu5krtUD5rR89uq4P4LM,4233
3
+ swebot_client-0.9.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
4
+ swebot_client-0.9.7.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any