glaip-sdk 0.0.3__py3-none-any.whl → 0.0.4__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.
@@ -5,17 +5,22 @@ Authors:
5
5
  """
6
6
 
7
7
  import json
8
+ import os
8
9
  from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any
9
12
 
10
13
  import click
11
14
  from rich.console import Console
12
15
  from rich.panel import Panel
16
+ from rich.text import Text
13
17
 
14
18
  from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
15
19
  from glaip_sdk.exceptions import AgentTimeoutError
16
20
  from glaip_sdk.utils import is_uuid
17
21
 
18
22
  from ..utils import (
23
+ _fuzzy_pick_for_resources,
19
24
  build_renderer,
20
25
  coerce_to_row,
21
26
  get_client,
@@ -28,6 +33,31 @@ from ..utils import (
28
33
  console = Console()
29
34
 
30
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
+
31
61
  def _format_datetime(dt):
32
62
  """Format datetime object to readable string."""
33
63
  if isinstance(dt, datetime):
@@ -37,14 +67,454 @@ def _format_datetime(dt):
37
67
  return dt
38
68
 
39
69
 
70
+ def _fetch_full_agent_details(client, agent):
71
+ """Fetch full agent details by ID to ensure all fields are populated."""
72
+ try:
73
+ agent_id = str(getattr(agent, "id", "")).strip()
74
+ if agent_id:
75
+ return client.agents.get_agent_by_id(agent_id)
76
+ except Exception:
77
+ # If fetching full details fails, continue with the resolved object
78
+ pass
79
+ return agent
80
+
81
+
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
+ def _get_agent_model_name(agent):
190
+ """Extract model name from agent configuration."""
191
+ # Try different possible locations for model name
192
+ if hasattr(agent, "agent_config") and agent.agent_config:
193
+ if isinstance(agent.agent_config, dict):
194
+ return agent.agent_config.get("lm_name") or agent.agent_config.get("model")
195
+
196
+ if hasattr(agent, "model") and agent.model:
197
+ return agent.model
198
+
199
+ # Default fallback
200
+ return DEFAULT_MODEL
201
+
202
+
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
+ def _resolve_resources_by_name(
427
+ client, items: tuple[str, ...], resource_type: str, find_func, label: str
428
+ ) -> list[str]:
429
+ """Resolve resource names/IDs to IDs, handling ambiguity.
430
+
431
+ Args:
432
+ client: API client
433
+ items: Tuple of resource names/IDs
434
+ resource_type: Type of resource ("tool" or "agent")
435
+ find_func: Function to find resources by name
436
+ label: Label for error messages
437
+
438
+ Returns:
439
+ List of resolved resource IDs
440
+ """
441
+ out = []
442
+ for ref in list(items or ()):
443
+ if is_uuid(ref):
444
+ out.append(ref)
445
+ continue
446
+
447
+ matches = find_func(name=ref)
448
+ if not matches:
449
+ raise click.ClickException(f"{label} not found: {ref}")
450
+ if len(matches) > 1:
451
+ raise click.ClickException(
452
+ f"Multiple {resource_type}s named '{ref}'. Use ID instead."
453
+ )
454
+ out.append(str(matches[0].id))
455
+ return out
456
+
457
+
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
+ 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
+ )
504
+
505
+
40
506
  @click.group(name="agents", no_args_is_help=True)
41
507
  def agents_group():
42
508
  """Agent management operations."""
43
509
  pass
44
510
 
45
511
 
46
- def _resolve_agent(ctx, client, ref, select=None):
47
- """Resolve agent reference (ID or name) with ambiguity handling."""
512
+ def _resolve_agent(ctx, client, ref, select=None, interface_preference="fuzzy"):
513
+ """Resolve agent reference (ID or name) with ambiguity handling.
514
+
515
+ Args:
516
+ interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
517
+ """
48
518
  return resolve_resource(
49
519
  ctx,
50
520
  ref,
@@ -52,13 +522,17 @@ def _resolve_agent(ctx, client, ref, select=None):
52
522
  find_by_name=client.agents.find_agents,
53
523
  label="Agent",
54
524
  select=select,
525
+ interface_preference=interface_preference,
55
526
  )
56
527
 
57
528
 
58
529
  @agents_group.command(name="list")
530
+ @click.option(
531
+ "--simple", is_flag=True, help="Show simple table without interactive picker"
532
+ )
59
533
  @output_flags()
60
534
  @click.pass_context
61
- def list_agents(ctx):
535
+ def list_agents(ctx, simple):
62
536
  """List all agents."""
63
537
  try:
64
538
  client = get_client(ctx)
@@ -80,6 +554,15 @@ def list_agents(ctx):
80
554
  row["id"] = str(row["id"])
81
555
  return row
82
556
 
557
+ # Use fuzzy picker for interactive agent selection and details (default behavior)
558
+ # Skip if --simple flag is used
559
+ if not simple and console.is_terminal and os.isatty(1) and len(agents) > 0:
560
+ picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
561
+ if picked_agent:
562
+ _display_agent_details(ctx, client, picked_agent)
563
+ return
564
+
565
+ # Show simple table (either --simple flag or non-interactive)
83
566
  output_list(ctx, agents, "🤖 Available Agents", columns, transform_agent)
84
567
 
85
568
  except Exception as e:
@@ -89,50 +572,62 @@ def list_agents(ctx):
89
572
  @agents_group.command()
90
573
  @click.argument("agent_ref")
91
574
  @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
575
+ @click.option(
576
+ "--export",
577
+ type=click.Path(dir_okay=False, writable=True),
578
+ help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
579
+ )
92
580
  @output_flags()
93
581
  @click.pass_context
94
- def get(ctx, agent_ref, select):
95
- """Get agent details."""
582
+ def get(ctx, agent_ref, select, export):
583
+ """Get agent details.
584
+
585
+ Examples:
586
+ aip agents get my-agent
587
+ aip agents get my-agent --export agent.json # Exports complete configuration as JSON
588
+ aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
589
+ """
96
590
  try:
97
591
  client = get_client(ctx)
98
592
 
99
- # Resolve agent with ambiguity handling
100
- agent = _resolve_agent(ctx, client, agent_ref, select)
593
+ # Resolve agent with ambiguity handling - use questionary interface for traditional UX
594
+ agent = _resolve_agent(
595
+ ctx, client, agent_ref, select, interface_preference="questionary"
596
+ )
101
597
 
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
598
+ # Handle export option
599
+ if export:
600
+ export_path = Path(export)
601
+ # Auto-detect format from file extension
602
+ if export_path.suffix.lower() in [".yaml", ".yml"]:
603
+ detected_format = "yaml"
604
+ else:
605
+ detected_format = "json"
111
606
 
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
- }
607
+ # Always export comprehensive data - re-fetch agent with full details
608
+ try:
609
+ 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]")
613
+ )
614
+ console.print(
615
+ Text("[yellow]⚠️ Proceeding with available data[/yellow]")
616
+ )
132
617
 
133
- output_result(
134
- ctx, result_data, title="Agent Details", panel_title=f"🤖 {agent.name}"
135
- )
618
+ _export_agent_to_file(agent, export_path, detected_format)
619
+ console.print(
620
+ Text(
621
+ f"[green]✅ Complete agent configuration exported to: {export_path} (format: {detected_format})[/green]"
622
+ )
623
+ )
624
+
625
+ # Display full agent details using the standardized helper
626
+ _display_agent_details(ctx, client, agent)
627
+
628
+ # Show run suggestions (only in rich mode, not JSON)
629
+ if ctx.obj.get("view") != "json":
630
+ _display_run_suggestions(agent)
136
631
 
137
632
  except Exception as e:
138
633
  raise click.ClickException(str(e))
@@ -140,8 +635,9 @@ def get(ctx, agent_ref, select):
140
635
 
141
636
  @agents_group.command()
142
637
  @click.argument("agent_ref")
638
+ @click.argument("input_text", required=False)
143
639
  @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")
640
+ @click.option("--input", "input_option", help="Input text for the agent")
145
641
  @click.option("--chat-history", help="JSON string of chat history")
146
642
  @click.option(
147
643
  "--timeout",
@@ -173,18 +669,38 @@ def run(
173
669
  agent_ref,
174
670
  select,
175
671
  input_text,
672
+ input_option,
176
673
  chat_history,
177
674
  timeout,
178
675
  save,
179
676
  files,
180
677
  verbose,
181
678
  ):
182
- """Run an agent with input text (ID or name)."""
679
+ """Run an agent with input text.
680
+
681
+ Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
682
+
683
+ Examples:
684
+ aip agents run my-agent "Hello world"
685
+ aip agents run agent-123 "Process this data" --timeout 600
686
+ aip agents run my-agent --input "Hello world" # Legacy style
687
+ """
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
+ )
696
+
183
697
  try:
184
698
  client = get_client(ctx)
185
699
 
186
- # Resolve agent by ID or name (align with other commands)
187
- agent = _resolve_agent(ctx, client, agent_ref, select)
700
+ # Resolve agent by ID or name (align with other commands) - use fuzzy interface
701
+ agent = _resolve_agent(
702
+ ctx, client, agent_ref, select, interface_preference="fuzzy"
703
+ )
188
704
 
189
705
  # Parse chat history if provided
190
706
  parsed_chat_history = None
@@ -215,7 +731,7 @@ def run(
215
731
  # Ensure timeout is applied to the root client and subclients share its session
216
732
  run_kwargs = {
217
733
  "agent_id": agent.id,
218
- "message": input_text,
734
+ "message": final_input_text,
219
735
  "files": list(files),
220
736
  "agent_name": agent.name, # Pass agent name for better display
221
737
  "tty": tty_enabled,
@@ -276,7 +792,7 @@ def run(
276
792
 
277
793
  with open(save, "w", encoding="utf-8") as f:
278
794
  f.write(content)
279
- console.print(f"[green]Full debug output saved to: {save}[/green]")
795
+ console.print(Text(f"[green]Full debug output saved to: {save}[/green]"))
280
796
 
281
797
  except AgentTimeoutError as e:
282
798
  # Handle agent timeout errors with specific messages
@@ -293,8 +809,8 @@ def run(
293
809
 
294
810
 
295
811
  @agents_group.command()
296
- @click.option("--name", required=True, help="Agent name")
297
- @click.option("--instruction", required=True, help="Agent instruction (prompt)")
812
+ @click.option("--name", help="Agent name")
813
+ @click.option("--instruction", help="Agent instruction (prompt)")
298
814
  @click.option(
299
815
  "--model",
300
816
  help=f"Language model to use (e.g., {DEFAULT_MODEL}, default: {DEFAULT_MODEL})",
@@ -307,6 +823,12 @@ def run(
307
823
  type=int,
308
824
  help="Agent execution timeout in seconds (default: 300s)",
309
825
  )
826
+ @click.option(
827
+ "--import",
828
+ "import_file",
829
+ type=click.Path(exists=True, dir_okay=False),
830
+ help="Import agent configuration from JSON file",
831
+ )
310
832
  @output_flags()
311
833
  @click.pass_context
312
834
  def create(
@@ -317,48 +839,61 @@ def create(
317
839
  tools,
318
840
  agents,
319
841
  timeout,
842
+ import_file,
320
843
  ):
321
- """Create a new agent."""
844
+ """Create a new agent.
845
+
846
+ Examples:
847
+ aip agents create --name "My Agent" --instruction "You are a helpful assistant"
848
+ aip agents create --import agent.json
849
+ """
322
850
  try:
323
851
  client = get_client(ctx)
324
852
 
853
+ # Handle import from file
854
+ if import_file:
855
+ import_data = _load_agent_from_file(Path(import_file))
856
+
857
+ # Convert export format to import-compatible format
858
+ import_data = _convert_export_to_import_format(import_data)
859
+
860
+ # Merge CLI args with imported data
861
+ cli_args = {
862
+ "name": name,
863
+ "instruction": instruction,
864
+ "model": model,
865
+ "tools": tools or (),
866
+ "agents": agents or (),
867
+ "timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
868
+ }
869
+
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)
879
+
880
+ # Validate required fields
881
+ if not name:
882
+ raise click.ClickException("Agent name is required (--name or --import)")
883
+ if not instruction:
884
+ raise click.ClickException(
885
+ "Agent instruction is required (--instruction or --import)"
886
+ )
887
+
325
888
  # 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
889
+ resolved_tools = _resolve_resources_by_name(
890
+ client, tools, "tool", client.find_tools, "Tool"
891
+ )
892
+ resolved_agents = _resolve_resources_by_name(
893
+ client, agents, "agent", client.find_agents, "Agent"
894
+ )
895
+
896
+ # Create agent with comprehensive attribute support
362
897
  create_kwargs = {
363
898
  "name": name,
364
899
  "instruction": instruction,
@@ -367,9 +902,45 @@ def create(
367
902
  "timeout": timeout,
368
903
  }
369
904
 
370
- # Add model if specified
905
+ # Add model if specified (prioritize CLI model over imported model)
371
906
  if model:
372
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"]
917
+
918
+ # If importing from file, include all other detected attributes
919
+ if import_file:
920
+ # Add all other attributes from import data (excluding already handled ones and system-only fields)
921
+ excluded_fields = {
922
+ "name",
923
+ "instruction",
924
+ "model",
925
+ "tools",
926
+ "agents",
927
+ "timeout",
928
+ # System-only fields that shouldn't be passed to create_agent
929
+ "id",
930
+ "created_at",
931
+ "updated_at",
932
+ "agent_config",
933
+ "language_model_id",
934
+ "type",
935
+ "framework",
936
+ "version",
937
+ "tool_configs",
938
+ "mcps",
939
+ "a2a_profile",
940
+ }
941
+ for key, value in merged_data.items():
942
+ if key not in excluded_fields and value is not None:
943
+ create_kwargs[key] = value
373
944
 
374
945
  agent = client.agents.create_agent(**create_kwargs)
375
946
 
@@ -377,33 +948,17 @@ def create(
377
948
  click.echo(json.dumps(agent.model_dump(), indent=2))
378
949
  else:
379
950
  # 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
- )
951
+ _display_agent_creation_success(agent, model)
389
952
 
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",
399
- )
400
- console.print(panel)
953
+ # Show run suggestions (only in rich mode, not JSON)
954
+ if ctx.obj.get("view") != "json":
955
+ _display_run_suggestions(agent)
401
956
 
402
957
  except Exception as e:
403
958
  if ctx.obj.get("view") == "json":
404
959
  click.echo(json.dumps({"error": str(e)}, indent=2))
405
960
  else:
406
- console.print(f"[red]Error creating agent: {e}[/red]")
961
+ console.print(Text(f"[red]Error creating agent: {e}[/red]"))
407
962
  raise click.ClickException(str(e))
408
963
 
409
964
 
@@ -414,10 +969,21 @@ def create(
414
969
  @click.option("--tools", multiple=True, help="New tool names or IDs")
415
970
  @click.option("--agents", multiple=True, help="New sub-agent names")
416
971
  @click.option("--timeout", type=int, help="New timeout value")
972
+ @click.option(
973
+ "--import",
974
+ "import_file",
975
+ type=click.Path(exists=True, dir_okay=False),
976
+ help="Import agent configuration from JSON file",
977
+ )
417
978
  @output_flags()
418
979
  @click.pass_context
419
- def update(ctx, agent_id, name, instruction, tools, agents, timeout):
420
- """Update an existing agent."""
980
+ def update(ctx, agent_id, name, instruction, tools, agents, timeout, import_file):
981
+ """Update an existing agent.
982
+
983
+ Examples:
984
+ aip agents update my-agent --instruction "New instruction"
985
+ aip agents update my-agent --import agent.json
986
+ """
421
987
  try:
422
988
  client = get_client(ctx)
423
989
 
@@ -427,7 +993,32 @@ def update(ctx, agent_id, name, instruction, tools, agents, timeout):
427
993
  except Exception as e:
428
994
  raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
429
995
 
430
- # Build update data
996
+ # Handle import from file
997
+ if import_file:
998
+ import_data = _load_agent_from_file(Path(import_file))
999
+
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
431
1022
  update_data = {}
432
1023
  if name is not None:
433
1024
  update_data["name"] = name
@@ -440,6 +1031,31 @@ def update(ctx, agent_id, name, instruction, tools, agents, timeout):
440
1031
  if timeout is not None:
441
1032
  update_data["timeout"] = timeout
442
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
1058
+
443
1059
  if not update_data:
444
1060
  raise click.ClickException("No update fields specified")
445
1061
 
@@ -449,15 +1065,17 @@ def update(ctx, agent_id, name, instruction, tools, agents, timeout):
449
1065
  if ctx.obj.get("view") == "json":
450
1066
  click.echo(json.dumps(updated_agent.model_dump(), indent=2))
451
1067
  else:
452
- console.print(
453
- f"[green]✅ Agent '{updated_agent.name}' updated successfully[/green]"
454
- )
1068
+ _display_agent_update_success(updated_agent)
1069
+
1070
+ # Show run suggestions (only in rich mode, not JSON)
1071
+ if ctx.obj.get("view") != "json":
1072
+ _display_run_suggestions(updated_agent)
455
1073
 
456
1074
  except Exception as e:
457
1075
  if ctx.obj.get("view") == "json":
458
1076
  click.echo(json.dumps({"error": str(e)}, indent=2))
459
1077
  else:
460
- console.print(f"[red]Error updating agent: {e}[/red]")
1078
+ console.print(Text(f"[red]Error updating agent: {e}[/red]"))
461
1079
  raise click.ClickException(str(e))
462
1080
 
463
1081
 
@@ -482,7 +1100,7 @@ def delete(ctx, agent_id, yes):
482
1100
  f"Are you sure you want to delete agent '{agent.name}'?"
483
1101
  ):
484
1102
  if ctx.obj.get("view") != "json":
485
- console.print("Deletion cancelled.")
1103
+ console.print(Text("Deletion cancelled."))
486
1104
  return
487
1105
 
488
1106
  client.agents.delete_agent(agent.id)
@@ -496,12 +1114,12 @@ def delete(ctx, agent_id, yes):
496
1114
  )
497
1115
  else:
498
1116
  console.print(
499
- f"[green]✅ Agent '{agent.name}' deleted successfully[/green]"
1117
+ Text(f"[green]✅ Agent '{agent.name}' deleted successfully[/green]")
500
1118
  )
501
1119
 
502
1120
  except Exception as e:
503
1121
  if ctx.obj.get("view") == "json":
504
1122
  click.echo(json.dumps({"error": str(e)}, indent=2))
505
1123
  else:
506
- console.print(f"[red]Error deleting agent: {e}[/red]")
1124
+ console.print(Text(f"[red]Error deleting agent: {e}[/red]"))
507
1125
  raise click.ClickException(str(e))