glaip-sdk 0.6.5b6__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 (116) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +156 -32
  3. glaip_sdk/cli/auth.py +14 -8
  4. glaip_sdk/cli/commands/accounts.py +1 -1
  5. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  6. glaip_sdk/cli/commands/agents/_common.py +561 -0
  7. glaip_sdk/cli/commands/agents/create.py +151 -0
  8. glaip_sdk/cli/commands/agents/delete.py +64 -0
  9. glaip_sdk/cli/commands/agents/get.py +89 -0
  10. glaip_sdk/cli/commands/agents/list.py +129 -0
  11. glaip_sdk/cli/commands/agents/run.py +264 -0
  12. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  13. glaip_sdk/cli/commands/agents/update.py +112 -0
  14. glaip_sdk/cli/commands/common_config.py +15 -12
  15. glaip_sdk/cli/commands/configure.py +2 -3
  16. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  17. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  18. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  19. glaip_sdk/cli/commands/mcps/create.py +152 -0
  20. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  21. glaip_sdk/cli/commands/mcps/get.py +212 -0
  22. glaip_sdk/cli/commands/mcps/list.py +69 -0
  23. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  24. glaip_sdk/cli/commands/mcps/update.py +190 -0
  25. glaip_sdk/cli/commands/models.py +2 -4
  26. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  27. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  28. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  29. glaip_sdk/cli/commands/tools/_common.py +80 -0
  30. glaip_sdk/cli/commands/tools/create.py +228 -0
  31. glaip_sdk/cli/commands/tools/delete.py +61 -0
  32. glaip_sdk/cli/commands/tools/get.py +103 -0
  33. glaip_sdk/cli/commands/tools/list.py +69 -0
  34. glaip_sdk/cli/commands/tools/script.py +49 -0
  35. glaip_sdk/cli/commands/tools/update.py +102 -0
  36. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  37. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  38. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  39. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  40. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  41. glaip_sdk/cli/commands/update.py +163 -17
  42. glaip_sdk/cli/core/output.py +12 -7
  43. glaip_sdk/cli/entrypoint.py +20 -0
  44. glaip_sdk/cli/main.py +127 -39
  45. glaip_sdk/cli/pager.py +3 -3
  46. glaip_sdk/cli/resolution.py +2 -1
  47. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  48. glaip_sdk/cli/slash/agent_session.py +5 -2
  49. glaip_sdk/cli/slash/prompt.py +11 -0
  50. glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
  51. glaip_sdk/cli/slash/session.py +58 -13
  52. glaip_sdk/cli/slash/tui/__init__.py +26 -1
  53. glaip_sdk/cli/slash/tui/accounts.tcss +7 -5
  54. glaip_sdk/cli/slash/tui/accounts_app.py +70 -9
  55. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  56. glaip_sdk/cli/slash/tui/context.py +59 -0
  57. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  58. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  59. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  60. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  61. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  62. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  63. glaip_sdk/cli/slash/tui/toast.py +123 -0
  64. glaip_sdk/cli/transcript/history.py +1 -1
  65. glaip_sdk/cli/transcript/viewer.py +5 -3
  66. glaip_sdk/cli/update_notifier.py +215 -7
  67. glaip_sdk/cli/validators.py +1 -1
  68. glaip_sdk/client/__init__.py +2 -1
  69. glaip_sdk/client/_schedule_payloads.py +89 -0
  70. glaip_sdk/client/agents.py +50 -8
  71. glaip_sdk/client/hitl.py +136 -0
  72. glaip_sdk/client/main.py +7 -1
  73. glaip_sdk/client/mcps.py +44 -13
  74. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  75. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
  76. glaip_sdk/client/payloads/agent/responses.py +43 -0
  77. glaip_sdk/client/run_rendering.py +367 -3
  78. glaip_sdk/client/schedules.py +439 -0
  79. glaip_sdk/client/tools.py +57 -26
  80. glaip_sdk/hitl/__init__.py +48 -0
  81. glaip_sdk/hitl/base.py +64 -0
  82. glaip_sdk/hitl/callback.py +43 -0
  83. glaip_sdk/hitl/local.py +121 -0
  84. glaip_sdk/hitl/remote.py +523 -0
  85. glaip_sdk/models/__init__.py +17 -0
  86. glaip_sdk/models/agent_runs.py +2 -1
  87. glaip_sdk/models/schedule.py +224 -0
  88. glaip_sdk/registry/tool.py +273 -59
  89. glaip_sdk/runner/__init__.py +20 -3
  90. glaip_sdk/runner/deps.py +5 -8
  91. glaip_sdk/runner/langgraph.py +317 -42
  92. glaip_sdk/runner/logging_config.py +77 -0
  93. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
  94. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
  95. glaip_sdk/schedules/__init__.py +22 -0
  96. glaip_sdk/schedules/base.py +291 -0
  97. glaip_sdk/tools/base.py +44 -11
  98. glaip_sdk/utils/__init__.py +1 -0
  99. glaip_sdk/utils/bundler.py +138 -2
  100. glaip_sdk/utils/import_resolver.py +43 -11
  101. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  102. glaip_sdk/utils/runtime_config.py +15 -12
  103. glaip_sdk/utils/sync.py +31 -11
  104. glaip_sdk/utils/tool_detection.py +274 -6
  105. glaip_sdk/utils/tool_storage_provider.py +140 -0
  106. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +47 -37
  107. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  108. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  109. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  110. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  111. glaip_sdk/cli/commands/agents.py +0 -1509
  112. glaip_sdk/cli/commands/mcps.py +0 -1356
  113. glaip_sdk/cli/commands/tools.py +0 -576
  114. glaip_sdk/cli/utils.py +0 -263
  115. glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
  116. glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
