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.
- glaip_sdk/branding.py +27 -1
- glaip_sdk/cli/commands/agents.py +26 -17
- glaip_sdk/cli/commands/configure.py +39 -50
- glaip_sdk/cli/commands/mcps.py +1 -3
- glaip_sdk/cli/config.py +42 -0
- glaip_sdk/cli/display.py +92 -26
- glaip_sdk/cli/main.py +141 -124
- glaip_sdk/cli/mcp_validators.py +2 -2
- glaip_sdk/cli/pager.py +3 -2
- glaip_sdk/cli/parsers/json_input.py +2 -2
- glaip_sdk/cli/resolution.py +12 -10
- glaip_sdk/cli/slash/agent_session.py +7 -0
- glaip_sdk/cli/slash/prompt.py +21 -2
- glaip_sdk/cli/slash/session.py +15 -21
- glaip_sdk/cli/update_notifier.py +8 -2
- glaip_sdk/cli/utils.py +110 -53
- glaip_sdk/client/_agent_payloads.py +504 -0
- glaip_sdk/client/agents.py +194 -551
- glaip_sdk/client/base.py +92 -20
- glaip_sdk/client/main.py +6 -0
- glaip_sdk/client/run_rendering.py +275 -0
- glaip_sdk/config/constants.py +3 -0
- glaip_sdk/exceptions.py +15 -0
- glaip_sdk/models.py +5 -0
- glaip_sdk/payload_schemas/__init__.py +19 -0
- glaip_sdk/payload_schemas/agent.py +87 -0
- glaip_sdk/rich_components.py +12 -0
- glaip_sdk/utils/client_utils.py +12 -0
- glaip_sdk/utils/import_export.py +2 -2
- glaip_sdk/utils/rendering/formatting.py +5 -0
- glaip_sdk/utils/rendering/models.py +22 -0
- glaip_sdk/utils/rendering/renderer/base.py +9 -1
- glaip_sdk/utils/rendering/renderer/panels.py +0 -1
- glaip_sdk/utils/rendering/steps.py +59 -0
- glaip_sdk/utils/serialization.py +24 -3
- {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/METADATA +1 -1
- glaip_sdk-0.0.16.dist-info/RECORD +72 -0
- glaip_sdk-0.0.15.dist-info/RECORD +0 -67
- {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.16.dist-info}/WHEEL +0 -0
- {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)
|
glaip_sdk/cli/commands/agents.py
CHANGED
|
@@ -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=
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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()
|
glaip_sdk/cli/commands/mcps.py
CHANGED
|
@@ -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()
|
glaip_sdk/cli/config.py
ADDED
|
@@ -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__"):
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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"[
|
|
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"[
|
|
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]{
|
|
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"
|
|
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"
|