glaip-sdk 0.0.20__py3-none-any.whl → 0.7.7__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 (216) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1250 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +271 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  12. glaip_sdk/cli/commands/agents/_common.py +561 -0
  13. glaip_sdk/cli/commands/agents/create.py +151 -0
  14. glaip_sdk/cli/commands/agents/delete.py +64 -0
  15. glaip_sdk/cli/commands/agents/get.py +89 -0
  16. glaip_sdk/cli/commands/agents/list.py +129 -0
  17. glaip_sdk/cli/commands/agents/run.py +264 -0
  18. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  19. glaip_sdk/cli/commands/agents/update.py +112 -0
  20. glaip_sdk/cli/commands/common_config.py +104 -0
  21. glaip_sdk/cli/commands/configure.py +734 -143
  22. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  23. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  24. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  25. glaip_sdk/cli/commands/mcps/create.py +152 -0
  26. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  27. glaip_sdk/cli/commands/mcps/get.py +212 -0
  28. glaip_sdk/cli/commands/mcps/list.py +69 -0
  29. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  30. glaip_sdk/cli/commands/mcps/update.py +190 -0
  31. glaip_sdk/cli/commands/models.py +14 -12
  32. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  33. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  34. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  35. glaip_sdk/cli/commands/tools/_common.py +80 -0
  36. glaip_sdk/cli/commands/tools/create.py +228 -0
  37. glaip_sdk/cli/commands/tools/delete.py +61 -0
  38. glaip_sdk/cli/commands/tools/get.py +103 -0
  39. glaip_sdk/cli/commands/tools/list.py +69 -0
  40. glaip_sdk/cli/commands/tools/script.py +49 -0
  41. glaip_sdk/cli/commands/tools/update.py +102 -0
  42. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  43. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  44. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  45. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  46. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  47. glaip_sdk/cli/commands/update.py +164 -23
  48. glaip_sdk/cli/config.py +49 -7
  49. glaip_sdk/cli/constants.py +38 -0
  50. glaip_sdk/cli/context.py +8 -0
  51. glaip_sdk/cli/core/__init__.py +79 -0
  52. glaip_sdk/cli/core/context.py +124 -0
  53. glaip_sdk/cli/core/output.py +851 -0
  54. glaip_sdk/cli/core/prompting.py +649 -0
  55. glaip_sdk/cli/core/rendering.py +187 -0
  56. glaip_sdk/cli/display.py +45 -32
  57. glaip_sdk/cli/entrypoint.py +20 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +14 -17
  60. glaip_sdk/cli/main.py +344 -167
  61. glaip_sdk/cli/masking.py +21 -33
  62. glaip_sdk/cli/mcp_validators.py +5 -15
  63. glaip_sdk/cli/pager.py +15 -22
  64. glaip_sdk/cli/parsers/__init__.py +1 -3
  65. glaip_sdk/cli/parsers/json_input.py +11 -22
  66. glaip_sdk/cli/resolution.py +5 -10
  67. glaip_sdk/cli/rich_helpers.py +1 -3
  68. glaip_sdk/cli/slash/__init__.py +0 -9
  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 +65 -29
  72. glaip_sdk/cli/slash/prompt.py +24 -10
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +827 -232
  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 +12 -52
  91. glaip_sdk/cli/transcript/cache.py +258 -60
  92. glaip_sdk/cli/transcript/capture.py +72 -21
  93. glaip_sdk/cli/transcript/history.py +815 -0
  94. glaip_sdk/cli/transcript/launcher.py +1 -3
  95. glaip_sdk/cli/transcript/viewer.py +79 -329
  96. glaip_sdk/cli/update_notifier.py +385 -24
  97. glaip_sdk/cli/validators.py +16 -18
  98. glaip_sdk/client/__init__.py +3 -1
  99. glaip_sdk/client/_schedule_payloads.py +89 -0
  100. glaip_sdk/client/agent_runs.py +147 -0
  101. glaip_sdk/client/agents.py +370 -100
  102. glaip_sdk/client/base.py +78 -35
  103. glaip_sdk/client/hitl.py +136 -0
  104. glaip_sdk/client/main.py +25 -10
  105. glaip_sdk/client/mcps.py +166 -27
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +65 -74
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +583 -79
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +214 -56
  113. glaip_sdk/client/validators.py +20 -48
  114. glaip_sdk/config/constants.py +11 -0
  115. glaip_sdk/exceptions.py +1 -3
  116. glaip_sdk/hitl/__init__.py +48 -0
  117. glaip_sdk/hitl/base.py +64 -0
  118. glaip_sdk/hitl/callback.py +43 -0
  119. glaip_sdk/hitl/local.py +121 -0
  120. glaip_sdk/hitl/remote.py +523 -0
  121. glaip_sdk/icons.py +9 -3
  122. glaip_sdk/mcps/__init__.py +21 -0
  123. glaip_sdk/mcps/base.py +345 -0
  124. glaip_sdk/models/__init__.py +107 -0
  125. glaip_sdk/models/agent.py +47 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/mcp.py +33 -0
  129. glaip_sdk/models/schedule.py +224 -0
  130. glaip_sdk/models/tool.py +33 -0
  131. glaip_sdk/payload_schemas/__init__.py +1 -13
  132. glaip_sdk/payload_schemas/agent.py +1 -3
  133. glaip_sdk/registry/__init__.py +55 -0
  134. glaip_sdk/registry/agent.py +164 -0
  135. glaip_sdk/registry/base.py +139 -0
  136. glaip_sdk/registry/mcp.py +253 -0
  137. glaip_sdk/registry/tool.py +445 -0
  138. glaip_sdk/rich_components.py +58 -2
  139. glaip_sdk/runner/__init__.py +76 -0
  140. glaip_sdk/runner/base.py +84 -0
  141. glaip_sdk/runner/deps.py +112 -0
  142. glaip_sdk/runner/langgraph.py +872 -0
  143. glaip_sdk/runner/logging_config.py +77 -0
  144. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  145. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  146. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  147. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  148. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  149. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  150. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  151. glaip_sdk/schedules/__init__.py +22 -0
  152. glaip_sdk/schedules/base.py +291 -0
  153. glaip_sdk/tools/__init__.py +22 -0
  154. glaip_sdk/tools/base.py +468 -0
  155. glaip_sdk/utils/__init__.py +59 -12
  156. glaip_sdk/utils/a2a/__init__.py +34 -0
  157. glaip_sdk/utils/a2a/event_processor.py +188 -0
  158. glaip_sdk/utils/agent_config.py +4 -14
  159. glaip_sdk/utils/bundler.py +403 -0
  160. glaip_sdk/utils/client.py +111 -0
  161. glaip_sdk/utils/client_utils.py +46 -28
  162. glaip_sdk/utils/datetime_helpers.py +58 -0
  163. glaip_sdk/utils/discovery.py +78 -0
  164. glaip_sdk/utils/display.py +25 -21
  165. glaip_sdk/utils/export.py +143 -0
  166. glaip_sdk/utils/general.py +1 -36
  167. glaip_sdk/utils/import_export.py +15 -16
  168. glaip_sdk/utils/import_resolver.py +524 -0
  169. glaip_sdk/utils/instructions.py +101 -0
  170. glaip_sdk/utils/rendering/__init__.py +115 -1
  171. glaip_sdk/utils/rendering/formatting.py +38 -23
  172. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  173. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  174. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  175. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  176. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  177. glaip_sdk/utils/rendering/models.py +18 -8
  178. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  179. glaip_sdk/utils/rendering/renderer/base.py +534 -882
  180. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  181. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  182. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  183. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  184. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  185. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  186. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  187. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  188. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  189. glaip_sdk/utils/rendering/state.py +204 -0
  190. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  191. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  192. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  193. glaip_sdk/utils/rendering/steps/format.py +176 -0
  194. glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
  195. glaip_sdk/utils/rendering/timing.py +36 -0
  196. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  197. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  198. glaip_sdk/utils/resource_refs.py +29 -26
  199. glaip_sdk/utils/runtime_config.py +425 -0
  200. glaip_sdk/utils/serialization.py +32 -46
  201. glaip_sdk/utils/sync.py +162 -0
  202. glaip_sdk/utils/tool_detection.py +301 -0
  203. glaip_sdk/utils/tool_storage_provider.py +140 -0
  204. glaip_sdk/utils/validation.py +20 -28
  205. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +78 -23
  206. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  207. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  208. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  209. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  210. glaip_sdk/cli/commands/agents.py +0 -1412
  211. glaip_sdk/cli/commands/mcps.py +0 -1225
  212. glaip_sdk/cli/commands/tools.py +0 -597
  213. glaip_sdk/cli/utils.py +0 -1330
  214. glaip_sdk/models.py +0 -259
  215. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  216. glaip_sdk-0.0.20.dist-info/entry_points.txt +0 -3
