glaip-sdk 0.0.15__py3-none-any.whl → 0.0.16__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 (40) hide show
  1. glaip_sdk/branding.py +27 -1
  2. glaip_sdk/cli/commands/agents.py +26 -17
  3. glaip_sdk/cli/commands/configure.py +39 -50
  4. glaip_sdk/cli/commands/mcps.py +1 -3
  5. glaip_sdk/cli/config.py +42 -0
  6. glaip_sdk/cli/display.py +92 -26
  7. glaip_sdk/cli/main.py +141 -124
  8. glaip_sdk/cli/mcp_validators.py +2 -2
  9. glaip_sdk/cli/pager.py +3 -2
  10. glaip_sdk/cli/parsers/json_input.py +2 -2
  11. glaip_sdk/cli/resolution.py +12 -10
  12. glaip_sdk/cli/slash/agent_session.py +7 -0
  13. glaip_sdk/cli/slash/prompt.py +21 -2
  14. glaip_sdk/cli/slash/session.py +15 -21
  15. glaip_sdk/cli/update_notifier.py +8 -2
  16. glaip_sdk/cli/utils.py +110 -53
  17. glaip_sdk/client/_agent_payloads.py +504 -0
  18. glaip_sdk/client/agents.py +194 -551
  19. glaip_sdk/client/base.py +92 -20
  20. glaip_sdk/client/main.py +6 -0
  21. glaip_sdk/client/run_rendering.py +275 -0
  22. glaip_sdk/config/constants.py +3 -0
  23. glaip_sdk/exceptions.py +15 -0
  24. glaip_sdk/models.py +5 -0
  25. glaip_sdk/payload_schemas/__init__.py +19 -0
  26. glaip_sdk/payload_schemas/agent.py +87 -0
  27. glaip_sdk/rich_components.py +12 -0
  28. glaip_sdk/utils/client_utils.py +12 -0
  29. glaip_sdk/utils/import_export.py +2 -2
  30. glaip_sdk/utils/rendering/formatting.py +5 -0
  31. glaip_sdk/utils/rendering/models.py +22 -0
  32. glaip_sdk/utils/rendering/renderer/base.py +9 -1
  33. glaip_sdk/utils/rendering/renderer/panels.py +0 -1
  34. glaip_sdk/utils/rendering/steps.py +59 -0
  35. glaip_sdk/utils/serialization.py +24 -3
  36. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/METADATA +1 -1
  37. glaip_sdk-0.0.16.dist-info/RECORD +72 -0
  38. glaip_sdk-0.0.15.dist-info/RECORD +0 -67
  39. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/WHEEL +0 -0
  40. {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/entry_points.txt +0 -0
glaip_sdk/branding.py CHANGED
@@ -60,7 +60,8 @@ GDP Labs AI Agents Package
60
60
  version: str | None = None,
61
61
  package_name: str | None = None,
62
62
  ) -> None:
63
- """
63
+ """Initialize AIPBranding instance.
64
+
64
65
  Args:
65
66
  version: Explicit SDK version (overrides auto-detection).
66
67
  package_name: If set, attempt to read version from installed package.
@@ -105,6 +106,11 @@ GDP Labs AI Agents Package
105
106
  return banner
106
107
 
107
108
  def get_version_info(self) -> dict:
109
+ """Get comprehensive version information for the SDK.
110
+
111
+ Returns:
112
+ Dictionary containing version, Python version, platform, and architecture info
113
+ """
108
114
  return {
109
115
  "version": self.version,
110
116
  "python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
@@ -113,6 +119,11 @@ GDP Labs AI Agents Package
113
119
  }
114
120
 
115
121
  def display_welcome_panel(self, title: str = "Welcome to AIP") -> None:
122
+ """Display a welcome panel with branding.
123
+
124
+ Args:
125
+ title: Custom title for the welcome panel
126
+ """
116
127
  banner = self.get_welcome_banner()
