glaip-sdk 0.0.3__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 +146 -0
  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 +786 -271
  7. glaip_sdk/cli/commands/configure.py +19 -19
  8. glaip_sdk/cli/commands/mcps.py +151 -141
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +252 -178
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +27 -20
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +372 -213
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +632 -171
  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 +327 -104
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +43 -3
  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.3.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 -177
  45. glaip_sdk-0.0.3.dist-info/RECORD +0 -40
  46. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
  47. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
@@ -5,36 +5,207 @@ Authors:
5
5
  """
6
6
 
7
7
  import json
8
- from datetime import datetime
8
+ import os
9
+ from pathlib import Path
9
10
 
10
11
  import click
11
12
  from rich.console import Console
12
- from rich.panel import Panel
13
+ from rich.text import Text
13
14
 
14
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
15
- from glaip_sdk.exceptions import AgentTimeoutError
16
- from glaip_sdk.utils import is_uuid
17
-
18
- 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 (
46
+ _fuzzy_pick_for_resources,
19
47
  build_renderer,
20
48
  coerce_to_row,
21
49
  get_client,
22
50
  output_flags,
23
51
  output_list,
24
52
  output_result,
25
- resolve_resource,
26
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
27
69
 
28
70
  console = Console()
29
71
 
72
+ # Error message constants
73
+ AGENT_NOT_FOUND_ERROR = "Agent not found"
74
+
75
+
76
+ def _fetch_full_agent_details(client, agent):
77
+ """Fetch full agent details by ID to ensure all fields are populated."""
78
+ try:
79
+ agent_id = str(getattr(agent, "id", "")).strip()
80
+ if agent_id:
81
+ return client.agents.get_agent_by_id(agent_id)
82
+ except Exception:
83
+ # If fetching full details fails, continue with the resolved object
84
+ pass
85
+ return agent
86
+
87
+
88
+ def _get_agent_model_name(agent):
89
+ """Extract model name from agent configuration."""
90
+ # Try different possible locations for model name
91
+ if hasattr(agent, "agent_config") and agent.agent_config:
92
+ if isinstance(agent.agent_config, dict):
93
+ return agent.agent_config.get("lm_name") or agent.agent_config.get("model")
94
+
95
+ if hasattr(agent, "model") and agent.model:
96
+ return agent.model
97
+
98
+ # Default fallback
99
+ return DEFAULT_MODEL
100
+
101
+
102
+ def _resolve_resources_by_name(
103
+ _client, items: tuple[str, ...], resource_type: str, find_func, label: str
104
+ ) -> list[str]:
105
+ """Resolve resource names/IDs to IDs, handling ambiguity.
106
+
107
+ Args:
108
+ client: API client
109
+ items: Tuple of resource names/IDs
110
+ resource_type: Type of resource ("tool" or "agent")
111
+ find_func: Function to find resources by name
112
+ label: Label for error messages
113
+
114
+ Returns:
115
+ List of resolved resource IDs
116
+ """
117
+ out = []
118
+ for ref in list(items or ()):
119
+ if is_uuid(ref):
120
+ out.append(ref)
121
+ continue
122
+
123
+ matches = find_func(name=ref)
124
+ if not matches:
125
+ raise click.ClickException(f"{label} not found: {ref}")
126
+ if len(matches) > 1:
127
+ raise click.ClickException(
128
+ f"Multiple {resource_type}s named '{ref}'. Use ID instead."
129
+ )
130
+ out.append(str(matches[0].id))
131
+ return out
132
+
133
+
134
+ def _display_agent_details(ctx, client, agent):
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")
30
142
 
31
- def _format_datetime(dt):
32
- """Format datetime object to readable string."""
33
- if isinstance(dt, datetime):
34
- return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
35
- elif dt is None:
36
- return "N/A"
37
- return dt
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
+ )
38
209
 
39
210
 
40
211
  @click.group(name="agents", no_args_is_help=True)
@@ -43,26 +214,55 @@ def agents_group():
43
214
  pass
44
215
 
45
216
 
46
- def _resolve_agent(ctx, client, ref, select=None):
47
- """Resolve agent reference (ID or name) with ambiguity handling."""
48
- return resolve_resource(
217
+ def _resolve_agent(ctx, client, ref, select=None, interface_preference="fuzzy"):
218
+ """Resolve agent reference (ID or name) with ambiguity handling.
219
+
220
+ Args:
221
+ interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
222
+ """
223
+ return resolve_resource_reference(
49
224
  ctx,
225
+ client,
50
226
  ref,
51
- get_by_id=client.agents.get_agent_by_id,
52
- find_by_name=client.agents.find_agents,
53
- label="Agent",
227
+ "agent",
228
+ client.agents.get_agent_by_id,
229
+ client.agents.find_agents,
230
+ "Agent",
54
231
  select=select,
232
+ interface_preference=interface_preference,
55
233
  )
56
234
 
57
235
 
58
236
  @agents_group.command(name="list")
237
+ @click.option(
238
+ "--simple", is_flag=True, help="Show simple table without interactive picker"
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
+ )
59
253
  @output_flags()
60
254
  @click.pass_context
61
- def list_agents(ctx):
62
- """List all agents."""
255
+ def list_agents(ctx, simple, agent_type, framework, name, version, sync_langflow):
256
+ """List agents with optional filtering."""
63
257
  try:
64
258
  client = get_client(ctx)
65
- 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
+ )
66
266
 
67
267
  # Define table columns: (data_key, header, style, width)
68
268
  columns = [
@@ -80,6 +280,17 @@ def list_agents(ctx):
80
280
  row["id"] = str(row["id"])
81
281
  return row
82
282
 
283
+ # Use fuzzy picker for interactive agent selection and details (default behavior)
284
+ # Skip if --simple flag is used
285
+ if not simple and console.is_terminal and os.isatty(1) and len(agents) > 0:
286
+ picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
287
+ if picked_agent:
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))
291
+ return
292
+
293
+ # Show simple table (either --simple flag or non-interactive)
83
294
  output_list(ctx, agents, "🤖 Available Agents", columns, transform_agent)
84
295
 
85
296
  except Exception as e:
@@ -89,59 +300,171 @@ def list_agents(ctx):
89
300
  @agents_group.command()
90
301
  @click.argument("agent_ref")
91
302
  @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
303
+ @click.option(
304
+ "--export",
305
+ type=click.Path(dir_okay=False, writable=True),
306
+ help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
307
+ )
92
308
  @output_flags()
93
309
  @click.pass_context
94
- def get(ctx, agent_ref, select):
95
- """Get agent details."""
310
+ def get(ctx, agent_ref, select, export):
311
+ """Get agent details.
312
+
313
+ Examples:
314
+ aip agents get my-agent
315
+ aip agents get my-agent --export agent.json # Exports complete configuration as JSON
316
+ aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
317
+ """
96
318
  try:
97
319
  client = get_client(ctx)
98
320
 
99
- # Resolve agent with ambiguity handling
100
- agent = _resolve_agent(ctx, client, agent_ref, select)
321
+ # Resolve agent with ambiguity handling - use questionary interface for traditional UX
322
+ agent = _resolve_agent(
323
+ ctx, client, agent_ref, select, interface_preference="questionary"
324
+ )
101
325
 
102
- # If resolved by name, it may be a shallow object from list endpoint.
103
- # Fetch full details by ID to ensure instruction/tools are populated.
104
- try:
105
- agent_id = str(getattr(agent, "id", "")).strip()
106
- if agent_id:
107
- agent = client.agents.get_agent_by_id(agent_id)
108
- except Exception:
109
- # If fetching full details fails, continue with the resolved object.
110
- pass
326
+ # Handle export option
327
+ if export: # pragma: no cover - requires filesystem verification
328
+ export_path = Path(export)
329
+ # Auto-detect format from file extension
330
+ if export_path.suffix.lower() in [".yaml", ".yml"]:
331
+ detected_format = "yaml"
332
+ else:
333
+ detected_format = "json"
111
334
 
112
- # Create result data with all available fields from backend
113
- result_data = {
114
- "id": str(getattr(agent, "id", "N/A")),
115
- "name": getattr(agent, "name", "N/A"),
116
- "type": getattr(agent, "type", "N/A"),
117
- "framework": getattr(agent, "framework", "N/A"),
118
- "version": getattr(agent, "version", "N/A"),
119
- "description": getattr(agent, "description", "N/A"),
120
- "instruction": getattr(agent, "instruction", "") or "-",
121
- "created_at": _format_datetime(getattr(agent, "created_at", "N/A")),
122
- "updated_at": _format_datetime(getattr(agent, "updated_at", "N/A")),
123
- "metadata": getattr(agent, "metadata", "N/A"),
124
- "language_model_id": getattr(agent, "language_model_id", "N/A"),
125
- "agent_config": getattr(agent, "agent_config", "N/A"),
126
- "tool_configs": agent.tool_configs or {},
127
- "tools": getattr(agent, "tools", []),
128
- "agents": getattr(agent, "agents", []),
129
- "mcps": getattr(agent, "mcps", []),
130
- "a2a_profile": getattr(agent, "a2a_profile", "N/A"),
131
- }
335
+ # Always export comprehensive data - re-fetch agent with full details
336
+ try:
337
+ agent = client.agents.get_agent_by_id(agent.id)
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
+ ),
344
+ )
345
+ handle_rich_output(
346
+ ctx, Text("[yellow]⚠️ Proceeding with available data[/yellow]")
347
+ )
132
348
 
133
- output_result(
134
- ctx, result_data, title="Agent Details", panel_title=f"🤖 {agent.name}"
135
- )
349
+ export_resource_to_file(agent, export_path, detected_format)
350
+ handle_rich_output(
351
+ ctx,
352
+ Text(
353
+ f"[green]✅ Complete agent configuration exported to: {export_path} (format: {detected_format})[/green]"
354
+ ),
355
+ )
356
+
357
+ # Display full agent details using the standardized helper
358
+ _display_agent_details(ctx, client, agent)
359
+
360
+ # Show run suggestions via centralized display helper
361
+ handle_rich_output(ctx, display_agent_run_suggestions(agent))
136
362
 
137
363
  except Exception as e:
138
364
  raise click.ClickException(str(e))
139
365
 
140
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
+
141
463
  @agents_group.command()
142
464
  @click.argument("agent_ref")
465
+ @click.argument("input_text", required=False)
143
466
  @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
144
- @click.option("--input", "input_text", required=True, help="Input text for the agent")
467
+ @click.option("--input", "input_option", help="Input text for the agent")
145
468
  @click.option("--chat-history", help="JSON string of chat history")
146
469
  @click.option(
147
470
  "--timeout",
@@ -173,140 +496,83 @@ def run(
173
496
  agent_ref,
174
497
  select,
175
498
  input_text,
499
+ input_option,
176
500
  chat_history,
177
501
  timeout,
178
502
  save,
179
503
  files,
180
504
  verbose,
181
505
  ):
182
- """Run an agent with input text (ID or name)."""
183
- try:
184
- client = get_client(ctx)
506
+ """Run an agent with input text.
185
507
 