glaip_sdk/client/tools.py CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
6
7
  """
7
8
 
8
9
  import logging
@@ -16,11 +17,14 @@ from glaip_sdk.config.constants import (
16
17
  DEFAULT_TOOL_TYPE,
17
18
  DEFAULT_TOOL_VERSION,
18
19
  )
19
- from glaip_sdk.models import Tool
20
+ from glaip_sdk.models import ToolResponse
21
+ from glaip_sdk.tools import Tool
20
22
  from glaip_sdk.utils.client_utils import (
23
+ add_kwargs_to_payload,
21
24
  create_model_instances,
22
25
  find_by_name,
23
26
  )
27
+ from glaip_sdk.utils.resource_refs import is_uuid
24
28
 
25
29
  # API endpoints
26
30
  TOOLS_ENDPOINT = "/tools/"
@@ -58,11 +62,11 @@ class ToolClient(BaseClient):
58
62
  def get_tool_by_id(self, tool_id: str) -> Tool:
59
63
  """Get tool by ID."""
60
64
  data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
61
- return Tool(**data)._set_client(self)
65
+ response = ToolResponse(**data)
66
+ return Tool.from_response(response, client=self)
62
67
 
63
68
  def find_tools(self, name: str | None = None) -> list[Tool]:
64
69
  """Find tools by name."""
65
- # Backend doesn't support name query parameter, so we fetch all and filter client-side
66
70
  data = self._request("GET", TOOLS_ENDPOINT)
67
71
  tools = create_model_instances(data, Tool, self)
68
72
  return find_by_name(tools, name, case_sensitive=False)
@@ -96,11 +100,12 @@ class ToolClient(BaseClient):
96
100
  """
