glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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 (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.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
+ import time
10
12
  from collections.abc import Iterable, Mapping
11
13
  from typing import Any, NoReturn, Union
12
14
 
@@ -88,6 +90,11 @@ class BaseClient:
88
90
  client_log.info(f"Initializing client with API URL: {self.api_url}")
89
91
  self.http_client = self._build_client(timeout)
90
92
 
93
+ # Language model cache (shared by all clients)
94
+ self._lm_cache: list[dict[str, Any]] | None = None
95
+ self._lm_cache_time: float = 0.0
96
+ self._lm_cache_ttl: float = 300.0 # 5 minutes TTL
97
+
91
98
  def _build_client(self, timeout: float) -> httpx.Client:
92
99
  """Build HTTP client with configuration."""
93
100
  # For streaming operations, we need more generous read timeouts
@@ -480,6 +487,25 @@ class BaseClient:
480
487
  request_id=request_id,
481
488
  )
482
489
 
490
+ def list_language_models(self, force_refresh: bool = False) -> list[dict[str, Any]]:
491
+ """List available language models with TTL caching.
492
+
493
+ Args:
494
+ force_refresh: Whether to ignore cache and fetch fresh list.
495
+
496
+ Returns:
497
+ List of available language models.
498
+ """
499
+ now = time.monotonic()
500
+ if not force_refresh and self._lm_cache is not None:
501
+ if now - self._lm_cache_time < self._lm_cache_ttl:
502
+ return self._lm_cache
503
+
504
+ models = self._request("GET", "/language-models") or []
505
+ self._lm_cache = models
506
+ self._lm_cache_time = now
507
+ return models
508
+
483
509
  def close(self) -> None:
484
510
  """Close the HTTP client."""
485
511
  if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
@@ -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,
@@ -204,10 +212,6 @@ class Client(BaseClient):
204
212
  return self.mcps.get_mcp_tools_from_config(config)
205
213
 
206
214
  # Language Models
207
- def list_language_models(self) -> list[dict]:
208
- """List available language models."""
209
- data = self._request("GET", "/language-models")
210
- return data or []
211
215
 
212
216
  # ---- Timeout propagation ----
213
217
  @property
@@ -217,6 +221,11 @@ class Client(BaseClient):
217
221
 
218
222
  @timeout.setter
219
223
  def timeout(self, value: float) -> None: # type: ignore[override]
224
+ """Set the client timeout and propagate to sub-clients.
225
+
226
+ Args:
227
+ value: Timeout value in seconds.
228
+ """
220
229
  # Rebuild the root http client
221
230
  BaseClient.timeout.fset(self, value) # call parent setter
222
231
  # Propagate the new session to sub-clients so they don't hold a closed client
@@ -227,6 +236,8 @@ class Client(BaseClient):
227
236
  self.tools.http_client = self.http_client
228
237
  if hasattr(self, "mcps"):
229
238
  self.mcps.http_client = self.http_client
239
+ if hasattr(self, "schedules"):
240
+ self.schedules.http_client = self.http_client
230
241
  except Exception:
231
242
  pass
232
243
 
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
 
@@ -206,7 +331,23 @@ class MCPClient(BaseClient):
206
331
  def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
207
332
  """Get tools available from an MCP."""
208
333
  data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
209
- 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 []
210
351
 
211
352
  def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
212
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
+ ]