glaip-sdk 0.6.25__py3-none-any.whl → 0.6.26__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 (51) hide show
  1. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  2. glaip_sdk/cli/commands/agents/_common.py +561 -0
  3. glaip_sdk/cli/commands/agents/create.py +151 -0
  4. glaip_sdk/cli/commands/agents/delete.py +64 -0
  5. glaip_sdk/cli/commands/agents/get.py +89 -0
  6. glaip_sdk/cli/commands/agents/list.py +129 -0
  7. glaip_sdk/cli/commands/agents/run.py +264 -0
  8. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  9. glaip_sdk/cli/commands/agents/update.py +112 -0
  10. glaip_sdk/cli/commands/mcps/__init__.py +98 -0
  11. glaip_sdk/cli/commands/mcps/_common.py +490 -0
  12. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  13. glaip_sdk/cli/commands/mcps/create.py +153 -0
  14. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  15. glaip_sdk/cli/commands/mcps/get.py +212 -0
  16. glaip_sdk/cli/commands/mcps/list.py +69 -0
  17. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  18. glaip_sdk/cli/commands/mcps/update.py +146 -0
  19. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  20. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  21. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  22. glaip_sdk/cli/commands/tools/_common.py +80 -0
  23. glaip_sdk/cli/commands/tools/create.py +228 -0
  24. glaip_sdk/cli/commands/tools/delete.py +61 -0
  25. glaip_sdk/cli/commands/tools/get.py +103 -0
  26. glaip_sdk/cli/commands/tools/list.py +69 -0
  27. glaip_sdk/cli/commands/tools/script.py +49 -0
  28. glaip_sdk/cli/commands/tools/update.py +102 -0
  29. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  30. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  31. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  32. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  33. glaip_sdk/client/_agent_payloads.py +32 -500
  34. glaip_sdk/client/agents.py +1 -1
  35. glaip_sdk/client/main.py +1 -1
  36. glaip_sdk/client/mcps.py +44 -13
  37. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  38. glaip_sdk/client/payloads/agent/requests.py +495 -0
  39. glaip_sdk/client/payloads/agent/responses.py +43 -0
  40. glaip_sdk/client/tools.py +38 -3
  41. glaip_sdk/tools/base.py +41 -10
  42. glaip_sdk/utils/import_resolver.py +40 -2
  43. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/METADATA +1 -1
  44. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/RECORD +48 -16
  45. glaip_sdk/cli/commands/agents.py +0 -1502
  46. glaip_sdk/cli/commands/mcps.py +0 -1355
  47. glaip_sdk/cli/commands/tools.py +0 -575
  48. /glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +0 -0
  49. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/entry_points.txt +0 -0
  51. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/top_level.txt +0 -0
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
+ ]
@@ -0,0 +1,495 @@
1
+ """Agent request payload types and helpers.
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 collections.abc import Callable, Mapping, MutableMapping, Sequence
11
+ from copy import deepcopy
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ from glaip_sdk.config.constants import (
16
+ DEFAULT_AGENT_FRAMEWORK,
17
+ DEFAULT_AGENT_PROVIDER,
18
+ DEFAULT_AGENT_TYPE,
19
+ DEFAULT_AGENT_VERSION,
20
+ DEFAULT_MODEL,
21
+ )
22
+ from glaip_sdk.payload_schemas.agent import AgentImportOperation, get_import_field_plan
23
+ from glaip_sdk.utils.client_utils import extract_ids
24
+
25
+ _LM_CONFLICT_KEYS = {
26
+ "lm_provider",
27
+ "lm_name",
28
+ "lm_base_url",
29
+ "lm_hyperparameters",
30
+ }
31
+
32
+
33
+ def _copy_structure(value: Any) -> Any:
34
+ """Return a defensive copy for mutable payload structures."""
35
+ if isinstance(value, (dict, list, tuple)):
36
+ return deepcopy(value)
37
+ return value
38
+
39
+
40
+ def _sanitize_agent_config(
41
+ agent_config: Mapping[str, Any] | None,
42
+ ) -> dict[str, Any] | None:
43
+ """Remove legacy LM keys that conflict with modern language model fields."""
44
+ if agent_config is None:
45
+ return None
46
+
47
+ sanitized = deepcopy(agent_config)
48
+ for key in _LM_CONFLICT_KEYS:
49
+ sanitized.pop(key, None)
50
+ return sanitized
51
+
52
+
53
+ def _merge_execution_timeout(
54
+ agent_config: dict[str, Any] | None,
55
+ timeout: int | None,
56
+ ) -> dict[str, Any] | None:
57
+ """Merge execution timeout into agent_config if provided."""
58
+ if timeout is None:
59
+ return agent_config
60
+
61
+ merged = agent_config or {}
62
+ merged.setdefault("execution_timeout", timeout)
63
+ return merged
64
+
65
+
66
+ def merge_payload_fields(
67
+ payload: MutableMapping[str, Any],
68
+ extra_fields: Mapping[str, Any] | None,
69
+ operation: AgentImportOperation,
70
+ ) -> None:
71
+ """Merge additional fields into payload respecting schema hints."""
72
+ if not extra_fields:
73
+ return
74
+
75
+ for key, value in extra_fields.items():
76
+ plan = get_import_field_plan(key, operation)
77
+ if not plan.copy or value is None:
78
+ continue
79
+
80
+ copied_value = _copy_structure(value)
81
+ if plan.sanitize and isinstance(copied_value, dict):
82
+ copied_value = _sanitize_agent_config(copied_value)
83
+
84
+ payload[key] = copied_value
85
+
86
+
87
+ def resolve_language_model_fields(
88
+ *,
89
+ model: str | None,
90
+ language_model_id: str | None,
91
+ provider: str | None,
92
+ model_name: str | None,
93
+ default_provider: str = DEFAULT_AGENT_PROVIDER,
94
+ default_model: str = DEFAULT_MODEL,
95
+ ) -> dict[str, Any]:
96
+ """Resolve mutually exclusive language model specification fields."""
97
+ if language_model_id:
98
+ return {"language_model_id": language_model_id}
99
+
100
+ resolved_model = model_name or model or default_model
101
+ resolved_provider = provider if provider is not None else default_provider
102
+
103
+ result: dict[str, Any] = {}
104
+ if resolved_model is not None:
105
+ result["model_name"] = resolved_model
106
+ if resolved_provider:
107
+ result["provider"] = resolved_provider
108
+ return result
109
+
110
+
111
+ def _extract_ids_or_empty(items: Sequence[str | Any] | None) -> list[str]:
112
+ """Extract IDs, returning an empty list when no IDs are present."""
113
+ if items is None:
114
+ return []
115
+
116
+ try:
117
+ iterable = list(items)
118
+ except TypeError:
119
+ return []
120
+
121
+ extracted = extract_ids(iterable)
122
+ return extracted or []
123
+
124
+
125
+ def _extract_existing_ids(source: Any, attribute: str) -> list[str]:
126
+ """Extract IDs from an attribute on the current agent instance."""
127
+ if source is None:
128
+ return []
129
+ value = getattr(source, attribute, None)
130
+ if not value:
131
+ return []
132
+ return _extract_ids_or_empty(value)
133
+
134
+
135
+ def _resolve_relation_ids(
136
+ new_items: Sequence[str | Any] | None,
137
+ current_agent: Any,
138
+ attribute: str,
139
+ ) -> list[str]:
140
+ """Resolve relationship IDs favouring explicit values when provided."""
141
+ if new_items is not None:
142
+ return _extract_ids_or_empty(new_items)
143
+ return _extract_existing_ids(current_agent, attribute)
144
+
145
+
146
+ def _pick_optional(
147
+ new_value: Any,
148
+ fallback: Any,
149
+ *,
150
+ transform: Callable[[Any], Any] | None = None,
151
+ ) -> Any | None:
152
+ """Return new_value when present, otherwise fallback, applying transform."""
153
+ value = new_value if new_value is not None else fallback
154
+ if value is None:
155
+ return None
156
+ return transform(value) if transform else value
157
+
158
+
159
+ def _existing_language_model_fields(current_agent: Any) -> dict[str, Any]:
160
+ """Derive language model fields from the current agent or defaults."""
161
+ result: dict[str, Any] = {}
162
+
163
+ language_model_id = getattr(current_agent, "language_model_id", None)
164
+ if language_model_id:
165
+ result["language_model_id"] = language_model_id
166
+ return result
167
+
168
+ agent_config = getattr(current_agent, "agent_config", None)
169
+ if isinstance(agent_config, Mapping):
170
+ provider = agent_config.get("lm_provider")
171
+ model_name = agent_config.get("lm_name")
172
+ if provider:
173
+ result["provider"] = provider
174
+ if model_name:
175
+ result["model_name"] = model_name
176
+
177
+ if not result:
178
+ if DEFAULT_AGENT_PROVIDER:
179
+ result["provider"] = DEFAULT_AGENT_PROVIDER
180
+ result["model_name"] = DEFAULT_MODEL
181
+
182
+ return result
183
+
184
+
185
+ @dataclass(slots=True)
186
+ class AgentListParams:
187
+ """Structured query parameters for listing agents."""
188
+
189
+ agent_type: str | None = None
190
+ framework: str | None = None
191
+ name: str | None = None
192
+ version: str | None = None
193
+ limit: int | None = None
194
+ page: int | None = None
195
+ include_deleted: bool | None = None
196
+ created_at_start: str | None = None
197
+ created_at_end: str | None = None
198
+ updated_at_start: str | None = None
199
+ updated_at_end: str | None = None
200
+ sync_langflow_agents: bool | None = None
201
+ metadata: Mapping[str, str] | None = None
202
+
203
+ def to_query_params(self) -> dict[str, Any]:
204
+ """Convert the dataclass to API-ready query params."""
205
+ params = self._base_filter_params()
206
+ self._apply_pagination_params(params)
207
+ self._apply_timestamp_filters(params)
208
+ self._apply_metadata_filters(params)
209
+
210
+ return params
211
+
212
+ def _base_filter_params(self) -> dict[str, Any]:
213
+ """Build base filter parameters from non-None fields.
214
+
215
+ Returns:
216
+ Dictionary of filter parameters with non-None values.
217
+ """
218
+ return {
219
+ key: value
220
+ for key, value in (
221
+ ("agent_type", self.agent_type),
222
+ ("framework", self.framework),
223
+ ("name", self.name),
224
+ ("version", self.version),
225
+ )
226
+ if value is not None
227
+ }
228
+
229
+ def _apply_pagination_params(self, params: dict[str, Any]) -> None:
230
+ """Apply pagination parameters to the params dictionary.
231
+
232
+ Args:
233
+ params: Dictionary to update with pagination parameters.
234
+ """
235
+ if self.limit is not None:
236
+ if not 1 <= self.limit <= 100:
237
+ raise ValueError("limit must be between 1 and 100 inclusive")
238
+ params["limit"] = self.limit
239
+
240
+ if self.page is not None:
241
+ if self.page < 1:
242
+ raise ValueError("page must be >= 1")
243
+ params["page"] = self.page
244
+
245
+ if self.include_deleted is not None:
246
+ params["include_deleted"] = str(self.include_deleted).lower()
247
+
248
+ if self.sync_langflow_agents is not None:
249
+ params["sync_langflow_agents"] = str(self.sync_langflow_agents).lower()
250
+
251
+ def _apply_timestamp_filters(self, params: dict[str, Any]) -> None:
252
+ """Apply timestamp filter parameters to the params dictionary.
253
+
254
+ Args:
255
+ params: Dictionary to update with timestamp filter parameters.
256
+ """
257
+ timestamp_filters = {
258
+ "created_at_start": self.created_at_start,
259
+ "created_at_end": self.created_at_end,
260
+ "updated_at_start": self.updated_at_start,
261
+ "updated_at_end": self.updated_at_end,
262
+ }
263
+ for key, value in timestamp_filters.items():
264
+ if value is not None:
265
+ params[key] = value
266
+
267
+ def _apply_metadata_filters(self, params: dict[str, Any]) -> None:
268
+ """Apply metadata filter parameters to the params dictionary.
269
+
270
+ Args:
271
+ params: Dictionary to update with metadata filter parameters.
272
+ """
273
+ if not self.metadata:
274
+ return
275
+ for key, value in self.metadata.items():
276
+ if value is not None:
277
+ params[f"metadata.{key}"] = value
278
+
279
+
280
+ @dataclass(slots=True)
281
+ class AgentCreateRequest:
282
+ """Declarative representation of an agent creation payload."""
283
+
284
+ name: str
285
+ instruction: str
286
+ model: str | None = DEFAULT_MODEL
287
+ language_model_id: str | None = None
288
+ provider: str | None = None
289
+ model_name: str | None = None
290
+ agent_type: str = DEFAULT_AGENT_TYPE
291
+ framework: str = DEFAULT_AGENT_FRAMEWORK
292
+ version: str = DEFAULT_AGENT_VERSION
293
+ account_id: str | None = None
294
+ description: str | None = None
295
+ metadata: Mapping[str, Any] | None = None
296
+ tools: Sequence[str | Any] | None = None
297
+ tool_configs: Mapping[str, Any] | None = None
298
+ agents: Sequence[str | Any] | None = None
299
+ mcps: Sequence[str | Any] | None = None
300
+ agent_config: Mapping[str, Any] | None = None
301
+ timeout: int | None = None
302
+ a2a_profile: Mapping[str, Any] | None = None
303
+ extras: Mapping[str, Any] | None = None
304
+
305
+ def to_payload(self) -> dict[str, Any]:
306
+ """Materialise the request as a dict suitable for API submission."""
307
+ payload: dict[str, Any] = {
308
+ "name": self.name.strip(),
309
+ "instruction": self.instruction.strip(),
310
+ "type": self.agent_type,
311
+ "framework": self.framework,
312
+ "version": self.version,
313
+ }
314
+
315
+ payload.update(
316
+ resolve_language_model_fields(
317
+ model=self.model,
318
+ language_model_id=self.language_model_id,
319
+ provider=self.provider,
320
+ model_name=self.model_name,
321
+ )
322
+ )
323
+
324
+ if self.account_id is not None:
325
+ payload["account_id"] = self.account_id
326
+ if self.description is not None:
327
+ payload["description"] = self.description
328
+ if self.metadata is not None:
329
+ payload["metadata"] = _copy_structure(self.metadata)
330
+
331
+ if self.a2a_profile is not None:
332
+ payload["a2a_profile"] = _copy_structure(self.a2a_profile)
333
+
334
+ tool_ids = extract_ids(list(self.tools) if self.tools is not None else None)
335
+ if tool_ids:
336
+ payload["tools"] = tool_ids
337
+
338
+ agent_ids = extract_ids(list(self.agents) if self.agents is not None else None)
339
+ if agent_ids:
340
+ payload["agents"] = agent_ids
341
+
342
+ mcp_ids = extract_ids(list(self.mcps) if self.mcps is not None else None)
343
+ if mcp_ids:
344
+ payload["mcps"] = mcp_ids
345
+
346
+ if self.tool_configs is not None:
347
+ payload["tool_configs"] = _copy_structure(self.tool_configs)
348
+
349
+ effective_agent_config = _sanitize_agent_config(self.agent_config)
350
+ effective_agent_config = _merge_execution_timeout(effective_agent_config, self.timeout)
351
+ if effective_agent_config:
352
+ payload["agent_config"] = effective_agent_config
353
+
354
+ merge_payload_fields(payload, self.extras, "create")
355
+ return payload
356
+
357
+
358
+ @dataclass(slots=True)
359
+ class AgentUpdateRequest:
360
+ """Declarative representation of an agent update payload."""
361
+
362
+ name: str | None = None
363
+ instruction: str | None = None
364
+ description: str | None = None
365
+ model: str | None = None
366
+ language_model_id: str | None = None
367
+ provider: str | None = None
368
+ model_name: str | None = None
369
+ agent_type: str | None = None
370
+ framework: str | None = None
371
+ version: str | None = None
372
+ account_id: str | None = None
373
+ metadata: Mapping[str, Any] | None = None
374
+ tools: Sequence[str | Any] | None = None
375
+ tool_configs: Mapping[str, Any] | None = None
376
+ agents: Sequence[str | Any] | None = None
377
+ mcps: Sequence[str | Any] | None = None
378
+ agent_config: Mapping[str, Any] | None = None
379
+ a2a_profile: Mapping[str, Any] | None = None
380
+ extras: Mapping[str, Any] | None = None
381
+
382
+ def to_payload(self, current_agent: Any) -> dict[str, Any]:
383
+ """Materialise the request using current agent data as fallbacks."""
384
+ payload = _build_base_update_payload(self, current_agent)
385
+ payload.update(_resolve_update_language_model_fields(self, current_agent))
386
+ payload.update(_collect_optional_update_fields(self, current_agent))
387
+ payload.update(_collect_relationship_fields(self, current_agent))
388
+
389
+ agent_config_value = _resolve_agent_config_update(self, current_agent)
390
+ if agent_config_value is not None:
391
+ payload["agent_config"] = agent_config_value
392
+
393
+ merge_payload_fields(payload, self.extras, "update")
394
+ return payload
395
+
396
+
397
+ def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
398
+ """Populate immutable agent update fields using request data or existing agent defaults."""
399
+ # Support both "agent_type" (runtime class) and "type" (API response) attributes
400
+ current_type = getattr(current_agent, "agent_type", None) or getattr(current_agent, "type", None)
401
+ return {
402
+ "name": (request.name.strip() if request.name is not None else getattr(current_agent, "name", None)),
403
+ "instruction": (
404
+ request.instruction.strip()
405
+ if request.instruction is not None
406
+ else getattr(current_agent, "instruction", None)
407
+ ),
408
+ "type": request.agent_type or current_type or DEFAULT_AGENT_TYPE,
409
+ "framework": request.framework or getattr(current_agent, "framework", None) or DEFAULT_AGENT_FRAMEWORK,
410
+ "version": request.version or getattr(current_agent, "version", None) or DEFAULT_AGENT_VERSION,
411
+ }
412
+
413
+
414
+ def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
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)
431
+ fields = resolve_language_model_fields(
432
+ model=request.model,
433
+ language_model_id=request.language_model_id,
434
+ provider=request.provider,
435
+ model_name=request.model_name,
436
+ )
437
+ return fields
438
+
439
+
440
+ def _collect_optional_update_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
441
+ """Collect optional agent fields, preserving current values when updates are absent."""
442
+ result: dict[str, Any] = {}
443
+
444
+ for field_name, value in (
445
+ ("account_id", request.account_id),
446
+ ("description", request.description),
447
+ ):
448
+ resolved_value = _pick_optional(value, getattr(current_agent, field_name, None))
449
+ if resolved_value is not None:
450
+ result[field_name] = resolved_value
451
+
452
+ metadata_value = _pick_optional(
453
+ request.metadata,
454
+ getattr(current_agent, "metadata", None),
455
+ transform=_copy_structure,
456
+ )
457
+ if metadata_value is not None:
458
+ result["metadata"] = metadata_value
459
+
460
+ profile_value = _pick_optional(
461
+ request.a2a_profile,
462
+ getattr(current_agent, "a2a_profile", None),
463
+ transform=_copy_structure,
464
+ )
465
+ if profile_value is not None:
466
+ result["a2a_profile"] = profile_value
467
+
468
+ tool_configs_value = _pick_optional(
469
+ request.tool_configs,
470
+ getattr(current_agent, "tool_configs", None),
471
+ transform=_copy_structure,
472
+ )
473
+ if tool_configs_value is not None:
474
+ result["tool_configs"] = tool_configs_value
475
+
476
+ return result
477
+
478
+
479
+ def _collect_relationship_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
480
+ """Return relationship identifiers (tools/agents/mcps) for an update request."""
481
+ return {
482
+ "tools": _resolve_relation_ids(request.tools, current_agent, "tools"),
483
+ "agents": _resolve_relation_ids(request.agents, current_agent, "agents"),
484
+ "mcps": _resolve_relation_ids(request.mcps, current_agent, "mcps"),
485
+ }
486
+
487
+
488
+ def _resolve_agent_config_update(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any] | None:
489
+ """Determine the agent_config payload to send, if any."""
490
+ effective_agent_config = _sanitize_agent_config(request.agent_config)
491
+ if effective_agent_config is not None:
492
+ return effective_agent_config
493
+ if getattr(current_agent, "agent_config", None):
494
+ return _sanitize_agent_config(current_agent.agent_config)
495
+ return None
@@ -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]
glaip_sdk/client/tools.py CHANGED
@@ -439,9 +439,44 @@ class ToolClient(BaseClient):
439
439
  except OSError:
440
440
  pass # Ignore cleanup errors
441
441
 
442
- def update_tool(self, tool_id: str, **kwargs) -> Tool:
443
- """Update an existing tool."""
444
- data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
442
+ def update_tool(self, tool_id: str | Tool, **kwargs) -> Tool:
443
+ """Update an existing tool.
444
+
445
+ Notes:
446
+ - Payload construction is centralized via ``_build_update_payload`` to keep metadata
447
+ update and upload update flows consistent.
448
+ - Accepts either a tool ID or a ``Tool`` instance (avoids an extra fetch when callers
449
+ already have the current tool).
450
+ """
451
+ # Backward-compatible: allow passing a Tool instance to avoid an extra fetch.
452
+ if isinstance(tool_id, Tool):
453
+ current_tool = tool_id
454
+ if not current_tool.id:
455
+ raise ValueError("Tool instance has no id; cannot update.")
456
+ tool_id_value = str(current_tool.id)
457
+ else:
458
+ current_tool = None
459
+ tool_id_value = tool_id
460
+
461
+ if not kwargs:
462
+ data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id_value}", json={})
463
+ response = ToolResponse(**data)
464
+ return Tool.from_response(response, client=self)
465
+
466
+ if current_tool is None:
467
+ current_tool = self.get_tool_by_id(tool_id_value)
468
+
469
+ payload_kwargs = kwargs.copy()
470
+ name = payload_kwargs.pop("name", None)
471
+ description = payload_kwargs.pop("description", None)
472
+ update_payload = self._build_update_payload(
473
+ current_tool=current_tool,
474
+ name=name,
475
+ description=description,
476
+ **payload_kwargs,
477
+ )
478
+
479
+ data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id_value}", json=update_payload)
445
480
  response = ToolResponse(**data)
446
481
  return Tool.from_response(response, client=self)
447
482