186
- # Resolve agent by ID or name (align with other commands)
187
- agent = _resolve_agent(ctx, client, agent_ref, select)
508
+ Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
188
509
 
189
- # Parse chat history if provided
190
- parsed_chat_history = None
191
- if chat_history:
192
- try:
193
- parsed_chat_history = json.loads(chat_history)
194
- except json.JSONDecodeError:
195
- raise click.ClickException("Invalid JSON in chat history")
510
+ Examples:
511
+ aip agents run my-agent "Hello world"
512
+ aip agents run agent-123 "Process this data" --timeout 600
513
+ aip agents run my-agent --input "Hello world" # Legacy style
514
+ """
515
+ final_input_text = _validate_run_input(input_option, input_text)
196
516
 
197
- # Create custom renderer with CLI flags
198
- tty_enabled = bool((ctx.obj or {}).get("tty", True))
199
-
200
- # Build renderer and capturing console
201
- renderer, working_console = build_renderer(
202
- ctx,
203
- save_path=save,
204
- verbose=verbose,
205
- tty_enabled=tty_enabled,
517
+ try:
518
+ client = get_client(ctx)
519
+ agent = _resolve_agent(
520
+ ctx, client, agent_ref, select, interface_preference="fuzzy"
206
521
  )
207
522
 
208
- # Set HTTP timeout to match agent timeout exactly
209
- # This ensures the agent timeout controls the HTTP timeout
523
+ parsed_chat_history = _parse_chat_history(chat_history)
524
+ renderer, working_console = _setup_run_renderer(ctx, save, verbose)
525
+
210
526
  try:
211
527
  client.timeout = float(timeout)
212
528
  except Exception:
213
529
  pass
214
530
 
215
- # Ensure timeout is applied to the root client and subclients share its session
216
- run_kwargs = {
217
- "agent_id": agent.id,
218
- "message": input_text,
219
- "files": list(files),
220
- "agent_name": agent.name, # Pass agent name for better display
221
- "tty": tty_enabled,
222
- }
223
-
224
- # Add optional parameters
225
- if parsed_chat_history:
226
- run_kwargs["chat_history"] = parsed_chat_history
227
-
228
- # Pass custom renderer if available
229
- if renderer is not None:
230
- 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
+ )
231
539
 
232
- # Pass timeout to client (verbose mode is handled by the renderer)
233
540
  result = client.agents.run_agent(**run_kwargs, timeout=timeout)
234
541
 
235
- # Check if renderer already printed output (for streaming renderers)
236
- # Note: Auto-paging is handled by the renderer when view=="rich"
237
- printed_by_renderer = bool(renderer)
238
-
239
- # Resolve selected view from context (output_flags() stores it here)
240
- selected_view = (ctx.obj or {}).get("view", "rich")
241
-
242
- # Handle output format for fallback
243
- # Only print here if nothing was printed by the renderer
244
- if not printed_by_renderer:
245
- if selected_view == "json":
246
- click.echo(json.dumps({"output": result}, indent=2))
247
- elif selected_view == "md":
248
- click.echo(f"# Assistant\n\n{result}")
249
- elif selected_view == "plain":
250
- click.echo(result)
251
-
252
- # Save transcript if requested
253
- if save:
254
- ext = (save.rsplit(".", 1)[-1] or "").lower()
255
- if ext == "json":
256
- # Save both the result and captured output
257
- save_data = {
258
- "output": result or "",
259
- "full_debug_output": getattr(
260
- working_console, "get_captured_output", lambda: ""
261
- )(),
262
- "timestamp": "captured during agent execution",
263
- }
264
- content = json.dumps(save_data, indent=2)
265
- else:
266
- # For markdown/text files, save the full captured output if available
267
- # Get the full captured output including all tool panels and debug info (if available)
268
- full_output = getattr(
269
- working_console, "get_captured_output", lambda: ""
270
- )()
271
- if full_output:
272
- content = f"# Agent Debug Log\n\n{full_output}\n\n---\n\n## Final Result\n\n{result or ''}\n"
273
- else:
274
- # Fallback to simple format
275
- content = f"# Assistant\n\n{result or ''}\n"
276
-
277
- with open(save, "w", encoding="utf-8") as f:
278
- f.write(content)
279
- console.print(f"[green]Full debug output saved to: {save}[/green]")
542
+ _handle_run_output(ctx, result, renderer)
543
+ _save_run_transcript(save, result, working_console)
280
544
 
281
545
  except AgentTimeoutError as e:
282
- # Handle agent timeout errors with specific messages
283
546
  error_msg = str(e)
284
- if ctx.obj.get("view") == "json":
285
- click.echo(json.dumps({"error": error_msg}, indent=2))
286
- # Don't print the error message here - Click.ClickException will handle it
547
+ handle_json_output(ctx, error=Exception(error_msg))
287
548
  raise click.ClickException(error_msg)
288
549
  except Exception as e:
289
- if ctx.obj.get("view") == "json":
290
- click.echo(json.dumps({"error": str(e)}, indent=2))
291
- # Don't print the error message here - Click.ClickException will handle it
550
+ handle_json_output(ctx, error=e)
292
551
  raise click.ClickException(str(e))
293
552
 
294
553
 
295
554
  @agents_group.command()
296
- @click.option("--name", required=True, help="Agent name")
297
- @click.option("--instruction", required=True, help="Agent instruction (prompt)")
555
+ @click.option("--name", help="Agent name")
556
+ @click.option("--instruction", help="Agent instruction (prompt)")
298
557
  @click.option(
299
558
  "--model",
300
559
  help=f"Language model to use (e.g., {DEFAULT_MODEL}, default: {DEFAULT_MODEL})",
301
560
  )
302
561
  @click.option("--tools", multiple=True, help="Tool names or IDs to attach")
303
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")
304
564
  @click.option(
305
565
  "--timeout",
306
566
  default=DEFAULT_AGENT_RUN_TIMEOUT,
307
567
  type=int,
308
568
  help="Agent execution timeout in seconds (default: 300s)",
309
569
  )
570
+ @click.option(
571
+ "--import",
572
+ "import_file",
573
+ type=click.Path(exists=True, dir_okay=False),
574
+ help="Import agent configuration from JSON file",
575
+ )
310
576
  @output_flags()
311
577
  @click.pass_context
312
578
  def create(
@@ -316,97 +582,285 @@ def create(
316
582
  model,
317
583
  tools,
318
584
  agents,
585
+ mcps,
319
586
  timeout,
587
+ import_file,
320
588
  ):
321
- """Create a new agent."""
589
+ """Create a new agent.
590
+
591
+ Examples:
592
+ aip agents create --name "My Agent" --instruction "You are a helpful assistant"
593
+ aip agents create --import agent.json
594
+ """
322
595
  try:
323
596
  client = get_client(ctx)
324
597
 
598
+ # Initialize merged_data for cases without import_file
599
+ merged_data = {}
600
+
601
+ # Handle import from 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")
606
+
607
+ # Convert export format to import-compatible format
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)
612
+
613
+ # Merge CLI args with imported data
614
+ cli_args = {
615
+ "name": name,
616
+ "instruction": instruction,
617
+ "model": model,
618
+ "tools": tools or (),
619
+ "agents": agents or (),
620
+ "mcps": mcps or (),
621
+ "timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
622
+ }
623
+
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
+ }
636
+
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
649
+ if not name:
650
+ raise click.ClickException("Agent name is required (--name or --import)")
651
+ if not instruction:
652
+ raise click.ClickException(
653
+ "Agent instruction is required (--instruction or --import)"
654
+ )
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
+
325
662
  # Resolve tool and agent references: accept names or IDs
326
- def _resolve_tools(items: tuple[str, ...]) -> list[str]:
327
- out: list[str] = []
328
- for ref in list(items or ()): # tuple -> list
329
- if is_uuid(ref):
330
- out.append(ref)
331
- continue
332
- matches = client.find_tools(name=ref)
333
- if not matches:
334
- raise click.ClickException(f"Tool not found: {ref}")
335
- if len(matches) > 1:
336
- raise click.ClickException(
337
- f"Multiple tools named '{ref}'. Use ID instead."
338
- )
339
- out.append(str(matches[0].id))
340
- return out
341
-
342
- def _resolve_agents(items: tuple[str, ...]) -> list[str]:
343
- out: list[str] = []
344
- for ref in list(items or ()): # tuple -> list
345
- if is_uuid(ref):
346
- out.append(ref)
347
- continue
348
- matches = client.find_agents(name=ref)
349
- if not matches:
350
- raise click.ClickException(f"Agent not found: {ref}")
351
- if len(matches) > 1:
352
- raise click.ClickException(
353
- f"Multiple agents named '{ref}'. Use ID instead."
354
- )
355
- out.append(str(matches[0].id))
356
- return out
357
-
358
- resolved_tools = _resolve_tools(tools)
359
- resolved_agents = _resolve_agents(agents)
360
-
361
- # Create agent with optional model specification
663
+ resolved_tools = _resolve_resources_by_name(
664
+ client, tools, "tool", client.find_tools, "Tool"
665
+ )
666
+ resolved_agents = _resolve_resources_by_name(
667
+ client, agents, "agent", client.find_agents, "Agent"
668
+ )
669
+ resolved_mcps = _resolve_resources_by_name(
670
+ client, mcps, "mcp", client.find_mcps, "MCP"
671
+ )
672
+
673
+ # Create agent with comprehensive attribute support
362
674
  create_kwargs = {
363
675
  "name": name,
364
676
  "instruction": instruction,
365
677
  "tools": resolved_tools or None,
366
678
  "agents": resolved_agents or None,
679
+ "mcps": resolved_mcps or None,
367
680
  "timeout": timeout,
368
681
  }
369
682
 
370
- # Add model if specified
371
- if model:
372
- create_kwargs["model"] = model
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
+ )
701
+
702
+ # If importing from file, include all other detected attributes
703
+ if import_file:
704
+ # Add all other attributes from import data (excluding already handled ones and system-only fields)
705
+ excluded_fields = {
706
+ "name",
707
+ "instruction",
708
+ "model",
709
+ "language_model_id",
710
+ "tools",
711
+ "agents",
712
+ "timeout",
713
+ "agent_config", # handled explicitly above
714
+ # System-only fields that shouldn't be passed to create_agent
715
+ "id",
716
+ "created_at",
717
+ "updated_at",
718
+ "type",
719
+ "framework",
720
+ "version",
721
+ "tool_configs",
722
+ "mcps",
723
+ "a2a_profile",
724
+ }
725
+ for key, value in merged_data.items():
726
+ if key not in excluded_fields and value is not None:
727
+ create_kwargs[key] = value
373
728
 
374
729
  agent = client.agents.create_agent(**create_kwargs)
375
730
 
376
- if ctx.obj.get("view") == "json":
377
- click.echo(json.dumps(agent.model_dump(), indent=2))
378
- else:
379
- # Rich output
380
- lm = getattr(agent, "model", None)
381
- if not lm:
382
- cfg = getattr(agent, "agent_config", {}) or {}
383
- lm = (
384
- cfg.get("lm_name")
385
- or cfg.get("model")
386
- or model # Use CLI model if specified
387
- or f"{DEFAULT_MODEL} (backend default)"
388
- )
731
+ handle_json_output(ctx, agent.model_dump())
389
732
 
390
- panel = Panel(
391
- f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
392
- f"ID: {agent.id}\n"
393
- f"Model: {lm}\n"
394
- f"Type: {getattr(agent, 'type', 'config')}\n"
395
- f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
396
- f"Version: {getattr(agent, 'version', '1.0')}",
397
- title="🤖 Agent Created",
398
- border_style="green",
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)"
399
741
  )
400
- console.print(panel)
401
742
 
402
- 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
403
761
  if ctx.obj.get("view") == "json":
404
- click.echo(json.dumps({"error": str(e)}, indent=2))
405
- else:
406
- console.print(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)
407
769
  raise click.ClickException(str(e))
408
770
 
409
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
+
410
864
  @agents_group.command()
411
865
  @click.argument("agent_id")
412
866
  @click.option("--name", help="New agent name")
@@ -414,50 +868,56 @@ def create(
414
868
  @click.option("--tools", multiple=True, help="New tool names or IDs")
415
869
  @click.option("--agents", multiple=True, help="New sub-agent names")
416
870
  @click.option("--timeout", type=int, help="New timeout value")
871
+ @click.option(
872
+ "--import",
873
+ "import_file",
874
+ type=click.Path(exists=True, dir_okay=False),
875
+ help="Import agent configuration from JSON file",
876
+ )
417
877
  @output_flags()
418
878
  @click.pass_context
419
- def update(ctx, agent_id, name, instruction, tools, agents, timeout):
420
- """Update an existing agent."""
879
+ def update(ctx, agent_id, name, instruction, tools, agents, timeout, import_file):
880
+ """Update an existing agent.
881
+
882
+ Examples:
883
+ aip agents update my-agent --instruction "New instruction"
884
+ aip agents update my-agent --import agent.json
885
+ """
421
886
  try:
422
887
  client = get_client(ctx)
888
+ agent = _get_agent_for_update(client, agent_id)
423
889
 
424
- # Get agent by ID (no ambiguity handling needed)
425
- try:
426
- agent = client.agents.get_agent_by_id(agent_id)
427
- except Exception as e:
428
- 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
+ )
429
896
 
430
- # Build update data
431
- update_data = {}
432
- if name is not None:
433
- update_data["name"] = name
434
- if instruction is not None:
435
- update_data["instruction"] = instruction
436
- if tools:
437
- update_data["tools"] = list(tools)
438
- if agents:
439
- update_data["agents"] = list(agents)
440
- if timeout is not None:
441
- update_data["timeout"] = timeout
897
+ update_data = _build_update_data(name, instruction, tools, agents, timeout)
898
+
899
+ if merged_data:
900
+ _handle_update_import_config(import_file, merged_data, update_data)
442
901
 
443
902
  if not update_data:
444
903
  raise click.ClickException("No update fields specified")
445
904
 
446
- # Update agent
447
905
  updated_agent = client.agents.update_agent(agent.id, **update_data)
448
906
 
449
- if ctx.obj.get("view") == "json":
450
- click.echo(json.dumps(updated_agent.model_dump(), indent=2))
451
- else:
452
- console.print(
453
- f"[green]✅ Agent '{updated_agent.name}' updated successfully[/green]"
454
- )
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))
455
910
 
456
- except Exception as e:
911
+ except click.ClickException:
912
+ # Handle JSON output for ClickExceptions if view is JSON
457
913
  if ctx.obj.get("view") == "json":
458
- click.echo(json.dumps({"error": str(e)}, indent=2))
459
- else:
460
- console.print(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)
461
921
  raise click.ClickException(str(e))
462
922
 
463
923
 
@@ -477,31 +937,86 @@ def delete(ctx, agent_id, yes):
477
937
  except Exception as e:
478
938
  raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
479
939
 
480
- # Confirm deletion
481
- if not yes and not click.confirm(
482
- f"Are you sure you want to delete agent '{agent.name}'?"
483
- ):
484
- if ctx.obj.get("view") != "json":
485
- console.print("Deletion cancelled.")
940
+ # Confirm deletion when not forced
941
+ if not yes and not display_confirmation_prompt("Agent", agent.name):
486
942
  return
487
943
 
488
944
  client.agents.delete_agent(agent.id)
489
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
490
957
  if ctx.obj.get("view") == "json":
491
- click.echo(
492
- json.dumps(
493
- {"success": True, "message": f"Agent '{agent.name}' deleted"},
494
- indent=2,
495
- )
496
- )
497
- else:
498
- console.print(
499
- 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
+ ),
500
1016
  )
501
1017
 
502
1018
  except Exception as e:
503
- if ctx.obj.get("view") == "json":
504
- click.echo(json.dumps({"error": str(e)}, indent=2))
505
- else:
506
- console.print(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)
507
1022
  raise click.ClickException(str(e))