97
101
  return os.path.splitext(os.path.basename(file_path))[0]
98
102
 
99
- def _prepare_upload_data(
100
- self, name: str, framework: str, description: str | None = None, **kwargs
101
- ) -> dict:
103
+ def _prepare_upload_data(self, name: str, framework: str, description: str | None = None, **kwargs) -> dict:
102
104
  """Prepare upload data dictionary.
103
105
 
106
+ Uses the same payload building logic as _build_create_payload to ensure
107
+ consistency between upload and metadata-only tool creation.
108
+
104
109
  Args:
105
110
  name: Tool name
106
111
  framework: Tool framework
@@ -110,27 +115,19 @@ class ToolClient(BaseClient):
110
115
  Returns:
111
116
  dict: Upload data dictionary
112
117
  """
113
- data = {
114
- "name": name,
115
- "framework": framework,
116
- }
117
-
118
- if description:
119
- data["description"] = description
118
+ # Extract tool_type from kwargs if present, defaulting to DEFAULT_TOOL_TYPE
119
+ tool_type = kwargs.pop("tool_type", DEFAULT_TOOL_TYPE)
120
120
 
121
- # Handle tags if provided in kwargs
122
- if kwargs.get("tags"):
123
- if isinstance(kwargs["tags"], list):
124
- data["tags"] = ",".join(kwargs["tags"])
125
- else:
126
- data["tags"] = kwargs["tags"]
127
-
128
- # Include any other kwargs in the upload data
129
- for key, value in kwargs.items():
130
- if key not in ["tags"]: # tags already handled above
131
- data[key] = value
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
+ )
132
129
 
