glaip-sdk 0.0.0b99__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 (207) hide show
  1. glaip_sdk/__init__.py +52 -0
  2. glaip_sdk/_version.py +81 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1227 -0
  5. glaip_sdk/branding.py +211 -0
  6. glaip_sdk/cli/__init__.py +9 -0
  7. glaip_sdk/cli/account_store.py +540 -0
  8. glaip_sdk/cli/agent_config.py +78 -0
  9. glaip_sdk/cli/auth.py +705 -0
  10. glaip_sdk/cli/commands/__init__.py +5 -0
  11. glaip_sdk/cli/commands/accounts.py +746 -0
  12. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  13. glaip_sdk/cli/commands/agents/_common.py +561 -0
  14. glaip_sdk/cli/commands/agents/create.py +151 -0
  15. glaip_sdk/cli/commands/agents/delete.py +64 -0
  16. glaip_sdk/cli/commands/agents/get.py +89 -0
  17. glaip_sdk/cli/commands/agents/list.py +129 -0
  18. glaip_sdk/cli/commands/agents/run.py +264 -0
  19. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  20. glaip_sdk/cli/commands/agents/update.py +112 -0
  21. glaip_sdk/cli/commands/common_config.py +104 -0
  22. glaip_sdk/cli/commands/configure.py +895 -0
  23. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  24. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  25. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  26. glaip_sdk/cli/commands/mcps/create.py +152 -0
  27. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  28. glaip_sdk/cli/commands/mcps/get.py +212 -0
  29. glaip_sdk/cli/commands/mcps/list.py +69 -0
  30. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  31. glaip_sdk/cli/commands/mcps/update.py +190 -0
  32. glaip_sdk/cli/commands/models.py +67 -0
  33. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  34. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  35. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  36. glaip_sdk/cli/commands/tools/_common.py +80 -0
  37. glaip_sdk/cli/commands/tools/create.py +228 -0
  38. glaip_sdk/cli/commands/tools/delete.py +61 -0
  39. glaip_sdk/cli/commands/tools/get.py +103 -0
  40. glaip_sdk/cli/commands/tools/list.py +69 -0
  41. glaip_sdk/cli/commands/tools/script.py +49 -0
  42. glaip_sdk/cli/commands/tools/update.py +102 -0
  43. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  44. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  45. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  46. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  47. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  48. glaip_sdk/cli/commands/update.py +192 -0
  49. glaip_sdk/cli/config.py +95 -0
  50. glaip_sdk/cli/constants.py +38 -0
  51. glaip_sdk/cli/context.py +150 -0
  52. glaip_sdk/cli/core/__init__.py +79 -0
  53. glaip_sdk/cli/core/context.py +124 -0
  54. glaip_sdk/cli/core/output.py +851 -0
  55. glaip_sdk/cli/core/prompting.py +649 -0
  56. glaip_sdk/cli/core/rendering.py +187 -0
  57. glaip_sdk/cli/display.py +355 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +112 -0
  60. glaip_sdk/cli/main.py +686 -0
  61. glaip_sdk/cli/masking.py +136 -0
  62. glaip_sdk/cli/mcp_validators.py +287 -0
  63. glaip_sdk/cli/pager.py +266 -0
  64. glaip_sdk/cli/parsers/__init__.py +7 -0
  65. glaip_sdk/cli/parsers/json_input.py +177 -0
  66. glaip_sdk/cli/resolution.py +68 -0
  67. glaip_sdk/cli/rich_helpers.py +27 -0
  68. glaip_sdk/cli/slash/__init__.py +15 -0
  69. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  70. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  71. glaip_sdk/cli/slash/agent_session.py +285 -0
  72. glaip_sdk/cli/slash/prompt.py +256 -0
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +1724 -0
  75. glaip_sdk/cli/slash/tui/__init__.py +34 -0
  76. glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
  77. glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
  78. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  79. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  80. glaip_sdk/cli/slash/tui/context.py +59 -0
  81. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  82. glaip_sdk/cli/slash/tui/loading.py +58 -0
  83. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  84. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  85. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  86. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  87. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  88. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  89. glaip_sdk/cli/slash/tui/toast.py +123 -0
  90. glaip_sdk/cli/transcript/__init__.py +31 -0
  91. glaip_sdk/cli/transcript/cache.py +536 -0
  92. glaip_sdk/cli/transcript/capture.py +329 -0
  93. glaip_sdk/cli/transcript/export.py +38 -0
  94. glaip_sdk/cli/transcript/history.py +815 -0
  95. glaip_sdk/cli/transcript/launcher.py +77 -0
  96. glaip_sdk/cli/transcript/viewer.py +374 -0
  97. glaip_sdk/cli/update_notifier.py +369 -0
  98. glaip_sdk/cli/validators.py +238 -0
  99. glaip_sdk/client/__init__.py +12 -0
  100. glaip_sdk/client/_schedule_payloads.py +89 -0
  101. glaip_sdk/client/agent_runs.py +147 -0
  102. glaip_sdk/client/agents.py +1353 -0
  103. glaip_sdk/client/base.py +502 -0
  104. glaip_sdk/client/main.py +253 -0
  105. glaip_sdk/client/mcps.py +401 -0
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/payloads/agent/requests.py +495 -0
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +747 -0
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +690 -0
  113. glaip_sdk/client/validators.py +198 -0
  114. glaip_sdk/config/constants.py +52 -0
  115. glaip_sdk/exceptions.py +113 -0
  116. glaip_sdk/hitl/__init__.py +15 -0
  117. glaip_sdk/hitl/local.py +151 -0
  118. glaip_sdk/icons.py +25 -0
  119. glaip_sdk/mcps/__init__.py +21 -0
  120. glaip_sdk/mcps/base.py +345 -0
  121. glaip_sdk/models/__init__.py +107 -0
  122. glaip_sdk/models/agent.py +47 -0
  123. glaip_sdk/models/agent_runs.py +117 -0
  124. glaip_sdk/models/common.py +42 -0
  125. glaip_sdk/models/mcp.py +33 -0
  126. glaip_sdk/models/schedule.py +224 -0
  127. glaip_sdk/models/tool.py +33 -0
  128. glaip_sdk/payload_schemas/__init__.py +7 -0
  129. glaip_sdk/payload_schemas/agent.py +85 -0
  130. glaip_sdk/registry/__init__.py +55 -0
  131. glaip_sdk/registry/agent.py +164 -0
  132. glaip_sdk/registry/base.py +139 -0
  133. glaip_sdk/registry/mcp.py +253 -0
  134. glaip_sdk/registry/tool.py +393 -0
  135. glaip_sdk/rich_components.py +125 -0
  136. glaip_sdk/runner/__init__.py +59 -0
  137. glaip_sdk/runner/base.py +84 -0
  138. glaip_sdk/runner/deps.py +112 -0
  139. glaip_sdk/runner/langgraph.py +870 -0
  140. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  141. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  142. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  143. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  144. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  145. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  146. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  147. glaip_sdk/schedules/__init__.py +22 -0
  148. glaip_sdk/schedules/base.py +291 -0
  149. glaip_sdk/tools/__init__.py +22 -0
  150. glaip_sdk/tools/base.py +466 -0
  151. glaip_sdk/utils/__init__.py +86 -0
  152. glaip_sdk/utils/a2a/__init__.py +34 -0
  153. glaip_sdk/utils/a2a/event_processor.py +188 -0
  154. glaip_sdk/utils/agent_config.py +194 -0
  155. glaip_sdk/utils/bundler.py +267 -0
  156. glaip_sdk/utils/client.py +111 -0
  157. glaip_sdk/utils/client_utils.py +486 -0
  158. glaip_sdk/utils/datetime_helpers.py +58 -0
  159. glaip_sdk/utils/discovery.py +78 -0
  160. glaip_sdk/utils/display.py +135 -0
  161. glaip_sdk/utils/export.py +143 -0
  162. glaip_sdk/utils/general.py +61 -0
  163. glaip_sdk/utils/import_export.py +168 -0
  164. glaip_sdk/utils/import_resolver.py +530 -0
  165. glaip_sdk/utils/instructions.py +101 -0
  166. glaip_sdk/utils/rendering/__init__.py +115 -0
  167. glaip_sdk/utils/rendering/formatting.py +264 -0
  168. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  169. glaip_sdk/utils/rendering/layout/panels.py +156 -0
  170. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  171. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  172. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  173. glaip_sdk/utils/rendering/models.py +85 -0
  174. glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
  175. glaip_sdk/utils/rendering/renderer/base.py +1082 -0
  176. glaip_sdk/utils/rendering/renderer/config.py +27 -0
  177. glaip_sdk/utils/rendering/renderer/console.py +55 -0
  178. glaip_sdk/utils/rendering/renderer/debug.py +178 -0
  179. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  180. glaip_sdk/utils/rendering/renderer/stream.py +202 -0
  181. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  182. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  183. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  184. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  185. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  186. glaip_sdk/utils/rendering/state.py +204 -0
  187. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  188. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  189. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  190. glaip_sdk/utils/rendering/steps/format.py +176 -0
  191. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  192. glaip_sdk/utils/rendering/timing.py +36 -0
  193. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  194. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  195. glaip_sdk/utils/resource_refs.py +195 -0
  196. glaip_sdk/utils/run_renderer.py +41 -0
  197. glaip_sdk/utils/runtime_config.py +425 -0
  198. glaip_sdk/utils/serialization.py +424 -0
  199. glaip_sdk/utils/sync.py +142 -0
  200. glaip_sdk/utils/tool_detection.py +33 -0
  201. glaip_sdk/utils/tool_storage_provider.py +140 -0
  202. glaip_sdk/utils/validation.py +264 -0
  203. glaip_sdk-0.0.0b99.dist-info/METADATA +239 -0
  204. glaip_sdk-0.0.0b99.dist-info/RECORD +207 -0
  205. glaip_sdk-0.0.0b99.dist-info/WHEEL +5 -0
  206. glaip_sdk-0.0.0b99.dist-info/entry_points.txt +2 -0
  207. glaip_sdk-0.0.0b99.dist-info/top_level.txt +1 -0
