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/base.py CHANGED
@@ -3,10 +3,12 @@
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
9
10
  import os
11
+ from collections.abc import Iterable, Mapping
10
12
  from typing import Any, NoReturn, Union
11
13
 
12
14
  import httpx
@@ -151,12 +153,7 @@ class BaseClient:
151
153
  def timeout(self, value: float) -> None:
152
154
  """Set timeout and rebuild client."""
153
155
  self._timeout = value
154
- if (
155
- hasattr(self, "http_client")
156
- and self.http_client
157
- and not self._session_scoped
158
- and not self._parent_client
159
- ):
156
+ if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
160
157
  self.http_client.close()
161
158
  self.http_client = self._build_client(value)
162
159
 
@@ -246,18 +243,14 @@ class BaseClient:
246
243
  client_log.debug(f"Response status: {response.status_code}")
247
244
  return response
248
245
  except httpx.ConnectError as e:
249
- client_log.warning(
250
- f"Connection error on {method} {endpoint}, retrying once: {e}"
251
- )
246
+ client_log.warning(f"Connection error on {method} {endpoint}, retrying once: {e}")
252
247
  try:
253
248
  response = self.http_client.request(method, endpoint, **kwargs)
254
- client_log.debug(
255
- f"Retry successful, response status: {response.status_code}"
256
- )
249
+ client_log.debug(f"Retry successful, response status: {response.status_code}")
257
250
  return response
258
251
  except httpx.ConnectError:
259
252
  client_log.error(f"Retry failed for {method} {endpoint}: {e}")
260
- raise e
253
+ raise
261
254
 
262
255
  def _request(self, method: str, endpoint: str, **kwargs) -> Any:
263
256
  """Make HTTP request with error handling and unwrap success envelopes."""
@@ -298,7 +291,7 @@ class BaseClient:
298
291
  return parsed.get("data", parsed) if unwrap else parsed
299
292
  else:
300
293
  error_type = parsed.get("error", "UnknownError")
