toolplane-python-client 0.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.
- toolplane/__init__.py +106 -0
- toolplane/common/__init__.py +93 -0
- toolplane/common/base_config.py +129 -0
- toolplane/common/base_connection_manager.py +171 -0
- toolplane/common/base_session_manager.py +321 -0
- toolplane/common/base_tool_manager.py +347 -0
- toolplane/common/constants.py +47 -0
- toolplane/common/utils.py +310 -0
- toolplane/core/__init__.py +67 -0
- toolplane/core/config.py +107 -0
- toolplane/core/connection.py +285 -0
- toolplane/core/errors.py +298 -0
- toolplane/core/machine.py +480 -0
- toolplane/core/request.py +775 -0
- toolplane/core/session.py +332 -0
- toolplane/core/session_context.py +514 -0
- toolplane/core/task.py +130 -0
- toolplane/core/tool.py +329 -0
- toolplane/http_core/__init__.py +37 -0
- toolplane/http_core/http_config.py +97 -0
- toolplane/http_core/http_connection.py +409 -0
- toolplane/http_core/http_machine.py +298 -0
- toolplane/http_core/http_request.py +748 -0
- toolplane/http_core/http_session.py +348 -0
- toolplane/http_core/http_session_context.py +491 -0
- toolplane/http_core/http_task.py +101 -0
- toolplane/http_core/http_tool.py +400 -0
- toolplane/interfaces/__init__.py +27 -0
- toolplane/interfaces/client_interface.py +122 -0
- toolplane/interfaces/connection_interface.py +193 -0
- toolplane/interfaces/event_interface.py +290 -0
- toolplane/interfaces/request_interface.py +439 -0
- toolplane/interfaces/session_interface.py +288 -0
- toolplane/interfaces/tool_interface.py +441 -0
- toolplane/proto/__init__.py +0 -0
- toolplane/proto/service_pb2.py +315 -0
- toolplane/proto/service_pb2_grpc.py +2240 -0
- toolplane/provider_cli.py +268 -0
- toolplane/provider_registry.py +77 -0
- toolplane/provider_runtime.py +302 -0
- toolplane/toolkits/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/__init__.py +0 -0
- toolplane/toolkits/standalone_tools/create_directory.py +94 -0
- toolplane/toolkits/standalone_tools/create_file.py +124 -0
- toolplane/toolkits/standalone_tools/file_search.py +229 -0
- toolplane/toolkits/standalone_tools/grep_search.py +372 -0
- toolplane/toolkits/standalone_tools/launcher.py +146 -0
- toolplane/toolkits/standalone_tools/list_dir.py +395 -0
- toolplane/toolkits/standalone_tools/read_file.py +346 -0
- toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
- toolplane/toolkits/standalone_tools/run_tests.py +66 -0
- toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
- toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
- toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
- toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
- toolplane/toolkits/swe/__init__.py +35 -0
- toolplane/toolkits/swe/create_directory.py +15 -0
- toolplane/toolkits/swe/create_file.py +15 -0
- toolplane/toolkits/swe/descriptions.py +273 -0
- toolplane/toolkits/swe/execute_bash.py +93 -0
- toolplane/toolkits/swe/file_editor.py +775 -0
- toolplane/toolkits/swe/file_search.py +16 -0
- toolplane/toolkits/swe/finish.py +50 -0
- toolplane/toolkits/swe/grep_search.py +19 -0
- toolplane/toolkits/swe/list_dir.py +407 -0
- toolplane/toolkits/swe/read_file.py +18 -0
- toolplane/toolkits/swe/replace_string_in_file.py +17 -0
- toolplane/toolkits/swe/search.py +260 -0
- toolplane/toolkits/swe/semantic_search.py +20 -0
- toolplane/toolkits/swe/str_replace_editor.py +647 -0
- toolplane/toolkits/swe/submit.py +29 -0
- toolplane/toolkits/swe/swe_toolkit.py +1296 -0
- toolplane/toolplane_client.py +686 -0
- toolplane/toolplane_http_client.py +681 -0
- toolplane/utils/__init__.py +3 -0
- toolplane/utils/schema.py +146 -0
- toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
- toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
- toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
- toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
- toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""HTTP machine management for Toolplane client."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from toolplane.interfaces.event_interface import Event, EventType
|
|
9
|
+
|
|
10
|
+
from ..common.constants import DEFAULT_HEARTBEAT_INTERVAL
|
|
11
|
+
from ..common.utils import format_error_message
|
|
12
|
+
from ..core.errors import MachineError
|
|
13
|
+
from .http_connection import HTTPConnectionManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HTTPMachineManager:
|
|
17
|
+
"""Manages machine registration and heartbeats for HTTP client."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
connection_manager: HTTPConnectionManager,
|
|
22
|
+
event_emitter: Optional[Any] = None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize HTTP machine manager."""
|
|
25
|
+
self.connection_manager = connection_manager
|
|
26
|
+
self.machines: Dict[str, str] = {} # session_id -> machine_id
|
|
27
|
+
self.machines_lock = threading.RLock()
|
|
28
|
+
self._last_heartbeat: Dict[str, float] = {}
|
|
29
|
+
self._heartbeat_thread: Optional[threading.Thread] = None
|
|
30
|
+
self._running = False
|
|
31
|
+
self._event_emitter = event_emitter
|
|
32
|
+
|
|
33
|
+
def _emit_event(self, event_type: EventType, data: Dict[str, Any]) -> None:
|
|
34
|
+
if not self._event_emitter:
|
|
35
|
+
return
|
|
36
|
+
try:
|
|
37
|
+
self._event_emitter.emit(
|
|
38
|
+
Event(type=event_type, source="http_machine_manager", data=data)
|
|
39
|
+
)
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def _normalize_machine(self, machine: Dict[str, Any]) -> Dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"id": machine.get("id", ""),
|
|
46
|
+
"session_id": machine.get("sessionId", machine.get("session_id", "")),
|
|
47
|
+
"sdk_version": machine.get("sdkVersion", machine.get("sdk_version", "")),
|
|
48
|
+
"sdk_language": machine.get("sdkLanguage", machine.get("sdk_language", "")),
|
|
49
|
+
"ip": machine.get("ip", ""),
|
|
50
|
+
"created_at": machine.get("createdAt", machine.get("created_at", "")),
|
|
51
|
+
"last_ping_at": machine.get("lastPingAt", machine.get("last_ping_at", "")),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
def _clear_local_machine(self, session_id: str, machine_id: str) -> None:
|
|
55
|
+
with self.machines_lock:
|
|
56
|
+
if self.machines.get(session_id) == machine_id:
|
|
57
|
+
self.machines.pop(session_id, None)
|
|
58
|
+
self._last_heartbeat.pop(session_id, None)
|
|
59
|
+
|
|
60
|
+
def list_machines(self, session_id: str) -> List[Dict[str, Any]]:
|
|
61
|
+
"""List machines for a session."""
|
|
62
|
+
try:
|
|
63
|
+
self.connection_manager.ensure_connected()
|
|
64
|
+
response = self.connection_manager.list_machines(session_id)
|
|
65
|
+
machines = response.get("machines", [])
|
|
66
|
+
return [self._normalize_machine(machine) for machine in machines]
|
|
67
|
+
except Exception as e:
|
|
68
|
+
error_msg = format_error_message(
|
|
69
|
+
e, f"Failed to list machines for session {session_id}"
|
|
70
|
+
)
|
|
71
|
+
raise MachineError(error_msg)
|
|
72
|
+
|
|
73
|
+
def get_machine(self, session_id: str, machine_id: str) -> Dict[str, Any]:
|
|
74
|
+
"""Get a machine by ID."""
|
|
75
|
+
try:
|
|
76
|
+
self.connection_manager.ensure_connected()
|
|
77
|
+
response = self.connection_manager.get_machine(session_id, machine_id)
|
|
78
|
+
machine = response.get("machine", response)
|
|
79
|
+
if not isinstance(machine, dict):
|
|
80
|
+
raise MachineError(
|
|
81
|
+
f"Unexpected machine payload for session {session_id}: {machine}"
|
|
82
|
+
)
|
|
83
|
+
return self._normalize_machine(machine)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
error_msg = format_error_message(
|
|
86
|
+
e, f"Failed to get machine {machine_id} for session {session_id}"
|
|
87
|
+
)
|
|
88
|
+
raise MachineError(error_msg)
|
|
89
|
+
|
|
90
|
+
def register_machine(self, session_id: str) -> str:
|
|
91
|
+
"""Register a machine for a session."""
|
|
92
|
+
self.connection_manager.ensure_connected()
|
|
93
|
+
|
|
94
|
+
self._emit_event(
|
|
95
|
+
EventType.MACHINE_REGISTERED,
|
|
96
|
+
{
|
|
97
|
+
"session_id": session_id,
|
|
98
|
+
"phase": "start",
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
machine_id = str(uuid.uuid4())
|
|
104
|
+
|
|
105
|
+
payload = {
|
|
106
|
+
"sessionId": session_id,
|
|
107
|
+
"machineId": machine_id,
|
|
108
|
+
"sdkVersion": "1.0.0",
|
|
109
|
+
"sdkLanguage": "python-http",
|
|
110
|
+
"tools": [],
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
response = self.connection_manager.register_machine(payload)
|
|
114
|
+
actual_machine_id = response.get("id", machine_id)
|
|
115
|
+
|
|
116
|
+
# The registration response carries the per-machine credential
|
|
117
|
+
# exactly once; store it so subsequent provide-scoped calls
|
|
118
|
+
# present it (see HTTPConnectionManager._request_headers).
|
|
119
|
+
machine_token = response.get("machineToken", "")
|
|
120
|
+
if machine_token:
|
|
121
|
+
self.connection_manager.set_machine_token(machine_token)
|
|
122
|
+
|
|
123
|
+
with self.machines_lock:
|
|
124
|
+
self.machines[session_id] = actual_machine_id
|
|
125
|
+
self._last_heartbeat[session_id] = time.time()
|
|
126
|
+
|
|
127
|
+
self._emit_event(
|
|
128
|
+
EventType.MACHINE_REGISTERED,
|
|
129
|
+
{
|
|
130
|
+
"session_id": session_id,
|
|
131
|
+
"machine_id": actual_machine_id,
|
|
132
|
+
"phase": "success",
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return actual_machine_id
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
error_msg = format_error_message(
|
|
140
|
+
e, f"Failed to register machine for session {session_id}"
|
|
141
|
+
)
|
|
142
|
+
self._emit_event(
|
|
143
|
+
EventType.MACHINE_REGISTERED,
|
|
144
|
+
{
|
|
145
|
+
"session_id": session_id,
|
|
146
|
+
"phase": "error",
|
|
147
|
+
"error": error_msg,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
raise MachineError(error_msg)
|
|
151
|
+
|
|
152
|
+
def unregister_machine(
|
|
153
|
+
self,
|
|
154
|
+
session_id: str,
|
|
155
|
+
machine_id: Optional[str] = None,
|
|
156
|
+
reason: str = "explicit",
|
|
157
|
+
) -> bool:
|
|
158
|
+
"""Unregister a machine for a session."""
|
|
159
|
+
machine_id = machine_id or self.get_machine_id(session_id)
|
|
160
|
+
if not machine_id:
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
self._emit_event(
|
|
165
|
+
EventType.MACHINE_UNREGISTERED,
|
|
166
|
+
{
|
|
167
|
+
"session_id": session_id,
|
|
168
|
+
"machine_id": machine_id,
|
|
169
|
+
"phase": "start",
|
|
170
|
+
"reason": reason,
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Drain machine first
|
|
175
|
+
self.drain_machine(session_id, machine_id)
|
|
176
|
+
|
|
177
|
+
# Unregister machine
|
|
178
|
+
self.connection_manager.unregister_machine(session_id, machine_id)
|
|
179
|
+
|
|
180
|
+
self._clear_local_machine(session_id, machine_id)
|
|
181
|
+
|
|
182
|
+
self._emit_event(
|
|
183
|
+
EventType.MACHINE_UNREGISTERED,
|
|
184
|
+
{
|
|
185
|
+
"session_id": session_id,
|
|
186
|
+
"machine_id": machine_id,
|
|
187
|
+
"phase": "success",
|
|
188
|
+
"reason": reason,
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
except Exception as e:
|
|
195
|
+
error_msg = format_error_message(
|
|
196
|
+
e, f"Failed to unregister machine for session {session_id}"
|
|
197
|
+
)
|
|
198
|
+
self._emit_event(
|
|
199
|
+
EventType.MACHINE_UNREGISTERED,
|
|
200
|
+
{
|
|
201
|
+
"session_id": session_id,
|
|
202
|
+
"machine_id": machine_id,
|
|
203
|
+
"phase": "error",
|
|
204
|
+
"reason": reason,
|
|
205
|
+
"error": error_msg,
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
raise MachineError(error_msg)
|
|
209
|
+
|
|
210
|
+
def drain_machine(self, session_id: str, machine_id: Optional[str] = None) -> bool:
|
|
211
|
+
"""Drain a machine for a session."""
|
|
212
|
+
machine_id = machine_id or self.get_machine_id(session_id)
|
|
213
|
+
if not machine_id:
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
response = self.connection_manager.drain_machine(session_id, machine_id)
|
|
218
|
+
if response.get("drained", False):
|
|
219
|
+
self._clear_local_machine(session_id, machine_id)
|
|
220
|
+
return response.get("drained", False)
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
error_msg = format_error_message(
|
|
224
|
+
e, f"Failed to drain machine for session {session_id}"
|
|
225
|
+
)
|
|
226
|
+
raise MachineError(error_msg)
|
|
227
|
+
|
|
228
|
+
def get_machine_id(self, session_id: str) -> Optional[str]:
|
|
229
|
+
"""Get machine ID for a session."""
|
|
230
|
+
with self.machines_lock:
|
|
231
|
+
return self.machines.get(session_id)
|
|
232
|
+
|
|
233
|
+
def start_heartbeat(self, heartbeat_interval: int = DEFAULT_HEARTBEAT_INTERVAL):
|
|
234
|
+
"""Start heartbeat thread."""
|
|
235
|
+
if self._running:
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
self._running = True
|
|
239
|
+
self._heartbeat_thread = threading.Thread(
|
|
240
|
+
target=self._heartbeat_loop, args=(heartbeat_interval,), daemon=True
|
|
241
|
+
)
|
|
242
|
+
self._heartbeat_thread.start()
|
|
243
|
+
|
|
244
|
+
def stop_heartbeat(self):
|
|
245
|
+
"""Stop heartbeat thread."""
|
|
246
|
+
self._running = False
|
|
247
|
+
if self._heartbeat_thread:
|
|
248
|
+
self._heartbeat_thread.join(timeout=1)
|
|
249
|
+
|
|
250
|
+
def _heartbeat_loop(self, interval: int):
|
|
251
|
+
"""Heartbeat loop for all machines."""
|
|
252
|
+
while self._running:
|
|
253
|
+
try:
|
|
254
|
+
now = time.time()
|
|
255
|
+
|
|
256
|
+
with self.machines_lock:
|
|
257
|
+
for session_id, machine_id in self.machines.items():
|
|
258
|
+
if now - self._last_heartbeat.get(session_id, 0) < interval:
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
self.connection_manager.update_machine_ping(
|
|
263
|
+
session_id, machine_id
|
|
264
|
+
)
|
|
265
|
+
self._last_heartbeat[session_id] = now
|
|
266
|
+
|
|
267
|
+
except Exception:
|
|
268
|
+
self._emit_event(
|
|
269
|
+
EventType.MACHINE_HEARTBEAT_FAILED,
|
|
270
|
+
{
|
|
271
|
+
"session_id": session_id,
|
|
272
|
+
"machine_id": machine_id,
|
|
273
|
+
"error": "heartbeat_failed",
|
|
274
|
+
},
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
except Exception:
|
|
278
|
+
self._emit_event(
|
|
279
|
+
EventType.MACHINE_HEARTBEAT_FAILED,
|
|
280
|
+
{
|
|
281
|
+
"session_id": "*",
|
|
282
|
+
"error": "heartbeat_loop_error",
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
time.sleep(5) # Check frequently, but send only per interval
|
|
287
|
+
|
|
288
|
+
def cleanup_all(self):
|
|
289
|
+
"""Cleanup all machines."""
|
|
290
|
+
self.stop_heartbeat()
|
|
291
|
+
|
|
292
|
+
with self.machines_lock:
|
|
293
|
+
for session_id in list(self.machines.keys()):
|
|
294
|
+
try:
|
|
295
|
+
self.unregister_machine(session_id, reason="cleanup_all")
|
|
296
|
+
except Exception:
|
|
297
|
+
# Ignore cleanup errors
|
|
298
|
+
pass
|