133
- return data
130
+ return payload
134
131
 
135
132
  def _upload_tool_file(self, file_path: str, upload_data: dict) -> Tool:
136
133
  """Upload tool file to server.
@@ -154,7 +151,8 @@ class ToolClient(BaseClient):
154
151
  data=upload_data,
155
152
  )
156
153
 
157
- return Tool(**response)._set_client(self)
154
+ tool_response = ToolResponse(**response)
155
+ return Tool.from_response(tool_response, client=self)
158
156
 
159
157
  def _build_create_payload(
160
158
  self,
@@ -202,9 +200,7 @@ class ToolClient(BaseClient):
202
200
 
203
201
  # Add any other kwargs (excluding already handled ones)
204
202
  excluded_keys = {"tags", "version"}
205
- for key, value in kwargs.items():
206
- if key not in excluded_keys:
207
- payload[key] = value
203
+ add_kwargs_to_payload(payload, kwargs, excluded_keys)
208
204
 
209
205
  return payload
210
206
 
@@ -217,29 +213,21 @@ class ToolClient(BaseClient):
217
213
  elif hasattr(current_tool, "description") and current_tool.description:
218
214
  update_data["description"] = current_tool.description
219
215
 
220
- def _handle_tags_update(
221
- self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool
222
- ) -> None:
216
+ def _handle_tags_update(self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool) -> None:
223
217
  """Handle tags field in update payload."""
224
218
  if kwargs.get("tags"):
225
219
  if isinstance(kwargs["tags"], list):
226
- update_data["tags"] = ",".join(
227
- str(tag).strip() for tag in kwargs["tags"]
228
- )
220
+ update_data["tags"] = ",".join(str(tag).strip() for tag in kwargs["tags"])
229
221
  else:
230
222
  update_data["tags"] = str(kwargs["tags"])
231
223
  elif hasattr(current_tool, "tags") and current_tool.tags:
232
224
  # Preserve existing tags if present
233
225
  if isinstance(current_tool.tags, list):
234
- update_data["tags"] = ",".join(
235
- str(tag).strip() for tag in current_tool.tags
236
- )
226
+ update_data["tags"] = ",".join(str(tag).strip() for tag in current_tool.tags)
237
227
  else:
238
228
  update_data["tags"] = str(current_tool.tags)
239
229
 
240
- def _handle_additional_kwargs(
241
- self, update_data: dict[str, Any], kwargs: dict[str, Any]
242
- ) -> None:
230
+ def _handle_additional_kwargs(self, update_data: dict[str, Any], kwargs: dict[str, Any]) -> None:
243
231
  """Handle additional kwargs in update payload."""
244
232
  excluded_keys = {
245
233
  "tags",
@@ -286,16 +274,15 @@ class ToolClient(BaseClient):
286
274
  or getattr(current_tool, "type", None)
287
275
  or DEFAULT_TOOL_TYPE
288
276
  )
277
+ # Convert enum to string value for API payload
278
+ if hasattr(current_type, "value"):
279
+ current_type = current_type.value
289
280
 
290
281
  update_data = {
291
282
  "name": name if name is not None else current_tool.name,
292
283
  "type": current_type,
293
- "framework": kwargs.get(
294
- "framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)
295
- ),
296
- "version": kwargs.get(
297
- "version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)
298
- ),
284
+ "framework": kwargs.get("framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)),
285
+ "version": kwargs.get("version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)),
299
286
  }
300
287
 
301
288
  # Handle description update
@@ -355,9 +342,7 @@ class ToolClient(BaseClient):
355
342
 
356
343
  try:
357
344
  # Prepare upload data
358
- upload_data = self._prepare_upload_data(
359
- name=name, framework=framework, description=description, **kwargs
360
- )
345
+ upload_data = self._prepare_upload_data(name=name, framework=framework, description=description, **kwargs)
361
346
 
362
347
  # Upload file
363
348
  return self._upload_tool_file(temp_file_path, upload_data)
@@ -448,15 +433,187 @@ class ToolClient(BaseClient):
448
433
  except OSError:
449
434
  pass # Ignore cleanup errors
450
435
 
451
- def update_tool(self, tool_id: str, **kwargs) -> Tool:
452
- """Update an existing tool."""
453
- data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
454
- return Tool(**data)._set_client(self)
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)
455
476
 
456
477
  def delete_tool(self, tool_id: str) -> None:
457
478
  """Delete a tool."""
458
479
  self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
459
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
+
460
617
  def get_tool_script(self, tool_id: str) -> str:
461
618
  """Get the tool script content.