301
- message = parsed.get("message", DEFAULT_ERROR_MESSAGE)
294
+ message = self._format_error_dict({key: value for key, value in parsed.items() if key != "success"})
302
295
  self._raise_api_error(
303
296
  400,
304
297
  message,
@@ -341,12 +334,58 @@ class BaseClient:
341
334
  return validation_message
342
335
  return f"Validation error: {parsed}"
343
336
 
337
+ formatted_details = None
338
+ if "details" in parsed:
339
+ formatted_details = self._format_error_details(parsed["details"])
340
+
344
341
  message = parsed.get("message")
345
342
  if message:
343
+ if formatted_details:
344
+ return f"{message}\n{formatted_details}"
346
345
  return message
347
346
 
347
+ if formatted_details:
348
+ return formatted_details
349
+
348
350
  return str(parsed) if parsed else DEFAULT_ERROR_MESSAGE
349
351
 
352
+ def _format_error_details(self, details: Any) -> str | None:
353
+ """Render generic error details into a human-readable string."""
354
+ if details is None:
355
+ return None
356
+
357
+ if isinstance(details, dict):
358
+ return self._format_detail_mapping(details)
359
+
360
+ if isinstance(details, (list, tuple, set)):
361
+ return self._format_detail_iterable(details)
362
+
363
+ return f"Details: {details}"
364
+
365
+ @staticmethod
366
+ def _format_detail_mapping(details: Mapping[str, Any]) -> str | None:
367
+ """Format details provided as a mapping."""
368
+ entries = [f" {key}: {value}" for key, value in details.items()]
369
+ if not entries:
370
+ return None
371
+ return "Details:\n" + "\n".join(entries)
372
+
373
+ @staticmethod
374
+ def _format_detail_iterable(details: Iterable[Any]) -> str | None:
375
+ """Format details provided as an iterable collection."""
376
+ entries: list[str] = []
377
+ for item in details:
378
+ if isinstance(item, Mapping):
379
+ inner = ", ".join(f"{k}={v}" for k, v in item.items())
380
+ entries.append(f" - {inner if inner else '{}'}")
381
+ else:
382
+ entries.append(f" - {item}")
383
+
384
+ if not entries:
385
+ return None
386
+
387
+ return "Details:\n" + "\n".join(entries)
388
+
350
389
  def _format_validation_errors(self, errors: list[Any]) -> str | None:
351
390
  """Render validation errors into a human-readable string."""
352
391
  entries: list[str] = []
@@ -365,6 +404,22 @@ class BaseClient:
365
404
 
366
405
  return "Validation errors:\n" + "\n".join(entries)
367
406
 
407
+ @staticmethod
408
+ def _is_no_content_response(response: httpx.Response) -> bool:
409
+ """Return True when the response contains no content."""
410
+ return response.status_code == 204
411
+
412
+ @staticmethod
413
+ def _is_success_status(response: httpx.Response) -> bool:
414
+ """Return True for successful HTTP status codes."""
415
+ return 200 <= response.status_code < 300
416
+
417
+ def _handle_error_response(self, response: httpx.Response) -> None:
418
+ """Raise an API error for non-success responses."""
419
+ error_message = self._get_error_message(response)
420
+ parsed_content = self._parse_response_content(response)
421
+ self._raise_api_error(response.status_code, error_message, payload=parsed_content)
422
+
368
423
  def _handle_response(
369
424
  self,
370
425
  response: httpx.Response,
@@ -373,24 +428,17 @@ class BaseClient:
373
428
  ) -> Any:
374
429
  """Handle HTTP response with proper error handling."""
375
430
  # Handle no-content success before general error handling
376
- if response.status_code == 204:
431
+ if self._is_no_content_response(response):
377
432
  return None
378
433
 
379
- # Handle error status codes
380
- if not (200 <= response.status_code < 300):
381
- error_message = self._get_error_message(response)
382
- # Try to parse response content for payload
383
- parsed_content = self._parse_response_content(response)
384
- self._raise_api_error(
385
- response.status_code, error_message, payload=parsed_content
386
- )
387
- return None # Won't be reached but helps with type checking
388
-
389
- parsed = self._parse_response_content(response)
390
- if parsed is None:
391
- return None
434
+ if self._is_success_status(response):
435
+ parsed = self._parse_response_content(response)
436
+ if parsed is None:
437
+ return None
438
+ return self._handle_success_response(parsed, unwrap=unwrap)
392
439
 
393
- return self._handle_success_response(parsed, unwrap=unwrap)
440
+ self._handle_error_response(response)
441
+ return None
394
442
 
395
443
  def _raise_api_error(
396
444
  self,
@@ -435,12 +483,7 @@ class BaseClient:
435
483
 
436
484
  def close(self) -> None:
437
485
  """Close the HTTP client."""
438
- if (
439
- hasattr(self, "http_client")
440
- and self.http_client
441
- and not self._session_scoped
442
- and not self._parent_client
443
- ):
486
+ if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
444
487
  self.http_client.close()
445
488
 
446
489
  def __enter__(self) -> "BaseClient":
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env python3
2
+ """HITL REST client for manual approval operations.
3
+
4
+ Authors:
5
+ GLAIP SDK Team
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from glaip_sdk.client.base import BaseClient
11
+ from glaip_sdk.hitl.base import HITLDecision
12
+
13
+
14
+ class HITLClient(BaseClient):
15
+ """Client for HITL REST endpoints.
16
+
17
+ Use for manual approval workflows separate from agent runs.
18
+
19
+ Example:
20
+ >>> # List pending approvals
21
+ >>> pending = client.hitl.list_pending()
22
+ >>>
23
+ >>> # Approve a request
24
+ >>> client.hitl.approve(
25
+ ... request_id="bc4d0a77-7800-470e-a91c-7fd663a66b4d",
26
+ ... operator_input="Verified and approved",
27
+ ... )
28
+ """
29
+
30
+ def approve(
31
+ self,
32
+ request_id: str,
33
+ operator_input: str | None = None,
34
+ run_id: str | None = None,
35
+ ) -> dict[str, Any]:
36
+ """Approve a HITL request.
37
+
38
+ Args:
39
+ request_id: HITL request ID from SSE stream
40
+ operator_input: Optional notes/reason for approval
41
+ run_id: Optional client-side run correlation ID
42
+
43
+ Returns:
44
+ Response dict: {"status": "ok", "message": "..."}
45
+ """
46
+ return self._post_decision(
47
+ request_id,
48
+ HITLDecision.APPROVED,
49
+ operator_input,
50
+ run_id,
51
+ )
52
+
53
+ def reject(
54
+ self,
55
+ request_id: str,
56
+ operator_input: str | None = None,
57
+ run_id: str | None = None,
58
+ ) -> dict[str, Any]:
59
+ """Reject a HITL request.
60
+
61
+ Args:
62
+ request_id: HITL request ID
63
+ operator_input: Optional reason for rejection
64
+ run_id: Optional run correlation ID
65
+
66
+ Returns:
67
+ Response dict
68
+ """
69
+ return self._post_decision(
70
+ request_id,
71
+ HITLDecision.REJECTED,
72
+ operator_input,
73
+ run_id,
74
+ )
75
+
76
+ def skip(
77
+ self,
78
+ request_id: str,
79
+ operator_input: str | None = None,
80
+ run_id: str | None = None,
81
+ ) -> dict[str, Any]:
82
+ """Skip a HITL request.
83
+
84
+ Args:
85
+ request_id: HITL request ID
86
+ operator_input: Optional notes
87
+ run_id: Optional run correlation ID
88
+
89
+ Returns:
90
+ Response dict
91
+ """
92
+ return self._post_decision(
93
+ request_id,
94
+ HITLDecision.SKIPPED,
95
+ operator_input,
96
+ run_id,
97
+ )
98
+
99
+ def _post_decision(
100
+ self,
101
+ request_id: str,
102
+ decision: HITLDecision,
103
+ operator_input: str | None,
104
+ run_id: str | None,
105
+ ) -> dict[str, Any]:
106
+ """Post HITL decision to backend."""
107
+ payload = {
108
+ "request_id": request_id,
109
+ "decision": decision.value,
110
+ }
111
+
112
+ if operator_input:
113
+ payload["operator_input"] = operator_input
114
+ if run_id:
115
+ payload["run_id"] = run_id
116
+
117
+ return self._request("POST", "/agents/hitl/decision", json=payload)
118
+
119
+ def list_pending(self) -> list[dict[str, Any]]:
120
+ """List all pending HITL requests.
121
+
122
+ Returns:
123
+ List of pending request dicts with metadata:
124
+ [
125
+ {
126
+ "request_id": "...",
127
+ "tool": "...",
128
+ "arguments": {...},
129
+ "created_at": "...",
130
+ "agent_id": "...",
131
+ "hitl_metadata": {...},
132
+ },
133
+ ...
134
+ ]
135
+ """
136
+ return self._request("GET", "/agents/hitl/pending")
glaip_sdk/client/main.py CHANGED
@@ -3,15 +3,26 @@
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
- from typing import Any
9
+ from __future__ import annotations
10
+
11
+ from typing import TYPE_CHECKING, Any
9
12
 
10
13
  from glaip_sdk.client.agents import AgentClient
11
14
  from glaip_sdk.client.base import BaseClient
15
+ from glaip_sdk.client.hitl import HITLClient
12
16
  from glaip_sdk.client.mcps import MCPClient
17
+ from glaip_sdk.client.schedules import ScheduleClient
18
+ from glaip_sdk.client.shared import build_shared_config
13
19
  from glaip_sdk.client.tools import ToolClient
14
- from glaip_sdk.models import MCP, Agent, Tool
20
+
21
+ if TYPE_CHECKING: # pragma: no cover
22
+ from glaip_sdk.agents import Agent
23
+ from glaip_sdk.client.payloads.agent import AgentListResult
24
+ from glaip_sdk.mcps import MCP
25
+ from glaip_sdk.tools import Tool
15
26
 
16
27
 
17
28
  class Client(BaseClient):
@@ -25,15 +36,12 @@ class Client(BaseClient):
25
36
  """
26
37
  super().__init__(**kwargs)
27
38
  # Share the single httpx.Client + config with sub-clients
28
- shared_config = {
29
- "parent_client": self,
30
- "api_url": self.api_url,
31
- "api_key": self.api_key,
32
- "timeout": self._timeout,
33
- }
39
+ shared_config = build_shared_config(self)
34
40
  self.agents = AgentClient(**shared_config)
35
41
  self.tools = ToolClient(**shared_config)
36
42
  self.mcps = MCPClient(**shared_config)
43
+ self.schedules = ScheduleClient(**shared_config)
44
+ self.hitl = HITLClient(**shared_config)
37
45
 
38
46
  # ---- Core API Methods (Public Interface) ----
39
47
 
@@ -53,7 +61,7 @@ class Client(BaseClient):
53
61
  name: str | None = None,
54
62
  version: str | None = None,
55
63
  sync_langflow_agents: bool = False,
56
- ) -> list[Agent]:
64
+ ) -> AgentListResult:
57
65
  """List agents with optional filtering.
58
66
 
59
67
  Args:
@@ -64,7 +72,7 @@ class Client(BaseClient):
64
72
  sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
65
73
 
66
74
  Returns:
67
- List of agents matching the filters
75
+ AgentListResult with agents and pagination metadata. Supports iteration and indexing.
68
76
  """
