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.
Files changed (81) hide show
  1. toolplane/__init__.py +106 -0
  2. toolplane/common/__init__.py +93 -0
  3. toolplane/common/base_config.py +129 -0
  4. toolplane/common/base_connection_manager.py +171 -0
  5. toolplane/common/base_session_manager.py +321 -0
  6. toolplane/common/base_tool_manager.py +347 -0
  7. toolplane/common/constants.py +47 -0
  8. toolplane/common/utils.py +310 -0
  9. toolplane/core/__init__.py +67 -0
  10. toolplane/core/config.py +107 -0
  11. toolplane/core/connection.py +285 -0
  12. toolplane/core/errors.py +298 -0
  13. toolplane/core/machine.py +480 -0
  14. toolplane/core/request.py +775 -0
  15. toolplane/core/session.py +332 -0
  16. toolplane/core/session_context.py +514 -0
  17. toolplane/core/task.py +130 -0
  18. toolplane/core/tool.py +329 -0
  19. toolplane/http_core/__init__.py +37 -0
  20. toolplane/http_core/http_config.py +97 -0
  21. toolplane/http_core/http_connection.py +409 -0
  22. toolplane/http_core/http_machine.py +298 -0
  23. toolplane/http_core/http_request.py +748 -0
  24. toolplane/http_core/http_session.py +348 -0
  25. toolplane/http_core/http_session_context.py +491 -0
  26. toolplane/http_core/http_task.py +101 -0
  27. toolplane/http_core/http_tool.py +400 -0
  28. toolplane/interfaces/__init__.py +27 -0
  29. toolplane/interfaces/client_interface.py +122 -0
  30. toolplane/interfaces/connection_interface.py +193 -0
  31. toolplane/interfaces/event_interface.py +290 -0
  32. toolplane/interfaces/request_interface.py +439 -0
  33. toolplane/interfaces/session_interface.py +288 -0
  34. toolplane/interfaces/tool_interface.py +441 -0
  35. toolplane/proto/__init__.py +0 -0
  36. toolplane/proto/service_pb2.py +315 -0
  37. toolplane/proto/service_pb2_grpc.py +2240 -0
  38. toolplane/provider_cli.py +268 -0
  39. toolplane/provider_registry.py +77 -0
  40. toolplane/provider_runtime.py +302 -0
  41. toolplane/toolkits/__init__.py +0 -0
  42. toolplane/toolkits/standalone_tools/__init__.py +0 -0
  43. toolplane/toolkits/standalone_tools/create_directory.py +94 -0
  44. toolplane/toolkits/standalone_tools/create_file.py +124 -0
  45. toolplane/toolkits/standalone_tools/file_search.py +229 -0
  46. toolplane/toolkits/standalone_tools/grep_search.py +372 -0
  47. toolplane/toolkits/standalone_tools/launcher.py +146 -0
  48. toolplane/toolkits/standalone_tools/list_dir.py +395 -0
  49. toolplane/toolkits/standalone_tools/read_file.py +346 -0
  50. toolplane/toolkits/standalone_tools/replace_string_in_file.py +407 -0
  51. toolplane/toolkits/standalone_tools/run_tests.py +66 -0
  52. toolplane/toolkits/standalone_tools/semantic_search.py +485 -0
  53. toolplane/toolkits/standalone_tools/standalone_toolkit.py +979 -0
  54. toolplane/toolkits/standalone_tools/test_failure_analysis.py +618 -0
  55. toolplane/toolkits/standalone_tools/test_standalone_toolkit.py +517 -0
  56. toolplane/toolkits/swe/__init__.py +35 -0
  57. toolplane/toolkits/swe/create_directory.py +15 -0
  58. toolplane/toolkits/swe/create_file.py +15 -0
  59. toolplane/toolkits/swe/descriptions.py +273 -0
  60. toolplane/toolkits/swe/execute_bash.py +93 -0
  61. toolplane/toolkits/swe/file_editor.py +775 -0
  62. toolplane/toolkits/swe/file_search.py +16 -0
  63. toolplane/toolkits/swe/finish.py +50 -0
  64. toolplane/toolkits/swe/grep_search.py +19 -0
  65. toolplane/toolkits/swe/list_dir.py +407 -0
  66. toolplane/toolkits/swe/read_file.py +18 -0
  67. toolplane/toolkits/swe/replace_string_in_file.py +17 -0
  68. toolplane/toolkits/swe/search.py +260 -0
  69. toolplane/toolkits/swe/semantic_search.py +20 -0
  70. toolplane/toolkits/swe/str_replace_editor.py +647 -0
  71. toolplane/toolkits/swe/submit.py +29 -0
  72. toolplane/toolkits/swe/swe_toolkit.py +1296 -0
  73. toolplane/toolplane_client.py +686 -0
  74. toolplane/toolplane_http_client.py +681 -0
  75. toolplane/utils/__init__.py +3 -0
  76. toolplane/utils/schema.py +146 -0
  77. toolplane_python_client-0.1.0.dist-info/METADATA +543 -0
  78. toolplane_python_client-0.1.0.dist-info/RECORD +81 -0
  79. toolplane_python_client-0.1.0.dist-info/WHEEL +5 -0
  80. toolplane_python_client-0.1.0.dist-info/entry_points.txt +2 -0
  81. toolplane_python_client-0.1.0.dist-info/top_level.txt +1 -0