462
619
 
@@ -525,8 +682,9 @@ class ToolClient(BaseClient):
525
682
  data=update_payload,
526
683
  )
527
684
 
528
- return Tool(**response)._set_client(self)
685
+ tool_response = ToolResponse(**response)
686
+ return Tool.from_response(tool_response, client=self)
529
687
 
530
688
  except Exception as e:
531
- logger.error(f"Failed to update tool {tool_id} via file: {e}")
689
+ logger.error("Failed to update tool %s via file: %s", tool_id, e)
532
690
  raise
@@ -39,9 +39,7 @@ class ResourceValidator:
39
39
  if len(found_tools) == 1:
40
40
  return str(found_tools[0].id)
41
41
  elif len(found_tools) > 1:
42
- raise AmbiguousResourceError(
43
- f"Multiple tools found with name '{tool_name}': {[t.id for t in found_tools]}"
44
- )
42
+ raise AmbiguousResourceError(f"Multiple tools found with name '{tool_name}': {[t.id for t in found_tools]}")
45
43
  else:
46
44
  raise NotFoundError(f"Tool not found: {tool_name}")
47
45
 
@@ -51,9 +49,7 @@ class ResourceValidator:
51
49
  if len(found_tools) == 1:
52
50
  return str(found_tools[0].id)
53
51
  elif len(found_tools) > 1:
54
- raise AmbiguousResourceError(
55
- f"Multiple tools found with name '{tool.name}': {[t.id for t in found_tools]}"
56
- )
52
+ raise AmbiguousResourceError(f"Multiple tools found with name '{tool.name}': {[t.id for t in found_tools]}")
57
53
  else:
58
54
  raise NotFoundError(f"Tool not found: {tool.name}")
59
55
 
@@ -73,9 +69,7 @@ class ResourceValidator:
73
69
  elif hasattr(tool, "name") and tool.name is not None:
74
70
  return self._resolve_tool_by_name_attribute(tool, client)
75
71
  else:
76
- raise ValidationError(
77
- f"Invalid tool reference: {tool} - must have 'id' or 'name' attribute"
78
- )
72
+ raise ValidationError(f"Invalid tool reference: {tool} - must have 'id' or 'name' attribute")
79
73
 
80
74
  def _process_single_tool(self, tool: str | Tool, client: Any) -> str:
81
75
  """Process a single tool reference and return its ID."""
@@ -99,22 +93,14 @@ class ResourceValidator:
99
93
  try:
100
94
  tool_id = cls()._process_single_tool(tool, client)
101
95
  tool_ids.append(tool_id)
102
- except (AmbiguousResourceError, NotFoundError) as e:
96
+ except (AmbiguousResourceError, NotFoundError) as err:
103
97
  # Determine the tool name for the error message
104
- tool_name = (
105
- tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
106
- )
107
- raise ValidationError(
108
- f"Failed to resolve tool name '{tool_name}' to ID: {e}"
109
- )
110
- except Exception as e:
98
+ tool_name = tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
99
+ raise ValidationError(f"Failed to resolve tool name '{tool_name}' to ID: {err}") from err
100
+ except Exception as err:
111
101
  # For other exceptions, wrap them appropriately
112
- tool_name = (
113
- tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
114
- )
115
- raise ValidationError(
116
- f"Failed to resolve tool name '{tool_name}' to ID: {e}"
117
- )
102
+ tool_name = tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
103
+ raise ValidationError(f"Failed to resolve tool name '{tool_name}' to ID: {err}") from err
118
104
 