@@ -0,0 +1,690 @@
1
+ #!/usr/bin/env python3
2
+ """Tool client for AIP SDK.
3
+
4
+ Authors:
5
+ Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
7
+ """
8
+
9
+ import logging
10
+ import os
11
+ import tempfile
12
+ from typing import Any
13
+
14
+ from glaip_sdk.client.base import BaseClient
15
+ from glaip_sdk.config.constants import (
16
+ DEFAULT_TOOL_FRAMEWORK,
17
+ DEFAULT_TOOL_TYPE,
18
+ DEFAULT_TOOL_VERSION,
19
+ )
20
+ from glaip_sdk.models import ToolResponse
21
+ from glaip_sdk.tools import Tool
22
+ from glaip_sdk.utils.client_utils import (
23
+ add_kwargs_to_payload,
24
+ create_model_instances,
25
+ find_by_name,
26
+ )
27
+ from glaip_sdk.utils.resource_refs import is_uuid
28
+
29
+ # API endpoints
30
+ TOOLS_ENDPOINT = "/tools/"
31
+ TOOLS_UPLOAD_ENDPOINT = "/tools/upload"
32
+ TOOLS_UPLOAD_BY_ID_ENDPOINT_FMT = "/tools/{tool_id}/upload"
33
+
34
+ # Set up module-level logger
35
+ logger = logging.getLogger("glaip_sdk.tools")
36
+
37
+
38
+ class ToolClient(BaseClient):
39
+ """Client for tool operations."""
40
+
41
+ def __init__(self, *, parent_client: BaseClient | None = None, **kwargs):
42
+ """Initialize the tool client.
43
+
44
+ Args:
45
+ parent_client: Parent client to adopt session/config from
46
+ **kwargs: Additional arguments for standalone initialization
47
+ """
48
+ super().__init__(parent_client=parent_client, **kwargs)
49
+
50
+ def list_tools(self, tool_type: str | None = None) -> list[Tool]:
51
+ """List all tools, optionally filtered by type.
52
+
53
+ Args:
54
+ tool_type: Filter tools by type (e.g., "custom", "native")
55
+ """
56
+ endpoint = TOOLS_ENDPOINT
57
+ if tool_type:
58
+ endpoint += f"?type={tool_type}"
59
+ data = self._request("GET", endpoint)
60
+ return create_model_instances(data, Tool, self)
61
+
62
+ def get_tool_by_id(self, tool_id: str) -> Tool:
63
+ """Get tool by ID."""
64
+ data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
65
+ response = ToolResponse(**data)
66
+ return Tool.from_response(response, client=self)
67
+
68
+ def find_tools(self, name: str | None = None) -> list[Tool]:
69
+ """Find tools by name."""
70
+ data = self._request("GET", TOOLS_ENDPOINT)
71
+ tools = create_model_instances(data, Tool, self)
72
+ return find_by_name(tools, name, case_sensitive=False)
73
+
74
+ def _validate_and_read_file(self, file_path: str) -> str:
75
+ """Validate file exists and read its content.
76
+
77
+ Args:
78
+ file_path: Path to the file to read
79
+
80
+ Returns:
81
+ str: File content
82
+
83
+ Raises:
84
+ FileNotFoundError: If file doesn't exist
85
+ """
86
+ if not os.path.exists(file_path):
87
+ raise FileNotFoundError(f"Tool file not found: {file_path}")
88
+
89
+ with open(file_path, encoding="utf-8") as f:
90
+ return f.read()
91
+
92
+ def _extract_name_from_file(self, file_path: str) -> str:
93
+ """Extract tool name from file path.
94
+
95
+ Args:
96
+ file_path: Path to the file
97
+
98
+ Returns:
99
+ str: Extracted name (filename without extension)
100
+ """
101
+ return os.path.splitext(os.path.basename(file_path))[0]
102
+
103
+ def _prepare_upload_data(self, name: str, framework: str, description: str | None = None, **kwargs) -> dict:
104
+ """Prepare upload data dictionary.
105
+
106
+ Uses the same payload building logic as _build_create_payload to ensure
107
+ consistency between upload and metadata-only tool creation.
108
+
109
+ Args:
110
+ name: Tool name
111
+ framework: Tool framework
112
+ description: Optional description
113
+ **kwargs: Additional parameters
114
+
115
+ Returns:
116
+ dict: Upload data dictionary
117
+ """
118
+ # Extract tool_type from kwargs if present, defaulting to DEFAULT_TOOL_TYPE
119
+ tool_type = kwargs.pop("tool_type", DEFAULT_TOOL_TYPE)
120
+
121
+ # Use _build_create_payload to build the payload consistently
122
+ payload = self._build_create_payload(
123
+ name=name,
124
+ description=description,
125
+ framework=framework,
126
+ tool_type=tool_type,
127
+ **kwargs,
128
+ )
129
+
130
+ return payload
131
+
132
+ def _upload_tool_file(self, file_path: str, upload_data: dict) -> Tool:
133
+ """Upload tool file to server.
134
+
135
+ Args:
136
+ file_path: Path to temporary file to upload
137
+ upload_data: Dictionary with upload metadata
138
+
139
+ Returns:
140
+ Tool: Created tool object
141
+ """
142
+ with open(file_path, "rb") as fb:
143
+ files = {
144
+ "file": (os.path.basename(file_path), fb, "application/octet-stream"),
145
+ }
146
+
147
+ response = self._request(
148
+ "POST",
149
+ TOOLS_UPLOAD_ENDPOINT,
150
+ files=files,
151
+ data=upload_data,
152
+ )
153
+
154
+ tool_response = ToolResponse(**response)
155
+ return Tool.from_response(tool_response, client=self)
156
+
157
+ def _build_create_payload(
158
+ self,
159
+ name: str,
160
+ description: str | None = None,
161
+ framework: str = DEFAULT_TOOL_FRAMEWORK,
162
+ tool_type: str = DEFAULT_TOOL_TYPE,
163
+ **kwargs,
164
+ ) -> dict[str, Any]:
165
+ """Build payload for tool creation with proper metadata handling.
166
+
167
+ CENTRALIZED PAYLOAD BUILDING LOGIC:
168
+ - Handles file vs metadata-only tool creation
169
+ - Sets proper defaults and required fields
170
+ - Processes tags and other metadata consistently
171
+
172
+ Args:
173
+ name: Tool name
174
+ description: Tool description
175
+ framework: Tool framework (defaults to langchain)
176
+ tool_type: Tool type (defaults to custom)
177
+ **kwargs: Additional parameters (tags, version, etc.)
178
+
179
+ Returns:
180
+ Complete payload dictionary for tool creation
181
+ """
182
+ # Prepare the creation payload with required fields
183
+ payload: dict[str, any] = {
184
+ "name": name.strip(),
185
+ "type": tool_type,
186
+ "framework": framework,
187
+ "version": kwargs.get("version", DEFAULT_TOOL_VERSION),
188
+ }
189
+
190
+ # Add description if provided
191
+ if description:
192
+ payload["description"] = description.strip()
193
+
194
+ # Handle tags - convert list to comma-separated string for API
195
+ if kwargs.get("tags"):
196
+ if isinstance(kwargs["tags"], list):
197
+ payload["tags"] = ",".join(str(tag).strip() for tag in kwargs["tags"])
198
+ else:
199
+ payload["tags"] = str(kwargs["tags"])
200
+
201
+ # Add any other kwargs (excluding already handled ones)
202
+ excluded_keys = {"tags", "version"}
203
+ add_kwargs_to_payload(payload, kwargs, excluded_keys)
204
+
205
+ return payload
206
+
207
+ def _handle_description_update(
208
+ self, update_data: dict[str, Any], description: str | None, current_tool: Tool
209
+ ) -> None:
210
+ """Handle description field in update payload."""
211
+ if description is not None:
212
+ update_data["description"] = description.strip()
213
+ elif hasattr(current_tool, "description") and current_tool.description:
214
+ update_data["description"] = current_tool.description
215
+
216
+ def _handle_tags_update(self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool) -> None:
217
+ """Handle tags field in update payload."""
218
+ if kwargs.get("tags"):
219
+ if isinstance(kwargs["tags"], list):
220
+ update_data["tags"] = ",".join(str(tag).strip() for tag in kwargs["tags"])
221
+ else:
222
+ update_data["tags"] = str(kwargs["tags"])
223
+ elif hasattr(current_tool, "tags") and current_tool.tags:
224
+ # Preserve existing tags if present
225
+ if isinstance(current_tool.tags, list):
226
+ update_data["tags"] = ",".join(str(tag).strip() for tag in current_tool.tags)
227
+ else:
228
+ update_data["tags"] = str(current_tool.tags)
229
+
230
+ def _handle_additional_kwargs(self, update_data: dict[str, Any], kwargs: dict[str, Any]) -> None:
231
+ """Handle additional kwargs in update payload."""
232
+ excluded_keys = {
233
+ "tags",
234
+ "framework",
235
+ "version",
236
+ "type",
237
+ "tool_type",
238
+ "name",
239
+ "description",
240
+ }
241
+ for key, value in kwargs.items():
242
+ if key not in excluded_keys:
243
+ update_data[key] = value
244
+
245
+ def _build_update_payload(
246
+ self,
247
+ current_tool: Tool,
248
+ name: str | None = None,
249
+ description: str | None = None,
250
+ **kwargs,
251
+ ) -> dict[str, Any]:
252
+ """Build payload for tool update with proper current state preservation.
253
+
254
+ Args:
255
+ current_tool: Current tool object to update
256
+ name: New tool name (None to keep current)
257
+ description: New description (None to keep current)
258
+ **kwargs: Additional parameters (tags, framework, etc.)
259
+
260
+ Returns:
261
+ Complete payload dictionary for tool update
262
+
263
+ Notes:
264
+ - Preserves current values as defaults when new values not provided
265
+ - Handles metadata updates properly
266
+ """
267
+ # Prepare the update payload with current values as defaults
268
+ type_override = kwargs.pop("type", None)
269
+ if type_override is None:
270
+ type_override = kwargs.pop("tool_type", None)
271
+ current_type = (
272
+ type_override
273
+ or getattr(current_tool, "tool_type", None)
274
+ or getattr(current_tool, "type", None)
275
+ or DEFAULT_TOOL_TYPE
276
+ )
277
+ # Convert enum to string value for API payload
278
+ if hasattr(current_type, "value"):
279
+ current_type = current_type.value
280
+
281
+ update_data = {
282
+ "name": name if name is not None else current_tool.name,
283
+ "type": current_type,
284
+ "framework": kwargs.get("framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)),
285
+ "version": kwargs.get("version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)),
286
+ }
287
+
288
+ # Handle description update
289
+ self._handle_description_update(update_data, description, current_tool)
290
+
291
+ # Handle tags update
292
+ self._handle_tags_update(update_data, kwargs, current_tool)
293
+
294
+ # Handle additional kwargs
295
+ self._handle_additional_kwargs(update_data, kwargs)
296
+
297
+ return update_data
298
+
299
+ def _create_tool_from_file(
300
+ self,
301
+ file_path: str,
302
+ name: str | None = None,
303
+ description: str | None = None,
304
+ framework: str = "langchain",
305
+ **kwargs,
306
+ ) -> Tool:
307
+ """Create tool from file content using upload endpoint.
308
+
309
+ Args:
310
+ file_path: Path to tool file
311
+ name: Optional tool name (auto-detected if not provided)
312
+ description: Optional tool description
313
+ framework: Tool framework
314
+ **kwargs: Additional parameters
315
+
316
+ Returns:
317
+ Tool: Created tool object
318
+ """
319
+ # Read and validate file
320
+ file_content = self._validate_and_read_file(file_path)
321
+
322
+ # Auto-detect name if not provided
323
+ if not name:
324
+ name = self._extract_name_from_file(file_path)
325
+
326
+ # Handle description - generate default if not provided or empty
327
+ if description is None or description == "":
328
+ # Generate default description based on tool_type if available
329
+ tool_type = kwargs.get("tool_type", "custom")
330
+ description = f"A {tool_type} tool"
331
+
332
+ # Create temporary file for upload
333
+ with tempfile.NamedTemporaryFile(
334
+ mode="w",
335
+ suffix=".py",
336
+ prefix=f"{name}_",
337
+ delete=False,
338
+ encoding="utf-8",
339
+ ) as temp_file:
340
+ temp_file.write(file_content)
341
+ temp_file_path = temp_file.name
342
+
343
+ try:
344
+ # Prepare upload data
345
+ upload_data = self._prepare_upload_data(name=name, framework=framework, description=description, **kwargs)
346
+
347
+ # Upload file
348
+ return self._upload_tool_file(temp_file_path, upload_data)
349
+
350
+ finally:
351
+ # Clean up temporary file
352
+ try:
353
+ os.unlink(temp_file_path)
354
+ except OSError:
355
+ pass # Ignore cleanup errors
356
+
357
+ def create_tool(
358
+ self,
359
+ file_path: str,
360
+ name: str | None = None,
361
+ description: str | None = None,
362
+ framework: str = "langchain",
363
+ **kwargs,
364
+ ) -> Tool:
365
+ """Create a new tool from a file.
366
+
367
+ Args:
368
+ file_path: File path to tool script (required) - file content will be read and processed as plugin
369
+ name: Tool name (auto-detected from file if not provided)
370
+ description: Tool description (auto-generated if not provided)
371
+ framework: Tool framework (defaults to "langchain")
372
+ **kwargs: Additional tool parameters
373
+ """
374
+ return self._create_tool_from_file(
375
+ file_path=file_path,
376
+ name=name,
377
+ description=description,
378
+ framework=framework,
379
+ **kwargs,
380
+ )
381
+
382
+ def create_tool_from_code(
383
+ self,
384
+ name: str,
385
+ code: str,
386
+ framework: str = "langchain",
387
+ description: str | None = None,
388
+ tags: list[str] | None = None,
389
+ ) -> Tool:
390
+ """Create a new tool plugin from code string.
391
+
392
+ This method uses the /tools/upload endpoint which properly processes
393
+ and registers tool plugins, unlike the regular create_tool method
394
+ which only creates metadata.
395
+
396
+ Args:
397
+ name: Name for the tool (used for temporary file naming)
398
+ code: Python code containing the tool plugin
399
+ framework: Tool framework (defaults to "langchain")
400
+ description: Optional tool description
401
+ tags: Optional list of tags
402
+
403
+ Returns:
404
+ Tool: The created tool object
405
+ """
406
+ # Create a temporary file with the tool code
407
+ with tempfile.NamedTemporaryFile(
408
+ mode="w",
409
+ suffix=".py",
410
+ prefix=f"{name}_",
411
+ delete=False,
412
+ encoding="utf-8",
413
+ ) as temp_file:
414
+ temp_file.write(code)
415
+ temp_file_path = temp_file.name
416
+
417
+ try:
418
+ # Prepare upload data using shared helper
419
+ upload_data = self._prepare_upload_data(
420
+ name=name,
421
+ framework=framework,
422
+ description=description,
423
+ tags=tags if tags else None,
424
+ )
425
+
426
+ # Upload file using shared helper
427
+ return self._upload_tool_file(temp_file_path, upload_data)
428
+
429
+ finally:
430
+ # Clean up the temporary file
431
+ try:
432
+ os.unlink(temp_file_path)
433
+ except OSError:
434
+ pass # Ignore cleanup errors
435
+
436
+ def update_tool(self, tool_id: str | Tool, **kwargs) -> Tool:
437
+ """Update an existing tool.
438
+
439
+ Notes:
440
+ - Payload construction is centralized via ``_build_update_payload`` to keep metadata
441
+ update and upload update flows consistent.
442
+ - Accepts either a tool ID or a ``Tool`` instance (avoids an extra fetch when callers
443
+ already have the current tool).
444
+ """
445
+ # Backward-compatible: allow passing a Tool instance to avoid an extra fetch.
446
+ if isinstance(tool_id, Tool):
447
+ current_tool = tool_id
448
+ if not current_tool.id:
449
+ raise ValueError("Tool instance has no id; cannot update.")
450
+ tool_id_value = str(current_tool.id)
451
+ else:
452
+ current_tool = None
453
+ tool_id_value = tool_id
454
+
455
+ if not kwargs:
456
+ data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id_value}", json={})
457
+ response = ToolResponse(**data)
458
+ return Tool.from_response(response, client=self)
459
+
460
+ if current_tool is None:
461
+ current_tool = self.get_tool_by_id(tool_id_value)
462
+
463
+ payload_kwargs = kwargs.copy()
464
+ name = payload_kwargs.pop("name", None)
465
+ description = payload_kwargs.pop("description", None)
466
+ update_payload = self._build_update_payload(
467
+ current_tool=current_tool,
468
+ name=name,
469
+ description=description,
470
+ **payload_kwargs,
471
+ )
472
+
473
+ data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id_value}", json=update_payload)
474
+ response = ToolResponse(**data)
475
+ return Tool.from_response(response, client=self)
476
+
477
+ def delete_tool(self, tool_id: str) -> None:
478
+ """Delete a tool."""
479
+ self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
480
+
481
+ def upsert_tool(
482
+ self,
483
+ identifier: str | Tool,
484
+ code: str | None = None,
485
+ description: str | None = None,
486
+ framework: str = "langchain",
487
+ **kwargs,
488
+ ) -> Tool:
489
+ """Create or update a tool by instance, ID, or name.
490
+
491
+ Args:
492
+ identifier: Tool instance, ID (UUID string), or name
493
+ code: Python code containing the tool plugin (required for create)
494
+ description: Tool description
495
+ framework: Tool framework (defaults to "langchain")
496
+ **kwargs: Additional parameters (tags, version, etc.)
497
+
498
+ Returns:
499
+ The created or updated tool.
500
+
501
+ Example:
502
+ >>> # By name with code (creates if not exists)
503
+ >>> tool = client.tools.upsert_tool(
504
+ ... "greeting",
505
+ ... code=bundled_source,
506
+ ... description="A greeting tool",
507
+ ... )
508
+ >>> # By instance
509
+ >>> tool = client.tools.upsert_tool(existing_tool, code=new_code)
510
+ >>> # By ID
511
+ >>> tool = client.tools.upsert_tool("uuid-here", code=new_code)
512
+ """
513
+ # Handle Tool instance
514
+ if isinstance(identifier, Tool):
515
+ if identifier.id:
516
+ logger.info("Updating tool by instance: %s", identifier.name)
517
+ return self._do_tool_upsert_update(
518
+ identifier.id,
519
+ identifier.name,
520
+ code,
521
+ description,
522
+ framework,
523
+ **kwargs,
524
+ )
525
+ identifier = identifier.name
526
+
527
+ # Handle string (ID or name)
528
+ if isinstance(identifier, str):
529
+ if is_uuid(identifier):
530
+ logger.info("Updating tool by ID: %s", identifier)
531
+ existing = self.get_tool_by_id(identifier)
532
+ return self._do_tool_upsert_update(identifier, existing.name, code, description, framework, **kwargs)
533
+
534
+ # It's a name - find or create
535
+ return self._upsert_tool_by_name(identifier, code, description, framework, **kwargs)
536
+
537
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
538
+
539
+ def _do_tool_upsert_update(
540
+ self,
541
+ tool_id: str,
542
+ name: str | None,
543
+ code: str | None,
544
+ description: str | None,
545
+ framework: str,
546
+ **kwargs,
547
+ ) -> Tool:
548
+ """Perform the update part of tool upsert."""
549
+ if code:
550
+ # Update via file upload
551
+ with tempfile.NamedTemporaryFile(
552
+ mode="w",
553
+ suffix=".py",
554
+ prefix=f"{name or 'tool'}_",
555
+ delete=False,
556
+ encoding="utf-8",
557
+ ) as temp_file:
558
+ temp_file.write(code)
559
+ temp_file_path = temp_file.name
560
+
561
+ try:
562
+ return self.update_tool_via_file(
563
+ tool_id,
564
+ temp_file_path,
565
+ name=name,
566
+ description=description,
567
+ framework=framework,
568
+ **kwargs,
569
+ )
570
+ finally:
571
+ try:
572
+ os.unlink(temp_file_path)
573
+ except OSError:
574
+ pass
575
+ else:
576
+ # Metadata-only update
577
+ update_kwargs = {"framework": framework, **kwargs}
578
+ if name:
579
+ update_kwargs["name"] = name
580
+ if description:
581
+ update_kwargs["description"] = description
582
+ return self.update_tool(tool_id, **update_kwargs)
583
+
584
+ def _upsert_tool_by_name(
585
+ self,
586
+ name: str,
587
+ code: str | None,
588
+ description: str | None,
589
+ framework: str,
590
+ **kwargs,
591
+ ) -> Tool:
592
+ """Find tool by name and update, or create if not found."""
593
+ existing = self.find_tools(name)
594
+ name_lower = name.lower()
595
+ exact_matches = [tool for tool in existing if tool.name and tool.name.lower() == name_lower]
596
+
597
+ if len(exact_matches) == 1:
598
+ logger.info("Updating existing tool: %s", name)
599
+ return self._do_tool_upsert_update(exact_matches[0].id, name, code, description, framework, **kwargs)
600
+
601
+ if len(exact_matches) > 1:
602
+ raise ValueError(f"Multiple tools found with name '{name}'")
603
+
604
+ # Create new tool - code is required
605
+ if not code:
606
+ raise ValueError(f"Tool '{name}' not found and no code provided for creation")
607
+
608
+ logger.info("Creating new tool: %s", name)
609
+ return self.create_tool_from_code(
610
+ name=name,
611
+ code=code,
612
+ framework=framework,
613
+ description=description,
614
+ **kwargs,
615
+ )
616
+
617
+ def get_tool_script(self, tool_id: str) -> str:
618
+ """Get the tool script content.
619
+
620
+ Args:
621
+ tool_id: The ID of the tool
622
+
623
+ Returns:
624
+ str: The tool script content
625
+
626
+ Raises:
627
+ Exception: If the tool script cannot be retrieved
628
+ """
629
+ try:
630
+ response = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}/script")
631
+ return response.get("script", "") or response.get("content", "")
632
+ except Exception as e:
633
+ logger.error(f"Failed to get tool script for {tool_id}: {e}")
634
+ raise
635
+
636
+ def update_tool_via_file(self, tool_id: str, file_path: str, **kwargs) -> Tool:
637
+ """Update a tool plugin via file upload.
638
+
639
+ Args:
640
+ tool_id: The ID of the tool to update
641
+ file_path: Path to the new tool file
642
+ **kwargs: Additional metadata to update (name, description, tags, etc.)
643
+
644
+ Returns:
645
+ Tool: The updated tool object
646
+
647
+ Raises:
648
+ FileNotFoundError: If the file doesn't exist
649
+ Exception: If the update fails
650
+ """
651
+ # Validate file exists
652
+ self._validate_and_read_file(file_path)
653
+
654
+ # Fetch current metadata to ensure required fields are preserved
655
+ current_tool = self.get_tool_by_id(tool_id)
656
+
657
+ payload_kwargs = kwargs.copy()
658
+ name = payload_kwargs.pop("name", None)
659
+ description = payload_kwargs.pop("description", None)
660
+ update_payload = self._build_update_payload(
661
+ current_tool=current_tool,
662
+ name=name,
663
+ description=description,
664
+ **payload_kwargs,
665
+ )
666
+
667
+ try:
668
+ # Prepare multipart upload
669
+ with open(file_path, "rb") as fb:
670
+ files = {
671
+ "file": (
672
+ os.path.basename(file_path),
673
+ fb,
674
+ "application/octet-stream",
675
+ ),
676
+ }
677
+
678
+ response = self._request(
679
+ "PUT",
680
+ TOOLS_UPLOAD_BY_ID_ENDPOINT_FMT.format(tool_id=tool_id),
681
+ files=files,
682
+ data=update_payload,
683
+ )
684
+
685
+ tool_response = ToolResponse(**response)
686
+ return Tool.from_response(tool_response, client=self)
687
+
688
+ except Exception as e:
689
+ logger.error("Failed to update tool %s via file: %s", tool_id, e)
690
+ raise