@@ -13,11 +13,15 @@ from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
13
13
  from contextlib import asynccontextmanager
14
14
  from os import PathLike
15
15
  from pathlib import Path
16
- from typing import Any, BinaryIO
16
+ from typing import TYPE_CHECKING, Any, BinaryIO
17
+
18
+ if TYPE_CHECKING:
19
+ from glaip_sdk.client.schedules import ScheduleClient
20
+ from glaip_sdk.hitl.remote import RemoteHITLHandler
17
21
 
18
22
  import httpx
19
23
  from glaip_sdk.agents import Agent
20
- from glaip_sdk.client._agent_payloads import (
24
+ from glaip_sdk.client.payloads.agent import (
21
25
  AgentCreateRequest,
22
26
  AgentListParams,
23
27
  AgentListResult,
@@ -264,6 +268,7 @@ class AgentClient(BaseClient):
264
268
  self._tool_client: ToolClient | None = None
265
269
  self._mcp_client: MCPClient | None = None
266
270
  self._runs_client: AgentRunsClient | None = None
271
+ self._schedule_client: ScheduleClient | None = None
267
272
 
268
273
  def list_agents(
269
274
  self,
@@ -411,6 +416,7 @@ class AgentClient(BaseClient):
411
416
  timeout_seconds: float,
412
417
  agent_name: str | None,
413
418
  meta: dict[str, Any],
419
+ hitl_handler: "RemoteHITLHandler | None" = None,
414
420
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
415
421
  """Process stream events from an HTTP response.
416
422
 
@@ -420,6 +426,7 @@ class AgentClient(BaseClient):
420
426
  timeout_seconds: Timeout in seconds.
421
427
  agent_name: Optional agent name.
422
428
  meta: Metadata dictionary.
429
+ hitl_handler: Optional HITL handler for approval callbacks.
423
430
 
424
431
  Returns:
425
432
  Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
@@ -431,6 +438,7 @@ class AgentClient(BaseClient):
431
438
  timeout_seconds,
432
439
  agent_name,
433
440
  meta,
441
+ hitl_handler=hitl_handler,
434
442
  )
435
443
 
436
444
  def _finalize_renderer(
@@ -453,13 +461,11 @@ class AgentClient(BaseClient):
453
461
  Returns:
454
462
  Final text string.
455
463
  """
464
+ from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
465
+
456
466
  manager = self._get_renderer_manager()
457
- return manager.finalize_renderer(
458
- renderer,
459
- final_text,
460
- stats_usage,
461
- started_monotonic,
462
- finished_monotonic,
467
+ return finalize_render_manager(
468
+ manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
463
469
  )
464
470
 
465
471
  def _get_tool_client(self) -> ToolClient:
@@ -482,6 +488,20 @@ class AgentClient(BaseClient):
482
488
  self._mcp_client = MCPClient(parent_client=self)
483
489
  return self._mcp_client
484
490
 
491
+ @property
492
+ def schedules(self) -> "ScheduleClient":
493
+ """Get or create the schedule client instance.
494
+
495
+ Returns:
496
+ ScheduleClient instance.
497
+ """
498
+ if self._schedule_client is None:
499
+ # Import here to avoid circular import
500
+ from glaip_sdk.client.schedules import ScheduleClient # noqa: PLC0415
501
+
502
+ self._schedule_client = ScheduleClient(parent_client=self)
503
+ return self._schedule_client
504
+
485
505
  def _normalise_reference_entry(
486
506
  self,
487
507
  entry: Any,
@@ -1096,6 +1116,7 @@ class AgentClient(BaseClient):
1096
1116
  *,
1097
1117
  renderer: RichStreamRenderer | str | None = "auto",
1098
1118
  runtime_config: dict[str, Any] | None = None,
1119
+ hitl_handler: "RemoteHITLHandler | None" = None,
1099
1120
  **kwargs,
1100
1121
  ) -> str:
1101
1122
  """Run an agent with a message, streaming via a renderer.
@@ -1113,6 +1134,8 @@ class AgentClient(BaseClient):
1113
1134
  "mcp_configs": {"mcp-id": {"setting": "on"}},
1114
1135
  "agent_config": {"planning": True},
1115
1136
  }
1137
+ hitl_handler: Optional RemoteHITLHandler for approval callbacks.
1138
+ Set GLAIP_HITL_AUTO_APPROVE=true for auto-approval without handler.
1116
1139
  **kwargs: Additional arguments to pass to the run API.
1117
1140
 
1118
1141
  Returns:
@@ -1169,6 +1192,7 @@ class AgentClient(BaseClient):
1169
1192
  timeout_seconds,
1170
1193
  agent_name,
1171
1194
  meta,
1195
+ hitl_handler=hitl_handler,
1172
1196
  )
1173
1197
 
1174
1198
  except KeyboardInterrupt:
@@ -1185,6 +1209,13 @@ class AgentClient(BaseClient):
1185
1209
  if multipart_data:
1186
1210
  multipart_data.close()
1187
1211
 
1212
+ # Wait for pending HITL decisions before returning
1213
+ if hitl_handler and hasattr(hitl_handler, "wait_for_pending_decisions"):
1214
+ try:
1215
+ hitl_handler.wait_for_pending_decisions(timeout=30)
1216
+ except Exception as e:
1217
+ logger.warning(f"Error waiting for HITL decisions: {e}")
1218
+
1188
1219
  return self._finalize_renderer(
1189
1220
  r,
1190
1221
  final_text,
@@ -1266,6 +1297,7 @@ class AgentClient(BaseClient):
1266
1297
  *,
1267
1298
  request_timeout: float | None = None,
1268
1299
  runtime_config: dict[str, Any] | None = None,
1300
+ hitl_handler: "RemoteHITLHandler | None" = None,
1269
1301
  **kwargs,
1270
1302
  ) -> AsyncGenerator[dict, None]:
1271
1303
  """Async run an agent with a message, yielding streaming JSON chunks.
@@ -1282,16 +1314,26 @@ class AgentClient(BaseClient):
1282
1314
  "mcp_configs": {"mcp-id": {"setting": "on"}},
1283
1315
  "agent_config": {"planning": True},
1284
1316
  }
1317
+ hitl_handler: Optional HITL handler for remote approval requests.
1318
+ Note: Async HITL support is currently deferred. This parameter
1319
+ is accepted for API consistency but will raise NotImplementedError
1320
+ if provided.
1285
1321
  **kwargs: Additional arguments (chat_history, pii_mapping, etc.)
1286
1322
 
1287
1323
  Yields:
1288
1324
  Dictionary containing parsed JSON chunks from the streaming response
1289
1325
 
1290
1326
  Raises:
1327
+ NotImplementedError: If hitl_handler is provided (async HITL not yet supported)
1291
1328
  AgentTimeoutError: When agent execution times out
1292
1329
  httpx.TimeoutException: When general timeout occurs
1293
1330
  Exception: For other unexpected errors
1294
1331
  """
1332
+ if hitl_handler is not None:
1333
+ raise NotImplementedError(
1334
+ "Async HITL support is currently deferred. "
1335
+ "Please use the synchronous run_agent() method with hitl_handler."
1336
+ )
1295
1337
  # Include runtime_config in kwargs only when caller hasn't already provided it
1296
1338
  if runtime_config is not None and "runtime_config" not in kwargs:
1297
1339
  kwargs["runtime_config"] = runtime_config
@@ -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
@@ -12,13 +12,15 @@ from typing import TYPE_CHECKING, Any
12
12
 
13
13
  from glaip_sdk.client.agents import AgentClient
14
14
  from glaip_sdk.client.base import BaseClient
15
+ from glaip_sdk.client.hitl import HITLClient
15
16
  from glaip_sdk.client.mcps import MCPClient
17
+ from glaip_sdk.client.schedules import ScheduleClient
16
18
  from glaip_sdk.client.shared import build_shared_config
17
19
  from glaip_sdk.client.tools import ToolClient
18
20
 
19
21
  if TYPE_CHECKING: # pragma: no cover
20
22
  from glaip_sdk.agents import Agent
21
- from glaip_sdk.client._agent_payloads import AgentListResult
23
+ from glaip_sdk.client.payloads.agent import AgentListResult
22
24
  from glaip_sdk.mcps import MCP
23
25
  from glaip_sdk.tools import Tool
24
26
 
@@ -38,6 +40,8 @@ class Client(BaseClient):
38
40
  self.agents = AgentClient(**shared_config)
39
41
  self.tools = ToolClient(**shared_config)
40
42
  self.mcps = MCPClient(**shared_config)
43
+ self.schedules = ScheduleClient(**shared_config)
44
+ self.hitl = HITLClient(**shared_config)
41
45
 
42
46
  # ---- Core API Methods (Public Interface) ----
43
47
 
@@ -236,6 +240,8 @@ class Client(BaseClient):
236
240
  self.tools.http_client = self.http_client
237
241
  if hasattr(self, "mcps"):
238
242
  self.mcps.http_client = self.http_client
243
+ if hasattr(self, "schedules"):
244
+ self.schedules.http_client = self.http_client
239
245
  except Exception:
240
246
  pass
241
247
 
glaip_sdk/client/mcps.py CHANGED
@@ -85,26 +85,56 @@ class MCPClient(BaseClient):
85
85
  response = MCPResponse(**full_mcp_data)
86
86
  return MCP.from_response(response, client=self)
87
87
 
88
- def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
88
+ def update_mcp(self, mcp_id: str | MCP, **kwargs) -> MCP:
89
89
  """Update an existing MCP.
90
90
 
91
- Automatically chooses between PUT (full update) and PATCH (partial update)
92
- based on the provided fields:
93
- - Uses PUT if name, config, and transport are all provided (full update)
94
- - 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.
95
96
  """
96
- # 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
+
97
107
  required_fields = {"name", "config", "transport"}
98
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
+ )
99
128
 
100
- if required_fields.issubset(provided_fields):
101
- # All required fields provided - use full update (PUT)
102
- method = "PUT"
129
+ if method == "PUT":
130
+ json_payload = full_payload
103
131
  else:
104
- # Partial update - use PATCH
105
- 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"]
106
136
 
107
- data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id}", json=kwargs)
137
+ data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id_value}", json=json_payload)
108
138
  response = MCPResponse(**data)
109
139
  return MCP.from_response(response, client=self)
110
140
 
@@ -188,7 +218,8 @@ class MCPClient(BaseClient):
188
218
  **kwargs,
189
219
  ) -> MCP:
190
220
  """Find by name and update, or create if not found."""
191
- existing = self.find_mcps(name)
221
+ all_mcps = self.list_mcps()
222
+ existing = [mcp for mcp in all_mcps if mcp.name.lower() == name.lower()]
192
223
 
193
224
  if len(existing) == 1:
194
225
  logger.info("Updating existing MCP: %s", name)
@@ -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
+ ]
@@ -1,11 +1,15 @@
1
- #!/usr/bin/env python3
2
- """Shared helpers for Agent client payload construction and query handling."""
1
+ """Agent request payload types and helpers.
3
2
 
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ # pylint: disable=duplicate-code
4
8
  from __future__ import annotations
5
9
 
6
10
  from collections.abc import Callable, Mapping, MutableMapping, Sequence
7
11
  from copy import deepcopy
8
- from dataclasses import dataclass, field
12
+ from dataclasses import dataclass
9
13
  from typing import Any
10
14
 
11
15
  from glaip_sdk.config.constants import (
@@ -273,38 +277,6 @@ class AgentListParams:
273
277
  params[f"metadata.{key}"] = value
274
278
 
275
279
 
276
- @dataclass(slots=True)
277
- class AgentListResult:
278
- """Structured response for list_agents that retains pagination metadata."""
279
-
280
- items: list[Any] = field(default_factory=list)
281
- total: int | None = None
282
- page: int | None = None
283
- limit: int | None = None
284
- has_next: bool | None = None
285
- has_prev: bool | None = None
286
- message: str | None = None
287
-
288
- def __len__(self) -> int: # pragma: no cover - simple delegation
289
- """Return the number of items in the result list."""
290
- return len(self.items)
291
-
292
- def __iter__(self): # pragma: no cover - simple delegation
293
- """Return an iterator over the items in the result list."""
294
- return iter(self.items)
295
-
296
- def __getitem__(self, index: int) -> Any: # pragma: no cover - simple delegation
297
- """Get an item from the result list by index.
298
-
299
- Args:
300
- index: Index of the item to retrieve.
301
-
302
- Returns:
303
- The item at the specified index.
304
- """
305
- return self.items[index]
306
-
307
-
308
280
  @dataclass(slots=True)
309
281
  class AgentCreateRequest:
310
282
  """Declarative representation of an agent creation payload."""
@@ -422,16 +394,6 @@ class AgentUpdateRequest:
422
394
  return payload
423
395
 
424
396
 
425
- __all__ = [
426
- "AgentCreateRequest",
427
- "AgentListParams",
428
- "AgentListResult",
429
- "AgentUpdateRequest",
430
- "merge_payload_fields",
431
- "resolve_language_model_fields",
432
- ]
433
-
434
-
435
397
  def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
436
398
  """Populate immutable agent update fields using request data or existing agent defaults."""
437
399
  # Support both "agent_type" (runtime class) and "type" (API response) attributes
@@ -451,14 +413,27 @@ def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any)
451
413
 
452
414
  def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
453
415
  """Resolve the language-model portion of an update request with sensible fallbacks."""
416
+ # Check if any LM inputs were provided
417
+ has_lm_inputs = any(
418
+ [
419
+ request.model is not None,
420
+ request.language_model_id is not None,
421
+ request.provider is not None,
422
+ request.model_name is not None,
423
+ ]
424
+ )
425
+
426
+ if not has_lm_inputs:
427
+ # No LM inputs provided - preserve existing fields
428
+ return _existing_language_model_fields(current_agent)
429
+
430
+ # LM inputs provided - resolve them (may return defaults if only partial info)
454
431
  fields = resolve_language_model_fields(
455
432
  model=request.model,
456
433
  language_model_id=request.language_model_id,
457
434
  provider=request.provider,
458
435
  model_name=request.model_name,
459
436
  )
460
- if not fields:
461
- fields = _existing_language_model_fields(current_agent)
462
437
  return fields
463
438
 
464
439
 
@@ -0,0 +1,43 @@
1
+ """Agent response payload types.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ # pylint: disable=duplicate-code
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class AgentListResult:
16
+ """Structured response for list_agents that retains pagination metadata."""
17
+
18
+ items: list[Any] = field(default_factory=list)
19
+ total: int | None = None
20
+ page: int | None = None
21
+ limit: int | None = None
22
+ has_next: bool | None = None
23
+ has_prev: bool | None = None
24
+ message: str | None = None
25
+
26
+ def __len__(self) -> int: # pragma: no cover - simple delegation
27
+ """Return the number of items in the result list."""
28
+ return len(self.items)
29
+
30
+ def __iter__(self): # pragma: no cover - simple delegation
31
+ """Return an iterator over the items in the result list."""
32
+ return iter(self.items)
33
+
34
+ def __getitem__(self, index: int) -> Any: # pragma: no cover - simple delegation
35
+ """Get an item from the result list by index.
36
+
37
+ Args:
38
+ index: Index of the item to retrieve.
39
+
40
+ Returns:
41
+ The item at the specified index.
42
+ """
43
+ return self.items[index]