glaip-sdk 0.5.5__py3-none-any.whl → 0.6.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 (38) hide show
  1. glaip_sdk/__init__.py +4 -1
  2. glaip_sdk/agents/__init__.py +27 -0
  3. glaip_sdk/agents/base.py +989 -0
  4. glaip_sdk/cli/commands/tools.py +2 -5
  5. glaip_sdk/client/_agent_payloads.py +10 -9
  6. glaip_sdk/client/agents.py +70 -8
  7. glaip_sdk/client/base.py +1 -0
  8. glaip_sdk/client/main.py +12 -4
  9. glaip_sdk/client/mcps.py +112 -10
  10. glaip_sdk/client/tools.py +151 -7
  11. glaip_sdk/mcps/__init__.py +21 -0
  12. glaip_sdk/mcps/base.py +345 -0
  13. glaip_sdk/models/__init__.py +65 -31
  14. glaip_sdk/models/agent.py +47 -0
  15. glaip_sdk/models/agent_runs.py +0 -1
  16. glaip_sdk/models/common.py +42 -0
  17. glaip_sdk/models/mcp.py +33 -0
  18. glaip_sdk/models/tool.py +33 -0
  19. glaip_sdk/registry/__init__.py +55 -0
  20. glaip_sdk/registry/agent.py +164 -0
  21. glaip_sdk/registry/base.py +139 -0
  22. glaip_sdk/registry/mcp.py +251 -0
  23. glaip_sdk/registry/tool.py +238 -0
  24. glaip_sdk/tools/__init__.py +22 -0
  25. glaip_sdk/tools/base.py +435 -0
  26. glaip_sdk/utils/__init__.py +50 -9
  27. glaip_sdk/utils/bundler.py +267 -0
  28. glaip_sdk/utils/client.py +111 -0
  29. glaip_sdk/utils/client_utils.py +26 -7
  30. glaip_sdk/utils/discovery.py +78 -0
  31. glaip_sdk/utils/import_resolver.py +500 -0
  32. glaip_sdk/utils/instructions.py +101 -0
  33. glaip_sdk/utils/sync.py +142 -0
  34. {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.0.dist-info}/METADATA +5 -3
  35. {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.0.dist-info}/RECORD +37 -17
  36. glaip_sdk/models.py +0 -241
  37. {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.0.dist-info}/WHEEL +0 -0
  38. {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,238 @@
1
+ """Tool registry for glaip_sdk.
2
+
3
+ This module provides the ToolRegistry that caches deployed tools
4
+ to avoid redundant API calls when deploying agents with tools.
5
+
6
+ Authors:
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ from glaip_sdk.registry.base import BaseRegistry
16
+
17
+ if TYPE_CHECKING:
18
+ from glaip_sdk.tools import Tool
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ToolRegistry(BaseRegistry["Tool"]):
24
+ """Registry for tools.
25
+
26
+ Resolves tool references to glaip_sdk.models.Tool objects.
27
+ Caches results to avoid redundant API calls and duplicate uploads.
28
+
29
+ Handles:
30
+ - Tool classes (LangChain BaseTool subclasses) → upload, cache, return Tool
31
+ - glaip_sdk.models.Tool → return as-is (uses tool.id)
32
+ - String names → lookup on platform, cache, return Tool
33
+
34
+ Attributes:
35
+ _cache: Internal cache mapping names to Tool objects.
36
+
37
+ Example:
38
+ >>> registry = get_tool_registry()
39
+ >>> tool = registry.resolve(WebSearchTool)
40
+ >>> print(tool.id)
41
+ """
42
+
43
+ def _get_name_from_model_fields(self, ref: type) -> str | None:
44
+ """Extract name from Pydantic model_fields if available."""
45
+ model_fields = getattr(ref, "model_fields", {})
46
+ if "name" not in model_fields:
47
+ return None
48
+ field_info = model_fields["name"]
49
+ default = getattr(field_info, "default", None)
50
+ return default if isinstance(default, str) else None
51
+
52
+ def _get_string_attr(self, obj: Any, attr: str) -> str | None:
53
+ """Get attribute if it's a string, otherwise None."""
54
+ value = getattr(obj, attr, None)
55
+ return value if isinstance(value, str) else None
56
+
57
+ def _extract_name(self, ref: Any) -> str:
58
+ """Extract tool name from a reference.
59
+
60
+ Args:
61
+ ref: A tool class, instance, dict, or string name.
62
+
63
+ Returns:
64
+ The extracted tool name.
65
+
66
+ Raises:
67
+ ValueError: If name cannot be extracted from the reference.
68
+ """
69
+ if isinstance(ref, str):
70
+ return ref
71
+
72
+ # Dict from API response - extract name or id
73
+ if isinstance(ref, dict):
74
+ return ref.get("name") or ref.get("id") or ""
75
+
76
+ # Tool instance (not a class) with name attribute
77
+ if not isinstance(ref, type):
78
+ name = self._get_string_attr(ref, "name")
79
+ if name:
80
+ return name
81
+
82
+ # Tool class - try direct attribute first, then model_fields
83
+ if isinstance(ref, type):
84
+ name = self._get_string_attr(ref, "name") or self._get_name_from_model_fields(ref)
85
+ if name:
86
+ return name
87
+
88
+ raise ValueError(f"Cannot extract name from: {ref}")
89
+
90
+ def _resolve_and_cache(self, ref: Any, name: str) -> Tool:
91
+ """Resolve tool reference - upload if class, find if string/native.
92
+
93
+ Args:
94
+ ref: The tool reference to resolve.
95
+ name: The extracted tool name.
96
+
97
+ Returns:
98
+ The resolved glaip_sdk.models.Tool object.
99
+
100
+ Raises:
101
+ ValueError: If the tool cannot be resolved.
102
+ """
103
+ # Lazy imports to avoid circular dependency
104
+ from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
105
+ from glaip_sdk.utils.sync import update_or_create_tool # noqa: PLC0415
106
+
107
+ # Already deployed tool (glaip_sdk.models.Tool with ID) - just cache and return
108
+ if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
109
+ if ref.id is not None:
110
+ logger.debug("Caching already deployed tool: %s", name)
111
+ self._cache[name] = ref
112
+ return ref
113
+
114
+ # Tool without ID (e.g., Tool.from_native()) - look up on platform
115
+ logger.info("Looking up native tool: %s", name)
116
+ tool = find_tool(name)
117
+ if tool:
118
+ self._cache[name] = tool
119
+ return tool
120
+ raise ValueError(f"Native tool not found on platform: {name}")
121
+
122
+ # Custom tool class - upload it
123
+ if self._is_custom_tool(ref):
124
+ logger.info("Uploading custom tool: %s", name)
125
+ tool = update_or_create_tool(ref)
126
+ self._cache[name] = tool
127
+ if tool.id:
128
+ self._cache[tool.id] = tool
129
+ return tool
130
+
131
+ # Dict from API response - use ID directly if available
132
+ if isinstance(ref, dict):
133
+ tool_id = ref.get("id")
134
+ if tool_id:
135
+ from glaip_sdk.tools.base import Tool # noqa: PLC0415
136
+
137
+ tool = Tool(id=tool_id, name=ref.get("name", ""))
138
+ self._cache[name] = tool
139
+ return tool
140
+ raise ValueError(f"Tool dict missing 'id': {ref}")
141
+
142
+ # String name - look up on platform (could be native or existing tool)
143
+ if isinstance(ref, str):
144
+ logger.info("Looking up tool by name: %s", name)
145
+ tool = find_tool(name)
146
+ if tool:
147
+ self._cache[name] = tool
148
+ return tool
149
+ raise ValueError(f"Tool not found on platform: {name}")
150
+
151
+ raise ValueError(f"Could not resolve tool reference: {ref}")
152
+
153
+ def _is_custom_tool(self, ref: Any) -> bool:
154
+ """Check if reference is a custom tool class/instance.
155
+
156
+ Args:
157
+ ref: The reference to check.
158
+
159
+ Returns:
160
+ True if ref is a custom tool that needs uploading.
161
+ """
162
+ try:
163
+ from langchain_core.tools import BaseTool # noqa: PLC0415
164
+
165
+ # LangChain BaseTool class
166
+ if isinstance(ref, type) and issubclass(ref, BaseTool):
167
+ return True
168
+
169
+ # LangChain BaseTool instance
170
+ if isinstance(ref, BaseTool):
171
+ return True
172
+ except ImportError:
173
+ pass
174
+
175
+ return False
176
+
177
+ def resolve(self, ref: Any) -> Tool:
178
+ """Resolve a tool reference to a platform Tool object.
179
+
180
+ Overrides base resolve to handle SDK tools differently.
181
+
182
+ Args:
183
+ ref: The tool reference to resolve.
184
+
185
+ Returns:
186
+ The resolved glaip_sdk.models.Tool object.
187
+ """
188
+ # Check if it's a Tool instance (not a class)
189
+ if hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type):
190
+ # If Tool has an ID, it's already deployed - return as-is
191
+ if ref.id is not None:
192
+ name = self._extract_name(ref)
193
+ if name not in self._cache:
194
+ self._cache[name] = ref
195
+ return ref
196
+
197
+ # Tool without ID (e.g., from Tool.from_native()) - needs platform lookup
198
+ # Fall through to normal resolution
199
+
200
+ return super().resolve(ref)
201
+
202
+
203
+ class _ToolRegistrySingleton:
204
+ """Singleton holder for ToolRegistry to avoid global statement."""
205
+
206
+ _instance: ToolRegistry | None = None
207
+
208
+ @classmethod
209
+ def get_instance(cls) -> ToolRegistry:
210
+ """Get or create the singleton instance.
211
+
212
+ Returns:
213
+ The global ToolRegistry instance.
214
+ """
215
+ if cls._instance is None:
216
+ cls._instance = ToolRegistry()
217
+ return cls._instance
218
+
219
+ @classmethod
220
+ def reset(cls) -> None:
221
+ """Reset the singleton instance (for testing)."""
222
+ cls._instance = None
223
+
224
+
225
+ def get_tool_registry() -> ToolRegistry:
226
+ """Get the singleton ToolRegistry instance.
227
+
228
+ Returns a global ToolRegistry that caches tools across the session.
229
+
230
+ Returns:
231
+ The global ToolRegistry instance.
232
+
233
+ Example:
234
+ >>> from glaip_sdk.registry import get_tool_registry
235
+ >>> registry = get_tool_registry()
236
+ >>> tool = registry.resolve("web_search")
237
+ """
238
+ return _ToolRegistrySingleton.get_instance()
@@ -0,0 +1,22 @@
1
+ """Tool package for GL AIP platform.
2
+
3
+ This package provides the Tool class, ToolType enum, and ToolRegistry
4
+ for managing tools on the GL AIP platform.
5
+
6
+ Example:
7
+ >>> from glaip_sdk.tools import Tool, ToolType, get_tool_registry
8
+ >>> native_tool = Tool.from_native("web_search")
9
+ >>> custom_tool = Tool.from_langchain(MyCustomTool)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from glaip_sdk.registry.tool import ToolRegistry, get_tool_registry
15
+ from glaip_sdk.tools.base import Tool, ToolType
16
+
17
+ __all__ = [
18
+ "Tool",
19
+ "ToolType",
20
+ "ToolRegistry",
21
+ "get_tool_registry",
22
+ ]
@@ -0,0 +1,435 @@
1
+ """Tool class for lazy tool references.
2
+
3
+ This module provides the Tool class that serves as a lazy reference
4
+ to tools on the GL AIP platform. Tools are only resolved when
5
+ Agent.deploy() is called.
6
+
7
+ The Tool class also supports runtime operations (update, delete, get_script)
8
+ when retrieved from the API via client.tools.get().
9
+
10
+ Authors:
11
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
12
+
13
+ Example - Lazy Reference:
14
+ >>> from glaip_sdk.tools import Tool
15
+ >>>
16
+ >>> # Reference a native platform tool
17
+ >>> time_tool = Tool.from_native("time_tool")
18
+ >>>
19
+ >>> # Reference a custom LangChain tool
20
+ >>> greeting_tool = Tool.from_langchain(GreetingTool)
21
+ >>>
22
+ >>> # Use in an agent
23
+ >>> class MyAgent(Agent):
24
+ ... @property
25
+ ... def tools(self) -> list:
26
+ ... return [time_tool, greeting_tool]
27
+
28
+ Example - Runtime Operations:
29
+ >>> from glaip_sdk import Glaip
30
+ >>>
31
+ >>> client = Glaip()
32
+ >>> tool = client.tools.get("tool-123")
33
+ >>> script = tool.get_script() # Get tool script content
34
+ >>> tool.update(description="Updated description")
35
+ >>> tool.delete()
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ from enum import StrEnum
41
+ from typing import TYPE_CHECKING, Any
42
+
43
+ if TYPE_CHECKING:
44
+ from glaip_sdk.models import ToolResponse
45
+
46
+ _TOOL_NOT_DEPLOYED_MSG = "Tool not available on platform. No ID set."
47
+ _CLIENT_NOT_AVAILABLE_MSG = "Client not available. Use client.tools.get() to get a client-connected tool."
48
+
49
+
50
+ class ToolType(StrEnum):
51
+ """Type of tool reference."""
52
+
53
+ NATIVE = "native"
54
+ CUSTOM = "custom"
55
+
56
+
57
+ class Tool:
58
+ """Tool class for GL AIP platform.
59
+
60
+ Supports both lazy references and runtime operations:
61
+ - Lazy reference: Created via from_native() or from_langchain()
62
+ - Runtime: Created via from_response() or client.tools.get()
63
+
64
+ Use factory methods to create Tool instances:
65
+ - Tool.from_native(name) - Reference a native platform tool
66
+ - Tool.from_langchain(tool_class) - Reference a custom LangChain tool
67
+ - Tool.from_response(response, client) - From API response
68
+
69
+ Attributes:
70
+ name: Tool name (for native tools) or from tool_class.
71
+ id: Tool ID on the platform (set after deployment or from API).
72
+ tool_class: LangChain BaseTool class (for custom tools) or None.
73
+ tool_type: Type of tool (native or custom).
74
+ description: Tool description (from API response).
75
+ tool_script: Tool script content (from API response).
76
+
77
+ Example - Lazy Reference:
78
+ >>> # Native tool
79
+ >>> time_tool = Tool.from_native("time_tool")
80
+ >>>
81
+ >>> # Custom tool
82
+ >>> greeting_tool = Tool.from_langchain(GreetingTool)
83
+
84
+ Example - Runtime Operations:
85
+ >>> tool = client.tools.get("tool-123")
86
+ >>> tool.update(description="New description")
87
+ >>> tool.delete()
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ name: str | None = None,
93
+ tool_class: type | None = None,
94
+ tool_type: str | ToolType | None = None,
95
+ *,
96
+ id: str | None = None, # noqa: A002 - Allow shadowing builtin for API compat
97
+ description: str | None = None,
98
+ tool_script: str | None = None,
99
+ tool_file: str | None = None,
100
+ framework: str | None = None,
101
+ version: str | None = None,
102
+ tags: str | list[str] | None = None,
103
+ type: (str | ToolType | None) = None, # noqa: A002 - Backward compat alias for tool_type
104
+ _client: Any = None,
105
+ ) -> None:
106
+ """Initialize a Tool.
107
+
108
+ Args:
109
+ name: Tool name (for native tools).
110
+ tool_class: LangChain BaseTool class (for custom tools).
111
+ tool_type: Type of tool (native or custom). Accepts str or ToolType.
112
+ id: Tool ID on the platform.
113
+ description: Tool description.
114
+ tool_script: Tool script content.
115
+ tool_file: Tool file path.
116
+ framework: Tool framework.
117
+ version: Tool version.
118
+ tags: Tool tags.
119
+ type: Backward compatibility alias for tool_type.
120
+ _client: Internal client reference.
121
+ """
122
+ self.name = name
123
+ self.tool_class = tool_class
124
+ # Use type as alias for tool_type (backward compatibility)
125
+ effective_type = tool_type if tool_type is not None else type
126
+ if effective_type is None:
127
+ effective_type = ToolType.NATIVE
128
+ # Normalize type to ToolType enum
129
+ if isinstance(effective_type, str):
130
+ self._type = ToolType(effective_type) if effective_type in ToolType.__members__.values() else effective_type
131
+ else:
132
+ self._type = effective_type
133
+ self._id = id
134
+ self.description = description
135
+ self.tool_script = tool_script
136
+ self.tool_file = tool_file
137
+ self.framework = framework
138
+ self.version = version
139
+ self.tags = tags
140
+ self._client = _client
141
+
142
+ @property
143
+ def tool_type(self) -> str | ToolType:
144
+ """Tool type (native or custom)."""
145
+ return self._type
146
+
147
+ @tool_type.setter
148
+ def tool_type(self, value: str | ToolType) -> None:
149
+ """Set the tool type."""
150
+ if isinstance(value, str):
151
+ self._type = ToolType(value) if value in ToolType.__members__.values() else value
152
+ else:
153
+ self._type = value
154
+
155
+ @property
156
+ def type(
157
+ self,
158
+ ) -> str | ToolType: # noqa: A003 - Allow shadowing builtin for API compat
159
+ """Tool type (native or custom). Alias for 'tool_type' for backward compatibility."""
160
+ return self._type
161
+
162
+ @type.setter
163
+ def type(self, value: str | ToolType) -> None: # noqa: A003
164
+ """Set the tool type. Alias for 'tool_type' for backward compatibility."""
165
+ self.tool_type = value
166
+
167
+ @property
168
+ def id(self) -> str | None: # noqa: A003 - Allow shadowing builtin for API compat
169
+ """Tool ID on the platform."""
170
+ return self._id
171
+
172
+ @id.setter
173
+ def id(self, value: str | None) -> None: # noqa: A003
174
+ """Set the tool ID."""
175
+ self._id = value
176
+
177
+ def __repr__(self) -> str:
178
+ """Return string representation."""
179
+ if self._id:
180
+ return f"Tool(id={self._id!r}, name={self.name!r})"
181
+ if self.type == ToolType.NATIVE:
182
+ return f"Tool.from_native({self.name!r})"
183
+ if self.tool_class is not None:
184
+ return f"Tool.from_langchain({self.tool_class.__name__})"
185
+ return f"Tool(name={self.name!r}, type={self.type})"
186
+
187
+ def __eq__(self, other: object) -> bool:
188
+ """Check equality based on id if available, else name and type."""
189
+ if not isinstance(other, Tool):
190
+ return NotImplemented
191
+ if self._id and other._id:
192
+ return self._id == other._id
193
+ return self.name == other.name and self.type == other.type
194
+
195
+ def __hash__(self) -> int:
196
+ """Hash based on id if available, else name and type."""
197
+ if self._id:
198
+ return hash(self._id)
199
+ return hash((self.name, self.type))
200
+
201
+ def model_dump(self, *, exclude_none: bool = False) -> dict[str, Any]:
202
+ """Return a dict representation of the Tool.
203
+
204
+ Provides Pydantic-style serialization for backward compatibility.
205
+
206
+ Args:
207
+ exclude_none: If True, exclude None values from the output.
208
+
209
+ Returns:
210
+ Dictionary containing Tool attributes.
211
+ """
212
+ data = {
213
+ "id": self._id,
214
+ "name": self.name,
215
+ "type": str(self.type) if self.type else None,
216
+ "description": self.description,
217
+ "tool_script": self.tool_script,
218
+ "tool_file": self.tool_file,
219
+ "framework": self.framework,
220
+ "version": self.version,
221
+ "tags": self.tags,
222
+ }
223
+ if exclude_none:
224
+ return {k: v for k, v in data.items() if v is not None}
225
+ return data
226
+
227
+ @classmethod
228
+ def from_native(cls, name: str) -> Tool:
229
+ """Create a reference to a native platform tool.
230
+
231
+ Native tools are pre-existing tools on the GL AIP platform
232
+ that don't require uploading (e.g., "time_tool", "web_search").
233
+
234
+ Args:
235
+ name: The name of the native tool on the platform.
236
+
237
+ Returns:
238
+ A Tool reference that will be resolved during Agent.deploy().
239
+
240
+ Example:
241
+ >>> time_tool = Tool.from_native("time_tool")
242
+ >>> web_search = Tool.from_native("web_search")
243
+ """
244
+ return cls(name=name, type=ToolType.NATIVE)
245
+
246
+ @classmethod
247
+ def from_langchain(cls, tool_class: type) -> Tool:
248
+ """Create a reference to a custom LangChain tool.
249
+
250
+ Custom tools are user-defined LangChain BaseTool subclasses
251
+ that will be uploaded to the platform during deployment.
252
+
253
+ Args:
254
+ tool_class: A LangChain BaseTool subclass.
255
+
256
+ Returns:
257
+ A Tool reference that will be uploaded during Agent.deploy().
258
+
259
+ Example:
260
+ >>> from langchain_core.tools import BaseTool
261
+ >>>
262
+ >>> class GreetingTool(BaseTool):
263
+ ... name: str = "greeting_tool"
264
+ ... description: str = "Greets the user"
265
+ ... def _run(self, name: str) -> str:
266
+ ... return f"Hello, {name}!"
267
+ >>>
268
+ >>> greeting_tool = Tool.from_langchain(GreetingTool)
269
+ """
270
+ return cls(tool_class=tool_class, type=ToolType.CUSTOM)
271
+
272
+ def get_import_path(self) -> str | None:
273
+ """Get the import path for custom tools.
274
+
275
+ Returns:
276
+ Import path string for custom tools, None for native tools.
277
+ """
278
+ if self.tool_class is None:
279
+ return None
280
+ return f"{self.tool_class.__module__}.{self.tool_class.__name__}"
281
+
282
+ def get_name(self) -> str:
283
+ """Get the tool name.
284
+
285
+ Returns:
286
+ The tool name (from name attribute or tool_class).
287
+
288
+ Raises:
289
+ ValueError: If name cannot be determined.
290
+ """
291
+ if self.name is not None:
292
+ return self.name
293
+
294
+ if self.tool_class is not None:
295
+ # LangChain BaseTool - get name from model_fields
296
+ if hasattr(self.tool_class, "model_fields"):
297
+ name_field = self.tool_class.model_fields.get("name")
298
+ if name_field and name_field.default:
299
+ return name_field.default
300
+
301
+ # Direct name attribute
302
+ if hasattr(self.tool_class, "name"):
303
+ return self.tool_class.name
304
+
305
+ raise ValueError(f"Cannot determine name for tool: {self}")
306
+
307
+ # ─────────────────────────────────────────────────────────────────
308
+ # Runtime Methods (require client connection)
309
+ # ─────────────────────────────────────────────────────────────────
310
+
311
+ def _set_client(self, client: Any) -> Tool:
312
+ """Set the client reference for this tool.
313
+
314
+ Args:
315
+ client: The Glaip client instance.
316
+
317
+ Returns:
318
+ Self for method chaining.
319
+ """
320
+ self._client = client
321
+ return self
322
+
323
+ def get_script(self) -> str:
324
+ """Get the tool script content.
325
+
326
+ Returns:
327
+ The tool script content, or a placeholder message.
328
+ """
329
+ if self.tool_script:
330
+ return self.tool_script
331
+ elif self.tool_file:
332
+ return f"Script content from file: {self.tool_file}"
333
+ else:
334
+ return "No script content available"
335
+
336
+ def update(self, **kwargs: Any) -> Tool:
337
+ """Update the tool with new configuration.
338
+
339
+ Supports both metadata updates and file uploads.
340
+ Pass 'file' parameter to update tool code via file upload.
341
+
342
+ Args:
343
+ **kwargs: Tool properties to update (name, description, etc.).
344
+
345
+ Returns:
346
+ Self with updated properties.
347
+
348
+ Raises:
349
+ ValueError: If the tool has no ID.
350
+ RuntimeError: If client is not available.
351
+ """
352
+ if not self._id:
353
+ raise ValueError(_TOOL_NOT_DEPLOYED_MSG)
354
+ if not self._client:
355
+ raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
356
+
357
+ # Check if file upload is requested
358
+ if "file" in kwargs:
359
+ file_path = kwargs.pop("file")
360
+ response = self._client.tools.update_via_file(self._id, file_path, **kwargs)
361
+ else:
362
+ response = self._client.tools.update(tool_id=self._id, **kwargs)
363
+
364
+ # Update local properties from response
365
+ if hasattr(response, "name") and response.name:
366
+ self.name = response.name
367
+ if hasattr(response, "description"):
368
+ self.description = response.description
369
+ if hasattr(response, "tool_script"):
370
+ self.tool_script = response.tool_script
371
+
372
+ return self
373
+
374
+ def delete(self) -> None:
375
+ """Delete the tool from the platform.
376
+
377
+ Raises:
378
+ ValueError: If the tool has no ID.
379
+ RuntimeError: If client is not available.
380
+ """
381
+ if not self._id:
382
+ raise ValueError(_TOOL_NOT_DEPLOYED_MSG)
383
+ if not self._client:
384
+ raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
385
+
386
+ self._client.tools.delete(tool_id=self._id)
387
+ self._id = None
388
+ self._client = None
389
+
390
+ @classmethod
391
+ def from_response(
392
+ cls,
393
+ response: ToolResponse,
394
+ client: Any = None,
395
+ ) -> Tool:
396
+ """Create a Tool instance from an API response.
397
+
398
+ This allows you to work with tools retrieved from the API
399
+ as full Tool instances with all methods available.
400
+
401
+ Args:
402
+ response: The ToolResponse from an API call.
403
+ client: The Glaip client instance for API operations.
404
+
405
+ Returns:
406
+ A Tool instance initialized from the response.
407
+
408
+ Example:
409
+ >>> response = client.tools.get("tool-123")
410
+ >>> tool = Tool.from_response(response, client)
411
+ >>> script = tool.get_script()
412
+ """
413
+ # Use tool_type from backend; infer CUSTOM when code is present but tool_type is missing
414
+ raw_type = getattr(response, "tool_type", None)
415
+ if raw_type is None and (
416
+ getattr(response, "tool_script", None) is not None or getattr(response, "tool_file", None) is not None
417
+ ):
418
+ raw_type = ToolType.CUSTOM
419
+
420
+ tool = cls(
421
+ name=response.name,
422
+ id=response.id,
423
+ tool_type=raw_type,
424
+ description=getattr(response, "description", None),
425
+ tool_script=getattr(response, "tool_script", None),
426
+ tool_file=getattr(response, "tool_file", None),
427
+ framework=getattr(response, "framework", None),
428
+ version=getattr(response, "version", None),
429
+ tags=getattr(response, "tags", None),
430
+ )
431
+
432
+ if client:
433
+ tool._set_client(client)
434
+
435
+ return tool