glaip-sdk 0.0.7__py3-none-any.whl → 0.6.5b6__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 (161) hide show
  1. glaip_sdk/__init__.py +6 -3
  2. glaip_sdk/_version.py +12 -5
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1126 -0
  5. glaip_sdk/branding.py +79 -15
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +699 -0
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents.py +503 -183
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +774 -137
  14. glaip_sdk/cli/commands/mcps.py +1124 -181
  15. glaip_sdk/cli/commands/models.py +25 -10
  16. glaip_sdk/cli/commands/tools.py +144 -92
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +61 -0
  19. glaip_sdk/cli/config.py +95 -0
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +150 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +143 -53
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +24 -18
  30. glaip_sdk/cli/main.py +420 -145
  31. glaip_sdk/cli/masking.py +136 -0
  32. glaip_sdk/cli/mcp_validators.py +287 -0
  33. glaip_sdk/cli/pager.py +266 -0
  34. glaip_sdk/cli/parsers/__init__.py +7 -0
  35. glaip_sdk/cli/parsers/json_input.py +177 -0
  36. glaip_sdk/cli/resolution.py +28 -21
  37. glaip_sdk/cli/rich_helpers.py +27 -0
  38. glaip_sdk/cli/slash/__init__.py +15 -0
  39. glaip_sdk/cli/slash/accounts_controller.py +500 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +282 -0
  42. glaip_sdk/cli/slash/prompt.py +245 -0
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +1679 -0
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +31 -0
  52. glaip_sdk/cli/transcript/cache.py +536 -0
  53. glaip_sdk/cli/transcript/capture.py +329 -0
  54. glaip_sdk/cli/transcript/export.py +38 -0
  55. glaip_sdk/cli/transcript/history.py +815 -0
  56. glaip_sdk/cli/transcript/launcher.py +77 -0
  57. glaip_sdk/cli/transcript/viewer.py +372 -0
  58. glaip_sdk/cli/update_notifier.py +290 -0
  59. glaip_sdk/cli/utils.py +247 -1238
  60. glaip_sdk/cli/validators.py +16 -18
  61. glaip_sdk/client/__init__.py +2 -1
  62. glaip_sdk/client/_agent_payloads.py +520 -0
  63. glaip_sdk/client/agent_runs.py +147 -0
  64. glaip_sdk/client/agents.py +940 -574
  65. glaip_sdk/client/base.py +163 -48
  66. glaip_sdk/client/main.py +35 -12
  67. glaip_sdk/client/mcps.py +126 -18
  68. glaip_sdk/client/run_rendering.py +415 -0
  69. glaip_sdk/client/shared.py +21 -0
  70. glaip_sdk/client/tools.py +195 -37
  71. glaip_sdk/client/validators.py +20 -48
  72. glaip_sdk/config/constants.py +15 -5
  73. glaip_sdk/exceptions.py +16 -9
  74. glaip_sdk/icons.py +25 -0
  75. glaip_sdk/mcps/__init__.py +21 -0
  76. glaip_sdk/mcps/base.py +345 -0
  77. glaip_sdk/models/__init__.py +90 -0
  78. glaip_sdk/models/agent.py +47 -0
  79. glaip_sdk/models/agent_runs.py +116 -0
  80. glaip_sdk/models/common.py +42 -0
  81. glaip_sdk/models/mcp.py +33 -0
  82. glaip_sdk/models/tool.py +33 -0
  83. glaip_sdk/payload_schemas/__init__.py +7 -0
  84. glaip_sdk/payload_schemas/agent.py +85 -0
  85. glaip_sdk/registry/__init__.py +55 -0
  86. glaip_sdk/registry/agent.py +164 -0
  87. glaip_sdk/registry/base.py +139 -0
  88. glaip_sdk/registry/mcp.py +253 -0
  89. glaip_sdk/registry/tool.py +231 -0
  90. glaip_sdk/rich_components.py +98 -2
  91. glaip_sdk/runner/__init__.py +59 -0
  92. glaip_sdk/runner/base.py +84 -0
  93. glaip_sdk/runner/deps.py +115 -0
  94. glaip_sdk/runner/langgraph.py +597 -0
  95. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  96. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  97. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +158 -0
  98. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  99. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  100. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  101. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +177 -0
  102. glaip_sdk/tools/__init__.py +22 -0
  103. glaip_sdk/tools/base.py +435 -0
  104. glaip_sdk/utils/__init__.py +59 -13
  105. glaip_sdk/utils/a2a/__init__.py +34 -0
  106. glaip_sdk/utils/a2a/event_processor.py +188 -0
  107. glaip_sdk/utils/agent_config.py +53 -40
  108. glaip_sdk/utils/bundler.py +267 -0
  109. glaip_sdk/utils/client.py +111 -0
  110. glaip_sdk/utils/client_utils.py +58 -26
  111. glaip_sdk/utils/datetime_helpers.py +58 -0
  112. glaip_sdk/utils/discovery.py +78 -0
  113. glaip_sdk/utils/display.py +65 -32
  114. glaip_sdk/utils/export.py +143 -0
  115. glaip_sdk/utils/general.py +1 -36
  116. glaip_sdk/utils/import_export.py +20 -25
  117. glaip_sdk/utils/import_resolver.py +492 -0
  118. glaip_sdk/utils/instructions.py +101 -0
  119. glaip_sdk/utils/rendering/__init__.py +115 -1
  120. glaip_sdk/utils/rendering/formatting.py +85 -43
  121. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  122. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +51 -19
  123. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  124. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  125. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  126. glaip_sdk/utils/rendering/models.py +39 -7
  127. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  128. glaip_sdk/utils/rendering/renderer/base.py +672 -759
  129. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  130. glaip_sdk/utils/rendering/renderer/debug.py +75 -22
  131. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  132. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  133. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  134. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  135. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  136. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  137. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  138. glaip_sdk/utils/rendering/state.py +204 -0
  139. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  140. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  141. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  142. glaip_sdk/utils/rendering/steps/format.py +176 -0
  143. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  144. glaip_sdk/utils/rendering/timing.py +36 -0
  145. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  146. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  147. glaip_sdk/utils/resource_refs.py +29 -26
  148. glaip_sdk/utils/runtime_config.py +422 -0
  149. glaip_sdk/utils/serialization.py +184 -51
  150. glaip_sdk/utils/sync.py +142 -0
  151. glaip_sdk/utils/tool_detection.py +33 -0
  152. glaip_sdk/utils/validation.py +21 -30
  153. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +58 -12
  154. glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
  155. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
  156. glaip_sdk/models.py +0 -250
  157. glaip_sdk/utils/rendering/renderer/progress.py +0 -118
  158. glaip_sdk/utils/rendering/steps.py +0 -232
  159. glaip_sdk/utils/rich_utils.py +0 -29
  160. glaip_sdk-0.0.7.dist-info/RECORD +0 -55
  161. {glaip_sdk-0.0.7.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
@@ -13,6 +13,7 @@ from typing import Any
13
13
 
14
14
  import click
15
15
 
16
+ from glaip_sdk.cli.utils import handle_best_effort_check
16
17
  from glaip_sdk.utils.validation import (
17
18
  coerce_timeout,
18
19
  validate_agent_instruction,
@@ -42,7 +43,7 @@ def validate_agent_name_cli(name: str) -> str:
42
43
  try:
43
44
  return validate_agent_name(name)
44
45
  except ValueError as e:
45
- raise click.ClickException(str(e))
46
+ raise click.ClickException(str(e)) from e
46
47
 
47
48
 
48
49
  def validate_agent_instruction_cli(instruction: str) -> str:
@@ -60,7 +61,7 @@ def validate_agent_instruction_cli(instruction: str) -> str:
60
61
  try:
61
62
  return validate_agent_instruction(instruction)
62
63
  except ValueError as e:
63
- raise click.ClickException(str(e))
64
+ raise click.ClickException(str(e)) from e
64
65
 
65
66
 
66
67
  def validate_timeout_cli(timeout: int) -> int:
@@ -78,7 +79,7 @@ def validate_timeout_cli(timeout: int) -> int:
78
79
  try:
79
80
  return validate_timeout(timeout)
80
81
  except ValueError as e:
81
- raise click.ClickException(str(e))
82
+ raise click.ClickException(str(e)) from e
82
83
 
83
84
 
84
85
  def validate_tool_name_cli(name: str) -> str:
@@ -96,7 +97,7 @@ def validate_tool_name_cli(name: str) -> str:
96
97
  try:
97
98
  return validate_tool_name(name)
98
99
  except ValueError as e:
99
- raise click.ClickException(str(e))
100
+ raise click.ClickException(str(e)) from e
100
101
 
101
102
 
102
103
  def validate_mcp_name_cli(name: str) -> str:
@@ -114,7 +115,7 @@ def validate_mcp_name_cli(name: str) -> str:
114
115
  try:
115
116
  return validate_mcp_name(name)
116
117
  except ValueError as e:
117
- raise click.ClickException(str(e))
118
+ raise click.ClickException(str(e)) from e
118
119
 
119
120
 
120
121
  def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Path:
@@ -133,7 +134,7 @@ def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Pa
133
134
  try:
134
135
  return validate_file_path(file_path, must_exist)
135
136
  except ValueError as e:
136
- raise click.ClickException(str(e))
137
+ raise click.ClickException(str(e)) from e
137
138
 
138
139
 
139
140
  def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -> Path:
@@ -152,7 +153,7 @@ def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -
152
153
  try:
153
154
  return validate_directory_path(dir_path, must_exist)
154
155
  except ValueError as e:
155
- raise click.ClickException(str(e))
156
+ raise click.ClickException(str(e)) from e
156
157
 
157
158
 
158
159
  def validate_url_cli(url: str) -> str:
@@ -170,7 +171,7 @@ def validate_url_cli(url: str) -> str:
170
171
  try:
171
172
  return validate_url(url)
172
173
  except ValueError as e:
173
- raise click.ClickException(str(e))
174
+ raise click.ClickException(str(e)) from e
174
175
 
175
176
 
176
177
  def validate_api_key_cli(api_key: str) -> str:
@@ -188,7 +189,7 @@ def validate_api_key_cli(api_key: str) -> str:
188
189
  try:
189
190
  return validate_api_key(api_key)
190
191
  except ValueError as e:
191
- raise click.ClickException(str(e))
192
+ raise click.ClickException(str(e)) from e
192
193
 
193
194
 
194
195
  def coerce_timeout_cli(value: int | float | str) -> int:
@@ -206,7 +207,7 @@ def coerce_timeout_cli(value: int | float | str) -> int:
206
207
  try:
207
208
  return coerce_timeout(value)
208
209
  except ValueError as e:
209
- raise click.ClickException(str(e))
210
+ raise click.ClickException(str(e)) from e
210
211
 
211
212
 
212
213
  def validate_name_uniqueness_cli(
@@ -226,15 +227,12 @@ def validate_name_uniqueness_cli(
226
227
  Raises:
227
228
  click.ClickException: If name is not unique
228
229
  """
229
- try:
230
+
231
+ def _check_duplicate() -> None:
230
232
  existing = finder_func(name=name)
231
233
  if existing:
232
234
  raise click.ClickException(
233
- f"A {resource_type.lower()} named '{name}' already exists. "
234
- "Please choose a unique name."
235
+ f"A {resource_type.lower()} named '{name}' already exists. Please choose a unique name."
235
236
  )
236
- except click.ClickException:
237
- raise
238
- except Exception:
239
- # Non-fatal: best-effort duplicate check
240
- pass
237
+
238
+ handle_best_effort_check(_check_duplicate)
@@ -5,6 +5,7 @@ Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
6
  """
7
7
 
8
+ from glaip_sdk.client.agent_runs import AgentRunsClient
8
9
  from glaip_sdk.client.main import Client
9
10
 
10
- __all__ = ["Client"]
11
+ __all__ = ["AgentRunsClient", "Client"]
@@ -0,0 +1,520 @@
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 AgentImportOperation, get_import_field_plan
19
+ from glaip_sdk.utils.client_utils import extract_ids
20
+
21
+ _LM_CONFLICT_KEYS = {
22
+ "lm_provider",
23
+ "lm_name",
24
+ "lm_base_url",
25
+ "lm_hyperparameters",
26
+ }
27
+
28
+
29
+ def _copy_structure(value: Any) -> Any:
30
+ """Return a defensive copy for mutable payload structures."""
31
+ if isinstance(value, (dict, list, tuple)):
32
+ return deepcopy(value)
33
+ return value
34
+
35
+
36
+ def _sanitize_agent_config(
37
+ agent_config: Mapping[str, Any] | None,
38
+ ) -> dict[str, Any] | None:
39
+ """Remove legacy LM keys that conflict with modern language model fields."""
40
+ if agent_config is None:
41
+ return None
42
+
43
+ sanitized = deepcopy(agent_config)
44
+ for key in _LM_CONFLICT_KEYS:
45
+ sanitized.pop(key, None)
46
+ return sanitized
47
+
48
+
49
+ def _merge_execution_timeout(
50
+ agent_config: dict[str, Any] | None,
51
+ timeout: int | None,
52
+ ) -> dict[str, Any] | None:
53
+ """Merge execution timeout into agent_config if provided."""
54
+ if timeout is None:
55
+ return agent_config
56
+
57
+ merged = agent_config or {}
58
+ merged.setdefault("execution_timeout", timeout)
59
+ return merged
60
+
61
+
62
+ def merge_payload_fields(
63
+ payload: MutableMapping[str, Any],
64
+ extra_fields: Mapping[str, Any] | None,
65
+ operation: AgentImportOperation,
66
+ ) -> None:
67
+ """Merge additional fields into payload respecting schema hints."""
68
+ if not extra_fields:
69
+ return
70
+
71
+ for key, value in extra_fields.items():
72
+ plan = get_import_field_plan(key, operation)
73
+ if not plan.copy or value is None:
74
+ continue
75
+
76
+ copied_value = _copy_structure(value)
77
+ if plan.sanitize and isinstance(copied_value, dict):
78
+ copied_value = _sanitize_agent_config(copied_value)
79
+
80
+ payload[key] = copied_value
81
+
82
+
83
+ def resolve_language_model_fields(
84
+ *,
85
+ model: str | None,
86
+ language_model_id: str | None,
87
+ provider: str | None,
88
+ model_name: str | None,
89
+ default_provider: str = DEFAULT_AGENT_PROVIDER,
90
+ default_model: str = DEFAULT_MODEL,
91
+ ) -> dict[str, Any]:
92
+ """Resolve mutually exclusive language model specification fields."""
93
+ if language_model_id:
94
+ return {"language_model_id": language_model_id}
95
+
96
+ resolved_model = model_name or model or default_model
97
+ resolved_provider = provider if provider is not None else default_provider
98
+
99
+ result: dict[str, Any] = {}
100
+ if resolved_model is not None:
101
+ result["model_name"] = resolved_model
102
+ if resolved_provider:
103
+ result["provider"] = resolved_provider
104
+ return result
105
+
106
+
107
+ def _extract_ids_or_empty(items: Sequence[str | Any] | None) -> list[str]:
108
+ """Extract IDs, returning an empty list when no IDs are present."""
109
+ if items is None:
110
+ return []
111
+
112
+ try:
113
+ iterable = list(items)
114
+ except TypeError:
115
+ return []
116
+
117
+ extracted = extract_ids(iterable)
118
+ return extracted or []
119
+
120
+
121
+ def _extract_existing_ids(source: Any, attribute: str) -> list[str]:
122
+ """Extract IDs from an attribute on the current agent instance."""
123
+ if source is None:
124
+ return []
125
+ value = getattr(source, attribute, None)
126
+ if not value:
127
+ return []
128
+ return _extract_ids_or_empty(value)
129
+
130
+
131
+ def _resolve_relation_ids(
132
+ new_items: Sequence[str | Any] | None,
133
+ current_agent: Any,
134
+ attribute: str,
135
+ ) -> list[str]:
136
+ """Resolve relationship IDs favouring explicit values when provided."""
137
+ if new_items is not None:
138
+ return _extract_ids_or_empty(new_items)
139
+ return _extract_existing_ids(current_agent, attribute)
140
+
141
+
142
+ def _pick_optional(
143
+ new_value: Any,
144
+ fallback: Any,
145
+ *,
146
+ transform: Callable[[Any], Any] | None = None,
147
+ ) -> Any | None:
148
+ """Return new_value when present, otherwise fallback, applying transform."""
149
+ value = new_value if new_value is not None else fallback
150
+ if value is None:
151
+ return None
152
+ return transform(value) if transform else value
153
+
154
+
155
+ def _existing_language_model_fields(current_agent: Any) -> dict[str, Any]:
156
+ """Derive language model fields from the current agent or defaults."""
157
+ result: dict[str, Any] = {}
158
+
159
+ language_model_id = getattr(current_agent, "language_model_id", None)
160
+ if language_model_id:
161
+ result["language_model_id"] = language_model_id
162
+ return result
163
+
164
+ agent_config = getattr(current_agent, "agent_config", None)
165
+ if isinstance(agent_config, Mapping):
166
+ provider = agent_config.get("lm_provider")
167
+ model_name = agent_config.get("lm_name")
168
+ if provider:
169
+ result["provider"] = provider
170
+ if model_name:
171
+ result["model_name"] = model_name
172
+
173
+ if not result:
174
+ if DEFAULT_AGENT_PROVIDER:
175
+ result["provider"] = DEFAULT_AGENT_PROVIDER
176
+ result["model_name"] = DEFAULT_MODEL
177
+
178
+ return result
179
+
180
+
181
+ @dataclass(slots=True)
182
+ class AgentListParams:
183
+ """Structured query parameters for listing agents."""
184
+
185
+ agent_type: str | None = None
186
+ framework: str | None = None
187
+ name: str | None = None
188
+ version: str | None = None
189
+ limit: int | None = None
190
+ page: int | None = None
191
+ include_deleted: bool | None = None
192
+ created_at_start: str | None = None
193
+ created_at_end: str | None = None
194
+ updated_at_start: str | None = None
195
+ updated_at_end: str | None = None
196
+ sync_langflow_agents: bool | None = None
197
+ metadata: Mapping[str, str] | None = None
198
+
199
+ def to_query_params(self) -> dict[str, Any]:
200
+ """Convert the dataclass to API-ready query params."""
201
+ params = self._base_filter_params()
202
+ self._apply_pagination_params(params)
203
+ self._apply_timestamp_filters(params)
204
+ self._apply_metadata_filters(params)
205
+
206
+ return params
207
+
208
+ def _base_filter_params(self) -> dict[str, Any]:
209
+ """Build base filter parameters from non-None fields.
210
+
211
+ Returns:
212
+ Dictionary of filter parameters with non-None values.
213
+ """
214
+ return {
215
+ key: value
216
+ for key, value in (
217
+ ("agent_type", self.agent_type),
218
+ ("framework", self.framework),
219
+ ("name", self.name),
220
+ ("version", self.version),
221
+ )
222
+ if value is not None
223
+ }
224
+
225
+ def _apply_pagination_params(self, params: dict[str, Any]) -> None:
226
+ """Apply pagination parameters to the params dictionary.
227
+
228
+ Args:
229
+ params: Dictionary to update with pagination parameters.
230
+ """
231
+ if self.limit is not None:
232
+ if not 1 <= self.limit <= 100:
233
+ raise ValueError("limit must be between 1 and 100 inclusive")
234
+ params["limit"] = self.limit
235
+
236
+ if self.page is not None:
237
+ if self.page < 1:
238
+ raise ValueError("page must be >= 1")
239
+ params["page"] = self.page
240
+
241
+ if self.include_deleted is not None:
242
+ params["include_deleted"] = str(self.include_deleted).lower()
243
+
244
+ if self.sync_langflow_agents is not None:
245
+ params["sync_langflow_agents"] = str(self.sync_langflow_agents).lower()
246
+
247
+ def _apply_timestamp_filters(self, params: dict[str, Any]) -> None:
248
+ """Apply timestamp filter parameters to the params dictionary.
249
+
250
+ Args:
251
+ params: Dictionary to update with timestamp filter parameters.
252
+ """
253
+ timestamp_filters = {
254
+ "created_at_start": self.created_at_start,
255
+ "created_at_end": self.created_at_end,
256
+ "updated_at_start": self.updated_at_start,
257
+ "updated_at_end": self.updated_at_end,
258
+ }
259
+ for key, value in timestamp_filters.items():
260
+ if value is not None:
261
+ params[key] = value
262
+
263
+ def _apply_metadata_filters(self, params: dict[str, Any]) -> None:
264
+ """Apply metadata filter parameters to the params dictionary.
265
+
266
+ Args:
267
+ params: Dictionary to update with metadata filter parameters.
268
+ """
269
+ if not self.metadata:
270
+ return
271
+ for key, value in self.metadata.items():
272
+ if value is not None:
273
+ params[f"metadata.{key}"] = value
274
+
275
+
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
+ @dataclass(slots=True)
309
+ class AgentCreateRequest:
310
+ """Declarative representation of an agent creation payload."""
311
+
312
+ name: str
313
+ instruction: str
314
+ model: str | None = DEFAULT_MODEL
315
+ language_model_id: str | None = None
316
+ provider: str | None = None
317
+ model_name: str | None = None
318
+ agent_type: str = DEFAULT_AGENT_TYPE
319
+ framework: str = DEFAULT_AGENT_FRAMEWORK
320
+ version: str = DEFAULT_AGENT_VERSION
321
+ account_id: str | None = None
322
+ description: str | None = None
323
+ metadata: Mapping[str, Any] | None = None
324
+ tools: Sequence[str | Any] | None = None
325
+ tool_configs: Mapping[str, Any] | None = None
326
+ agents: Sequence[str | Any] | None = None
327
+ mcps: Sequence[str | Any] | None = None
328
+ agent_config: Mapping[str, Any] | None = None
329
+ timeout: int | None = None
330
+ a2a_profile: Mapping[str, Any] | None = None
331
+ extras: Mapping[str, Any] | None = None
332
+
333
+ def to_payload(self) -> dict[str, Any]:
334
+ """Materialise the request as a dict suitable for API submission."""
335
+ payload: dict[str, Any] = {
336
+ "name": self.name.strip(),
337
+ "instruction": self.instruction.strip(),
338
+ "type": self.agent_type,
339
+ "framework": self.framework,
340
+ "version": self.version,
341
+ }
342
+
343
+ payload.update(
344
+ resolve_language_model_fields(
345
+ model=self.model,
346
+ language_model_id=self.language_model_id,
347
+ provider=self.provider,
348
+ model_name=self.model_name,
349
+ )
350
+ )
351
+
352
+ if self.account_id is not None:
353
+ payload["account_id"] = self.account_id
354
+ if self.description is not None:
355
+ payload["description"] = self.description
356
+ if self.metadata is not None:
357
+ payload["metadata"] = _copy_structure(self.metadata)
358
+
359
+ if self.a2a_profile is not None:
360
+ payload["a2a_profile"] = _copy_structure(self.a2a_profile)
361
+
362
+ tool_ids = extract_ids(list(self.tools) if self.tools is not None else None)
363
+ if tool_ids:
364
+ payload["tools"] = tool_ids
365
+
366
+ agent_ids = extract_ids(list(self.agents) if self.agents is not None else None)
367
+ if agent_ids:
368
+ payload["agents"] = agent_ids
369
+
370
+ mcp_ids = extract_ids(list(self.mcps) if self.mcps is not None else None)
371
+ if mcp_ids:
372
+ payload["mcps"] = mcp_ids
373
+
374
+ if self.tool_configs is not None:
375
+ payload["tool_configs"] = _copy_structure(self.tool_configs)
376
+
377
+ effective_agent_config = _sanitize_agent_config(self.agent_config)
378
+ effective_agent_config = _merge_execution_timeout(effective_agent_config, self.timeout)
379
+ if effective_agent_config:
380
+ payload["agent_config"] = effective_agent_config
381
+
382
+ merge_payload_fields(payload, self.extras, "create")
383
+ return payload
384
+
385
+
386
+ @dataclass(slots=True)
387
+ class AgentUpdateRequest:
388
+ """Declarative representation of an agent update payload."""
389
+
390
+ name: str | None = None
391
+ instruction: str | None = None
392
+ description: str | None = None
393
+ model: str | None = None
394
+ language_model_id: str | None = None
395
+ provider: str | None = None
396
+ model_name: str | None = None
397
+ agent_type: str | None = None
398
+ framework: str | None = None
399
+ version: str | None = None
400
+ account_id: str | None = None
401
+ metadata: Mapping[str, Any] | None = None
402
+ tools: Sequence[str | Any] | None = None
403
+ tool_configs: Mapping[str, Any] | None = None
404
+ agents: Sequence[str | Any] | None = None
405
+ mcps: Sequence[str | Any] | None = None
406
+ agent_config: Mapping[str, Any] | None = None
407
+ a2a_profile: Mapping[str, Any] | None = None
408
+ extras: Mapping[str, Any] | None = None
409
+
410
+ def to_payload(self, current_agent: Any) -> dict[str, Any]:
411
+ """Materialise the request using current agent data as fallbacks."""
412
+ payload = _build_base_update_payload(self, current_agent)
413
+ payload.update(_resolve_update_language_model_fields(self, current_agent))
414
+ payload.update(_collect_optional_update_fields(self, current_agent))
415
+ payload.update(_collect_relationship_fields(self, current_agent))
416
+
417
+ agent_config_value = _resolve_agent_config_update(self, current_agent)
418
+ if agent_config_value is not None:
419
+ payload["agent_config"] = agent_config_value
420
+
421
+ merge_payload_fields(payload, self.extras, "update")
422
+ return payload
423
+
424
+
425
+ __all__ = [
426
+ "AgentCreateRequest",
427
+ "AgentListParams",
428
+ "AgentListResult",
429
+ "AgentUpdateRequest",
430
+ "merge_payload_fields",
431
+ "resolve_language_model_fields",
432
+ ]
433
+
434
+
435
+ def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
436
+ """Populate immutable agent update fields using request data or existing agent defaults."""
437
+ # Support both "agent_type" (runtime class) and "type" (API response) attributes
438
+ current_type = getattr(current_agent, "agent_type", None) or getattr(current_agent, "type", None)
439
+ return {
440
+ "name": (request.name.strip() if request.name is not None else getattr(current_agent, "name", None)),
441
+ "instruction": (
442
+ request.instruction.strip()
443
+ if request.instruction is not None
444
+ else getattr(current_agent, "instruction", None)
445
+ ),
446
+ "type": request.agent_type or current_type or DEFAULT_AGENT_TYPE,
447
+ "framework": request.framework or getattr(current_agent, "framework", None) or DEFAULT_AGENT_FRAMEWORK,
448
+ "version": request.version or getattr(current_agent, "version", None) or DEFAULT_AGENT_VERSION,
449
+ }
450
+
451
+
452
+ def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
453
+ """Resolve the language-model portion of an update request with sensible fallbacks."""
454
+ fields = resolve_language_model_fields(
455
+ model=request.model,
456
+ language_model_id=request.language_model_id,
457
+ provider=request.provider,
458
+ model_name=request.model_name,
459
+ )
460
+ if not fields:
461
+ fields = _existing_language_model_fields(current_agent)
462
+ return fields
463
+
464
+
465
+ def _collect_optional_update_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
466
+ """Collect optional agent fields, preserving current values when updates are absent."""
467
+ result: dict[str, Any] = {}
468
+
469
+ for field_name, value in (
470
+ ("account_id", request.account_id),
471
+ ("description", request.description),
472
+ ):
473
+ resolved_value = _pick_optional(value, getattr(current_agent, field_name, None))
474
+ if resolved_value is not None:
475
+ result[field_name] = resolved_value
476
+
477
+ metadata_value = _pick_optional(
478
+ request.metadata,
479
+ getattr(current_agent, "metadata", None),
480
+ transform=_copy_structure,
481
+ )
482
+ if metadata_value is not None:
483
+ result["metadata"] = metadata_value
484
+
485
+ profile_value = _pick_optional(
486
+ request.a2a_profile,
487
+ getattr(current_agent, "a2a_profile", None),
488
+ transform=_copy_structure,
489
+ )
490
+ if profile_value is not None:
491
+ result["a2a_profile"] = profile_value
492
+
493
+ tool_configs_value = _pick_optional(
494
+ request.tool_configs,
495
+ getattr(current_agent, "tool_configs", None),
496
+ transform=_copy_structure,
497
+ )
498
+ if tool_configs_value is not None:
499
+ result["tool_configs"] = tool_configs_value
500
+
501
+ return result
502
+
503
+
504
+ def _collect_relationship_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
505
+ """Return relationship identifiers (tools/agents/mcps) for an update request."""
506
+ return {
507
+ "tools": _resolve_relation_ids(request.tools, current_agent, "tools"),
508
+ "agents": _resolve_relation_ids(request.agents, current_agent, "agents"),
509
+ "mcps": _resolve_relation_ids(request.mcps, current_agent, "mcps"),
510
+ }
511
+
512
+
513
+ def _resolve_agent_config_update(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any] | None:
514
+ """Determine the agent_config payload to send, if any."""
515
+ effective_agent_config = _sanitize_agent_config(request.agent_config)
516
+ if effective_agent_config is not None:
517
+ return effective_agent_config
518
+ if getattr(current_agent, "agent_config", None):
519
+ return _sanitize_agent_config(current_agent.agent_config)
520
+ return None