69
77
  return self.agents.list_agents(
70
78
  agent_type=agent_type,
@@ -217,6 +225,11 @@ class Client(BaseClient):
217
225
 
218
226
  @timeout.setter
219
227
  def timeout(self, value: float) -> None: # type: ignore[override]
228
+ """Set the client timeout and propagate to sub-clients.
229
+
230
+ Args:
231
+ value: Timeout value in seconds.
232
+ """
220
233
  # Rebuild the root http client
221
234
  BaseClient.timeout.fset(self, value) # call parent setter
222
235
  # Propagate the new session to sub-clients so they don't hold a closed client
@@ -227,6 +240,8 @@ class Client(BaseClient):
227
240
  self.tools.http_client = self.http_client
228
241
  if hasattr(self, "mcps"):
229
242
  self.mcps.http_client = self.http_client
243
+ if hasattr(self, "schedules"):
244
+ self.schedules.http_client = self.http_client
230
245
  except Exception:
231
246
  pass
232
247
 
glaip_sdk/client/mcps.py CHANGED
@@ -3,18 +3,22 @@
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
9
10
  from typing import Any
10
11
 
11
12
  from glaip_sdk.client.base import BaseClient
12
- from glaip_sdk.config.constants import (
13
- DEFAULT_MCP_TRANSPORT,
14
- DEFAULT_MCP_TYPE,
13
+ from glaip_sdk.config.constants import DEFAULT_MCP_TRANSPORT, DEFAULT_MCP_TYPE
14
+ from glaip_sdk.mcps import MCP
15
+ from glaip_sdk.models import MCPResponse
16
+ from glaip_sdk.utils.client_utils import (
17
+ add_kwargs_to_payload,
18
+ create_model_instances,
19
+ find_by_name,
15
20
  )
16
- from glaip_sdk.models import MCP
17
- from glaip_sdk.utils.client_utils import create_model_instances, find_by_name
21
+ from glaip_sdk.utils.resource_refs import is_uuid
18
22
 
19
23
  # API endpoints
20
24
  MCPS_ENDPOINT = "/mcps/"
@@ -45,7 +49,8 @@ class MCPClient(BaseClient):
45
49
  def get_mcp_by_id(self, mcp_id: str) -> MCP:
46
50
  """Get MCP by ID."""
47
51
  data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}")
48
- return MCP(**data)._set_client(self)
52
+ response = MCPResponse(**data)
53
+ return MCP.from_response(response, client=self)
49
54
 
50
55
  def find_mcps(self, name: str | None = None) -> list[MCP]:
51
56
  """Find MCPs by name."""
@@ -77,34 +82,156 @@ class MCPClient(BaseClient):
77
82
  get_endpoint_fmt=f"{MCPS_ENDPOINT}{{id}}",
78
83
  json=payload,
79
84
  )
