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,400 @@
|
|
|
1
|
+
"""HTTP tool management for Toolplane client."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from toolplane.utils.schema import generate_schema_from_function
|
|
8
|
+
|
|
9
|
+
from ..common.base_tool_manager import BaseToolManager
|
|
10
|
+
from ..common.utils import validate_tool_name
|
|
11
|
+
from ..core.errors import ToolError, ToolplaneAPIError
|
|
12
|
+
from .http_connection import HTTPConnectionManager
|
|
13
|
+
|
|
14
|
+
# Gateway JSON enums arrive as their proto names (lowercased here); map the
|
|
15
|
+
# terminal ones onto the normalized status names used by the waiters.
|
|
16
|
+
_RESPONSE_STATUS_NAMES = {
|
|
17
|
+
"request_status_done": "done",
|
|
18
|
+
"request_status_failed": "failed",
|
|
19
|
+
"request_status_cancelled": "cancelled",
|
|
20
|
+
"done": "done",
|
|
21
|
+
"failed": "failed",
|
|
22
|
+
"failure": "failed",
|
|
23
|
+
"cancelled": "cancelled",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HTTPToolManager(BaseToolManager):
|
|
28
|
+
"""Manages tool registration and execution for HTTP client."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, connection_manager: HTTPConnectionManager):
|
|
31
|
+
"""Initialize HTTP tool manager."""
|
|
32
|
+
super().__init__(connection_manager)
|
|
33
|
+
|
|
34
|
+
def _normalize_tool(self, tool: Dict[str, Any]) -> Dict[str, Any]:
|
|
35
|
+
schema = tool.get("schema", {})
|
|
36
|
+
if isinstance(schema, str):
|
|
37
|
+
try:
|
|
38
|
+
schema = json.loads(schema)
|
|
39
|
+
except Exception:
|
|
40
|
+
schema = {}
|
|
41
|
+
|
|
42
|
+
if not isinstance(schema, dict):
|
|
43
|
+
schema = {}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"id": tool.get("id", ""),
|
|
47
|
+
"name": tool.get("name", ""),
|
|
48
|
+
"description": tool.get("description", ""),
|
|
49
|
+
"schema": schema,
|
|
50
|
+
"config": tool.get("config", {}),
|
|
51
|
+
"created_at": tool.get("createdAt", tool.get("created_at", "")),
|
|
52
|
+
"last_ping_at": tool.get("lastPingAt", tool.get("last_ping_at", "")),
|
|
53
|
+
"session_id": tool.get("sessionId", tool.get("session_id", "")),
|
|
54
|
+
"tags": tool.get("tags", []),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def register_tool(
|
|
58
|
+
self,
|
|
59
|
+
session_id: str,
|
|
60
|
+
machine_id: str,
|
|
61
|
+
name: str,
|
|
62
|
+
func: Callable,
|
|
63
|
+
schema: Optional[Dict] = None,
|
|
64
|
+
description: Optional[str] = None,
|
|
65
|
+
stream: bool = False,
|
|
66
|
+
tags: Optional[List[str]] = None,
|
|
67
|
+
):
|
|
68
|
+
"""Register a tool for a session."""
|
|
69
|
+
if tags is None:
|
|
70
|
+
tags = []
|
|
71
|
+
|
|
72
|
+
# Validate tool name
|
|
73
|
+
if not validate_tool_name(name):
|
|
74
|
+
raise ToolError(f"Invalid tool name: {name}")
|
|
75
|
+
|
|
76
|
+
# Generate schema if not provided
|
|
77
|
+
if schema is None:
|
|
78
|
+
schema = generate_schema_from_function(func)
|
|
79
|
+
|
|
80
|
+
# Add description and tags to schema
|
|
81
|
+
if description:
|
|
82
|
+
schema["description"] = description
|
|
83
|
+
schema["tags"] = tags
|
|
84
|
+
|
|
85
|
+
# Store tool locally
|
|
86
|
+
if session_id not in self.tools:
|
|
87
|
+
self.tools[session_id] = {}
|
|
88
|
+
self.tool_schemas[session_id] = {}
|
|
89
|
+
self.streaming_tools[session_id] = set()
|
|
90
|
+
|
|
91
|
+
self.tools[session_id][name] = func
|
|
92
|
+
self.tool_schemas[session_id][name] = schema
|
|
93
|
+
|
|
94
|
+
if stream:
|
|
95
|
+
self.streaming_tools[session_id].add(name)
|
|
96
|
+
|
|
97
|
+
# Register with server
|
|
98
|
+
self._register_tool_with_server(session_id, machine_id, name, schema)
|
|
99
|
+
|
|
100
|
+
def _register_tool_with_server(
|
|
101
|
+
self, session_id: str, machine_id: str, name: str, schema: Dict
|
|
102
|
+
):
|
|
103
|
+
"""Register tool with server."""
|
|
104
|
+
try:
|
|
105
|
+
self.connection_manager.ensure_connected()
|
|
106
|
+
|
|
107
|
+
payload = {
|
|
108
|
+
"sessionId": session_id,
|
|
109
|
+
"machineId": machine_id,
|
|
110
|
+
"name": name,
|
|
111
|
+
"description": schema.get("description", ""),
|
|
112
|
+
"schema": json.dumps(schema.get("schema", {})),
|
|
113
|
+
"tags": schema.get("tags", []),
|
|
114
|
+
"config": {}, # Default config
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
self.connection_manager.register_tool(payload)
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
raise ToolError(f"Failed to register tool {name} with server: {e}")
|
|
121
|
+
|
|
122
|
+
def unregister_tool(self, session_id: str, name: str):
|
|
123
|
+
"""Unregister a tool from a session."""
|
|
124
|
+
try:
|
|
125
|
+
# Remove from local storage
|
|
126
|
+
if session_id in self.tools:
|
|
127
|
+
self.tools[session_id].pop(name, None)
|
|
128
|
+
self.tool_schemas[session_id].pop(name, None)
|
|
129
|
+
self.streaming_tools[session_id].discard(name)
|
|
130
|
+
|
|
131
|
+
# Remove from server
|
|
132
|
+
self._unregister_tool_from_server(session_id, name)
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
raise ToolError(f"Failed to unregister tool {name}: {e}")
|
|
136
|
+
|
|
137
|
+
def _unregister_tool_from_server(self, session_id: str, name: str):
|
|
138
|
+
"""Unregister tool from server."""
|
|
139
|
+
try:
|
|
140
|
+
self.connection_manager.ensure_connected()
|
|
141
|
+
|
|
142
|
+
# Get tool by name first
|
|
143
|
+
tool_response = self.connection_manager.get_tool_by_name(session_id, name)
|
|
144
|
+
|
|
145
|
+
if "tool" in tool_response:
|
|
146
|
+
tool_id = tool_response["tool"].get("id")
|
|
147
|
+
if tool_id:
|
|
148
|
+
self.connection_manager.delete_tool(session_id, tool_id)
|
|
149
|
+
|
|
150
|
+
except Exception as e:
|
|
151
|
+
raise ToolError(f"Failed to unregister tool {name} from server: {e}")
|
|
152
|
+
|
|
153
|
+
def get_tool(self, session_id: str, name: str) -> Optional[Callable]:
|
|
154
|
+
"""Get a tool function by name."""
|
|
155
|
+
return self.tools.get(session_id, {}).get(name)
|
|
156
|
+
|
|
157
|
+
def is_streaming_tool(self, session_id: str, name: str) -> bool:
|
|
158
|
+
"""Check if a tool is a streaming tool."""
|
|
159
|
+
return name in self.streaming_tools.get(session_id, set())
|
|
160
|
+
|
|
161
|
+
def get_session_tools(self, session_id: str) -> Dict[str, Callable]:
|
|
162
|
+
"""Get all tools for a session."""
|
|
163
|
+
return self.tools.get(session_id, {})
|
|
164
|
+
|
|
165
|
+
def _get_available_tools_from_server(self, session_id: str) -> Dict[str, Any]:
|
|
166
|
+
"""Get available tools from server."""
|
|
167
|
+
# Check cache first
|
|
168
|
+
now = time.time()
|
|
169
|
+
cached = self._tool_cache.get(session_id)
|
|
170
|
+
if cached and now - cached[0] < 30: # 30 second cache
|
|
171
|
+
return {"tools": cached[1]}
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
self.connection_manager.ensure_connected()
|
|
175
|
+
|
|
176
|
+
response = self.connection_manager.list_tools(session_id)
|
|
177
|
+
|
|
178
|
+
# Handle case where response might be a string or dict
|
|
179
|
+
if isinstance(response, str):
|
|
180
|
+
try:
|
|
181
|
+
response = json.loads(response)
|
|
182
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
183
|
+
return {"tools": []}
|
|
184
|
+
|
|
185
|
+
if not isinstance(response, dict):
|
|
186
|
+
return {"tools": []}
|
|
187
|
+
|
|
188
|
+
tools = []
|
|
189
|
+
for tool in response.get("tools", []):
|
|
190
|
+
try:
|
|
191
|
+
# Handle case where tool might be a string
|
|
192
|
+
if isinstance(tool, str):
|
|
193
|
+
try:
|
|
194
|
+
tool = json.loads(tool)
|
|
195
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
if not isinstance(tool, dict):
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
tools.append(self._normalize_tool(tool))
|
|
202
|
+
except Exception:
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Cache result
|
|
206
|
+
self._tool_cache[session_id] = (now, tools)
|
|
207
|
+
return {"tools": tools}
|
|
208
|
+
|
|
209
|
+
except Exception as e:
|
|
210
|
+
raise ToolError(f"Failed to get available tools: {e}")
|
|
211
|
+
|
|
212
|
+
def list_tools(self, session_id: str) -> List[Dict[str, Any]]:
|
|
213
|
+
"""List tools for a session."""
|
|
214
|
+
return self.get_available_tools(session_id).get("tools", [])
|
|
215
|
+
|
|
216
|
+
def get_tool_by_id(self, session_id: str, tool_id: str) -> Dict[str, Any]:
|
|
217
|
+
"""Get a tool by ID."""
|
|
218
|
+
try:
|
|
219
|
+
self.connection_manager.ensure_connected()
|
|
220
|
+
response = self.connection_manager.get_tool_by_id(session_id, tool_id)
|
|
221
|
+
payload = response.get("tool", response)
|
|
222
|
+
if not isinstance(payload, dict):
|
|
223
|
+
raise ToolError(f"Unexpected tool lookup payload for {tool_id}")
|
|
224
|
+
return self._normalize_tool(payload)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
raise ToolError(f"Failed to get tool {tool_id}: {e}")
|
|
227
|
+
|
|
228
|
+
def get_tool_by_name(self, session_id: str, tool_name: str) -> Dict[str, Any]:
|
|
229
|
+
"""Get a tool by name."""
|
|
230
|
+
try:
|
|
231
|
+
self.connection_manager.ensure_connected()
|
|
232
|
+
response = self.connection_manager.get_tool_by_name(session_id, tool_name)
|
|
233
|
+
payload = response.get("tool", response)
|
|
234
|
+
if not isinstance(payload, dict):
|
|
235
|
+
raise ToolError(f"Unexpected tool lookup payload for {tool_name}")
|
|
236
|
+
return self._normalize_tool(payload)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
raise ToolError(f"Failed to get tool {tool_name}: {e}")
|
|
239
|
+
|
|
240
|
+
def delete_tool(self, session_id: str, tool_id: str) -> bool:
|
|
241
|
+
"""Delete a tool by ID."""
|
|
242
|
+
try:
|
|
243
|
+
self.connection_manager.ensure_connected()
|
|
244
|
+
|
|
245
|
+
tool_name = None
|
|
246
|
+
try:
|
|
247
|
+
tool = self.get_tool_by_id(session_id, tool_id)
|
|
248
|
+
tool_name = tool.get("name")
|
|
249
|
+
except Exception:
|
|
250
|
+
tool_name = None
|
|
251
|
+
|
|
252
|
+
response = self.connection_manager.delete_tool(session_id, tool_id)
|
|
253
|
+
success = (
|
|
254
|
+
bool(response.get("success", False))
|
|
255
|
+
if isinstance(response, dict)
|
|
256
|
+
else bool(response)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if success:
|
|
260
|
+
self.get_available_tools.cache_clear()
|
|
261
|
+
self._tool_cache.pop(session_id, None)
|
|
262
|
+
with self._lock:
|
|
263
|
+
if tool_name and session_id in self.tools:
|
|
264
|
+
self.tools[session_id].pop(tool_name, None)
|
|
265
|
+
self.tool_schemas[session_id].pop(tool_name, None)
|
|
266
|
+
self.streaming_tools[session_id].discard(tool_name)
|
|
267
|
+
|
|
268
|
+
return success
|
|
269
|
+
except Exception as e:
|
|
270
|
+
raise ToolError(f"Failed to delete tool {tool_id}: {e}")
|
|
271
|
+
|
|
272
|
+
def _execute_tool_on_server(
|
|
273
|
+
self,
|
|
274
|
+
session_id: str,
|
|
275
|
+
tool_name: str,
|
|
276
|
+
params: Dict,
|
|
277
|
+
idempotency_key: str = "",
|
|
278
|
+
timeout_seconds: int = 0,
|
|
279
|
+
wait_timeout_seconds: int = 0,
|
|
280
|
+
):
|
|
281
|
+
"""Execute a tool and return ``(request_id, terminal_status, result)``.
|
|
282
|
+
|
|
283
|
+
``terminal_status``/``result`` are populated only when the
|
|
284
|
+
server-side long-poll observed a terminal state.
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
self.connection_manager.ensure_connected()
|
|
288
|
+
|
|
289
|
+
response = self.connection_manager.execute_tool(
|
|
290
|
+
session_id,
|
|
291
|
+
tool_name,
|
|
292
|
+
json.dumps(params),
|
|
293
|
+
idempotency_key,
|
|
294
|
+
timeout_seconds=timeout_seconds,
|
|
295
|
+
wait_timeout_seconds=wait_timeout_seconds,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Classify the terminal status BEFORE the generic error check:
|
|
299
|
+
# server-side FAILED/CANCELLED long-poll responses carry an
|
|
300
|
+
# error, and callers need the typed tuple (with the request id)
|
|
301
|
+
# rather than a generic ToolError.
|
|
302
|
+
status_name = _RESPONSE_STATUS_NAMES.get(
|
|
303
|
+
str(response.get("status", "")).lower()
|
|
304
|
+
)
|
|
305
|
+
if wait_timeout_seconds > 0 and status_name in (
|
|
306
|
+
"done",
|
|
307
|
+
"failed",
|
|
308
|
+
"cancelled",
|
|
309
|
+
):
|
|
310
|
+
result_value = None
|
|
311
|
+
if status_name == "done" and response.get("result") is not None:
|
|
312
|
+
raw = response["result"]
|
|
313
|
+
if isinstance(raw, str):
|
|
314
|
+
try:
|
|
315
|
+
result_value = json.loads(raw)
|
|
316
|
+
except ValueError:
|
|
317
|
+
result_value = raw
|
|
318
|
+
else:
|
|
319
|
+
result_value = raw
|
|
320
|
+
return response.get("requestId"), status_name, result_value
|
|
321
|
+
|
|
322
|
+
if response.get("error"):
|
|
323
|
+
raise ToolError(f"Tool execution failed: {response.get('error')}")
|
|
324
|
+
return response.get("requestId"), None, None
|
|
325
|
+
|
|
326
|
+
except ToolplaneAPIError:
|
|
327
|
+
raise
|
|
328
|
+
except Exception as e:
|
|
329
|
+
raise ToolError(f"Failed to execute tool {tool_name}: {e}")
|
|
330
|
+
|
|
331
|
+
def _stream_tool_on_server(
|
|
332
|
+
self,
|
|
333
|
+
session_id: str,
|
|
334
|
+
tool_name: str,
|
|
335
|
+
params: Dict,
|
|
336
|
+
idempotency_key: str = "",
|
|
337
|
+
timeout_seconds: int = 0,
|
|
338
|
+
):
|
|
339
|
+
"""Stream tool execution."""
|
|
340
|
+
try:
|
|
341
|
+
self.connection_manager.ensure_connected()
|
|
342
|
+
|
|
343
|
+
response = self.connection_manager.stream_execute_tool(
|
|
344
|
+
session_id, tool_name, json.dumps(params), idempotency_key
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Track buffer for backpressure
|
|
348
|
+
self.connection_manager.current_buffer_size = 0
|
|
349
|
+
|
|
350
|
+
# Parse newline-delimited JSON (NDJSON)
|
|
351
|
+
for line in response.iter_lines(decode_unicode=True):
|
|
352
|
+
if not line:
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
# Check buffer size and apply backpressure if needed
|
|
356
|
+
line_size = len(line.encode("utf-8"))
|
|
357
|
+
self.connection_manager.current_buffer_size += line_size
|
|
358
|
+
|
|
359
|
+
if (
|
|
360
|
+
self.connection_manager.current_buffer_size
|
|
361
|
+
> self.connection_manager.config.max_buffer_size
|
|
362
|
+
):
|
|
363
|
+
# Apply backpressure by pausing before processing
|
|
364
|
+
time.sleep(0.05)
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
chunk = json.loads(line)
|
|
368
|
+
yield chunk
|
|
369
|
+
|
|
370
|
+
# Update buffer tracking as we process
|
|
371
|
+
self.connection_manager.current_buffer_size -= line_size
|
|
372
|
+
|
|
373
|
+
if chunk.get("isFinal"):
|
|
374
|
+
break
|
|
375
|
+
|
|
376
|
+
except ValueError:
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
except Exception as e:
|
|
380
|
+
raise ToolError(f"Failed to stream tool {tool_name}: {e}")
|
|
381
|
+
|
|
382
|
+
def cleanup_session_tools(self, session_id: str):
|
|
383
|
+
"""Cleanup all tools for a session."""
|
|
384
|
+
if session_id in self.tools:
|
|
385
|
+
for tool_name in list(self.tools[session_id].keys()):
|
|
386
|
+
try:
|
|
387
|
+
self.unregister_tool(session_id, tool_name)
|
|
388
|
+
except Exception:
|
|
389
|
+
# Ignore cleanup errors
|
|
390
|
+
pass
|
|
391
|
+
|
|
392
|
+
# Clear local storage
|
|
393
|
+
self.tools.pop(session_id, None)
|
|
394
|
+
self.tool_schemas.pop(session_id, None)
|
|
395
|
+
self.streaming_tools.pop(session_id, None)
|
|
396
|
+
|
|
397
|
+
def cleanup_all(self):
|
|
398
|
+
"""Cleanup all tools."""
|
|
399
|
+
for session_id in list(self.tools.keys()):
|
|
400
|
+
self.cleanup_session_tools(session_id)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Interface definitions for Toolplane client components."""
|
|
2
|
+
|
|
3
|
+
from .client_interface import ClientProtocol, IClientFactory, IToolplaneClient
|
|
4
|
+
from .connection_interface import IConnectionManager, IConnectionStrategy
|
|
5
|
+
from .event_interface import IEventEmitter, IEventHandler
|
|
6
|
+
from .request_interface import IRequestManager, IRequestProcessor
|
|
7
|
+
from .session_interface import ISessionContext, ISessionManager
|
|
8
|
+
from .tool_interface import IToolExecutor, IToolManager
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
# Client interfaces
|
|
12
|
+
"IToolplaneClient",
|
|
13
|
+
"IClientFactory",
|
|
14
|
+
"ClientProtocol",
|
|
15
|
+
# Component interfaces
|
|
16
|
+
"IConnectionManager",
|
|
17
|
+
"IConnectionStrategy",
|
|
18
|
+
"ISessionManager",
|
|
19
|
+
"ISessionContext",
|
|
20
|
+
"IToolManager",
|
|
21
|
+
"IToolExecutor",
|
|
22
|
+
"IRequestManager",
|
|
23
|
+
"IRequestProcessor",
|
|
24
|
+
# Event interfaces
|
|
25
|
+
"IEventEmitter",
|
|
26
|
+
"IEventHandler",
|
|
27
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Client interface definitions for protocol-agnostic Toolplane clients."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from .session_interface import ISessionContext
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClientProtocol(Enum):
|
|
11
|
+
"""Supported client protocols."""
|
|
12
|
+
|
|
13
|
+
HTTP = "http"
|
|
14
|
+
GRPC = "grpc"
|
|
15
|
+
WEBSOCKET = "websocket"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class IToolplaneClient(Protocol):
|
|
20
|
+
"""Protocol interface for Toolplane clients."""
|
|
21
|
+
|
|
22
|
+
def connect(self) -> bool:
|
|
23
|
+
"""Connect to Toolplane server."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def disconnect(self) -> None:
|
|
27
|
+
"""Disconnect from Toolplane server."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def create_session(
|
|
31
|
+
self,
|
|
32
|
+
session_id: Optional[str] = None,
|
|
33
|
+
user_id: Optional[str] = None,
|
|
34
|
+
name: Optional[str] = None,
|
|
35
|
+
description: Optional[str] = None,
|
|
36
|
+
namespace: Optional[str] = None,
|
|
37
|
+
register_machine: bool = False,
|
|
38
|
+
) -> ISessionContext:
|
|
39
|
+
"""Create a new session."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def get_session(self, session_id: str) -> Optional[ISessionContext]:
|
|
43
|
+
"""Get session context by ID."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def list_sessions(self) -> List[ISessionContext]:
|
|
47
|
+
"""List all session contexts."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def start(self) -> None:
|
|
51
|
+
"""Start the client."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def stop(self) -> None:
|
|
55
|
+
"""Stop the client."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class IClientFactory(ABC):
|
|
60
|
+
"""Abstract factory for creating protocol-specific Toolplane clients."""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def create_client(
|
|
64
|
+
self, protocol: ClientProtocol, config: Dict[str, Any]
|
|
65
|
+
) -> IToolplaneClient:
|
|
66
|
+
"""Create a client for the specified protocol."""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def get_supported_protocols(self) -> List[ClientProtocol]:
|
|
71
|
+
"""Get list of supported protocols."""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def validate_config(self, protocol: ClientProtocol, config: Dict[str, Any]) -> bool:
|
|
76
|
+
"""Validate configuration for the specified protocol."""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BaseClientFactory(IClientFactory):
|
|
81
|
+
"""Base implementation of client factory with registry pattern."""
|
|
82
|
+
|
|
83
|
+
def __init__(self):
|
|
84
|
+
self._client_classes: Dict[ClientProtocol, type] = {}
|
|
85
|
+
self._config_validators: Dict[ClientProtocol, Callable] = {}
|
|
86
|
+
|
|
87
|
+
def register_client(
|
|
88
|
+
self,
|
|
89
|
+
protocol: ClientProtocol,
|
|
90
|
+
client_class: type,
|
|
91
|
+
config_validator: Optional[Callable] = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Register a client implementation for a protocol."""
|
|
94
|
+
self._client_classes[protocol] = client_class
|
|
95
|
+
if config_validator:
|
|
96
|
+
self._config_validators[protocol] = config_validator
|
|
97
|
+
|
|
98
|
+
def create_client(
|
|
99
|
+
self, protocol: ClientProtocol, config: Dict[str, Any]
|
|
100
|
+
) -> IToolplaneClient:
|
|
101
|
+
"""Create a client for the specified protocol."""
|
|
102
|
+
if protocol not in self._client_classes:
|
|
103
|
+
raise ValueError(f"Unsupported protocol: {protocol}")
|
|
104
|
+
|
|
105
|
+
if not self.validate_config(protocol, config):
|
|
106
|
+
raise ValueError(f"Invalid configuration for protocol: {protocol}")
|
|
107
|
+
|
|
108
|
+
client_class = self._client_classes[protocol]
|
|
109
|
+
return client_class(**config)
|
|
110
|
+
|
|
111
|
+
def get_supported_protocols(self) -> List[ClientProtocol]:
|
|
112
|
+
"""Get list of supported protocols."""
|
|
113
|
+
return list(self._client_classes.keys())
|
|
114
|
+
|
|
115
|
+
def validate_config(self, protocol: ClientProtocol, config: Dict[str, Any]) -> bool:
|
|
116
|
+
"""Validate configuration for the specified protocol."""
|
|
117
|
+
if protocol in self._config_validators:
|
|
118
|
+
try:
|
|
119
|
+
return self._config_validators[protocol](config)
|
|
120
|
+
except Exception:
|
|
121
|
+
return False
|
|
122
|
+
return True # No validator means valid by default
|