117
128
  panel = AIPPanel(
118
129
  banner,
@@ -123,6 +134,7 @@ GDP Labs AI Agents Package
123
134
  self.console.print(panel)
124
135
 
125
136
  def display_version_panel(self) -> None:
137
+ """Display a panel with comprehensive version information."""
126
138
  v = self.get_version_info()
127
139
  version_text = (
128
140
  f"[{TITLE_STYLE}]AIP SDK Version Information[/{TITLE_STYLE}]\n\n"
@@ -140,6 +152,11 @@ GDP Labs AI Agents Package
140
152
  self.console.print(panel)
141
153
 
142
154
  def display_status_banner(self, status: str = "ready") -> None:
155
+ """Display a status banner for the current state.
156
+
157
+ Args:
158
+ status: Current status to display
159
+ """
143
160
  # Keep it simple (no emoji); easy to parse in logs/CI
144
161
  banner = f"[{LABEL}]AIP[/{LABEL}] - {status.title()}"
145
162
  self.console.print(banner)
@@ -148,4 +165,13 @@ GDP Labs AI Agents Package
148
165
  def create_from_sdk(
149
166
  cls, sdk_version: str | None = None, package_name: str | None = None
150
167
  ) -> AIPBranding:
168
+ """Create AIPBranding instance from SDK package information.
169
+
170
+ Args:
171
+ sdk_version: Explicit SDK version override
172
+ package_name: Package name to read version from
173
+
174
+ Returns:
175
+ AIPBranding instance
176
+ """
151
177
  return cls(version=sdk_version, package_name=package_name)
@@ -78,7 +78,6 @@ AGENT_NOT_FOUND_ERROR = "Agent not found"
78
78
 
79
79
  def _safe_agent_attribute(agent: Any, name: str) -> Any:
80
80
  """Return attribute value for ``name`` while filtering Mock sentinels."""
81
-
82
81
  try:
83
82
  value = getattr(agent, name)
84
83
  except Exception:
@@ -91,7 +90,6 @@ def _safe_agent_attribute(agent: Any, name: str) -> Any:
91
90
 
92
91
  def _coerce_mapping_candidate(candidate: Any) -> dict[str, Any] | None:
93
92
  """Convert a mapping-like candidate to a plain dict when possible."""
94
-
95
93
  if candidate is None:
96
94
  return None
97
95
  if isinstance(candidate, Mapping):
@@ -101,7 +99,6 @@ def _coerce_mapping_candidate(candidate: Any) -> dict[str, Any] | None:
101
99
 
102
100
  def _call_agent_method(agent: Any, method_name: str) -> dict[str, Any] | None:
103
101
  """Attempt to call the named method and coerce its output to a dict."""
104
-
105
102
  method = getattr(agent, method_name, None)
106
103
  if not callable(method):
107
104
  return None
@@ -114,7 +111,6 @@ def _call_agent_method(agent: Any, method_name: str) -> dict[str, Any] | None:
114
111
 
115
112
  def _coerce_agent_via_methods(agent: Any) -> dict[str, Any] | None:
116
113
  """Try standard serialisation helpers to produce a mapping."""
117
-
118
114
  for attr in ("model_dump", "dict", "to_dict"):
119
115
  mapping = _call_agent_method(agent, attr)
120
116
  if mapping is not None:
@@ -124,7 +120,6 @@ def _coerce_agent_via_methods(agent: Any) -> dict[str, Any] | None:
124
120
 
125
121
  def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
126
122
  """Construct a minimal mapping from well-known agent attributes."""
127
-
128
123
  fallback_fields = (
129
124
  "id",
130
125
  "name",
@@ -136,6 +131,7 @@ def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
136
131
  "agents",
137
132
  "mcps",
138
133
  "timeout",
134
+ "tool_configs",
139
135
  )
140
136
 
141
137
  fallback: dict[str, Any] = {}
@@ -149,7 +145,6 @@ def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
149
145
 
150
146
  def _prepare_agent_output(agent: Any) -> dict[str, Any]:
151
147
  """Build a JSON-serialisable mapping for CLI output."""
152
-
153
148
  method_mapping = _coerce_agent_via_methods(agent)
154
149
  if method_mapping is not None:
155
150
  return method_mapping
@@ -256,11 +251,11 @@ def _format_fallback_agent_data(client: Any, agent: Any) -> dict:
256
251
  "metadata",
257
252
  "language_model_id",
258
253
  "agent_config",
259
- "tool_configs",
260
254
  "tools",
261
255
  "agents",
262
256
  "mcps",
263
257
  "a2a_profile",
258
+ "tool_configs",
264
259
  ]
265
260
 
266
261
  result_data = build_resource_result_data(full_agent, fields)
@@ -297,8 +292,7 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
297
292
  output_result(
298
293
  ctx,
299
294
  formatted_data,
300
- title="Agent Details",
301
- panel_title=panel_title,
295
+ title=panel_title,
302
296
  )
303
297
  else:
304
298
  # Fall back to Pydantic model data if raw fetch fails
@@ -318,7 +312,6 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
318
312
  ctx,
319
313
  result_data,
320
314
  title="Agent Details",
321
- panel_title=f"🤖 {result_data.get('name', 'Unknown')}",
322
315
  )
