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