glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -1,1369 +0,0 @@
1
- """Agent CLI commands for AIP SDK.
2
-
3
- Authors:
4
- Raymond Christopher (raymond.christopher@gdplabs.id)
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- import json
10
- import os
11
- from collections.abc import Mapping
12
- from pathlib import Path
13
- from typing import Any
14
-
15
- import click
16
- from rich.console import Console
17
-
18
- from glaip_sdk.branding import (
19
- ACCENT_STYLE,
20
- ERROR_STYLE,
21
- INFO,
22
- SUCCESS,
23
- SUCCESS_STYLE,
24
- WARNING_STYLE,
25
- )
26
- from glaip_sdk.cli.agent_config import (
27
- merge_agent_config_with_cli_args as merge_import_with_cli_args,
28
- )
29
- from glaip_sdk.cli.agent_config import (
30
- resolve_agent_language_model_selection as resolve_language_model_selection,
31
- )
32
- from glaip_sdk.cli.agent_config import (
33
- sanitize_agent_config_for_cli as sanitize_agent_config,
34
- )
35
- from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
36
- from glaip_sdk.cli.display import (
37
- build_resource_result_data,
38
- display_agent_run_suggestions,
39
- display_confirmation_prompt,
40
- display_creation_success,
41
- display_deletion_success,
42
- display_update_success,
43
- handle_json_output,
44
- handle_rich_output,
45
- print_api_error,
46
- )
47
- from glaip_sdk.cli.io import (
48
- export_resource_to_file_with_validation as export_resource_to_file,
49
- )
50
- from glaip_sdk.cli.io import (
51
- fetch_raw_resource_details,
52
- )
53
- from glaip_sdk.cli.io import (
54
- load_resource_from_file_with_validation as load_resource_from_file,
55
- )
56
- from glaip_sdk.cli.resolution import resolve_resource_reference
57
- from glaip_sdk.cli.rich_helpers import markup_text, print_markup
58
- from glaip_sdk.cli.transcript import (
59
- maybe_launch_post_run_viewer,
60
- store_transcript_for_session,
61
- )
62
- from glaip_sdk.cli.utils import (
63
- _fuzzy_pick_for_resources,
64
- build_renderer,
65
- coerce_to_row,
66
- get_client,
67
- output_list,
68
- output_result,
69
- spinner_context,
70
- )
71
- from glaip_sdk.cli.validators import (
72
- validate_agent_instruction_cli as validate_agent_instruction,
73
- )
74
- from glaip_sdk.cli.validators import (
75
- validate_agent_name_cli as validate_agent_name,
76
- )
77
- from glaip_sdk.cli.validators import (
78
- validate_timeout_cli as validate_timeout,
79
- )
80
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
81
- from glaip_sdk.exceptions import AgentTimeoutError
82
- from glaip_sdk.icons import ICON_AGENT
83
- from glaip_sdk.utils import format_datetime, is_uuid
84
- from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
85
- from glaip_sdk.utils.import_export import convert_export_to_import_format
86
- from glaip_sdk.utils.rendering.renderer.toggle import TranscriptToggleController
87
- from glaip_sdk.utils.validation import coerce_timeout
88
-
89
- console = Console()
90
-
91
- # Error message constants
92
- AGENT_NOT_FOUND_ERROR = "Agent not found"
93
-
94
-
95
- def _safe_agent_attribute(agent: Any, name: str) -> Any:
96
- """Return attribute value for ``name`` while filtering Mock sentinels."""
97
- try:
98
- value = getattr(agent, name)
99
- except Exception:
100
- return None
101
-
102
- if hasattr(value, "_mock_name"):
103
- return None
104
- return value
105
-
106
-
107
- def _coerce_mapping_candidate(candidate: Any) -> dict[str, Any] | None:
108
- """Convert a mapping-like candidate to a plain dict when possible."""
109
- if candidate is None:
110
- return None
111
- if isinstance(candidate, Mapping):
112
- return dict(candidate)
113
- return None
114
-
115
-
116
- def _call_agent_method(agent: Any, method_name: str) -> dict[str, Any] | None:
117
- """Attempt to call the named method and coerce its output to a dict."""
118
- method = getattr(agent, method_name, None)
119
- if not callable(method):
120
- return None
121
- try:
122
- candidate = method()
123
- except Exception:
124
- return None
125
- return _coerce_mapping_candidate(candidate)
126
-
127
-
128
- def _coerce_agent_via_methods(agent: Any) -> dict[str, Any] | None:
129
- """Try standard serialisation helpers to produce a mapping."""
130
- for attr in ("model_dump", "dict", "to_dict"):
131
- mapping = _call_agent_method(agent, attr)
132
- if mapping is not None:
133
- return mapping
134
- return None
135
-
136
-
137
- def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
138
- """Construct a minimal mapping from well-known agent attributes."""
139
- fallback_fields = (
140
- "id",
141
- "name",
142
- "instruction",
143
- "description",
144
- "model",
145
- "agent_config",
146
- "tools",
147
- "agents",
148
- "mcps",
149
- "timeout",
150
- "tool_configs",
151
- )
152
-
153
- fallback: dict[str, Any] = {}
154
- for field in fallback_fields:
155
- value = _safe_agent_attribute(agent, field)
156
- if value is not None:
157
- fallback[field] = value
158
-
159
- return fallback or {"name": str(agent)}
160
-
161
-
162
- def _prepare_agent_output(agent: Any) -> dict[str, Any]:
163
- """Build a JSON-serialisable mapping for CLI output."""
164
- method_mapping = _coerce_agent_via_methods(agent)
165
- if method_mapping is not None:
166
- return method_mapping
167
-
168
- intrinsic = _coerce_mapping_candidate(agent)
169
- if intrinsic is not None:
170
- return intrinsic
171
-
172
- return _build_fallback_agent_mapping(agent)
173
-
174
-
175
- def _fetch_full_agent_details(client: Any, agent: Any) -> Any | None:
176
- """Fetch full agent details by ID to ensure all fields are populated."""
177
- try:
178
- agent_id = str(getattr(agent, "id", "")).strip()
179
- if agent_id:
180
- return client.agents.get_agent_by_id(agent_id)
181
- except Exception:
182
- # If fetching full details fails, continue with the resolved object
183
- pass
184
- return agent
185
-
186
-
187
- def _normalise_model_name(value: Any) -> str | None:
188
- """Return a cleaned model name or None when not usable."""
189
- if value is None:
190
- return None
191
- if isinstance(value, str):
192
- cleaned = value.strip()
193
- return cleaned or None
194
- if isinstance(value, bool):
195
- return None
196
- return str(value)
197
-
198
-
199
- def _model_from_config(agent: Any) -> str | None:
200
- """Extract a usable model name from an agent's configuration mapping."""
201
- config = getattr(agent, "agent_config", None)
202
- if not config or not isinstance(config, dict):
203
- return None
204
-
205
- for key in ("lm_name", "model"):
206
- normalised = _normalise_model_name(config.get(key))
207
- if normalised:
208
- return normalised
209
- return None
210
-
211
-
212
- def _get_agent_model_name(agent: Any) -> str | None:
213
- """Extract model name from agent configuration."""
214
- config_model = _model_from_config(agent)
215
- if config_model:
216
- return config_model
217
-
218
- normalised_attr = _normalise_model_name(getattr(agent, "model", None))
219
- if normalised_attr:
220
- return normalised_attr
221
-
222
- return DEFAULT_MODEL
223
-
224
-
225
- def _resolve_resources_by_name(
226
- _client: Any, items: tuple[str, ...], resource_type: str, find_func: Any, label: str
227
- ) -> list[str]:
228
- """Resolve resource names/IDs to IDs, handling ambiguity.
229
-
230
- Args:
231
- client: API client
232
- items: Tuple of resource names/IDs
233
- resource_type: Type of resource ("tool" or "agent")
234
- find_func: Function to find resources by name
235
- label: Label for error messages
236
-
237
- Returns:
238
- List of resolved resource IDs
239
- """
240
- out = []
241
- for ref in items or ():
242
- if is_uuid(ref):
243
- out.append(ref)
244
- continue
245
-
246
- matches = find_func(name=ref)
247
- if not matches:
248
- raise click.ClickException(f"{label} not found: {ref}")
249
- if len(matches) > 1:
250
- raise click.ClickException(f"Multiple {resource_type}s named '{ref}'. Use ID instead.")
251
- out.append(str(matches[0].id))
252
- return out
253
-
254
-
255
- def _fetch_and_format_raw_agent_data(client: Any, agent: Any) -> dict | None:
256
- """Fetch raw agent data and format it for display."""
257
- try:
258
- raw_agent_data = fetch_raw_resource_details(client, agent, "agents")
259
- if not raw_agent_data:
260
- return None
261
-
262
- # Format dates for better display
263
- formatted_data = raw_agent_data.copy()
264
- if "created_at" in formatted_data:
265
- formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
266
- if "updated_at" in formatted_data:
267
- formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
268
-
269
- return formatted_data
270
- except Exception:
271
- return None
272
-
273
-
274
- def _format_fallback_agent_data(client: Any, agent: Any) -> dict:
275
- """Format fallback agent data using Pydantic model."""
276
- full_agent = _fetch_full_agent_details(client, agent)
277
-
278
- # Define fields to extract
279
- fields = [
280
- "id",
281
- "name",
282
- "type",
283
- "framework",
284
- "version",
285
- "description",
286
- "instruction",
287
- "created_at",
288
- "updated_at",
289
- "metadata",
290
- "language_model_id",
291
- "agent_config",
292
- "tools",
293
- "agents",
294
- "mcps",
295
- "a2a_profile",
296
- "tool_configs",
297
- ]
298
-
299
- result_data = build_resource_result_data(full_agent, fields)
300
-
301
- # Handle missing instruction
302
- if result_data.get("instruction") in ["N/A", None, ""]:
303
- result_data["instruction"] = "-"
304
-
305
- # Format dates for better display
306
- for date_field in ["created_at", "updated_at"]:
307
- if result_data.get(date_field) and result_data[date_field] not in ["N/A", None]:
308
- result_data[date_field] = format_datetime(result_data[date_field])
309
-
310
- return result_data
311
-
312
-
313
- def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
314
- """Display full agent details using raw API data to preserve ALL fields."""
315
- if agent is None:
316
- handle_rich_output(ctx, markup_text(f"[{ERROR_STYLE}]❌ No agent provided[/]"))
317
- return
318
-
319
- # Try to fetch and format raw agent data first
320
- with spinner_context(
321
- ctx,
322
- "[bold blue]Loading agent details…[/bold blue]",
323
- console_override=console,
324
- ):
325
- formatted_data = _fetch_and_format_raw_agent_data(client, agent)
326
-
327
- if formatted_data:
328
- # Use raw API data - this preserves ALL fields including account_id
329
- panel_title = f"{ICON_AGENT} {formatted_data.get('name', 'Unknown')}"
330
- output_result(
331
- ctx,
332
- formatted_data,
333
- title=panel_title,
334
- )
335
- else:
336
- # Fall back to Pydantic model data if raw fetch fails
337
- handle_rich_output(
338
- ctx,
339
- markup_text(f"[{WARNING_STYLE}]Falling back to Pydantic model data[/]"),
340
- )
341
-
342
- with spinner_context(
343
- ctx,
344
- "[bold blue]Preparing fallback agent details…[/bold blue]",
345
- console_override=console,
346
- ):
347
- result_data = _format_fallback_agent_data(client, agent)
348
-
349
- # Display using output_result
350
- output_result(
351
- ctx,
352
- result_data,
353
- title="Agent Details",
354
- )
355
-
356
-
357
- @click.group(name="agents", no_args_is_help=True)
358
- def agents_group() -> None:
359
- """Agent management operations."""
360
- pass
361
-
362
-
363
- def _resolve_agent(
364
- ctx: Any,
365
- client: Any,
366
- ref: str,
367
- select: int | None = None,
368
- interface_preference: str = "fuzzy",
369
- ) -> Any | None:
370
- """Resolve agent reference (ID or name) with ambiguity handling.
371
-
372
- Args:
373
- ctx: Click context object for CLI operations.
374
- client: AIP client instance for API operations.
375
- ref: Agent reference (ID or name) to resolve.
376
- select: Pre-selected agent index for non-interactive mode.
377
- interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list.
378
-
379
- Returns:
380
- Resolved agent object or None if not found.
381
- """
382
- return resolve_resource_reference(
383
- ctx,
384
- client,
385
- ref,
386
- "agent",
387
- client.agents.get_agent_by_id,
388
- client.agents.find_agents,
389
- "Agent",
390
- select=select,
391
- interface_preference=interface_preference,
392
- )
393
-
394
-
395
- @agents_group.command(name="list")
396
- @click.option("--simple", is_flag=True, help="Show simple table without interactive picker")
397
- @click.option("--type", "agent_type", help="Filter by agent type (config, code, a2a, langflow)")
398
- @click.option("--framework", help="Filter by framework (langchain, langgraph, google_adk)")
399
- @click.option("--name", help="Filter by partial name match (case-insensitive)")
400
- @click.option("--version", help="Filter by exact version match")
401
- @click.option(
402
- "--sync-langflow",
403
- is_flag=True,
404
- help="Sync with LangFlow server before listing (only applies when filtering by langflow type)",
405
- )
406
- @output_flags()
407
- @click.pass_context
408
- def list_agents(
409
- ctx: Any,
410
- simple: bool,
411
- agent_type: str | None,
412
- framework: str | None,
413
- name: str | None,
414
- version: str | None,
415
- sync_langflow: bool,
416
- ) -> None:
417
- """List agents with optional filtering."""
418
- try:
419
- client = get_client(ctx)
420
- with spinner_context(
421
- ctx,
422
- "[bold blue]Fetching agents…[/bold blue]",
423
- console_override=console,
424
- ):
425
- agents = client.agents.list_agents(
426
- agent_type=agent_type,
427
- framework=framework,
428
- name=name,
429
- version=version,
430
- sync_langflow_agents=sync_langflow,
431
- )
432
-
433
- # Define table columns: (data_key, header, style, width)
434
- columns = [
435
- ("id", "ID", "dim", 36),
436
- ("name", "Name", ACCENT_STYLE, None),
437
- ("type", "Type", WARNING_STYLE, None),
438
- ("framework", "Framework", INFO, None),
439
- ("version", "Version", SUCCESS, None),
440
- ]
441
-
442
- # Transform function for safe attribute access
443
- def transform_agent(agent: Any) -> dict[str, Any]:
444
- row = coerce_to_row(agent, ["id", "name", "type", "framework", "version"])
445
- # Ensure id is always a string
446
- row["id"] = str(row["id"])
447
- return row
448
-
449
- # Use fuzzy picker for interactive agent selection and details (default behavior)
450
- # Skip if --simple flag is used, a name filter is applied, or non-rich output is requested
451
- ctx_obj = ctx.obj if isinstance(getattr(ctx, "obj", None), dict) else {}
452
- current_view = ctx_obj.get("view")
453
- interactive_enabled = (
454
- not simple
455
- and name is None
456
- and current_view not in {"json", "plain", "md"}
457
- and console.is_terminal
458
- and os.isatty(1)
459
- and len(agents) > 0
460
- )
461
-
462
- if interactive_enabled:
463
- picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
464
- if picked_agent:
465
- _display_agent_details(ctx, client, picked_agent)
466
- # Show run suggestions via centralized display helper
467
- handle_rich_output(ctx, display_agent_run_suggestions(picked_agent))
468
- return
469
-
470
- # Show simple table (either --simple flag or non-interactive)
471
- output_list(
472
- ctx,
473
- agents,
474
- f"{ICON_AGENT} Available Agents",
475
- columns,
476
- transform_agent,
477
- skip_picker=simple or any(param is not None for param in (agent_type, framework, name, version)),
478
- use_pager=False,
479
- )
480
-
481
- except Exception as e:
482
- raise click.ClickException(str(e)) from e
483
-
484
-
485
- @agents_group.command()
486
- @click.argument("agent_ref")
487
- @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
488
- @click.option(
489
- "--export",
490
- type=click.Path(dir_okay=False, writable=True),
491
- help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
492
- )
493
- @output_flags()
494
- @click.pass_context
495
- def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> None:
496
- """Get agent details.
497
-
498
- Examples:
499
- aip agents get my-agent
500
- aip agents get my-agent --export agent.json # Exports complete configuration as JSON
501
- aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
502
- """
503
- try:
504
- client = get_client(ctx)
505
-
506
- # Resolve agent with ambiguity handling - use questionary interface for traditional UX
507
- agent = _resolve_agent(ctx, client, agent_ref, select, interface_preference="questionary")
508
-
509
- # Handle export option
510
- if export:
511
- export_path = Path(export)
512
- # Auto-detect format from file extension
513
- detected_format = detect_export_format(export_path)
514
-
515
- # Always export comprehensive data - re-fetch agent with full details
516
- try:
517
- with spinner_context(
518
- ctx,
519
- "[bold blue]Fetching complete agent data…[/bold blue]",
520
- console_override=console,
521
- ):
522
- agent = client.agents.get_agent_by_id(agent.id)
523
- except Exception as e:
524
- handle_rich_output(
525
- ctx,
526
- markup_text(f"[{WARNING_STYLE}]⚠️ Could not fetch full agent details: {e}[/]"),
527
- )
528
- handle_rich_output(
529
- ctx,
530
- markup_text(f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]"),
531
- )
532
-
533
- export_resource_to_file(agent, export_path, detected_format)
534
- handle_rich_output(
535
- ctx,
536
- markup_text(
537
- f"[{SUCCESS_STYLE}]✅ Complete agent configuration exported to: {export_path} "
538
- f"(format: {detected_format})[/]"
539
- ),
540
- )
541
-
542
- # Display full agent details using the standardized helper
543
- _display_agent_details(ctx, client, agent)
544
-
545
- # Show run suggestions via centralized display helper
546
- handle_rich_output(ctx, display_agent_run_suggestions(agent))
547
-
548
- except Exception as e:
549
- raise click.ClickException(str(e)) from e
550
-
551
-
552
- def _validate_run_input(input_option: str | None, input_text: str | None) -> str:
553
- """Validate and determine the final input text for agent run."""
554
- final_input_text = input_option if input_option else input_text
555
-
556
- if not final_input_text:
557
- raise click.ClickException("Input text is required. Use either positional argument or --input option.")
558
-
559
- return final_input_text
560
-
561
-
562
- def _parse_chat_history(chat_history: str | None) -> list[dict[str, Any]] | None:
563
- """Parse chat history JSON if provided."""
564
- if not chat_history:
565
- return None
566
-
567
- try:
568
- return json.loads(chat_history)
569
- except json.JSONDecodeError as err:
570
- raise click.ClickException("Invalid JSON in chat history") from err
571
-
572
-
573
- def _setup_run_renderer(ctx: Any, save: str | None, verbose: bool) -> Any:
574
- """Set up renderer and working console for agent run."""
575
- tty_enabled = bool(get_ctx_value(ctx, "tty", True))
576
- return build_renderer(
577
- ctx,
578
- save_path=save,
579
- verbose=verbose,
580
- _tty_enabled=tty_enabled,
581
- )
582
-
583
-
584
- def _maybe_attach_transcript_toggle(ctx: Any, renderer: Any) -> None:
585
- """Attach transcript toggle controller when interactive TTY is available."""
586
- if renderer is None:
587
- return
588
-
589
- console_obj = getattr(renderer, "console", None)
590
- if console_obj is None or not getattr(console_obj, "is_terminal", False):
591
- return
592
-
593
- tty_enabled = bool(get_ctx_value(ctx, "tty", True))
594
- if not tty_enabled:
595
- return
596
-
597
- controller = TranscriptToggleController(enabled=True)
598
- renderer.transcript_controller = controller
599
-
600
-
601
- def _prepare_run_kwargs(
602
- agent: Any,
603
- final_input_text: str,
604
- files: list[str] | None,
605
- parsed_chat_history: list[dict[str, Any]] | None,
606
- renderer: Any,
607
- tty_enabled: bool,
608
- ) -> dict[str, Any]:
609
- """Prepare kwargs for agent run."""
610
- run_kwargs = {
611
- "agent_id": agent.id,
612
- "message": final_input_text,
613
- "files": list(files),
614
- "agent_name": agent.name,
615
- "tty": tty_enabled,
616
- }
617
-
618
- if parsed_chat_history:
619
- run_kwargs["chat_history"] = parsed_chat_history
620
-
621
- if renderer is not None:
622
- run_kwargs["renderer"] = renderer
623
-
624
- return run_kwargs
625
-
626
-
627
- def _handle_run_output(ctx: Any, result: Any, renderer: Any) -> None:
628
- """Handle output formatting for agent run results."""
629
- printed_by_renderer = bool(renderer)
630
- selected_view = get_ctx_value(ctx, "view", "rich")
631
-
632
- if not printed_by_renderer:
633
- if selected_view == "json":
634
- handle_json_output(ctx, {"output": result})
635
- elif selected_view == "md":
636
- click.echo(f"# Assistant\n\n{result}")
637
- elif selected_view == "plain":
638
- click.echo(result)
639
-
640
-
641
- def _save_run_transcript(save: str | None, result: Any, working_console: Any) -> None:
642
- """Save transcript to file if requested."""
643
- if not save:
644
- return
645
-
646
- ext = (save.rsplit(".", 1)[-1] or "").lower()
647
- if ext == "json":
648
- save_data = {
649
- "output": result or "",
650
- "full_debug_output": getattr(working_console, "get_captured_output", lambda: "")(),
651
- "timestamp": "captured during agent execution",
652
- }
653
- content = json.dumps(save_data, indent=2)
654
- else:
655
- full_output = getattr(working_console, "get_captured_output", lambda: "")()
656
- if full_output:
657
- content = f"# Agent Debug Log\n\n{full_output}\n\n---\n\n## Final Result\n\n{result or ''}\n"
658
- else:
659
- content = f"# Assistant\n\n{result or ''}\n"
660
-
661
- with open(save, "w", encoding="utf-8") as f:
662
- f.write(content)
663
- print_markup(f"[{SUCCESS_STYLE}]Full debug output saved to: {save}[/]", console=console)
664
-
665
-
666
- @agents_group.command()
667
- @click.argument("agent_ref")
668
- @click.argument("input_text", required=False)
669
- @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
670
- @click.option("--input", "input_option", help="Input text for the agent")
671
- @click.option("--chat-history", help="JSON string of chat history")
672
- @click.option(
673
- "--timeout",
674
- default=DEFAULT_AGENT_RUN_TIMEOUT,
675
- type=int,
676
- help="Agent execution timeout in seconds (default: 300s)",
677
- )
678
- @click.option(
679
- "--save",
680
- type=click.Path(dir_okay=False, writable=True),
681
- help="Save transcript to file (md or json)",
682
- )
683
- @click.option(
684
- "--file",
685
- "files",
686
- multiple=True,
687
- type=click.Path(exists=True),
688
- help="Attach file(s)",
689
- )
690
- @click.option(
691
- "--verbose/--no-verbose",
692
- default=False,
693
- help="Show detailed SSE events during streaming",
694
- )
695
- @output_flags()
696
- @click.pass_context
697
- def run(
698
- ctx: Any,
699
- agent_ref: str,
700
- select: int | None,
701
- input_text: str | None,
702
- input_option: str | None,
703
- chat_history: str | None,
704
- timeout: float | None,
705
- save: str | None,
706
- files: tuple[str, ...] | None,
707
- verbose: bool,
708
- ) -> None:
709
- """Run an agent with input text.
710
-
711
- Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
712
-
713
- Examples:
714
- aip agents run my-agent "Hello world"
715
- aip agents run agent-123 "Process this data" --timeout 600
716
- aip agents run my-agent --input "Hello world" # Legacy style
717
- """
718
- final_input_text = _validate_run_input(input_option, input_text)
719
-
720
- if verbose:
721
- _emit_verbose_guidance(ctx)
722
- return
723
-
724
- try:
725
- client = get_client(ctx)
726
- agent = _resolve_agent(ctx, client, agent_ref, select, interface_preference="fuzzy")
727
-
728
- parsed_chat_history = _parse_chat_history(chat_history)
729
- renderer, working_console = _setup_run_renderer(ctx, save, verbose)
730
- _maybe_attach_transcript_toggle(ctx, renderer)
731
-
732
- try:
733
- client.timeout = float(timeout)
734
- except Exception:
735
- pass
736
-
737
- run_kwargs = _prepare_run_kwargs(
738
- agent,
739
- final_input_text,
740
- files,
741
- parsed_chat_history,
742
- renderer,
743
- bool(get_ctx_value(ctx, "tty", True)),
744
- )
745
-
746
- result = client.agents.run_agent(**run_kwargs, timeout=timeout)
747
-
748
- slash_mode = _running_in_slash_mode(ctx)
749
- agent_id = str(_safe_agent_attribute(agent, "id") or "") or None
750
- agent_name = _safe_agent_attribute(agent, "name")
751
- model_hint = _get_agent_model_name(agent)
752
-
753
- transcript_context = store_transcript_for_session(
754
- ctx,
755
- renderer,
756
- final_result=result,
757
- agent_id=agent_id,
758
- agent_name=agent_name,
759
- model=model_hint,
760
- source="slash" if slash_mode else "cli",
761
- )
762
-
763
- _handle_run_output(ctx, result, renderer)
764
- _save_run_transcript(save, result, working_console)
765
- maybe_launch_post_run_viewer(
766
- ctx,
767
- transcript_context,
768
- console=console,
769
- slash_mode=slash_mode,
770
- )
771
-
772
- except AgentTimeoutError as e:
773
- error_msg = str(e)
774
- handle_json_output(ctx, error=Exception(error_msg))
775
- raise click.ClickException(error_msg) from e
776
- except Exception as e:
777
- _handle_command_exception(ctx, e)
778
-
779
-
780
- def _running_in_slash_mode(ctx: Any) -> bool:
781
- ctx_obj = getattr(ctx, "obj", None)
782
- return isinstance(ctx_obj, dict) and bool(ctx_obj.get("_slash_session"))
783
-
784
-
785
- def _emit_verbose_guidance(ctx: Any) -> None:
786
- if _running_in_slash_mode(ctx):
787
- message = (
788
- "[dim]Tip:[/] Verbose streaming has been retired in the command palette. Run the agent normally and open "
789
- "the post-run viewer (Ctrl+T) to inspect the transcript."
790
- )
791
- else:
792
- message = (
793
- "[dim]Tip:[/] `--verbose` is no longer supported. Re-run without the flag and toggle the post-run viewer "
794
- "(Ctrl+T) for detailed output."
795
- )
796
- handle_rich_output(ctx, markup_text(message))
797
-
798
-
799
- def _handle_import_file_logic(
800
- import_file: str,
801
- model: str | None,
802
- name: str,
803
- instruction: str,
804
- tools: tuple[str, ...],
805
- agents: tuple[str, ...],
806
- mcps: tuple[str, ...],
807
- timeout: float | None,
808
- ) -> dict[str, Any]:
809
- """Handle import file logic and merge with CLI args."""
810
- import_data = load_resource_from_file(Path(import_file), "agent")
811
- import_data = convert_export_to_import_format(import_data)
812
- import_data = normalize_agent_config_for_import(import_data, model)
813
-
814
- cli_args = {
815
- "name": name,
816
- "instruction": instruction,
817
- "model": model,
818
- "tools": tools or (),
819
- "agents": agents or (),
820
- "mcps": mcps or (),
821
- "timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
822
- }
823
-
824
- return merge_import_with_cli_args(import_data, cli_args)
825
-
826
-
827
- def _build_cli_args_data(
828
- name: str,
829
- instruction: str,
830
- model: str | None,
831
- tools: tuple[str, ...],
832
- agents: tuple[str, ...],
833
- mcps: tuple[str, ...],
834
- timeout: float | None,
835
- ) -> dict[str, Any]:
836
- """Build merged data from CLI arguments."""
837
- return {
838
- "name": name,
839
- "instruction": instruction,
840
- "model": model,
841
- "tools": tools or (),
842
- "agents": agents or (),
843
- "mcps": mcps or (),
844
- "timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
845
- }
846
-
847
-
848
- def _extract_and_validate_fields(
849
- merged_data: dict[str, Any],
850
- ) -> tuple[str, str, str | None, tuple, tuple, tuple, Any]:
851
- """Extract and validate required fields from merged data."""
852
- name = merged_data.get("name")
853
- instruction = merged_data.get("instruction")
854
- model = merged_data.get("model")
855
- tools = tuple(merged_data.get("tools", ()))
856
- agents = tuple(merged_data.get("agents", ()))
857
- mcps = tuple(merged_data.get("mcps", ()))
858
- timeout = merged_data.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
859
-
860
- # Validate required fields
861
- if not name:
862
- raise click.ClickException("Agent name is required (--name or --import)")
863
- if not instruction:
864
- raise click.ClickException("Agent instruction is required (--instruction or --import)")
865
-
866
- return name, instruction, model, tools, agents, mcps, timeout
867
-
868
-
869
- def _validate_and_coerce_fields(name: str, instruction: str, timeout: Any) -> tuple[str, str, Any]:
870
- """Validate and coerce field values."""
871
- name = validate_agent_name(name)
872
- instruction = validate_agent_instruction(instruction)
873
- timeout = coerce_timeout(timeout)
874
- if timeout is not None:
875
- timeout = validate_timeout(timeout)
876
-
877
- return name, instruction, timeout
878
-
879
-
880
- def _resolve_resources(client: Any, tools: tuple, agents: tuple, mcps: tuple) -> tuple[list, list, list]:
881
- """Resolve tool, agent, and MCP references."""
882
- resolved_tools = _resolve_resources_by_name(client, tools, "tool", client.find_tools, "Tool")
883
- resolved_agents = _resolve_resources_by_name(client, agents, "agent", client.find_agents, "Agent")
884
- resolved_mcps = _resolve_resources_by_name(client, mcps, "mcp", client.find_mcps, "MCP")
885
-
886
- return resolved_tools, resolved_agents, resolved_mcps
887
-
888
-
889
- def _build_create_kwargs(
890
- name: str,
891
- instruction: str,
892
- resolved_tools: list,
893
- resolved_agents: list,
894
- resolved_mcps: list,
895
- timeout: Any,
896
- merged_data: dict[str, Any],
897
- model: str | None,
898
- import_file: str | None,
899
- ) -> dict[str, Any]:
900
- """Build create_agent kwargs with all necessary parameters."""
901
- create_kwargs = {
902
- "name": name,
903
- "instruction": instruction,
904
- "tools": resolved_tools or None,
905
- "agents": resolved_agents or None,
906
- "mcps": resolved_mcps or None,
907
- "timeout": timeout,
908
- }
909
-
910
- # Handle language model selection
911
- lm_selection_dict, should_strip_lm_identity = resolve_language_model_selection(merged_data, model)
912
- create_kwargs.update(lm_selection_dict)
913
-
914
- # Handle import file specific logic
915
- if import_file:
916
- _add_import_file_attributes(create_kwargs, merged_data, should_strip_lm_identity)
917
-
918
- return create_kwargs
919
-
920
-
921
- def _add_import_file_attributes(
922
- create_kwargs: dict[str, Any],
923
- merged_data: dict[str, Any],
924
- should_strip_lm_identity: bool,
925
- ) -> None:
926
- """Add import file specific attributes to create_kwargs."""
927
- agent_config_raw = merged_data.get("agent_config")
928
- if isinstance(agent_config_raw, dict):
929
- create_kwargs["agent_config"] = sanitize_agent_config(
930
- agent_config_raw, strip_lm_identity=should_strip_lm_identity
931
- )
932
-
933
- # Add other attributes from import data
934
- excluded_fields = {
935
- "name",
936
- "instruction",
937
- "model",
938
- "language_model_id",
939
- "tools",
940
- "agents",
941
- "timeout",
942
- "agent_config",
943
- "id",
944
- "created_at",
945
- "updated_at",
946
- "type",
947
- "framework",
948
- "version",
949
- "mcps",
950
- "a2a_profile",
951
- }
952
- for key, value in merged_data.items():
953
- if key not in excluded_fields and value is not None:
954
- create_kwargs[key] = value
955
-
956
-
957
- def _get_language_model_display_name(agent: Any, model: str | None) -> str:
958
- """Get display name for the language model."""
959
- lm_display = getattr(agent, "model", None)
960
- if not lm_display:
961
- cfg = getattr(agent, "agent_config", {}) or {}
962
- lm_display = cfg.get("lm_name") or cfg.get("model") or model or f"{DEFAULT_MODEL} (backend default)"
963
- return lm_display
964
-
965
-
966
- def _handle_successful_creation(ctx: Any, agent: Any, model: str | None) -> None:
967
- """Handle successful agent creation output."""
968
- handle_json_output(ctx, _prepare_agent_output(agent))
969
-
970
- lm_display = _get_language_model_display_name(agent, model)
971
-
972
- handle_rich_output(
973
- ctx,
974
- display_creation_success(
975
- "Agent",
976
- agent.name,
977
- agent.id,
978
- Model=lm_display,
979
- Type=getattr(agent, "type", "config"),
980
- Framework=getattr(agent, "framework", "langchain"),
981
- Version=getattr(agent, "version", "1.0"),
982
- ),
983
- )
984
- handle_rich_output(ctx, display_agent_run_suggestions(agent))
985
-
986
-
987
- def _handle_command_exception(ctx: Any, e: Exception) -> None:
988
- """Handle exceptions during command execution with consistent error handling."""
989
- if isinstance(e, click.ClickException):
990
- if get_ctx_value(ctx, "view") == "json":
991
- handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
992
- raise
993
-
994
- handle_json_output(ctx, error=e)
995
- if get_ctx_value(ctx, "view") != "json":
996
- print_api_error(e)
997
- raise click.ClickException(str(e)) from e
998
-
999
-
1000
- def _handle_creation_exception(ctx: Any, e: Exception) -> None:
1001
- """Handle exceptions during agent creation."""
1002
- _handle_command_exception(ctx, e)
1003
-
1004
-
1005
- @agents_group.command()
1006
- @click.option("--name", help="Agent name")
1007
- @click.option("--instruction", help="Agent instruction (prompt)")
1008
- @click.option(
1009
- "--model",
1010
- help=f"Language model to use (e.g., {DEFAULT_MODEL}, default: {DEFAULT_MODEL})",
1011
- )
1012
- @click.option("--tools", multiple=True, help="Tool names or IDs to attach")
1013
- @click.option("--agents", multiple=True, help="Sub-agent names or IDs to attach")
1014
- @click.option("--mcps", multiple=True, help="MCP names or IDs to attach")
1015
- @click.option(
1016
- "--timeout",
1017
- default=DEFAULT_AGENT_RUN_TIMEOUT,
1018
- type=int,
1019
- help="Agent execution timeout in seconds (default: 300s)",
1020
- )
1021
- @click.option(
1022
- "--import",
1023
- "import_file",
1024
- type=click.Path(exists=True, dir_okay=False),
1025
- help="Import agent configuration from JSON file",
1026
- )
1027
- @output_flags()
1028
- @click.pass_context
1029
- def create(
1030
- ctx: Any,
1031
- name: str,
1032
- instruction: str,
1033
- model: str | None,
1034
- tools: tuple[str, ...] | None,
1035
- agents: tuple[str, ...] | None,
1036
- mcps: tuple[str, ...] | None,
1037
- timeout: float | None,
1038
- import_file: str | None,
1039
- ) -> None:
1040
- """Create a new agent.
1041
-
1042
- Examples:
1043
- aip agents create --name "My Agent" --instruction "You are a helpful assistant"
1044
- aip agents create --import agent.json
1045
- """
1046
- try:
1047
- client = get_client(ctx)
1048
-
1049
- # Handle import file or CLI args
1050
- if import_file:
1051
- merged_data = _handle_import_file_logic(import_file, model, name, instruction, tools, agents, mcps, timeout)
1052
- else:
1053
- merged_data = _build_cli_args_data(name, instruction, model, tools, agents, mcps, timeout)
1054
-
1055
- # Extract and validate fields
1056
- (
1057
- name,
1058
- instruction,
1059
- model,
1060
- tools,
1061
- agents,
1062
- mcps,
1063
- timeout,
1064
- ) = _extract_and_validate_fields(merged_data)
1065
- name, instruction, timeout = _validate_and_coerce_fields(name, instruction, timeout)
1066
-
1067
- # Resolve resources
1068
- resolved_tools, resolved_agents, resolved_mcps = _resolve_resources(client, tools, agents, mcps)
1069
-
1070
- # Build create kwargs
1071
- create_kwargs = _build_create_kwargs(
1072
- name,
1073
- instruction,
1074
- resolved_tools,
1075
- resolved_agents,
1076
- resolved_mcps,
1077
- timeout,
1078
- merged_data,
1079
- model,
1080
- import_file,
1081
- )
1082
-
1083
- # Create agent
1084
- agent = client.agents.create_agent(**create_kwargs)
1085
-
1086
- # Handle successful creation
1087
- _handle_successful_creation(ctx, agent, model)
1088
-
1089
- except Exception as e:
1090
- _handle_creation_exception(ctx, e)
1091
-
1092
-
1093
- def _get_agent_for_update(client: Any, agent_id: str) -> Any:
1094
- """Retrieve agent by ID for update operation."""
1095
- try:
1096
- return client.agents.get_agent_by_id(agent_id)
1097
- except Exception as e:
1098
- raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}") from e
1099
-
1100
-
1101
- def _handle_update_import_file(
1102
- import_file: str | None,
1103
- name: str | None,
1104
- instruction: str | None,
1105
- tools: tuple[str, ...] | None,
1106
- agents: tuple[str, ...] | None,
1107
- mcps: tuple[str, ...] | None,
1108
- timeout: float | None,
1109
- ) -> tuple[
1110
- Any | None,
1111
- str | None,
1112
- str | None,
1113
- tuple[str, ...] | None,
1114
- tuple[str, ...] | None,
1115
- tuple[str, ...] | None,
1116
- float | None,
1117
- ]:
1118
- """Handle import file processing for agent update."""
1119
- if not import_file:
1120
- return None, name, instruction, tools, agents, mcps, timeout
1121
-
1122
- import_data = load_resource_from_file(Path(import_file), "agent")
1123
- import_data = convert_export_to_import_format(import_data)
1124
- import_data = normalize_agent_config_for_import(import_data, None)
1125
-
1126
- cli_args = {
1127
- "name": name,
1128
- "instruction": instruction,
1129
- "tools": tools or (),
1130
- "agents": agents or (),
1131
- "mcps": mcps or (),
1132
- "timeout": timeout,
1133
- }
1134
-
1135
- merged_data = merge_import_with_cli_args(import_data, cli_args)
1136
-
1137
- return (
1138
- merged_data,
1139
- merged_data.get("name"),
1140
- merged_data.get("instruction"),
1141
- tuple(merged_data.get("tools", ())),
1142
- tuple(merged_data.get("agents", ())),
1143
- tuple(merged_data.get("mcps", ())),
1144
- coerce_timeout(merged_data.get("timeout")),
1145
- )
1146
-
1147
-
1148
- def _build_update_data(
1149
- name: str | None,
1150
- instruction: str | None,
1151
- tools: tuple[str, ...] | None,
1152
- agents: tuple[str, ...] | None,
1153
- mcps: tuple[str, ...] | None,
1154
- timeout: float | None,
1155
- ) -> dict[str, Any]:
1156
- """Build the update data dictionary from provided parameters."""
1157
- update_data = {}
1158
- if name is not None:
1159
- update_data["name"] = name
1160
- if instruction is not None:
1161
- update_data["instruction"] = instruction
1162
- if tools:
1163
- update_data["tools"] = list(tools)
1164
- if agents:
1165
- update_data["agents"] = list(agents)
1166
- if mcps:
1167
- update_data["mcps"] = list(mcps)
1168
- if timeout is not None:
1169
- update_data["timeout"] = timeout
1170
- return update_data
1171
-
1172
-
1173
- def _handle_update_import_config(
1174
- import_file: str | None, merged_data: dict[str, Any], update_data: dict[str, Any]
1175
- ) -> None:
1176
- """Handle agent config and additional attributes for import-based updates."""
1177
- if not import_file:
1178
- return
1179
-
1180
- lm_selection, should_strip_lm_identity = resolve_language_model_selection(merged_data, None)
1181
- update_data.update(lm_selection)
1182
-
1183
- raw_cfg = merged_data.get("agent_config") if isinstance(merged_data, dict) else None
1184
- if isinstance(raw_cfg, dict):
1185
- update_data["agent_config"] = sanitize_agent_config(raw_cfg, strip_lm_identity=should_strip_lm_identity)
1186
-
1187
- excluded_fields = {
1188
- "name",
1189
- "instruction",
1190
- "tools",
1191
- "agents",
1192
- "timeout",
1193
- "agent_config",
1194
- "language_model_id",
1195
- "id",
1196
- "created_at",
1197
- "updated_at",
1198
- "type",
1199
- "framework",
1200
- "version",
1201
- "a2a_profile",
1202
- }
1203
- for key, value in merged_data.items():
1204
- if key not in excluded_fields and value is not None:
1205
- update_data[key] = value
1206
-
1207
-
1208
- @agents_group.command()
1209
- @click.argument("agent_id")
1210
- @click.option("--name", help="New agent name")
1211
- @click.option("--instruction", help="New instruction")
1212
- @click.option("--tools", multiple=True, help="New tool names or IDs")
1213
- @click.option("--agents", multiple=True, help="New sub-agent names")
1214
- @click.option("--mcps", multiple=True, help="New MCP names or IDs")
1215
- @click.option("--timeout", type=int, help="New timeout value")
1216
- @click.option(
1217
- "--import",
1218
- "import_file",
1219
- type=click.Path(exists=True, dir_okay=False),
1220
- help="Import agent configuration from JSON file",
1221
- )
1222
- @output_flags()
1223
- @click.pass_context
1224
- def update(
1225
- ctx: Any,
1226
- agent_id: str,
1227
- name: str | None,
1228
- instruction: str | None,
1229
- tools: tuple[str, ...] | None,
1230
- agents: tuple[str, ...] | None,
1231
- mcps: tuple[str, ...] | None,
1232
- timeout: float | None,
1233
- import_file: str | None,
1234
- ) -> None:
1235
- """Update an existing agent.
1236
-
1237
- Examples:
1238
- aip agents update my-agent --instruction "New instruction"
1239
- aip agents update my-agent --import agent.json
1240
- """
1241
- try:
1242
- client = get_client(ctx)
1243
- agent = _get_agent_for_update(client, agent_id)
1244
-
1245
- # Handle import file processing
1246
- (
1247
- merged_data,
1248
- name,
1249
- instruction,
1250
- tools,
1251
- agents,
1252
- mcps,
1253
- timeout,
1254
- ) = _handle_update_import_file(import_file, name, instruction, tools, agents, mcps, timeout)
1255
-
1256
- update_data = _build_update_data(name, instruction, tools, agents, mcps, timeout)
1257
-
1258
- if merged_data:
1259
- _handle_update_import_config(import_file, merged_data, update_data)
1260
-
1261
- if not update_data:
1262
- raise click.ClickException("No update fields specified")
1263
-
1264
- updated_agent = client.agents.update_agent(agent.id, **update_data)
1265
-
1266
- handle_json_output(ctx, _prepare_agent_output(updated_agent))
1267
- handle_rich_output(ctx, display_update_success("Agent", updated_agent.name))
1268
- handle_rich_output(ctx, display_agent_run_suggestions(updated_agent))
1269
-
1270
- except click.ClickException:
1271
- # Handle JSON output for ClickExceptions if view is JSON
1272
- if get_ctx_value(ctx, "view") == "json":
1273
- handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
1274
- # Re-raise ClickExceptions without additional processing
1275
- raise
1276
- except Exception as e:
1277
- _handle_command_exception(ctx, e)
1278
-
1279
-
1280
- @agents_group.command()
1281
- @click.argument("agent_id")
1282
- @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
1283
- @output_flags()
1284
- @click.pass_context
1285
- def delete(ctx: Any, agent_id: str, yes: bool) -> None:
1286
- """Delete an agent."""
1287
- try:
1288
- client = get_client(ctx)
1289
-
1290
- # Get agent by ID (no ambiguity handling needed)
1291
- try:
1292
- agent = client.agents.get_agent_by_id(agent_id)
1293
- except Exception as e:
1294
- raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}") from e
1295
-
1296
- # Confirm deletion when not forced
1297
- if not yes and not display_confirmation_prompt("Agent", agent.name):
1298
- return
1299
-
1300
- client.agents.delete_agent(agent.id)
1301
-
1302
- handle_json_output(
1303
- ctx,
1304
- {
1305
- "success": True,
1306
- "message": f"Agent '{agent.name}' deleted",
1307
- },
1308
- )
1309
- handle_rich_output(ctx, display_deletion_success("Agent", agent.name))
1310
-
1311
- except click.ClickException:
1312
- # Handle JSON output for ClickExceptions if view is JSON
1313
- if get_ctx_value(ctx, "view") == "json":
1314
- handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
1315
- # Re-raise ClickExceptions without additional processing
1316
- raise
1317
- except Exception as e:
1318
- _handle_command_exception(ctx, e)
1319
-
1320
-
1321
- @agents_group.command()
1322
- @click.option(
1323
- "--base-url",
1324
- help="Custom LangFlow server base URL (overrides LANGFLOW_BASE_URL env var)",
1325
- )
1326
- @click.option("--api-key", help="Custom LangFlow API key (overrides LANGFLOW_API_KEY env var)")
1327
- @output_flags()
1328
- @click.pass_context
1329
- def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1330
- """Sync agents with LangFlow server flows.
1331
-
1332
- This command fetches all flows from the configured LangFlow server and
1333
- creates/updates corresponding agents in the platform.
1334
-
1335
- The LangFlow server configuration can be provided via:
1336
- - Command options (--base-url, --api-key)
1337
- - Environment variables (LANGFLOW_BASE_URL, LANGFLOW_API_KEY)
1338
-
1339
- Examples:
1340
- aip agents sync-langflow
1341
- aip agents sync-langflow --base-url https://my-langflow.com --api-key my-key
1342
- """
1343
- try:
1344
- client = get_client(ctx)
1345
-
1346
- # Perform the sync
1347
- result = client.sync_langflow_agents(base_url=base_url, api_key=api_key)
1348
-
1349
- # Handle output format
1350
- handle_json_output(ctx, result)
1351
-
1352
- # Show success message for non-JSON output
1353
- if get_ctx_value(ctx, "view") != "json":
1354
- # Extract some useful info from the result
1355
- success_count = result.get("data", {}).get("created_count", 0) + result.get("data", {}).get(
1356
- "updated_count", 0
1357
- )
1358
- total_count = result.get("data", {}).get("total_processed", 0)
1359
-
1360
- handle_rich_output(
1361
- ctx,
1362
- markup_text(
1363
- f"[{SUCCESS_STYLE}]✅ Successfully synced {success_count} LangFlow agents "
1364
- f"({total_count} total processed)[/]"
1365
- ),
1366
- )
1367
-
1368
- except Exception as e:
1369
- _handle_command_exception(ctx, e)