80
- return MCP(**full_mcp_data)._set_client(self)
85
+ response = MCPResponse(**full_mcp_data)
86
+ return MCP.from_response(response, client=self)
81
87
 
82
- def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
88
+ def update_mcp(self, mcp_id: str | MCP, **kwargs) -> MCP:
83
89
  """Update an existing MCP.
84
90
 
85
- Automatically chooses between PUT (full update) and PATCH (partial update)
86
- based on the provided fields:
87
- - Uses PUT if name, config, and transport are all provided (full update)
88
- - Uses PATCH otherwise (partial update)
91
+ Notes:
92
+ - Payload construction is centralized via ``_build_update_payload`` so required
93
+ defaults (e.g., ``type``) and value normalization stay consistent across SDK and CLI.
94
+ - For backward compatibility, still chooses PATCH vs PUT based on which fields the
95
+ caller provided, but uses the SDK payload builder for the final payload.
89
96
  """
90
- # Check if all required fields for full update are provided
97
+ # Backward-compatible: allow passing an MCP instance to avoid an extra fetch.
98
+ if isinstance(mcp_id, MCP):
99
+ current_mcp = mcp_id
100
+ if not current_mcp.id:
101
+ raise ValueError("MCP instance has no id; cannot update.")
102
+ mcp_id_value = str(current_mcp.id)
103
+ else:
104
+ current_mcp = None
105
+ mcp_id_value = mcp_id
106
+
91
107
  required_fields = {"name", "config", "transport"}