toolplane/core/tool.py ADDED
@@ -0,0 +1,329 @@
1
+ """Tool management for Toolplane gRPC client."""
2
+
3
+ import json
4
+ from typing import Any, Dict, List
5
+
6
+ import grpc
7
+
8
+ from toolplane.proto.service_pb2 import (
9
+ DeleteToolRequest,
10
+ ExecuteToolRequest,
11
+ GetToolByIdRequest,
12
+ GetToolByNameRequest,
13
+ ListToolsRequest,
14
+ RegisterToolRequest,
15
+ RequestStatus,
16
+ )
17
+
18
+ from ..common.base_tool_manager import BaseToolManager
19
+ from ..common.utils import parse_json_safe, timestamp_to_iso
20
+ from .connection import ConnectionManager
21
+ from .errors import ToolError, api_error_from_rpc_error
22
+
23
+ # Wire enum -> normalized status name; UNSPECIFIED maps to None so an
24
+ # in-flight long-poll return is distinguishable from a terminal one.
25
+ _REQUEST_STATUS_NAMES = {
26
+ RequestStatus.REQUEST_STATUS_DONE: "done",
27
+ RequestStatus.REQUEST_STATUS_FAILED: "failed",
28
+ RequestStatus.REQUEST_STATUS_CANCELLED: "cancelled",
29
+ }
30
+
31
+
32
+ class ToolManager(BaseToolManager):
33
+ """Manages tool registration and execution for gRPC client."""
34
+
35
+ def __init__(self, connection_manager: ConnectionManager):
36
+ """Initialize gRPC tool manager."""
37
+ super().__init__(connection_manager)
38
+
39
+ def _normalize_tool(self, tool: Any) -> Dict[str, Any]:
40
+ try:
41
+ schema = parse_json_safe(tool.schema)
42
+ except Exception:
43
+ schema = {}
44
+
45
+ if not isinstance(schema, dict):
46
+ schema = {}
47
+
48
+ return {
49
+ "id": tool.id,
50
+ "name": tool.name,
51
+ "description": tool.description,
52
+ "schema": schema,
53
+ "config": dict(tool.config),
54
+ "created_at": timestamp_to_iso(tool.created_at),
55
+ "last_ping_at": timestamp_to_iso(tool.last_ping_at),
56
+ "session_id": tool.session_id,
57
+ "machine_id": tool.machine_id,
58
+ "tags": list(tool.tags),
59
+ }
60
+
61
+ def _register_tool_with_server(
62
+ self, session_id: str, machine_id: str, name: str, schema: Dict
63
+ ):
64
+ """Register tool with server."""
65
+ try:
66
+ self.connection_manager.ensure_connected()
67
+
68
+ request = RegisterToolRequest(
69
+ session_id=session_id,
70
+ machine_id=machine_id,
71
+ name=name,
72
+ description=schema.get("description", ""),
73
+ schema=json.dumps(schema.get("schema", {})),
74
+ tags=schema.get("tags", []),
75
+ )
76
+
77
+ self.connection_manager.tool_stub.RegisterTool(
78
+ request, metadata=self.connection_manager.get_metadata()
79
+ )
80
+
81
+ except grpc.RpcError as rpc_error:
82
+ self._handle_rpc_error(rpc_error)
83
+ raise api_error_from_rpc_error(
84
+ rpc_error, context=f"Failed to register tool {name} with server"
85
+ ) from rpc_error
86
+ except Exception as e:
87
+ raise ToolError(f"Failed to register tool {name} with server: {e}")
88
+
89
+ def _unregister_tool_from_server(self, session_id: str, name: str):
90
+ """Unregister tool from server."""
91
+ try:
92
+ self.connection_manager.ensure_connected()
93
+
94
+ # Get tool ID first
95
+ tool_request = GetToolByNameRequest(session_id=session_id, tool_name=name)
96
+
97
+ tool_response = self.connection_manager.tool_stub.GetToolByName(
98
+ tool_request, metadata=self.connection_manager.get_metadata()
99
+ )
100
+
101
+ # Delete the tool. machine_id makes this a properly-attributed
102
+ # provide-scoped call (per-machine credential in metadata).
103
+ delete_request = DeleteToolRequest(
104
+ session_id=session_id,
105
+ tool_id=tool_response.tool.id,
106
+ machine_id=tool_response.tool.machine_id,
107
+ )
108
+
109
+ self.connection_manager.tool_stub.DeleteTool(
110
+ delete_request, metadata=self.connection_manager.get_metadata()
111
+ )
112
+
113
+ except grpc.RpcError as rpc_error:
114
+ self._handle_rpc_error(rpc_error)
115
+ raise api_error_from_rpc_error(
116
+ rpc_error, context=f"Failed to unregister tool {name} from server"
117
+ ) from rpc_error
118
+ except Exception as e:
119
+ raise ToolError(f"Failed to unregister tool {name} from server: {e}")
120
+
121
+ def _get_available_tools_from_server(self, session_id: str) -> Dict[str, Any]:
122
+ """Get available tools from server."""
123
+ try:
124
+ self.connection_manager.ensure_connected()
125
+
126
+ request = ListToolsRequest(session_id=session_id)
127
+ response = self.connection_manager.tool_stub.ListTools(
128
+ request, metadata=self.connection_manager.get_metadata()
129
+ )
130
+
131
+ return {"tools": [self._normalize_tool(tool) for tool in response.tools]}
132
+
133
+ except grpc.RpcError as rpc_error:
134
+ self._handle_rpc_error(rpc_error)
135
+ raise api_error_from_rpc_error(
136
+ rpc_error, context="Failed to get available tools"
137
+ ) from rpc_error
138
+ except Exception as e:
139
+ raise ToolError(f"Failed to get available tools: {e}")
140
+
141
+ def list_tools(self, session_id: str) -> List[Dict[str, Any]]:
142
+ """List tools for a session."""
143
+ return self.get_available_tools(session_id).get("tools", [])
144
+
145
+ def get_tool_by_id(self, session_id: str, tool_id: str) -> Dict[str, Any]:
146
+ """Get a tool by ID."""
147
+ try:
148
+ self.connection_manager.ensure_connected()
149
+
150
+ request = GetToolByIdRequest(session_id=session_id, tool_id=tool_id)
151
+ response = self.connection_manager.tool_stub.GetToolById(
152
+ request, metadata=self.connection_manager.get_metadata()
153
+ )
154
+ return self._normalize_tool(response.tool)
155
+
156
+ except grpc.RpcError as rpc_error:
157
+ self._handle_rpc_error(rpc_error)
158
+ raise api_error_from_rpc_error(
159
+ rpc_error, context=f"Failed to get tool {tool_id}"
160
+ ) from rpc_error
161
+ except Exception as e:
162
+ raise ToolError(f"Failed to get tool {tool_id}: {e}")
163
+
164
+ def get_tool_by_name(self, session_id: str, tool_name: str) -> Dict[str, Any]:
165
+ """Get a tool by name."""
166
+ try:
167
+ self.connection_manager.ensure_connected()
168
+
169
+ request = GetToolByNameRequest(session_id=session_id, tool_name=tool_name)
170
+ response = self.connection_manager.tool_stub.GetToolByName(
171
+ request, metadata=self.connection_manager.get_metadata()
172
+ )
173
+ return self._normalize_tool(response.tool)
174
+
175
+ except grpc.RpcError as rpc_error:
176
+ self._handle_rpc_error(rpc_error)
177
+ raise api_error_from_rpc_error(
178
+ rpc_error, context=f"Failed to get tool {tool_name}"
179
+ ) from rpc_error
180
+ except Exception as e:
181
+ raise ToolError(f"Failed to get tool {tool_name}: {e}")
182
+
183
+ def delete_tool(self, session_id: str, tool_id: str) -> bool:
184
+ """Delete a tool by ID."""
185
+ try:
186
+ self.connection_manager.ensure_connected()
187
+
188
+ tool_name = None
189
+ try:
190
+ tool = self.get_tool_by_id(session_id, tool_id)
191
+ tool_name = tool.get("name")
192
+ except Exception:
193
+ tool_name = None
194
+
195
+ request = DeleteToolRequest(
196
+ session_id=session_id,
197
+ tool_id=tool_id,
198
+ machine_id=str(tool.get("machine_id", "") or ""),
199
+ )
200
+ response = self.connection_manager.tool_stub.DeleteTool(
201
+ request, metadata=self.connection_manager.get_metadata()
202
+ )
203
+
204
+ if response.success:
205
+ self.get_available_tools.cache_clear()
206
+ with self._lock:
207
+ if tool_name and session_id in self.tools:
208
+ self.tools[session_id].pop(tool_name, None)
209
+ self.tool_schemas[session_id].pop(tool_name, None)
210
+ self.streaming_tools[session_id].discard(tool_name)
211
+
212
+ return response.success
213
+
214
+ except grpc.RpcError as rpc_error:
215
+ self._handle_rpc_error(rpc_error)
216
+ raise api_error_from_rpc_error(
217
+ rpc_error, context=f"Failed to delete tool {tool_id}"
218
+ ) from rpc_error
219
+ except Exception as e:
220
+ raise ToolError(f"Failed to delete tool {tool_id}: {e}")
221
+
222
+ def _execute_tool_on_server(
223
+ self,
224
+ session_id: str,
225
+ tool_name: str,
226
+ params: Dict,
227
+ idempotency_key: str = "",
228
+ timeout_seconds: int = 0,
229
+ wait_timeout_seconds: int = 0,
230
+ ):
231
+ """Execute tool on server.
232
+
233
+ Returns ``(request_id, terminal_status, result)`` where
234
+ ``terminal_status`` is the normalized status name when the
235
+ server-side long-poll (a positive ``wait_timeout_seconds``) observed
236
+ a terminal state (``result`` carries the parsed result value for a
237
+ successful run), and both are ``None`` when the request is still in
238
+ flight.
239
+ """
240
+ try:
241
+ self.connection_manager.ensure_connected()
242
+
243
+ request = ExecuteToolRequest(
244
+ session_id=session_id,
245
+ tool_name=tool_name,
246
+ input=json.dumps(params),
247
+ idempotency_key=idempotency_key,
248
+ timeout_seconds=timeout_seconds,
249
+ wait_timeout_seconds=wait_timeout_seconds,
250
+ )
251
+
252
+ response = self.connection_manager.tool_stub.InvokeTool(
253
+ request, metadata=self.connection_manager.get_metadata()
254
+ )
255
+
256
+ # Classify the terminal status BEFORE the generic error check:
257
+ # server-side FAILED/CANCELLED long-poll responses carry an
258
+ # error, and callers need the typed tuple (with the request id)
259
+ # rather than a generic ToolError.
260
+ status_name = _REQUEST_STATUS_NAMES.get(response.status)
261
+ if wait_timeout_seconds > 0 and status_name in (
262
+ "done",
263
+ "failed",
264
+ "cancelled",
265
+ ):
266
+ result_value = None
267
+ if status_name == "done" and response.result:
268
+ try:
269
+ result_value = json.loads(response.result)
270
+ except (TypeError, ValueError):
271
+ result_value = response.result
272
+ return response.request_id, status_name, result_value
273
+
274
+ if response.error:
275
+ raise ToolError(f"Tool execution failed: {response.error}")
276
+ return response.request_id, None, None
277
+
278
+ except grpc.RpcError as rpc_error:
279
+ self._handle_rpc_error(rpc_error)
280
+ raise api_error_from_rpc_error(
281
+ rpc_error, context=f"Failed to execute tool {tool_name}"
282
+ ) from rpc_error
283
+ except Exception as e:
284
+ raise ToolError(f"Failed to execute tool {tool_name}: {e}")
285
+
286
+ def _stream_tool_on_server(
287
+ self,
288
+ session_id: str,
289
+ tool_name: str,
290
+ params: Dict,
291
+ idempotency_key: str = "",
292
+ timeout_seconds: int = 0,
293
+ ):
294
+ """Stream tool execution on server."""
295
+ try:
296
+ self.connection_manager.ensure_connected()
297
+
298
+ request = ExecuteToolRequest(
299
+ session_id=session_id,
300
+ tool_name=tool_name,
301
+ input=json.dumps(params),
302
+ idempotency_key=idempotency_key,
303
+ timeout_seconds=timeout_seconds,
304
+ )
305
+
306
+ # Use streaming endpoint
307
+ for chunk in self.connection_manager.tool_stub.StreamExecuteTool(
308
+ request, metadata=self.connection_manager.get_metadata()
309
+ ):
310
+ yield chunk
311
+
312
+ except grpc.RpcError as rpc_error:
313
+ self._handle_rpc_error(rpc_error)
314
+ raise api_error_from_rpc_error(
315
+ rpc_error, context=f"Failed to stream tool {tool_name}"
316
+ ) from rpc_error
317
+ except Exception as e:
318
+ raise ToolError(f"Failed to stream tool {tool_name}: {e}")
319
+
320
+ def _handle_rpc_error(self, rpc_error: grpc.RpcError):
321
+ """Reset the channel when the connection itself is broken.
322
+
323
+ Only UNAVAILABLE indicates a channel-level failure. Status errors
324
+ like UNAUTHENTICATED (bad or revoked key) or FAILED_PRECONDITION
325
+ (state conflict) are deterministic per-call outcomes: recycling the
326
+ channel would churn reconnection and re-registration for no effect.
327
+ """
328
+ if rpc_error.code() == grpc.StatusCode.UNAVAILABLE:
329
+ self.connection_manager.mark_unhealthy()
@@ -0,0 +1,37 @@
1
+ """HTTP-specific core modules for Toolplane client."""
2
+
3
+ from ..core.errors import (
4
+ ConnectionError,
5
+ MachineError,
6
+ RequestError,
7
+ SessionError,
8
+ TaskError,
9
+ ToolError,
10
+ ToolplaneError,
11
+ )
12
+ from .http_config import HTTPClientConfig
13
+ from .http_connection import HTTPConnectionManager
14
+ from .http_machine import HTTPMachineManager
15
+ from .http_request import HTTPRequestManager
16
+ from .http_session import HTTPSessionManager
17
+ from .http_session_context import HTTPSessionContext
18
+ from .http_task import HTTPTaskManager
19
+ from .http_tool import HTTPToolManager
20
+
21
+ __all__ = [
22
+ "HTTPClientConfig",
23
+ "HTTPConnectionManager",
24
+ "HTTPMachineManager",
25
+ "HTTPToolManager",
26
+ "HTTPRequestManager",
27
+ "HTTPTaskManager",
28
+ "HTTPSessionManager",
29
+ "HTTPSessionContext",
30
+ "ToolplaneError",
31
+ "ConnectionError",
32
+ "ToolError",
33
+ "SessionError",
34
+ "MachineError",
35
+ "RequestError",
36
+ "TaskError",
37
+ ]
@@ -0,0 +1,97 @@
1
+ """HTTP configuration management for Toolplane client."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, Optional
5
+
6
+ from ..common.base_config import BaseConfig
7
+ from ..common.constants import (
8
+ DEFAULT_MAX_RETRIES,
9
+ DEFAULT_REQUEST_TIMEOUT,
10
+ DEFAULT_RETRY_BACKOFF_MS,
11
+ HTTP_DEFAULT_PORT,
12
+ )
13
+
14
+
15
+ @dataclass
16
+ class HTTPClientConfig(BaseConfig):
17
+ """Configuration for HTTP Toolplane client."""
18
+
19
+ server_host: str = "localhost"
20
+ server_port: int = HTTP_DEFAULT_PORT
21
+ use_tls: bool = False
22
+ tls_cert_path: Optional[str] = None
23
+ tls_key_path: Optional[str] = None
24
+ tls_ca_cert_path: Optional[str] = None
25
+
26
+ def normalize(self):
27
+ """Normalize HTTP-specific configuration values."""
28
+ # Normalize server host
29
+ if self.server_host.startswith("http://"):
30
+ self.server_host = self.server_host[7:]
31
+ elif self.server_host.startswith("https://"):
32
+ self.server_host = self.server_host[8:]
33
+ self.use_tls = True
34
+
35
+ # Extract port from host if present
36
+ if ":" in self.server_host:
37
+ host_parts = self.server_host.split(":")
38
+ if len(host_parts) == 2:
39
+ try:
40
+ self.server_port = int(host_parts[1])
41
+ self.server_host = host_parts[0]
42
+ except ValueError:
43
+ pass
44
+
45
+ # Ensure timeout and retry values are reasonable
46
+ if self.request_timeout <= 0:
47
+ self.request_timeout = DEFAULT_REQUEST_TIMEOUT
48
+ if self.max_retries < 0:
49
+ self.max_retries = DEFAULT_MAX_RETRIES
50
+ if self.retry_backoff_ms < 0:
51
+ self.retry_backoff_ms = DEFAULT_RETRY_BACKOFF_MS
52
+
53
+ def get_auth_info(self) -> Dict[str, Any]:
54
+ """Get HTTP authentication information."""
55
+ return {
56
+ "headers": self.get_headers(),
57
+ "server_url": self.server_url,
58
+ }
59
+
60
+ def get_headers(self) -> Dict[str, str]:
61
+ """Get HTTP headers for requests."""
62
+ headers = {"Content-Type": "application/json"}
63
+ if self.api_key:
64
+ headers["Authorization"] = f"Bearer {self.api_key}"
65
+ # Keep legacy header for backward compatibility
66
+ headers["Grpc-Metadata-api_key"] = self.api_key
67
+ return headers
68
+
69
+ def get_server_address(self) -> str:
70
+ """Get formatted server address."""
71
+ return f"{self.server_host}:{self.server_port}"
72
+
73
+ @property
74
+ def server_url(self) -> str:
75
+ """Get complete server URL with protocol."""
76
+ protocol = "https" if self.use_tls else "http"
77
+ return f"{protocol}://{self.server_host}:{self.server_port}"
78
+
79
+ def copy(self) -> "HTTPClientConfig":
80
+ """Create a copy of the configuration."""
81
+ return HTTPClientConfig(**self.to_dict())
82
+
83
+ def merge(self, other: "HTTPClientConfig") -> "HTTPClientConfig":
84
+ """Merge this configuration with another."""
85
+ merged_dict = self.to_dict()
86
+ merged_dict.update(other.to_dict())
87
+ return HTTPClientConfig(**merged_dict)
88
+
89
+ def to_dict(self) -> Dict[str, Any]:
90
+ """Convert configuration to dictionary."""
91
+ result = super().to_dict()
92
+ result.update(
93
+ {
94
+ "server_url": self.server_url,
95
+ }
96
+ )
97
+ return result