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