119
105
  return tool_ids
120
106
 
@@ -158,9 +144,7 @@ class ResourceValidator:
158
144
  elif hasattr(agent, "name") and agent.name is not None:
159
145
  return self._resolve_agent_by_name_attribute(agent, client)
160
146
  else:
161
- raise ValidationError(
162
- f"Invalid agent reference: {agent} - must have 'id' or 'name' attribute"
163
- )
147
+ raise ValidationError(f"Invalid agent reference: {agent} - must have 'id' or 'name' attribute")
164
148
 
165
149
  def _process_single_agent(self, agent: str | Any, client: Any) -> str:
166
150
  """Process a single agent reference and return its ID."""
@@ -184,26 +168,14 @@ class ResourceValidator:
184
168
  try:
185
169
  agent_id = cls()._process_single_agent(agent, client)
186
170
  agent_ids.append(agent_id)
187
- except (AmbiguousResourceError, NotFoundError) as e:
171
+ except (AmbiguousResourceError, NotFoundError) as err:
188
172
  # Determine the agent name for the error message
189
- agent_name = (
190
- agent
191
- if isinstance(agent, str)
192
- else getattr(agent, "name", str(agent))
193
- )
194
- raise ValidationError(
195
- f"Failed to resolve agent name '{agent_name}' to ID: {e}"
196
- )
197
- except Exception as e:
173
+ agent_name = agent if isinstance(agent, str) else getattr(agent, "name", str(agent))
174
+ raise ValidationError(f"Failed to resolve agent name '{agent_name}' to ID: {err}") from err
175
+ except Exception as err:
198
176
  # For other exceptions, wrap them appropriately
199
- agent_name = (
200
- agent
201
- if isinstance(agent, str)
202
- else getattr(agent, "name", str(agent))
203
- )
204
- raise ValidationError(
205
- f"Failed to resolve agent name '{agent_name}' to ID: {e}"
206
- )
177
+ agent_name = agent if isinstance(agent, str) else getattr(agent, "name", str(agent))
178
+ raise ValidationError(f"Failed to resolve agent name '{agent_name}' to ID: {err}") from err
207
179
 
208
180
  return agent_ids
209
181
 
@@ -213,8 +185,8 @@ class ResourceValidator:
213
185
  for tool_id in tool_ids:
214
186
  try:
215
187
  client.get_tool_by_id(tool_id)
216
- except NotFoundError:
217
- raise ValidationError(f"Tool not found: {tool_id}")
188
+ except NotFoundError as err:
189
+ raise ValidationError(f"Tool not found: {tool_id}") from err
218
190
 
219
191
  @classmethod
220
192
  def validate_agents_exist(cls, agent_ids: list[str], client: Any) -> None:
@@ -222,5 +194,5 @@ class ResourceValidator:
222
194
  for agent_id in agent_ids:
223
195
  try:
224
196
  client.get_agent_by_id(agent_id)
225
- except NotFoundError:
226
- raise ValidationError(f"Agent not found: {agent_id}")
197
+ except NotFoundError as err:
198
+ raise ValidationError(f"Agent not found: {agent_id}") from err
@@ -39,3 +39,14 @@ DEFAULT_MCP_TRANSPORT = "stdio"
39
39
 
40
40
  # Default error messages
41
41
  DEFAULT_ERROR_MESSAGE = "Unknown error"
42
+
43
+ # Agent configuration fields used for CLI args and payload building
44
+ AGENT_CONFIG_FIELDS = (
45
+ "name",
46
+ "instruction",
47
+ "model",
48
+ "tools",
49
+ "agents",
50
+ "mcps",
51
+ "timeout",
52
+ )
glaip_sdk/exceptions.py CHANGED
@@ -107,9 +107,7 @@ class AgentTimeoutError(TimeoutError):
107
107
  agent_name: Optional name of the agent that timed out
