glaip-sdk 0.0.4__py3-none-any.whl → 0.0.5__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 (47) hide show
  1. glaip_sdk/__init__.py +5 -5
  2. glaip_sdk/branding.py +18 -17
  3. glaip_sdk/cli/__init__.py +1 -1
  4. glaip_sdk/cli/agent_config.py +82 -0
  5. glaip_sdk/cli/commands/__init__.py +3 -3
  6. glaip_sdk/cli/commands/agents.py +570 -673
  7. glaip_sdk/cli/commands/configure.py +2 -2
  8. glaip_sdk/cli/commands/mcps.py +148 -143
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +250 -179
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +14 -18
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +305 -264
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +631 -191
  19. glaip_sdk/client/base.py +66 -4
  20. glaip_sdk/client/main.py +226 -0
  21. glaip_sdk/client/mcps.py +143 -18
  22. glaip_sdk/client/tools.py +146 -11
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +42 -2
  25. glaip_sdk/rich_components.py +29 -0
  26. glaip_sdk/utils/__init__.py +18 -171
  27. glaip_sdk/utils/agent_config.py +181 -0
  28. glaip_sdk/utils/client_utils.py +159 -79
  29. glaip_sdk/utils/display.py +100 -0
  30. glaip_sdk/utils/general.py +94 -0
  31. glaip_sdk/utils/import_export.py +140 -0
  32. glaip_sdk/utils/rendering/formatting.py +6 -1
  33. glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
  34. glaip_sdk/utils/rendering/renderer/base.py +340 -247
  35. glaip_sdk/utils/rendering/renderer/debug.py +3 -2
  36. glaip_sdk/utils/rendering/renderer/panels.py +11 -10
  37. glaip_sdk/utils/rendering/steps.py +1 -1
  38. glaip_sdk/utils/resource_refs.py +192 -0
  39. glaip_sdk/utils/rich_utils.py +29 -0
  40. glaip_sdk/utils/serialization.py +285 -0
  41. glaip_sdk/utils/validation.py +273 -0
  42. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
  43. glaip_sdk-0.0.5.dist-info/RECORD +55 -0
  44. glaip_sdk/cli/commands/init.py +0 -93
  45. glaip_sdk-0.0.4.dist-info/RECORD +0 -41
  46. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
  47. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
@@ -6,20 +6,43 @@ Authors:
6
6
 
7
7
  import json
8
8
  import os
9
- from datetime import datetime
10
9
  from pathlib import Path
11
- from typing import Any
12
10
 
13
11
  import click
14
12
  from rich.console import Console
15
- from rich.panel import Panel
16
13
  from rich.text import Text
17
14
 