323
316
 
324
317
 
@@ -338,7 +331,14 @@ def _resolve_agent(
338
331
  """Resolve agent reference (ID or name) with ambiguity handling.
339
332
 
340
333
  Args:
341
- interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
334
+ ctx: Click context object for CLI operations.
335
+ client: AIP client instance for API operations.
336
+ ref: Agent reference (ID or name) to resolve.
337
+ select: Pre-selected agent index for non-interactive mode.
338
+ interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list.
339
+
340
+ Returns:
341
+ Resolved agent object or None if not found.
342
342
  """
343
343
  return resolve_resource_reference(
344
344
  ctx,
@@ -878,7 +878,6 @@ def _add_import_file_attributes(
878
878
  "type",
879
879
  "framework",
880
880
  "version",
881
- "tool_configs",
882
881
  "mcps",
883
882
  "a2a_profile",
884
883
  }
@@ -1050,6 +1049,7 @@ def _handle_update_import_file(
1050
1049
  instruction: str | None,
1051
1050
  tools: tuple[str, ...] | None,
1052
1051
  agents: tuple[str, ...] | None,
1052
+ mcps: tuple[str, ...] | None,
1053
1053
  timeout: float | None,
1054
1054
  ) -> tuple[
1055
1055
  Any | None,
@@ -1057,11 +1057,12 @@ def _handle_update_import_file(
1057
1057
  str | None,
1058
1058
  tuple[str, ...] | None,
1059
1059
  tuple[str, ...] | None,
1060
+ tuple[str, ...] | None,
1060
1061
  float | None,
1061
1062
  ]:
1062
1063
  """Handle import file processing for agent update."""
1063
1064
  if not import_file:
1064
- return None, name, instruction, tools, agents, timeout
1065
+ return None, name, instruction, tools, agents, mcps, timeout
1065
1066
 
1066
1067
  import_data = load_resource_from_file(Path(import_file), "agent")
1067
1068
  import_data = convert_export_to_import_format(import_data)
@@ -1072,6 +1073,7 @@ def _handle_update_import_file(
1072
1073
  "instruction": instruction,
1073
1074
  "tools": tools or (),
1074
1075
  "agents": agents or (),
1076
+ "mcps": mcps or (),
1075
1077
  "timeout": timeout,
1076
1078
  }
1077
1079
 
@@ -1083,6 +1085,7 @@ def _handle_update_import_file(
1083
1085
  merged_data.get("instruction"),
1084
1086
  tuple(merged_data.get("tools", ())),
1085
1087
  tuple(merged_data.get("agents", ())),
1088
+ tuple(merged_data.get("mcps", ())),
1086
1089
  coerce_timeout(merged_data.get("timeout")),
1087
1090
  )
1088
1091
 
@@ -1092,6 +1095,7 @@ def _build_update_data(
1092
1095
  instruction: str | None,
1093
1096
  tools: tuple[str, ...] | None,
1094
1097
  agents: tuple[str, ...] | None,
1098
+ mcps: tuple[str, ...] | None,
1095
1099
  timeout: float | None,
1096
1100
  ) -> dict[str, Any]:
1097
1101
  """Build the update data dictionary from provided parameters."""
@@ -1104,6 +1108,8 @@ def _build_update_data(
1104
1108
  update_data["tools"] = list(tools)
1105
1109
  if agents:
1106
1110
  update_data["agents"] = list(agents)
1111
+ if mcps:
1112
+ update_data["mcps"] = list(mcps)
1107
1113
  if timeout is not None:
1108
1114
  update_data["timeout"] = timeout
1109
1115
  return update_data
@@ -1141,8 +1147,6 @@ def _handle_update_import_config(
1141
1147
  "type",
1142
1148
  "framework",
1143
1149
  "version",
1144
- "tool_configs",
1145
- "mcps",
1146
1150
  "a2a_profile",
1147
1151
  }
1148
1152
  for key, value in merged_data.items():
@@ -1156,6 +1160,7 @@ def _handle_update_import_config(
1156
1160
  @click.option("--instruction", help="New instruction")
1157
1161
  @click.option("--tools", multiple=True, help="New tool names or IDs")
1158
1162
  @click.option("--agents", multiple=True, help="New sub-agent names")
1163
+ @click.option("--mcps", multiple=True, help="New MCP names or IDs")
1159
1164
  @click.option("--timeout", type=int, help="New timeout value")
1160
1165
  @click.option(
1161
1166
  "--import",
@@ -1172,6 +1177,7 @@ def update(
1172
1177
  instruction: str | None,
1173
1178
  tools: tuple[str, ...] | None,
1174
1179
  agents: tuple[str, ...] | None,
1180
+ mcps: tuple[str, ...] | None,
1175
1181
  timeout: float | None,
1176
1182
  import_file: str | None,
1177
1183
  ) -> None:
@@ -1192,12 +1198,15 @@ def update(
1192
1198
  instruction,
1193
1199
  tools,
1194
1200
  agents,
1201
+ mcps,
1195
1202
  timeout,
1196
1203
  ) = _handle_update_import_file(
1197
- import_file, name, instruction, tools, agents, timeout
1204
+ import_file, name, instruction, tools, agents, mcps, timeout
1198
1205
  )
1199
1206
 
1200
- update_data = _build_update_data(name, instruction, tools, agents, timeout)
1207
+ update_data = _build_update_data(
1208
+ name, instruction, tools, agents, mcps, timeout
1209
+ )
1201
1210
 
1202
1211
  if merged_data:
1203
1212
  _handle_update_import_config(import_file, merged_data, update_data)
@@ -5,51 +5,20 @@ Authors:
5
5
  """
6
6
 
7
7
  import getpass
8
- import os
9
- from pathlib import Path
10
- from typing import Any
11
8
 
12
9
  import click
13
- import yaml
14
10
  from rich.console import Console
15
11
  from rich.text import Text
16
12
 
17
13
  from glaip_sdk import Client
18
14
  from glaip_sdk._version import __version__ as _SDK_VERSION
19
15
  from glaip_sdk.branding import AIPBranding
16
+ from glaip_sdk.cli.config import CONFIG_FILE, load_config, save_config
17
+ from glaip_sdk.cli.utils import command_hint
20
18
  from glaip_sdk.rich_components import AIPTable
21
19
 
22
20
  console = Console()
23
21
 
24
- CONFIG_DIR = Path.home() / ".aip"
25
- CONFIG_FILE = CONFIG_DIR / "config.yaml"
26
-
27
-
28
- def load_config() -> dict[str, Any]:
29
- """Load configuration from file."""
30
- if not CONFIG_FILE.exists():
31
- return {}
32
-
33
- try:
34
- with open(CONFIG_FILE) as f:
35
- return yaml.safe_load(f) or {}
36
- except yaml.YAMLError:
37
- return {}
38
-
39
-
40
- def save_config(config: dict[str, Any]) -> None:
41
- """Save configuration to file."""
42
- CONFIG_DIR.mkdir(exist_ok=True)
43
-
44
- with open(CONFIG_FILE, "w") as f:
45
- yaml.dump(config, f, default_flow_style=False)
46
-
47
- # Set secure file permissions
48
- try:
49
- os.chmod(CONFIG_FILE, 0o600)
50
- except Exception: # pragma: no cover - platform dependent best effort
51
- pass
52
-
53
22
 
54
23
  @click.group()
55
24
  def config_group() -> None:
@@ -60,13 +29,16 @@ def config_group() -> None:
60
29
  @config_group.command("list")
61
30
  def list_config() -> None:
62
31
  """List current configuration."""
63
-
64
32
  config = load_config()
65
33
 
66
34
  if not config:
67
- console.print(
68
- "[yellow]No configuration found. Run 'aip config configure' to set up.[/yellow]"
69
- )
35
+ hint = command_hint("config configure", slash_command="login")
36
+ if hint:
37
+ console.print(
38
+ f"[yellow]No configuration found. Run '{hint}' to set up.[/yellow]"
39
+ )
40
+ else:
41
+ console.print("[yellow]No configuration found.[/yellow]")
70
42
  return
71
43
 
72
44
  table = AIPTable(title="🔧 AIP Configuration")
@@ -90,7 +62,6 @@ def list_config() -> None:
90
62
  @click.argument("value")
91
63
  def set_config(key: str, value: str) -> None:
92
64
  """Set a configuration value."""
93
-
94
65
  valid_keys = ["api_url", "api_key"]
95
66
 
96
67
  if key not in valid_keys:
@@ -114,7 +85,6 @@ def set_config(key: str, value: str) -> None:
114
85
  @click.argument("key")
115
86
  def get_config(key: str) -> None:
116
87
  """Get a configuration value."""
117
-
118
88
  config = load_config()
119
89
 
120
90
  if key not in config:
@@ -135,7 +105,6 @@ def get_config(key: str) -> None:
135
105
  @click.argument("key")
136
106
  def unset_config(key: str) -> None:
137
107
  """Remove a configuration value."""
138
-
139
108
  config = load_config()
140
109
 
141
110
  if key not in config:
@@ -152,7 +121,6 @@ def unset_config(key: str) -> None:
152
121
  @click.option("--force", is_flag=True, help="Skip confirmation prompt")
153
122
  def reset_config(force: bool) -> None:
154
123
  """Reset all configuration to defaults."""
155
-
156
124
  if not force:
157
125
  console.print("[yellow]This will remove all AIP configuration.[/yellow]")
158
126
  confirm = input("Are you sure? (y/N): ").strip().lower()
@@ -160,13 +128,28 @@ def reset_config(force: bool) -> None:
160
128
  console.print("Cancelled.")
161
129
  return
162
130
 
163
- if CONFIG_FILE.exists():
164
- CONFIG_FILE.unlink()
165
- console.print(
166
- "✅ Configuration reset. Run 'aip config configure' to set up again."
167
- )
168
- else:
131
+ config_data = load_config()
132
+ file_exists = CONFIG_FILE.exists()
133
+
134
+ if not file_exists and not config_data:
169
135
  console.print("[yellow]No configuration found to reset.[/yellow]")
136
+ console.print("✅ Configuration reset (nothing to remove).")
137
+ return
138
+
139
+ if file_exists:
140
+ try:
141
+ CONFIG_FILE.unlink()
142
+ except FileNotFoundError: # pragma: no cover - defensive cleanup
143
+ pass
144
+ else:
145
+ # In-memory configuration (e.g., tests) needs explicit clearing
146
+ save_config({})
147
+
148
+ hint = command_hint("config configure", slash_command="login")
149
+ message = "✅ Configuration reset."
150
+ if hint:
151
+ message += f" Run '{hint}' to set up again."
152
+ console.print(message)
170
153
 
171
154
 
172
155
  def _configure_interactive() -> None:
@@ -232,11 +215,17 @@ def _configure_interactive() -> None:
232
215
  except Exception as e:
233
216
  console.print(Text(f"❌ Connection failed: {e}"))
234
217
  console.print(" Please check your API URL and key")
235
- console.print(" You can run 'aip status' later to test again")
218
+ hint_status = command_hint("status", slash_command="status")
219
+ if hint_status:
220
+ console.print(f" You can run '{hint_status}' later to test again")
236
221
 
237
222
  console.print("\n💡 You can now use AIP CLI commands!")
238
- console.print(" • Run 'aip status' to check connection")
239
- console.print(" • Run 'aip agents list' to see your agents")
223
+ hint_status = command_hint("status", slash_command="status")
224
+ if hint_status:
225
+ console.print(f" • Run '{hint_status}' to check connection")
226
+ hint_agents = command_hint("agents list", slash_command="agents")
227
+ if hint_agents:
228
+ console.print(f" • Run '{hint_agents}' to see your agents")
240
229
 
241
230
 
242
231
  @config_group.command()
@@ -582,9 +582,7 @@ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
582
582
  "status": getattr(mcp, "status", "N/A"),
583
583
  "connection_status": getattr(mcp, "connection_status", "N/A"),
584
584
  }
585
- output_result(
586
- ctx, result_data, title="MCP Details", panel_title=f"🔌 {mcp.name}"
587
- )
585
+ output_result(ctx, result_data, title=f"🔌 {mcp.name}")
588
586
 
589
587
 
590
588
  @mcps_group.command()
@@ -0,0 +1,42 @@
1
+ """Configuration management utilities.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import yaml
12
+
13
+ CONFIG_DIR = Path.home() / ".aip"
14
+ CONFIG_FILE = CONFIG_DIR / "config.yaml"
15
+
16
+
17
+ def load_config() -> dict[str, Any]:
18
+ """Load configuration from file."""
19
+ if not CONFIG_FILE.exists():
20
+ return {}
21
+
22
+ try:
23
+ with open(CONFIG_FILE) as f:
24
+ return yaml.safe_load(f) or {}
25
+ except yaml.YAMLError:
26
+ return {}
27
+
28
+
29
+ def save_config(config: dict[str, Any]) -> None:
30
+ """Save configuration to file."""
31
+ CONFIG_DIR.mkdir(exist_ok=True)
32
+
33
+ with open(CONFIG_FILE, "w") as f:
34
+ yaml.dump(config, f, default_flow_style=False)
35
+
36
+ # Set secure file permissions
37
+ try:
38
+ os.chmod(CONFIG_FILE, 0o600)
39
+ except (
40
+ OSError
41
+ ): # pragma: no cover - permission errors are expected in some environments
42
+ pass
glaip_sdk/cli/display.py CHANGED
@@ -15,6 +15,7 @@ from rich.console import Console
15
15
  from rich.panel import Panel
16
16
  from rich.text import Text
17
17
 
18
+ from glaip_sdk.cli.utils import command_hint
18
19
  from glaip_sdk.rich_components import AIPPanel
19
20
 
20
21
  console = Console()
@@ -102,30 +103,80 @@ def print_api_error(e: Exception) -> None:
102
103
  - Extracts status_code, error_type, and payload from APIError exceptions
103
104
  - Provides consistent error reporting across CLI commands
104
105
  - Handles both JSON and Rich output formats
106
+ - Special handling for validation errors with detailed field-level errors
105
107
  """
106
- if hasattr(e, "__dict__"): # Check if it's an APIError-like object
107
- error_info = {
108
- "error": str(e),
109
- "status_code": getattr(e, "status_code", None),
110
- "error_type": getattr(e, "error_type", None),
111
- "details": getattr(e, "payload", None),
112
- }
113
-
114
- # Filter out None values
115
- error_info = {k: v for k, v in error_info.items() if v is not None}
116
-
117
- # For JSON view, just return the structured error
118
- # (CLI commands handle the JSON formatting)
119
- if hasattr(e, "status_code"):
120
- console.print(f"[red]API Error: {e}[/red]")
121
- if hasattr(e, "status_code"):
122
- console.print(f"[yellow]Status: {e.status_code}[/yellow]")
123
- if hasattr(e, "payload"):
124
- console.print(f"[yellow]Details: {e.payload}[/yellow]")
108
+ if not hasattr(e, "__dict__"):
109
+ console.print(f"[red]Error: {e}[/red]")
110
+ return
111
+
112
+ if not hasattr(e, "status_code"):
113
+ console.print(f"[red]Error: {e}[/red]")
114
+ return
115
+
116
+ console.print(f"[red]API Error: {e}[/red]")
117
+ status_code = getattr(e, "status_code", None)
118
+ if status_code is not None:
119
+ console.print(f"[yellow]Status: {status_code}[/yellow]")
120
+
121
+ payload = getattr(e, "payload", _MISSING)
122
+ if payload is _MISSING:
123
+ return
124
+
125
+ if payload:
126
+ if not _print_structured_payload(payload):
127
+ console.print(f"[yellow]Details: {payload}[/yellow]")
128
+ else:
129
+ console.print(f"[yellow]Details: {payload}[/yellow]")
130
+
131
+
132
+ def _print_structured_payload(payload: Any) -> bool:
133
+ """Print structured payloads with enhanced formatting. Returns True if handled."""
134
+ if not isinstance(payload, dict):
135
+ return False
136
+
137
+ if "detail" in payload and _print_validation_details(payload["detail"]):
138
+ return True
139
+
140
+ if "details" in payload and _print_details_field(payload["details"]):
141
+ return True
142
+
143
+ return False
144
+
145
+
146
+ def _print_validation_details(detail: Any) -> bool:
147
+ """Render FastAPI-style validation errors."""
148
+ if not isinstance(detail, list) or not detail:
149
+ return False
150
+
151
+ console.print("[red]Validation Errors:[/red]")
152
+ for error in detail:
153
+ if isinstance(error, dict):
154
+ loc = " -> ".join(str(x) for x in error.get("loc", []))
155
+ msg = error.get("msg", "Unknown error")
156
+ error_type = error.get("type", "unknown")
157
+ location = loc if loc else "field"
158
+ console.print(f" [yellow]• {location}:[/yellow] {msg}")
159
+ if error_type != "unknown":
160
+ console.print(f" [dim]({error_type})[/dim]")
125
161
  else:
126
- console.print(f"[red]Error: {e}[/red]")
162
+ console.print(f" [yellow]•[/yellow] {error}")
163
+ return True
164
+
165
+
166
+ def _print_details_field(details: Any) -> bool:
167
+ """Render custom error details from API payloads."""
168
+ if not details:
169
+ return False
170
+
171
+ console.print("[red]Error Details:[/red]")
172
+ if isinstance(details, str):
173
+ console.print(f" [yellow]•[/yellow] {details}")
174
+ elif isinstance(details, list):
175
+ for detail in details:
176
+ console.print(f" [yellow]•[/yellow] {detail}")
127
177
  else:
128
- console.print(f"[red]Error: {e}[/red]")
178
+ console.print(f" [yellow]•[/yellow] {details}")
179
+ return True
129
180
 
130
181
 
131
182
  _MISSING = object()
@@ -133,7 +184,6 @@ _MISSING = object()
133
184
 
134
185
  def build_resource_result_data(resource: Any, fields: list[str]) -> dict[str, Any]:
135
186
  """Return a normalized mapping of ``fields`` extracted from ``resource``."""
136
-
137
187
  result: dict[str, Any] = {}
138
188
  for field in fields:
139
189
  try:
@@ -244,14 +294,30 @@ def display_confirmation_prompt(resource_type: str, resource_name: str) -> bool:
244
294
 
245
295
  def display_agent_run_suggestions(agent: Any) -> Panel:
246
296
  """Return a panel with post-creation suggestions for an agent."""
297
+ agent_id = getattr(agent, "id", "")
298
+ agent_name = getattr(agent, "name", "")
299
+ run_hint_id = command_hint(
300
+ f'agents run {agent_id} "Your message here"',
301
+ slash_command=None,
302
+ )
303
+ run_hint_name = command_hint(
304
+ f'agents run "{agent_name}" "Your message here"',
305
+ slash_command=None,
306
+ )
307
+
308
+ cli_section = ""
309
+ if run_hint_id and run_hint_name:
310
+ cli_section = (
311
+ "📋 Prefer the CLI instead?\n"
312
+ f" [green]{run_hint_id}[/green]\n"
313
+ f" [green]{run_hint_name}[/green]\n\n"
314
+ )
247
315
 
248
316
  return AIPPanel(
249
317
  f"[bold blue]💡 Next Steps:[/bold blue]\n\n"
250
- f"🚀 Start chatting with [bold]{agent.name}[/bold] right here:\n"
318
+ f"🚀 Start chatting with [bold]{agent_name}[/bold] right here:\n"
251
319
  f" Type your message below and press Enter to run it immediately.\n\n"
252
- f"📋 Prefer the CLI instead?\n"
253
- f' [green]aip agents run {agent.id} "Your message here"[/green]\n'
254
- f' [green]aip agents run "{agent.name}" "Your message here"[/green]\n\n'
320
+ f"{cli_section}"
255
321
  f"🔧 Available options:\n"
256
322
  f" [dim]--chat-history[/dim] Include previous conversation\n"
257
323
  f" [dim]--file[/dim] Attach files\n"