92
108
  provided_fields = set(kwargs.keys())
109
+ method = "PUT" if required_fields.issubset(provided_fields) else "PATCH"
110
+
111
+ if not kwargs:
112
+ data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id_value}", json={})
113
+ response = MCPResponse(**data)
114
+ return MCP.from_response(response, client=self)
115
+
116
+ if current_mcp is None:
117
+ current_mcp = self.get_mcp_by_id(mcp_id_value)
118
+
119
+ payload_kwargs = kwargs.copy()
120
+ name = payload_kwargs.pop("name", None)
121
+ description = payload_kwargs.pop("description", None)
122
+ full_payload = self._build_update_payload(
123
+ current_mcp=current_mcp,
124
+ name=name,
125
+ description=description,
126
+ **payload_kwargs,
127
+ )
93
128
 
94
- if required_fields.issubset(provided_fields):
95
- # All required fields provided - use full update (PUT)
96
- method = "PUT"
129
+ if method == "PUT":
130
+ json_payload = full_payload
97
131
  else:
98
- # Partial update - use PATCH
99
- method = "PATCH"
132
+ json_payload = {key: full_payload[key] for key in provided_fields if key in full_payload}
133
+ json_payload["type"] = full_payload["type"]
134
+ if "config" in provided_fields and "transport" not in provided_fields and "transport" in full_payload:
135
+ json_payload["transport"] = full_payload["transport"]
100
136
 
101
- data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id}", json=kwargs)
102
- return MCP(**data)._set_client(self)
137
+ data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id_value}", json=json_payload)
138
+ response = MCPResponse(**data)
139
+ return MCP.from_response(response, client=self)
103
140
 
104
141
  def delete_mcp(self, mcp_id: str) -> None:
105
142
  """Delete an MCP."""
106
143
  self._request("DELETE", f"{MCPS_ENDPOINT}{mcp_id}")
107
144
 