108
108
  """
109
109
  agent_info = f" for agent '{agent_name}'" if agent_name else ""
110
- message = (
111
- f"Agent execution timed out after {timeout_seconds} seconds{agent_info}"
112
- )
110
+ message = f"Agent execution timed out after {timeout_seconds} seconds{agent_info}"
113
111
  super().__init__(message)
114
112
  self.timeout_seconds = timeout_seconds
115
113
  self.agent_name = agent_name
@@ -0,0 +1,48 @@
1
+ """Human-in-the-Loop (HITL) utilities for glaip-sdk.
2
+
3
+ This package provides utilities for HITL approval workflows in both local
4
+ and remote agent execution modes.
5
+
6
+ For local development, LocalPromptHandler is automatically injected when
7
+ agent_config.hitl_enabled is True. No manual setup required.
8
+
9
+ For remote execution, use RemoteHITLHandler to handle HITL events programmatically.
10
+
11
+ Authors:
12
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
13
+ GLAIP SDK Team
14
+ """
15
+
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ # These don't require aip_agents, so import them directly
19
+ from glaip_sdk.hitl.base import HITLCallback, HITLDecision, HITLRequest, HITLResponse
20
+ from glaip_sdk.hitl.callback import PauseResumeCallback
21
+ from glaip_sdk.hitl.remote import RemoteHITLHandler
22
+
23
+ if TYPE_CHECKING:
24
+ from glaip_sdk.hitl.local import LocalPromptHandler
25
+
26
+ __all__ = [
27
+ "LocalPromptHandler",
28
+ "PauseResumeCallback",
29
+ "HITLCallback",
30
+ "HITLDecision",
31
+ "HITLRequest",
32
+ "HITLResponse",
33
+ "RemoteHITLHandler",
34
+ ]
35
+
36
+
37
+ def __getattr__(name: str) -> Any: # noqa: ANN401
38
+ """Lazy import for LocalPromptHandler.
39
+
40
+ This defers the import of aip_agents until LocalPromptHandler is actually accessed,
41
+ preventing ImportError when aip-agents is not installed but HITL is not being used.
42
+ """
43
+ if name == "LocalPromptHandler":
44
+ from glaip_sdk.hitl.local import LocalPromptHandler # noqa: PLC0415
45
+
46
+ globals()["LocalPromptHandler"] = LocalPromptHandler
47
+ return LocalPromptHandler
48
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
glaip_sdk/hitl/base.py ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env python3
2
+ """Base types for HITL approval handling.
3
+
4
+ Authors:
5
+ GLAIP SDK Team
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Any, Protocol, runtime_checkable
11
+
12
+
13
+ class HITLDecision(str, Enum):
14
+ """HITL decision types."""
15
+
16
+ APPROVED = "approved"
17
+ REJECTED = "rejected"
18
+ SKIPPED = "skipped"
19
+
20
+
21
+ @dataclass
22
+ class HITLRequest:
23
+ """HITL approval request from SSE stream."""
24
+
25
+ request_id: str
26
+ tool_name: str
27
+ tool_args: dict[str, Any]
28
+ timeout_at: str # ISO 8601, authoritative deadline
29
+ timeout_seconds: int # Informational, fallback only
30
+
31
+ # Raw metadata for advanced use cases
32
+ hitl_metadata: dict[str, Any]
33
+ tool_metadata: dict[str, Any]
34
+
35
+
36
+ @dataclass
37
+ class HITLResponse:
38
+ """HITL decision response."""
39
+
40
+ decision: HITLDecision
41
+ operator_input: str | None = None
42
+
43
+
44
+ @runtime_checkable
45
+ class HITLCallback(Protocol):
46
+ """Protocol for HITL approval callbacks.
47
+
48
+ Callbacks should complete within the computed callback timeout.
49
+ Callbacks should handle exceptions internally or let them propagate.
50
+ """
51
+
52
+ def __call__(self, request: HITLRequest) -> HITLResponse:
53
+ """Handle HITL approval request.
54
+
55
+ Args:
56
+ request: HITL request with tool info and metadata
57
+
58
+ Returns:
59
+ HITLResponse with decision and optional operator input
60
+
61
+ Raises:
62
+ Any exception will be caught, logged, and treated as REJECTED.
63
+ """
64
+ ...