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