18
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
19
- from glaip_sdk.exceptions import AgentTimeoutError
20
- from glaip_sdk.utils import is_uuid
21
-
22
- from ..utils import (
15
+ from glaip_sdk.cli.agent_config import (
16
+ merge_agent_config_with_cli_args as merge_import_with_cli_args,
17
+ )
18
+ from glaip_sdk.cli.agent_config import (
19
+ resolve_agent_language_model_selection as resolve_language_model_selection,
20
+ )
21
+ from glaip_sdk.cli.agent_config import (
22
+ sanitize_agent_config_for_cli as sanitize_agent_config,
23
+ )
24
+ from glaip_sdk.cli.display import (
25
+ build_resource_result_data,
26
+ display_agent_run_suggestions,
27
+ display_confirmation_prompt,
28
+ display_creation_success,
29
+ display_deletion_success,
30
+ display_update_success,
31
+ handle_json_output,
32
+ handle_rich_output,
33
+ print_api_error,
34
+ )
35
+ from glaip_sdk.cli.io import (
36
+ export_resource_to_file_with_validation as export_resource_to_file,
37
+ )
38
+ from glaip_sdk.cli.io import (
39
+ fetch_raw_resource_details,
40
+ )
41
+ from glaip_sdk.cli.io import (
42
+ load_resource_from_file_with_validation as load_resource_from_file,
43
+ )
44
+ from glaip_sdk.cli.resolution import resolve_resource_reference
45
+ from glaip_sdk.cli.utils import (
23
46
  _fuzzy_pick_for_resources,
24
47
  build_renderer,
25
48
  coerce_to_row,
@@ -27,44 +50,27 @@ from ..utils import (
27
50
  output_flags,
28
51
  output_list,
29
52
  output_result,
30
- resolve_resource,
31
53
  )
54
+ from glaip_sdk.cli.validators import (
55
+ validate_agent_instruction_cli as validate_agent_instruction,
56
+ )
57
+ from glaip_sdk.cli.validators import (
58
+ validate_agent_name_cli as validate_agent_name,
59
+ )
60
+ from glaip_sdk.cli.validators import (
61
+ validate_timeout_cli as validate_timeout,
62
+ )
63
+ from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
64
+ from glaip_sdk.exceptions import AgentTimeoutError
65
+ from glaip_sdk.utils import format_datetime, is_uuid
66
+ from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
67
+ from glaip_sdk.utils.import_export import convert_export_to_import_format
68
+ from glaip_sdk.utils.validation import coerce_timeout
32
69
 
33
70
  console = Console()
34
71
 
35
-
36
- def _display_run_suggestions(agent):
37
- """Display helpful suggestions for running the agent."""
38
- console.print()
39
- console.print(
40
- Panel(
41
- f"[bold blue]💡 Next Steps:[/bold blue]\n\n"
42
- f"🚀 Run this agent:\n"
43
- f' [green]aip agents run {agent.id} "Your message here"[/green]\n\n'
44
- f"📋 Or use the agent name:\n"
45
- f' [green]aip agents run "{agent.name}" "Your message here"[/green]\n\n'
46
- f"🔧 Available options:\n"
47
- f" [dim]--chat-history[/dim] Include previous conversation\n"
48
- f" [dim]--file[/dim] Attach files\n"
49
- f" [dim]--input[/dim] Alternative input method\n"
50
- f" [dim]--timeout[/dim] Set execution timeout\n"
51
- f" [dim]--save[/dim] Save transcript to file\n"
52
- f" [dim]--verbose[/dim] Show detailed execution\n\n"
53
- f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]",
54
- title="🤖 Ready to Run Agent",
55
- border_style="blue",
56
- padding=(0, 1),
57
- )
58
- )
59
-
60
-
61
- def _format_datetime(dt):
62
- """Format datetime object to readable string."""
63
- if isinstance(dt, datetime):
64
- return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
65
- elif dt is None:
66
- return "N/A"
67
- return dt
72
+ # Error message constants
73
+ AGENT_NOT_FOUND_ERROR = "Agent not found"
68
74
 
69
75
 
70
76
  def _fetch_full_agent_details(client, agent):
@@ -79,113 +85,6 @@ def _fetch_full_agent_details(client, agent):
79
85
  return agent
80
86
 
81
87
 
82
- def _build_agent_result_data(agent):
83
- """Build standardized result data for agent display."""
84
- return {
85
- "id": str(getattr(agent, "id", "N/A")),
86
- "name": getattr(agent, "name", "N/A"),
87
- "type": getattr(agent, "type", "N/A"),
88
- "framework": getattr(agent, "framework", "N/A"),
89
- "version": getattr(agent, "version", "N/A"),
90
- "description": getattr(agent, "description", "N/A"),
91
- "instruction": getattr(agent, "instruction", "") or "-",
92
- "created_at": _format_datetime(getattr(agent, "created_at", "N/A")),
93
- "updated_at": _format_datetime(getattr(agent, "updated_at", "N/A")),
94
- "metadata": getattr(agent, "metadata", "N/A"),
95
- "language_model_id": getattr(agent, "language_model_id", "N/A"),
96
- "agent_config": getattr(agent, "agent_config", "N/A"),
97
- "tool_configs": getattr(agent, "tool_configs", {}),
98
- "tools": getattr(agent, "tools", []),
99
- "agents": getattr(agent, "agents", []),
100
- "mcps": getattr(agent, "mcps", []),
101
- "a2a_profile": getattr(agent, "a2a_profile", "N/A"),
102
- }
103
-
104
-
105
- def _get_agent_attributes(agent):
106
- """Dynamically get all relevant agent attributes for export."""
107
- # Exclude these attributes from export (methods, private attrs, computed properties)
108
- exclude_attrs = {
109
- "id",
110
- "created_at",
111
- "updated_at", # System-managed fields
112
- "_client",
113
- "_raw_data", # Internal fields
114
- }
115
-
116
- # Methods and callable attributes to exclude
117
- exclude_callables = {
118
- "model_dump",
119
- "dict",
120
- "json", # Pydantic methods
121
- "get",
122
- "post",
123
- "put",
124
- "delete", # HTTP methods
125
- "save",
126
- "refresh",
127
- "update", # ORM methods
128
- }
129
-
130
- export_data = {}
131
-
132
- # Method 1: Try Pydantic model_dump() if available (best for structured data)
133
- if hasattr(agent, "model_dump") and callable(agent.model_dump):
134
- try:
135
- # Get all model fields
136
- all_data = agent.model_dump()
137
- # Filter out excluded attributes
138
- for key, value in all_data.items():
139
- if key not in exclude_attrs and key not in exclude_callables:
140
- export_data[key] = value
141
- return export_data
142
- except Exception:
143
- # Fall back to manual attribute detection
144
- pass
145
-
146
- # Method 2: Manual attribute inspection with filtering
147
- # Get all non-private, non-method attributes
148
- all_attrs = []
149
- if hasattr(agent, "__dict__"):
150
- all_attrs.extend(agent.__dict__.keys())
151
- if hasattr(agent, "__annotations__"):
152
- all_attrs.extend(agent.__annotations__.keys())
153
-
154
- # Remove duplicates and filter
155
- all_attrs = list(set(all_attrs))
156
-
157
- for attr in all_attrs:
158
- # Skip excluded attributes
159
- if (
160
- attr in exclude_attrs
161
- or attr.startswith("_") # Private attributes
162
- or attr in exclude_callables
163
- ):
164
- continue
165
-
166
- # Skip callable attributes (methods, functions)
167
- attr_value = getattr(agent, attr, None)
168
- if callable(attr_value):
169
- continue
170
-
171
- # Add the attribute to export data
172
- export_data[attr] = attr_value
173
-
174
- return export_data
175
-
176
-
177
- def _build_agent_export_data(agent):
178
- """Build comprehensive export data for agent (always includes all fields)."""
179
- # Get all available agent attributes dynamically
180
- all_agent_data = _get_agent_attributes(agent)
181
-
182
- # Always include all detected attributes for comprehensive export
183
- # Add default timeout if not present
184
- if "timeout" not in all_agent_data:
185
- all_agent_data["timeout"] = DEFAULT_AGENT_RUN_TIMEOUT
186
- return all_agent_data
187
-
188
-
189
88
  def _get_agent_model_name(agent):
190
89
  """Extract model name from agent configuration."""
191
90
  # Try different possible locations for model name
@@ -200,231 +99,8 @@ def _get_agent_model_name(agent):
200
99
  return DEFAULT_MODEL
201
100
 
202
101
 
203
- def _extract_tool_ids(agent):
204
- """Extract tool IDs from agent tools list for import compatibility."""
205
- tools = getattr(agent, "tools", [])
206
- if not tools:
207
- return []
208
-
209
- ids = []
210
- for tool in tools:
211
- if isinstance(tool, dict):
212
- tool_id = tool.get("id")
213
- if tool_id:
214
- ids.append(tool_id)
215
- else:
216
- # Fallback to name if ID not available
217
- name = tool.get("name", "")
218
- if name:
219
- ids.append(name)
220
- elif hasattr(tool, "id"):
221
- ids.append(tool.id)
222
- elif hasattr(tool, "name"):
223
- ids.append(tool.name)
224
- else:
225
- ids.append(str(tool))
226
- return ids
227
-
228
-
229
- def _extract_agent_ids(agent):
230
- """Extract agent IDs from agent agents list for import compatibility."""
231
- agents = getattr(agent, "agents", [])
232
- if not agents:
233
- return []
234
-
235
- ids = []
236
- for sub_agent in agents:
237
- if isinstance(sub_agent, dict):
238
- agent_id = sub_agent.get("id")
239
- if agent_id:
240
- ids.append(agent_id)
241
- else:
242
- # Fallback to name if ID not available
243
- name = sub_agent.get("name", "")
244
- if name:
245
- ids.append(name)
246
- elif hasattr(sub_agent, "id"):
247
- ids.append(sub_agent.id)
248
- elif hasattr(sub_agent, "name"):
249
- ids.append(sub_agent.name)
250
- else:
251
- ids.append(str(sub_agent))
252
- return ids
253
-
254
-
255
- def _export_agent_to_file(agent, file_path: Path, format: str = "json"):
256
- """Export agent to file (JSON or YAML) with comprehensive data."""
257
- export_data = _build_agent_export_data(agent)
258
-
259
- if format.lower() == "yaml" or file_path.suffix.lower() in [".yaml", ".yml"]:
260
- _write_yaml_to_file(file_path, export_data)
261
- else:
262
- _write_json_to_file(file_path, export_data)
263
-
264
-
265
- def _load_agent_from_file(file_path: Path) -> dict[str, Any]:
266
- """Load agent data from JSON or YAML file."""
267
- if file_path.suffix.lower() in [".yaml", ".yml"]:
268
- return _read_yaml_from_file(file_path)
269
- else:
270
- return _read_json_from_file(file_path)
271
-
272
-
273
- def _read_yaml_from_file(file_path: Path) -> dict[str, Any]:
274
- """Read YAML data from file."""
275
- try:
276
- import yaml
277
- except ImportError:
278
- raise click.ClickException(
279
- "PyYAML is required for YAML import. Install with: pip install PyYAML"
280
- )
281
-
282
- if not file_path.exists():
283
- raise FileNotFoundError(f"File not found: {file_path}")
284
-
285
- with open(file_path, encoding="utf-8") as f:
286
- data = yaml.safe_load(f)
287
-
288
- # Handle instruction_lines array format for user-friendly YAML
289
- if "instruction_lines" in data and isinstance(data["instruction_lines"], list):
290
- data["instruction"] = "\n\n".join(data["instruction_lines"])
291
- del data["instruction_lines"]
292
-
293
- # Handle instruction as list from YAML export (convert back to string)
294
- if "instruction" in data and isinstance(data["instruction"], list):
295
- data["instruction"] = "\n\n".join(data["instruction"])
296
-
297
- return data
298
-
299
-
300
- def _extract_ids_from_export(items: list) -> list[str]:
301
- """Extract IDs from export format (list of dicts with id/name fields)."""
302
- ids = []
303
- for item in items:
304
- if isinstance(item, dict):
305
- item_id = item.get("id")
306
- if item_id:
307
- ids.append(item_id)
308
- elif isinstance(item, str):
309
- ids.append(item)
310
- return ids
311
-
312
-
313
- def _convert_export_to_import_format(data: dict[str, Any]) -> dict[str, Any]:
314
- """Convert export format to import-compatible format (extract IDs from objects)."""
315
- import_data = data.copy()
316
-
317
- # Convert tools from dicts to IDs
318
- if "tools" in import_data and isinstance(import_data["tools"], list):
319
- import_data["tools"] = _extract_ids_from_export(import_data["tools"])
320
-
321
- # Convert agents from dicts to IDs
322
- if "agents" in import_data and isinstance(import_data["agents"], list):
323
- import_data["agents"] = _extract_ids_from_export(import_data["agents"])
324
-
325
- return import_data
326
-
327
-
328
- def _write_json_to_file(file_path: Path, data: dict[str, Any], indent: int = 2) -> None:
329
- """Write data to JSON file with consistent formatting."""
330
- with open(file_path, "w", encoding="utf-8") as f:
331
- json.dump(data, f, indent=indent)
332
-
333
-
334
- def _write_yaml_to_file(file_path: Path, data: dict[str, Any]) -> None:
335
- """Write data to YAML file with user-friendly formatting."""
336
- try:
337
- import yaml
338
- except ImportError:
339
- raise click.ClickException(
340
- "PyYAML is required for YAML export. Install with: pip install PyYAML"
341
- )
342
-
343
- # Custom YAML dumper for user-friendly instruction formatting
344
- class LiteralString(str):
345
- pass
346
-
347
- def literal_string_representer(dumper, data):
348
- # Use literal block scalar (|) for multiline strings to preserve formatting
349
- if "\n" in data:
350
- return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
351
- return dumper.represent_scalar("tag:yaml.org,2002:str", data)
352
-
353
- # Add custom representer to the YAML dumper
354
- yaml.add_representer(LiteralString, literal_string_representer)
355
-
356
- # Convert instruction to LiteralString for proper formatting
357
- if "instruction" in data and data["instruction"]:
358
- data["instruction"] = LiteralString(data["instruction"])
359
-
360
- with open(file_path, "w", encoding="utf-8") as f:
361
- yaml.dump(
362
- data, f, default_flow_style=False, allow_unicode=True, sort_keys=False
363
- )
364
-
365
-
366
- def _read_json_from_file(file_path: Path) -> dict[str, Any]:
367
- """Read JSON data from file with validation."""
368
- if not file_path.exists():
369
- raise FileNotFoundError(f"File not found: {file_path}")
370
-
371
- if file_path.suffix.lower() != ".json":
372
- raise ValueError(
373
- f"Unsupported file format: {file_path.suffix}. Only JSON files are supported."
374
- )
375
-
376
- with open(file_path, encoding="utf-8") as f:
377
- return json.load(f)
378
-
379
-
380
- def _merge_import_with_cli_args(
381
- import_data: dict[str, Any],
382
- cli_args: dict[str, Any],
383
- array_fields: list[str] = None,
384
- ) -> dict[str, Any]:
385
- """Merge imported data with CLI arguments, preferring CLI args.
386
-
387
- Args:
388
- import_data: Data loaded from import file
389
- cli_args: Arguments passed via CLI
390
- array_fields: Fields that should be combined (merged) rather than replaced
391
-
392
- Returns:
393
- Merged data dictionary
394
- """
395
- if array_fields is None:
396
- array_fields = ["tools", "agents"]
397
-
398
- merged = {}
399
-
400
- for key, cli_value in cli_args.items():
401
- if cli_value is not None and (
402
- not isinstance(cli_value, list | tuple) or len(cli_value) > 0
403
- ):
404
- # CLI value takes precedence (for non-empty values)
405
- if key in array_fields and key in import_data:
406
- # For array fields, combine CLI and imported values
407
- import_value = import_data[key]
408
- if isinstance(import_value, list):
409
- merged[key] = list(cli_value) + import_value
410
- else:
411
- merged[key] = cli_value
412
- else:
413
- merged[key] = cli_value
414
- elif key in import_data:
415
- # Use imported value if no CLI value
416
- merged[key] = import_data[key]
417
-
418
- # Add any import-only fields
419
- for key, import_value in import_data.items():
420
- if key not in merged:
421
- merged[key] = import_value
422
-
423
- return merged
424
-
425
-
426
102
  def _resolve_resources_by_name(
427
- client, items: tuple[str, ...], resource_type: str, find_func, label: str
103
+ _client, items: tuple[str, ...], resource_type: str, find_func, label: str
428
104
  ) -> list[str]:
429
105
  """Resolve resource names/IDs to IDs, handling ambiguity.
430
106
 
@@ -455,52 +131,81 @@ def _resolve_resources_by_name(
455
131
  return out
456
132
 
457
133
 
458
- def _display_agent_creation_success(agent, model=None, default_model=DEFAULT_MODEL):
459
- """Display success message for agent creation/update."""
460
- lm = getattr(agent, "model", None)
461
- if not lm:
462
- cfg = getattr(agent, "agent_config", {}) or {}
463
- lm = (
464
- cfg.get("lm_name")
465
- or cfg.get("model")
466
- or model # Use provided model if specified
467
- or f"{default_model} (backend default)"
468
- )
469
-
470
- panel = Panel(
471
- f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
472
- f"ID: {agent.id}\n"
473
- f"Model: {lm}\n"
474
- f"Type: {getattr(agent, 'type', 'config')}\n"
475
- f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
476
- f"Version: {getattr(agent, 'version', '1.0')}",
477
- title="🤖 Agent Created",
478
- border_style="green",
479
- padding=(0, 1),
480
- )
481
- console.print(panel)
482
-
483
-
484
- def _display_agent_update_success(agent):
485
- """Display success message for agent update."""
486
- console.print(Text(f"[green]✅ Agent '{agent.name}' updated successfully[/green]"))
487
-
488
-
489
134
  def _display_agent_details(ctx, client, agent):
490
- """Display full agent details in a standardized format."""
491
- # Fetch full details to ensure all fields are populated
492
- full_agent = _fetch_full_agent_details(client, agent)
493
-
494
- # Build result data
495
- result_data = _build_agent_result_data(full_agent)
496
-
497
- # Display using output_result
498
- output_result(
499
- ctx,
500
- result_data,
501
- title="Agent Details",
502
- panel_title=f"🤖 {full_agent.name}",
503
- )
135
+ """Display full agent details using raw API data to preserve ALL fields."""
136
+ if agent is None:
137
+ handle_rich_output(ctx, Text("[red]❌ No agent provided[/red]"))
138
+ return
139
+
140
+ # Try to fetch raw API data first to preserve ALL fields
141
+ raw_agent_data = fetch_raw_resource_details(client, agent, "agents")
142
+
143
+ if raw_agent_data:
144
+ # Use raw API data - this preserves ALL fields including account_id
145
+ # Format dates for better display (minimal postprocessing)
146
+ formatted_data = raw_agent_data.copy()
147
+ if "created_at" in formatted_data:
148
+ formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
149
+ if "updated_at" in formatted_data:
150
+ formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
151
+
152
+ # Display using output_result with raw data
153
+ output_result(
154
+ ctx,
155
+ formatted_data,
156
+ title="Agent Details",
157
+ panel_title=f"🤖 {raw_agent_data.get('name', 'Unknown')}",
158
+ )
159
+ else:
160
+ # Fall back to original method if raw fetch fails
161
+ handle_rich_output(
162
+ ctx, Text("[yellow]Falling back to Pydantic model data[/yellow]")
163
+ )
164
+ full_agent = _fetch_full_agent_details(client, agent)
165
+
166
+ # Build result data using standardized helper
167
+ fields = [
168
+ "id",
169
+ "name",
170
+ "type",
171
+ "framework",
172
+ "version",
173
+ "description",
174
+ "instruction",
175
+ "created_at",
176
+ "updated_at",
177
+ "metadata",
178
+ "language_model_id",
179
+ "agent_config",
180
+ "tool_configs",
181
+ "tools",
182
+ "agents",
183
+ "mcps",
184
+ "a2a_profile",
185
+ ]
186
+ result_data = build_resource_result_data(full_agent, fields)
187
+ if not result_data.get("instruction"):
188
+ result_data["instruction"] = "-" # pragma: no cover - cosmetic fallback
189
+
190
+ # Format dates for better display
191
+ if "created_at" in result_data and result_data["created_at"] not in [
192
+ "N/A",
193
+ None,
194
+ ]:
195
+ result_data["created_at"] = format_datetime(result_data["created_at"])
196
+ if "updated_at" in result_data and result_data["updated_at"] not in [
197
+ "N/A",
198
+ None,
199
+ ]:
200
+ result_data["updated_at"] = format_datetime(result_data["updated_at"])
201
+
202
+ # Display using output_result
203
+ output_result(
204
+ ctx,
205
+ result_data,
206
+ title="Agent Details",
207
+ panel_title=f"🤖 {full_agent.name}",
208
+ )
504
209
 
505
210
 
506
211
  @click.group(name="agents", no_args_is_help=True)
@@ -515,12 +220,14 @@ def _resolve_agent(ctx, client, ref, select=None, interface_preference="fuzzy"):
515
220
  Args:
516
221
  interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
517
222
  """
518
- return resolve_resource(
223
+ return resolve_resource_reference(
519
224
  ctx,
225
+ client,
520
226
  ref,
521
- get_by_id=client.agents.get_agent_by_id,
522
- find_by_name=client.agents.find_agents,
523
- label="Agent",
227
+ "agent",
228
+ client.agents.get_agent_by_id,
229
+ client.agents.find_agents,
230
+ "Agent",
524
231
  select=select,
525
232
  interface_preference=interface_preference,
526
233
  )
@@ -530,13 +237,32 @@ def _resolve_agent(ctx, client, ref, select=None, interface_preference="fuzzy"):
530
237
  @click.option(
531
238
  "--simple", is_flag=True, help="Show simple table without interactive picker"
532
239
  )
240
+ @click.option(
241
+ "--type", "agent_type", help="Filter by agent type (config, code, a2a, langflow)"
242
+ )
243
+ @click.option(
244
+ "--framework", help="Filter by framework (langchain, langgraph, google_adk)"
245
+ )
246
+ @click.option("--name", help="Filter by partial name match (case-insensitive)")
247
+ @click.option("--version", help="Filter by exact version match")
248
+ @click.option(
249
+ "--sync-langflow",
250
+ is_flag=True,
251
+ help="Sync with LangFlow server before listing (only applies when filtering by langflow type)",
252
+ )
533
253
  @output_flags()
534
254
  @click.pass_context
535
- def list_agents(ctx, simple):
536
- """List all agents."""
255
+ def list_agents(ctx, simple, agent_type, framework, name, version, sync_langflow):
256
+ """List agents with optional filtering."""
537
257
  try:
538
258
  client = get_client(ctx)
539
- agents = client.agents.list_agents()
259
+ agents = client.agents.list_agents(
260
+ agent_type=agent_type,
261
+ framework=framework,
262
+ name=name,
263
+ version=version,
264
+ sync_langflow_agents=sync_langflow,
265
+ )
540
266
 
541
267
  # Define table columns: (data_key, header, style, width)
542
268
  columns = [
@@ -560,6 +286,8 @@ def list_agents(ctx, simple):
560
286
  picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
561
287
  if picked_agent:
562
288
  _display_agent_details(ctx, client, picked_agent)
289
+ # Show run suggestions via centralized display helper
290
+ handle_rich_output(ctx, display_agent_run_suggestions(picked_agent))
563
291
  return
564
292
 
565
293
  # Show simple table (either --simple flag or non-interactive)
@@ -596,7 +324,7 @@ def get(ctx, agent_ref, select, export):
596
324
  )
597
325
 
598
326
  # Handle export option
599
- if export:
327
+ if export: # pragma: no cover - requires filesystem verification
600
328
  export_path = Path(export)
601
329
  # Auto-detect format from file extension
602
330
  if export_path.suffix.lower() in [".yaml", ".yml"]:
@@ -607,32 +335,131 @@ def get(ctx, agent_ref, select, export):
607
335
  # Always export comprehensive data - re-fetch agent with full details
608
336
  try:
609
337
  agent = client.agents.get_agent_by_id(agent.id)
610
- except Exception as e:
611
- console.print(
612
- Text(f"[yellow]⚠️ Could not fetch full agent details: {e}[/yellow]")
338
+ except Exception as e: # pragma: no cover - best-effort fallback messaging
339
+ handle_rich_output(
340
+ ctx,
341
+ Text(
342
+ f"[yellow]⚠️ Could not fetch full agent details: {e}[/yellow]"
343
+ ),
613
344
  )
614
- console.print(
615
- Text("[yellow]⚠️ Proceeding with available data[/yellow]")
345
+ handle_rich_output(
346
+ ctx, Text("[yellow]⚠️ Proceeding with available data[/yellow]")
616
347
  )
617
348
 
618
- _export_agent_to_file(agent, export_path, detected_format)
619
- console.print(
349
+ export_resource_to_file(agent, export_path, detected_format)
350
+ handle_rich_output(
351
+ ctx,
620
352
  Text(
621
353
  f"[green]✅ Complete agent configuration exported to: {export_path} (format: {detected_format})[/green]"
622
- )
354
+ ),
623
355
  )
624
356
 
625
357
  # Display full agent details using the standardized helper
626
358
  _display_agent_details(ctx, client, agent)
627
359
 
628
- # Show run suggestions (only in rich mode, not JSON)
629
- if ctx.obj.get("view") != "json":
630
- _display_run_suggestions(agent)
360
+ # Show run suggestions via centralized display helper
361
+ handle_rich_output(ctx, display_agent_run_suggestions(agent))
631
362
 
632
363
  except Exception as e:
633
364
  raise click.ClickException(str(e))
634
365
 
635
366
 
367
+ def _validate_run_input(input_option, input_text):
368
+ """Validate and determine the final input text for agent run."""
369
+ final_input_text = input_option if input_option else input_text
370
+
371
+ if not final_input_text:
372
+ raise click.ClickException(
373
+ "Input text is required. Use either positional argument or --input option."
374
+ )
375
+
376
+ return final_input_text
377
+
378
+
379
+ def _parse_chat_history(chat_history):
380
+ """Parse chat history JSON if provided."""
381
+ if not chat_history:
382
+ return None
383
+
384
+ try:
385
+ return json.loads(chat_history)
386
+ except json.JSONDecodeError:
387
+ raise click.ClickException("Invalid JSON in chat history")
388
+
389
+
390
+ def _setup_run_renderer(ctx, save, verbose):
391
+ """Set up renderer and working console for agent run."""
392
+ tty_enabled = bool((ctx.obj or {}).get("tty", True))
393
+ return build_renderer(
394
+ ctx,
395
+ save_path=save,
396
+ verbose=verbose,
397
+ _tty_enabled=tty_enabled,
398
+ )
399
+
400
+
401
+ def _prepare_run_kwargs(
402
+ agent, final_input_text, files, parsed_chat_history, renderer, tty_enabled
403
+ ):
404
+ """Prepare kwargs for agent run."""
405
+ run_kwargs = {
406
+ "agent_id": agent.id,
407
+ "message": final_input_text,
408
+ "files": list(files),
409
+ "agent_name": agent.name,
410
+ "tty": tty_enabled,
411
+ }
412
+
413
+ if parsed_chat_history:
414
+ run_kwargs["chat_history"] = parsed_chat_history
415
+
416
+ if renderer is not None:
417
+ run_kwargs["renderer"] = renderer
418
+
419
+ return run_kwargs
420
+
421
+
422
+ def _handle_run_output(ctx, result, renderer):
423
+ """Handle output formatting for agent run results."""
424
+ printed_by_renderer = bool(renderer)
425
+ selected_view = (ctx.obj or {}).get("view", "rich")
426
+
427
+ if not printed_by_renderer:
428
+ if selected_view == "json":
429
+ handle_json_output(ctx, {"output": result})
430
+ elif selected_view == "md":
431
+ click.echo(f"# Assistant\n\n{result}")
432
+ elif selected_view == "plain":
433
+ click.echo(result)
434
+
435
+
436
+ def _save_run_transcript(save, result, working_console):
437
+ """Save transcript to file if requested."""
438
+ if not save:
439
+ return
440
+
441
+ ext = (save.rsplit(".", 1)[-1] or "").lower()
442
+ if ext == "json":
443
+ save_data = {
444
+ "output": result or "",
445
+ "full_debug_output": getattr(
446
+ working_console, "get_captured_output", lambda: ""
447
+ )(),
448
+ "timestamp": "captured during agent execution",
449
+ }
450
+ content = json.dumps(save_data, indent=2)
451
+ else:
452
+ full_output = getattr(working_console, "get_captured_output", lambda: "")()
453
+ if full_output:
454
+ content = f"# Agent Debug Log\n\n{full_output}\n\n---\n\n## Final Result\n\n{result or ''}\n"
455
+ else:
456
+ content = f"# Assistant\n\n{result or ''}\n"
457
+
458
+ with open(save, "w", encoding="utf-8") as f:
459
+ f.write(content)
460
+ console.print(Text(f"[green]Full debug output saved to: {save}[/green]"))
461
+
462
+
636
463
  @agents_group.command()
637
464
  @click.argument("agent_ref")
638
465
  @click.argument("input_text", required=False)
@@ -685,126 +512,42 @@ def run(
685
512
  aip agents run agent-123 "Process this data" --timeout 600
686
513
  aip agents run my-agent --input "Hello world" # Legacy style
687
514
  """
688
- # Handle input precedence: --input option overrides positional argument
689
- final_input_text = input_option if input_option else input_text
690
-
691
- # Validate that we have input text from either positional argument or --input option
692
- if not final_input_text:
693
- raise click.ClickException(
694
- "Input text is required. Use either positional argument or --input option."
695
- )
515
+ final_input_text = _validate_run_input(input_option, input_text)
696
516
 
697
517
  try:
698
518
  client = get_client(ctx)
699
-
700
- # Resolve agent by ID or name (align with other commands) - use fuzzy interface
701
519
  agent = _resolve_agent(
702
520
  ctx, client, agent_ref, select, interface_preference="fuzzy"
703
521
  )
704
522
 
705
- # Parse chat history if provided
706
- parsed_chat_history = None
707
- if chat_history:
708
- try:
709
- parsed_chat_history = json.loads(chat_history)
710
- except json.JSONDecodeError:
711
- raise click.ClickException("Invalid JSON in chat history")
712
-
713
- # Create custom renderer with CLI flags
714
- tty_enabled = bool((ctx.obj or {}).get("tty", True))
715
-
716
- # Build renderer and capturing console
717
- renderer, working_console = build_renderer(
718
- ctx,
719
- save_path=save,
720
- verbose=verbose,
721
- tty_enabled=tty_enabled,
722
- )
523
+ parsed_chat_history = _parse_chat_history(chat_history)
524
+ renderer, working_console = _setup_run_renderer(ctx, save, verbose)
723
525
 
724
- # Set HTTP timeout to match agent timeout exactly
725
- # This ensures the agent timeout controls the HTTP timeout
726
526
  try:
727
527
  client.timeout = float(timeout)
728
528
  except Exception:
729
529
  pass
730
530
 
731
- # Ensure timeout is applied to the root client and subclients share its session
732
- run_kwargs = {
733
- "agent_id": agent.id,
734
- "message": final_input_text,
735
- "files": list(files),
736
- "agent_name": agent.name, # Pass agent name for better display
737
- "tty": tty_enabled,
738
- }
739
-
740
- # Add optional parameters
741
- if parsed_chat_history:
742
- run_kwargs["chat_history"] = parsed_chat_history
743
-
744
- # Pass custom renderer if available
745
- if renderer is not None:
746
- run_kwargs["renderer"] = renderer
531
+ run_kwargs = _prepare_run_kwargs(
532
+ agent,
533
+ final_input_text,
534
+ files,
535
+ parsed_chat_history,
536
+ renderer,
537
+ bool((ctx.obj or {}).get("tty", True)),
538
+ )
747
539
 
748
- # Pass timeout to client (verbose mode is handled by the renderer)
749
540
  result = client.agents.run_agent(**run_kwargs, timeout=timeout)
750
541
 
751
- # Check if renderer already printed output (for streaming renderers)
752
- # Note: Auto-paging is handled by the renderer when view=="rich"
753
- printed_by_renderer = bool(renderer)
754
-
755
- # Resolve selected view from context (output_flags() stores it here)
756
- selected_view = (ctx.obj or {}).get("view", "rich")
757
-
758
- # Handle output format for fallback
759
- # Only print here if nothing was printed by the renderer
760
- if not printed_by_renderer:
761
- if selected_view == "json":
762
- click.echo(json.dumps({"output": result}, indent=2))
763
- elif selected_view == "md":
764
- click.echo(f"# Assistant\n\n{result}")
765
- elif selected_view == "plain":
766
- click.echo(result)
767
-
768
- # Save transcript if requested
769
- if save:
770
- ext = (save.rsplit(".", 1)[-1] or "").lower()
771
- if ext == "json":
772
- # Save both the result and captured output
773
- save_data = {
774
- "output": result or "",
775
- "full_debug_output": getattr(
776
- working_console, "get_captured_output", lambda: ""
777
- )(),
778
- "timestamp": "captured during agent execution",
779
- }
780
- content = json.dumps(save_data, indent=2)
781
- else:
782
- # For markdown/text files, save the full captured output if available
783
- # Get the full captured output including all tool panels and debug info (if available)
784
- full_output = getattr(
785
- working_console, "get_captured_output", lambda: ""
786
- )()
787
- if full_output:
788
- content = f"# Agent Debug Log\n\n{full_output}\n\n---\n\n## Final Result\n\n{result or ''}\n"
789
- else:
790
- # Fallback to simple format
791
- content = f"# Assistant\n\n{result or ''}\n"
792
-
793
- with open(save, "w", encoding="utf-8") as f:
794
- f.write(content)
795
- console.print(Text(f"[green]Full debug output saved to: {save}[/green]"))
542
+ _handle_run_output(ctx, result, renderer)
543
+ _save_run_transcript(save, result, working_console)
796
544
 
797
545
  except AgentTimeoutError as e:
798
- # Handle agent timeout errors with specific messages
799
546
  error_msg = str(e)
800
- if ctx.obj.get("view") == "json":
801
- click.echo(json.dumps({"error": error_msg}, indent=2))
802
- # Don't print the error message here - Click.ClickException will handle it
547
+ handle_json_output(ctx, error=Exception(error_msg))
803
548
  raise click.ClickException(error_msg)
804
549
  except Exception as e:
805
- if ctx.obj.get("view") == "json":
806
- click.echo(json.dumps({"error": str(e)}, indent=2))
807
- # Don't print the error message here - Click.ClickException will handle it
550
+ handle_json_output(ctx, error=e)
808
551
  raise click.ClickException(str(e))
809
552
 
810
553
 
@@ -817,6 +560,7 @@ def run(
817
560
  )
818
561
  @click.option("--tools", multiple=True, help="Tool names or IDs to attach")
819
562
  @click.option("--agents", multiple=True, help="Sub-agent names or IDs to attach")
563
+ @click.option("--mcps", multiple=True, help="MCP names or IDs to attach")
820
564
  @click.option(
821
565
  "--timeout",
822
566
  default=DEFAULT_AGENT_RUN_TIMEOUT,
@@ -838,6 +582,7 @@ def create(
838
582
  model,
839
583
  tools,
840
584
  agents,
585
+ mcps,
841
586
  timeout,
842
587
  import_file,
843
588
  ):
@@ -850,12 +595,20 @@ def create(
850
595
  try:
851
596
  client = get_client(ctx)
852
597
 
598
+ # Initialize merged_data for cases without import_file
599
+ merged_data = {}
600
+
853
601
  # Handle import from file
854
- if import_file:
855
- import_data = _load_agent_from_file(Path(import_file))
602
+ if (
603
+ import_file
604
+ ): # pragma: no cover - exercised in higher-level integration tests
605
+ import_data = load_resource_from_file(Path(import_file), "agent")
856
606
 
857
607
  # Convert export format to import-compatible format
858
- import_data = _convert_export_to_import_format(import_data)
608
+ import_data = convert_export_to_import_format(import_data)
609
+
610
+ # Auto-normalize agent config (extract LM settings from agent_config)
611
+ import_data = normalize_agent_config_for_import(import_data, model)
859
612
 
860
613
  # Merge CLI args with imported data
861
614
  cli_args = {
@@ -864,20 +617,35 @@ def create(
864
617
  "model": model,
865
618
  "tools": tools or (),
866
619
  "agents": agents or (),
620
+ "mcps": mcps or (),
867
621
  "timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
868
622
  }
869
623
 
870
- merged_data = _merge_import_with_cli_args(import_data, cli_args)
871
-
872
- # Extract merged values
873
- name = merged_data.get("name")
874
- instruction = merged_data.get("instruction")
875
- model = merged_data.get("model")
876
- tools = tuple(merged_data.get("tools", ()))
877
- agents = tuple(merged_data.get("agents", ()))
878
- timeout = merged_data.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
624
+ merged_data = merge_import_with_cli_args(import_data, cli_args)
625
+ else:
626
+ # No import file - use CLI args directly
627
+ merged_data = {
628
+ "name": name,
629
+ "instruction": instruction,
630
+ "model": model,
631
+ "tools": tools or (),
632
+ "agents": agents or (),
633
+ "mcps": mcps or (),
634
+ "timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
635
+ }
879
636
 
880
- # Validate required fields
637
+ # Extract merged values
638
+ name = merged_data.get("name")
639
+ instruction = merged_data.get("instruction")
640
+ model = merged_data.get("model")
641
+ tools = tuple(merged_data.get("tools", ()))
642
+ agents = tuple(merged_data.get("agents", ()))
643
+ mcps = tuple(merged_data.get("mcps", ()))
644
+ timeout = merged_data.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
645
+ # Coerce timeout to proper integer type
646
+ timeout = coerce_timeout(timeout)
647
+
648
+ # Validate required fields using centralized validators
881
649
  if not name:
882
650
  raise click.ClickException("Agent name is required (--name or --import)")
883
651
  if not instruction:
@@ -885,6 +653,12 @@ def create(
885
653
  "Agent instruction is required (--instruction or --import)"
886
654
  )
887
655
 
656
+ # Apply validation
657
+ name = validate_agent_name(name)
658
+ instruction = validate_agent_instruction(instruction)
659
+ if timeout is not None:
660
+ timeout = validate_timeout(timeout)
661
+
888
662
  # Resolve tool and agent references: accept names or IDs
889
663
  resolved_tools = _resolve_resources_by_name(
890
664
  client, tools, "tool", client.find_tools, "Tool"
@@ -892,6 +666,9 @@ def create(
892
666
  resolved_agents = _resolve_resources_by_name(
893
667
  client, agents, "agent", client.find_agents, "Agent"
894
668
  )
669
+ resolved_mcps = _resolve_resources_by_name(
670
+ client, mcps, "mcp", client.find_mcps, "MCP"
671
+ )
895
672
 
896
673
  # Create agent with comprehensive attribute support
897
674
  create_kwargs = {
@@ -899,21 +676,28 @@ def create(
899
676
  "instruction": instruction,
900
677
  "tools": resolved_tools or None,
901
678
  "agents": resolved_agents or None,
679
+ "mcps": resolved_mcps or None,
902
680
  "timeout": timeout,
903
681
  }
904
682
 
905
- # Add model if specified (prioritize CLI model over imported model)
906
- if model:
907
- create_kwargs["model"] = model
908
- elif (
909
- import_file
910
- and "agent_config" in merged_data
911
- and merged_data["agent_config"]
912
- ):
913
- # Use lm_name from agent_config for cloning
914
- agent_config = merged_data["agent_config"]
915
- if isinstance(agent_config, dict) and "lm_name" in agent_config:
916
- create_kwargs["model"] = agent_config["lm_name"]
683
+ # Handle language model selection using helper function
684
+ lm_selection_dict, should_strip_lm_identity = resolve_language_model_selection(
685
+ merged_data, model
686
+ )
687
+ create_kwargs.update(lm_selection_dict)
688
+
689
+ # If importing from file, include agent_config (pass-through minus credentials)
690
+ if import_file:
691
+ agent_config_raw = (
692
+ merged_data.get("agent_config")
693
+ if isinstance(merged_data, dict)
694
+ else None
695
+ )
696
+ if isinstance(agent_config_raw, dict):
697
+ # If language_model_id is used, strip LM identity keys from agent_config to avoid conflicts
698
+ create_kwargs["agent_config"] = sanitize_agent_config(
699
+ agent_config_raw, strip_lm_identity=should_strip_lm_identity
700
+ )
917
701
 
918
702
  # If importing from file, include all other detected attributes
919
703
  if import_file:
@@ -922,15 +706,15 @@ def create(
922
706
  "name",
923
707
  "instruction",
924
708
  "model",
709
+ "language_model_id",
925
710
  "tools",
926
711
  "agents",
927
712
  "timeout",
713
+ "agent_config", # handled explicitly above
928
714
  # System-only fields that shouldn't be passed to create_agent
929
715
  "id",
930
716
  "created_at",
931
717
  "updated_at",
932
- "agent_config",
933
- "language_model_id",
934
718
  "type",
935
719
  "framework",
936
720
  "version",
@@ -944,24 +728,139 @@ def create(
944
728
 
945
729
  agent = client.agents.create_agent(**create_kwargs)
946
730
 
947
- if ctx.obj.get("view") == "json":
948
- click.echo(json.dumps(agent.model_dump(), indent=2))
949
- else:
950
- # Rich output
951
- _display_agent_creation_success(agent, model)
731
+ handle_json_output(ctx, agent.model_dump())
952
732
 
953
- # Show run suggestions (only in rich mode, not JSON)
954
- if ctx.obj.get("view") != "json":
955
- _display_run_suggestions(agent)
733
+ lm_display = getattr(agent, "model", None)
734
+ if not lm_display:
735
+ cfg = getattr(agent, "agent_config", {}) or {}
736
+ lm_display = (
737
+ cfg.get("lm_name")
738
+ or cfg.get("model")
739
+ or model
740
+ or f"{DEFAULT_MODEL} (backend default)"
741
+ )
956
742
 
957
- except Exception as e:
743
+ handle_rich_output(
744
+ ctx,
745
+ display_creation_success(
746
+ "Agent",
747
+ agent.name,
748
+ agent.id,
749
+ Model=lm_display,
750
+ Type=getattr(agent, "type", "config"),
751
+ Framework=getattr(agent, "framework", "langchain"),
752
+ Version=getattr(agent, "version", "1.0"),
753
+ ),
754
+ )
755
+ handle_rich_output(ctx, display_agent_run_suggestions(agent))
756
+
757
+ except (
758
+ click.ClickException
759
+ ): # pragma: no cover - error formatting verified elsewhere
760
+ # Handle JSON output for ClickExceptions if view is JSON
958
761
  if ctx.obj.get("view") == "json":
959
- click.echo(json.dumps({"error": str(e)}, indent=2))
960
- else:
961
- console.print(Text(f"[red]Error creating agent: {e}[/red]"))
762
+ handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
763
+ # Re-raise ClickExceptions without additional processing
764
+ raise
765
+ except Exception as e: # pragma: no cover - defensive logging path
766
+ handle_json_output(ctx, error=e)
767
+ if ctx.obj.get("view") != "json":
768
+ print_api_error(e)
962
769
  raise click.ClickException(str(e))
963
770
 
964
771
 
772
+ def _get_agent_for_update(client, agent_id):
773
+ """Retrieve agent by ID for update operation."""
774
+ try:
775
+ return client.agents.get_agent_by_id(agent_id)
776
+ except Exception as e:
777
+ raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
778
+
779
+
780
+ def _handle_update_import_file(import_file, name, instruction, tools, agents, timeout):
781
+ """Handle import file processing for agent update."""
782
+ if not import_file:
783
+ return None, name, instruction, tools, agents, timeout
784
+
785
+ import_data = load_resource_from_file(Path(import_file), "agent")
786
+ import_data = convert_export_to_import_format(import_data)
787
+ import_data = normalize_agent_config_for_import(import_data, None)
788
+
789
+ cli_args = {
790
+ "name": name,
791
+ "instruction": instruction,
792
+ "tools": tools or (),
793
+ "agents": agents or (),
794
+ "timeout": timeout,
795
+ }
796
+
797
+ merged_data = merge_import_with_cli_args(import_data, cli_args)
798
+
799
+ return (
800
+ merged_data,
801
+ merged_data.get("name"),
802
+ merged_data.get("instruction"),
803
+ tuple(merged_data.get("tools", ())),
804
+ tuple(merged_data.get("agents", ())),
805
+ coerce_timeout(merged_data.get("timeout")),
806
+ )
807
+
808
+
809
+ def _build_update_data(name, instruction, tools, agents, timeout):
810
+ """Build the update data dictionary from provided parameters."""
811
+ update_data = {}
812
+ if name is not None:
813
+ update_data["name"] = name
814
+ if instruction is not None:
815
+ update_data["instruction"] = instruction
816
+ if tools:
817
+ update_data["tools"] = list(tools)
818
+ if agents:
819
+ update_data["agents"] = list(agents)
820
+ if timeout is not None:
821
+ update_data["timeout"] = timeout
822
+ return update_data
823
+
824
+
825
+ def _handle_update_import_config(import_file, merged_data, update_data):
826
+ """Handle agent config and additional attributes for import-based updates."""
827
+ if not import_file:
828
+ return
829
+
830
+ lm_selection, should_strip_lm_identity = resolve_language_model_selection(
831
+ merged_data, None
832
+ )
833
+ update_data.update(lm_selection)
834
+
835
+ raw_cfg = merged_data.get("agent_config") if isinstance(merged_data, dict) else None
836
+ if isinstance(raw_cfg, dict):
837
+ update_data["agent_config"] = sanitize_agent_config(
838
+ raw_cfg, strip_lm_identity=should_strip_lm_identity
839
+ )
840
+
841
+ excluded_fields = {
842
+ "name",
843
+ "instruction",
844
+ "tools",
845
+ "agents",
846
+ "timeout",
847
+ "agent_config",
848
+ "language_model_id",
849
+ "id",
850
+ "created_at",
851
+ "updated_at",
852
+ "type",
853
+ "framework",
854
+ "version",
855
+ "tool_configs",
856
+ "mcps",
857
+ "a2a_profile",
858
+ }
859
+ for key, value in merged_data.items():
860
+ if key not in excluded_fields and value is not None:
861
+ update_data[key] = value
862
+
863
+
965
864
  @agents_group.command()
966
865
  @click.argument("agent_id")
967
866
  @click.option("--name", help="New agent name")
@@ -986,96 +885,39 @@ def update(ctx, agent_id, name, instruction, tools, agents, timeout, import_file
986
885
  """
987
886
  try:
988
887
  client = get_client(ctx)
888
+ agent = _get_agent_for_update(client, agent_id)
989
889
 
990
- # Get agent by ID (no ambiguity handling needed)
991
- try:
992
- agent = client.agents.get_agent_by_id(agent_id)
993
- except Exception as e:
994
- raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
890
+ # Handle import file processing
891
+ merged_data, name, instruction, tools, agents, timeout = (
892
+ _handle_update_import_file(
893
+ import_file, name, instruction, tools, agents, timeout
894
+ )
895
+ )
995
896
 
996
- # Handle import from file
997
- if import_file:
998
- import_data = _load_agent_from_file(Path(import_file))
897
+ update_data = _build_update_data(name, instruction, tools, agents, timeout)
999
898
 
1000
- # Convert export format to import-compatible format
1001
- import_data = _convert_export_to_import_format(import_data)
1002
-
1003
- # Merge CLI args with imported data
1004
- cli_args = {
1005
- "name": name,
1006
- "instruction": instruction,
1007
- "tools": tools or (),
1008
- "agents": agents or (),
1009
- "timeout": timeout,
1010
- }
1011
-
1012
- merged_data = _merge_import_with_cli_args(import_data, cli_args)
1013
-
1014
- # Extract merged values
1015
- name = merged_data.get("name")
1016
- instruction = merged_data.get("instruction")
1017
- tools = tuple(merged_data.get("tools", ()))
1018
- agents = tuple(merged_data.get("agents", ()))
1019
- timeout = merged_data.get("timeout")
1020
-
1021
- # Build update data with comprehensive attribute support
1022
- update_data = {}
1023
- if name is not None:
1024
- update_data["name"] = name
1025
- if instruction is not None:
1026
- update_data["instruction"] = instruction
1027
- if tools:
1028
- update_data["tools"] = list(tools)
1029
- if agents:
1030
- update_data["agents"] = list(agents)
1031
- if timeout is not None:
1032
- update_data["timeout"] = timeout
1033
-
1034
- # If importing from file, include all other detected attributes
1035
- if import_file:
1036
- # Add all other attributes from import data (excluding already handled ones and system-only fields)
1037
- excluded_fields = {
1038
- "name",
1039
- "instruction",
1040
- "tools",
1041
- "agents",
1042
- "timeout",
1043
- # System-only fields that shouldn't be passed to update_agent
1044
- "id",
1045
- "created_at",
1046
- "updated_at",
1047
- "agent_config",
1048
- "type",
1049
- "framework",
1050
- "version",
1051
- "tool_configs",
1052
- "mcps",
1053
- "a2a_profile",
1054
- }
1055
- for key, value in merged_data.items():
1056
- if key not in excluded_fields and value is not None:
1057
- update_data[key] = value
899
+ if merged_data:
900
+ _handle_update_import_config(import_file, merged_data, update_data)
1058
901
 
1059
902
  if not update_data:
1060
903
  raise click.ClickException("No update fields specified")
1061
904
 
1062
- # Update agent
1063
905
  updated_agent = client.agents.update_agent(agent.id, **update_data)
1064
906
 
1065
- if ctx.obj.get("view") == "json":
1066
- click.echo(json.dumps(updated_agent.model_dump(), indent=2))
1067
- else:
1068
- _display_agent_update_success(updated_agent)
907
+ handle_json_output(ctx, updated_agent.model_dump())
908
+ handle_rich_output(ctx, display_update_success("Agent", updated_agent.name))
909
+ handle_rich_output(ctx, display_agent_run_suggestions(updated_agent))
1069
910
 
1070
- # Show run suggestions (only in rich mode, not JSON)
1071
- if ctx.obj.get("view") != "json":
1072
- _display_run_suggestions(updated_agent)
1073
-
1074
- except Exception as e:
911
+ except click.ClickException:
912
+ # Handle JSON output for ClickExceptions if view is JSON
1075
913
  if ctx.obj.get("view") == "json":
1076
- click.echo(json.dumps({"error": str(e)}, indent=2))
1077
- else:
1078
- console.print(Text(f"[red]Error updating agent: {e}[/red]"))
914
+ handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
915
+ # Re-raise ClickExceptions without additional processing
916
+ raise
917
+ except Exception as e:
918
+ handle_json_output(ctx, error=e)
919
+ if ctx.obj.get("view") != "json":
920
+ print_api_error(e)
1079
921
  raise click.ClickException(str(e))
1080
922
 
1081
923
 
@@ -1095,31 +937,86 @@ def delete(ctx, agent_id, yes):
1095
937
  except Exception as e:
1096
938
  raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
1097
939
 
1098
- # Confirm deletion
1099
- if not yes and not click.confirm(
1100
- f"Are you sure you want to delete agent '{agent.name}'?"
1101
- ):
1102
- if ctx.obj.get("view") != "json":
1103
- console.print(Text("Deletion cancelled."))
940
+ # Confirm deletion when not forced
941
+ if not yes and not display_confirmation_prompt("Agent", agent.name):
1104
942
  return
1105
943
 
1106
944
  client.agents.delete_agent(agent.id)
1107
945
 
946
+ handle_json_output(
947
+ ctx,
948
+ {
949
+ "success": True,
950
+ "message": f"Agent '{agent.name}' deleted",
951
+ },
952
+ )
953
+ handle_rich_output(ctx, display_deletion_success("Agent", agent.name))
954
+
955
+ except click.ClickException:
956
+ # Handle JSON output for ClickExceptions if view is JSON
1108
957
  if ctx.obj.get("view") == "json":
1109
- click.echo(
1110
- json.dumps(
1111
- {"success": True, "message": f"Agent '{agent.name}' deleted"},
1112
- indent=2,
1113
- )
1114
- )
1115
- else:
1116
- console.print(
1117
- Text(f"[green]✅ Agent '{agent.name}' deleted successfully[/green]")
958
+ handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
959
+ # Re-raise ClickExceptions without additional processing
960
+ raise
961
+ except Exception as e:
962
+ handle_json_output(ctx, error=e)
963
+ if ctx.obj.get("view") != "json":
964
+ print_api_error(e)
965
+ raise click.ClickException(str(e))
966
+
967
+
968
+ @agents_group.command()
969
+ @click.option(
970
+ "--base-url",
971
+ help="Custom LangFlow server base URL (overrides LANGFLOW_BASE_URL env var)",
972
+ )
973
+ @click.option(
974
+ "--api-key", help="Custom LangFlow API key (overrides LANGFLOW_API_KEY env var)"
975
+ )
976
+ @output_flags()
977
+ @click.pass_context
978
+ def sync_langflow(ctx, base_url, api_key): # pragma: no cover - integration-only path
979
+ """Sync agents with LangFlow server flows.
980
+
981
+ This command fetches all flows from the configured LangFlow server and
982
+ creates/updates corresponding agents in the platform.
983
+
984
+ The LangFlow server configuration can be provided via:
985
+ - Command options (--base-url, --api-key)
986
+ - Environment variables (LANGFLOW_BASE_URL, LANGFLOW_API_KEY)
987
+
988
+ Examples:
989
+ aip agents sync-langflow
990
+ aip agents sync-langflow --base-url https://my-langflow.com --api-key my-key
991
+ """
992
+ try:
993
+ client = get_client(ctx)
994
+
995
+ # Perform the sync
996
+ result = client.sync_langflow_agents(base_url=base_url, api_key=api_key)
997
+
998
+ # Handle output format
999
+ handle_json_output(ctx, result)
1000
+
1001
+ # Show success message for non-JSON output
1002
+ if ctx.obj.get("view") != "json":
1003
+ from rich.text import Text
1004
+
1005
+ # Extract some useful info from the result
1006
+ success_count = result.get("data", {}).get("created_count", 0) + result.get(
1007
+ "data", {}
1008
+ ).get("updated_count", 0)
1009
+ total_count = result.get("data", {}).get("total_processed", 0)
1010
+
1011
+ handle_rich_output(
1012
+ ctx,
1013
+ Text(
1014
+ f"[green]✅ Successfully synced {success_count} LangFlow agents ({total_count} total processed)[/green]"
1015
+ ),
1118
1016
  )
1119
1017
 
1120
1018
  except Exception as e:
1121
- if ctx.obj.get("view") == "json":
1122
- click.echo(json.dumps({"error": str(e)}, indent=2))
1123
- else:
1124
- console.print(Text(f"[red]Error deleting agent: {e}[/red]"))
1019
+ handle_json_output(ctx, error=e)
1020
+ if ctx.obj.get("view") != "json":
1021
+ print_api_error(e)
1125
1022
  raise click.ClickException(str(e))