145
+ def upsert_mcp(
146
+ self,
147
+ identifier: str | MCP,
148
+ description: str | None = None,
149
+ config: dict[str, Any] | None = None,
150
+ **kwargs,
151
+ ) -> MCP:
152
+ """Create or update an MCP by instance, ID, or name.
153
+
154
+ Args:
155
+ identifier: MCP instance, ID (UUID string), or name
156
+ description: MCP description
157
+ config: MCP configuration dictionary
158
+ **kwargs: Additional parameters (transport, metadata, etc.)
159
+
160
+ Returns:
161
+ The created or updated MCP.
162
+
163
+ Example:
164
+ >>> # By name (creates if not exists)
165
+ >>> mcp = client.mcps.upsert_mcp(
166
+ ... "deepwiki",
167
+ ... transport="sse",
168
+ ... config={"url": "https://mcp.deepwiki.com/sse"},
169
+ ... )
170
+ >>> # By instance
171
+ >>> mcp = client.mcps.upsert_mcp(existing_mcp, description="Updated")
172
+ >>> # By ID
173
+ >>> mcp = client.mcps.upsert_mcp("uuid-here", description="Updated")
174
+ """
175
+ # Handle MCP instance
176
+ if isinstance(identifier, MCP):
177
+ if identifier.id:
178
+ logger.info("Updating MCP by instance: %s", identifier.name)
179
+ return self._do_upsert_update(identifier.id, identifier.name, description, config, **kwargs)
180
+ # MCP without ID - treat name as identifier
181
+ identifier = identifier.name
182
+
183
+ # Handle string (ID or name)
184
+ if isinstance(identifier, str):
185
+ if is_uuid(identifier):
186
+ logger.info("Updating MCP by ID: %s", identifier)
187
+ existing = self.get_mcp_by_id(identifier)
188
+ return self._do_upsert_update(identifier, existing.name, description, config, **kwargs)
189
+
190
+ # It's a name - find or create
191
+ return self._upsert_by_name(identifier, description, config, **kwargs)
192
+
193
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
194
+
195
+ def _do_upsert_update(
196
+ self,
197
+ mcp_id: str,
198
+ name: str | None,
199
+ description: str | None,
200
+ config: dict[str, Any] | None,
201
+ **kwargs,
202
+ ) -> MCP:
203
+ """Perform the update part of upsert."""
204
+ update_kwargs = {**kwargs}
205
+ if name is not None:
206
+ update_kwargs["name"] = name
207
+ if description is not None:
208
+ update_kwargs["description"] = description
209
+ if config is not None:
210
+ update_kwargs["config"] = config
211
+ return self.update_mcp(mcp_id, **update_kwargs)
212
+
213
+ def _upsert_by_name(
214
+ self,
215
+ name: str,
216
+ description: str | None,
217
+ config: dict[str, Any] | None,
218
+ **kwargs,
219
+ ) -> MCP:
220
+ """Find by name and update, or create if not found."""
221
+ all_mcps = self.list_mcps()
222
+ existing = [mcp for mcp in all_mcps if mcp.name.lower() == name.lower()]
223
+
224
+ if len(existing) == 1:
225
+ logger.info("Updating existing MCP: %s", name)
226
+ return self._do_upsert_update(existing[0].id, name, description, config, **kwargs)
227
+
228
+ if len(existing) > 1:
229
+ raise ValueError(f"Multiple MCPs found with name '{name}'")
230
+
231
+ # Create new MCP
232
+ logger.info("Creating new MCP: %s", name)
233
+ return self.create_mcp(name=name, description=description, config=config, **kwargs)
234
+
108
235
  def _build_create_payload(
109
236
  self,
110
237
  name: str,
@@ -147,9 +274,7 @@ class MCPClient(BaseClient):
147
274
 
148
275
  # Add any other kwargs (excluding already handled ones)
149
276
  excluded_keys = {"type"} # type is handled above
150
- for key, value in kwargs.items():
151
- if key not in excluded_keys:
152
- payload[key] = value
277
+ add_kwargs_to_payload(payload, kwargs, excluded_keys)
153
278
 
154
279
  return payload
155
280
 
@@ -179,9 +304,7 @@ class MCPClient(BaseClient):
179
304
  update_data = {
180
305
  "name": name if name is not None else current_mcp.name,
181
306
  "type": DEFAULT_MCP_TYPE, # Required by backend, MCPs are always server type
182
- "transport": kwargs.get(
183
- "transport", getattr(current_mcp, "transport", DEFAULT_MCP_TRANSPORT)
184
- ),
307
+ "transport": kwargs.get("transport", getattr(current_mcp, "transport", DEFAULT_MCP_TRANSPORT)),
185
308
  }
186
309
 
187
310
  # Handle description with proper None handling
@@ -208,7 +331,23 @@ class MCPClient(BaseClient):
208
331
  def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
209
332
  """Get tools available from an MCP."""
210
333
  data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
211
- return data or []
334
+ if data is None:
335
+ return []
336
+ if isinstance(data, list):
337
+ return data
338
+ if isinstance(data, dict):
339
+ if "tools" in data:
340
+ return data.get("tools", []) or []
341
+ logger.warning(
342
+ "Unexpected MCP tools response keys %s; returning empty list",
343
+ list(data.keys()),
344
+ )
345
+ return []
346
+ logger.warning(
347
+ "Unexpected MCP tools response type %s; returning empty list",
348
+ type(data).__name__,
349
+ )
350
+ return []
212
351
 
213
352
  def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
214
353
  """Test MCP connection using configuration.
@@ -0,0 +1,23 @@
1
+ """Agent payload types for requests and responses.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from glaip_sdk.client.payloads.agent.requests import (
8
+ AgentCreateRequest,
9
+ AgentListParams,
10
+ AgentUpdateRequest,
11
+ merge_payload_fields,
12
+ resolve_language_model_fields,
13
+ )
14
+ from glaip_sdk.client.payloads.agent.responses import AgentListResult
15
+
16
+ __all__ = [
17
+ "AgentCreateRequest",
18
+ "AgentListParams",
19
+ "AgentListResult",
20
+ "AgentUpdateRequest",
21
+ "merge_payload_fields",
22
+ "resolve_language_model_fields",
23
+ ]