glaip-sdk 0.0.14__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 +27 -20
- glaip_sdk/cli/commands/configure.py +39 -50
- glaip_sdk/cli/commands/mcps.py +2 -6
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +1 -3
- glaip_sdk/cli/config.py +42 -0
- glaip_sdk/cli/context.py +142 -0
- glaip_sdk/cli/display.py +92 -26
- glaip_sdk/cli/main.py +141 -124
- glaip_sdk/cli/masking.py +148 -0
- glaip_sdk/cli/mcp_validators.py +2 -2
- glaip_sdk/cli/pager.py +272 -0
- 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 +99 -369
- 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.14.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.14.dist-info/RECORD +0 -64
- {glaip_sdk-0.0.14.dist-info → glaip_sdk-0.0.16.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.14.dist-info → glaip_sdk-0.0.16.dist-info}/entry_points.txt +0 -0
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"
|
glaip_sdk/cli/main.py
CHANGED
|
@@ -19,11 +19,11 @@ from glaip_sdk.cli.commands.agents import agents_group
|
|
|
19
19
|
from glaip_sdk.cli.commands.configure import (
|
|
20
20
|
config_group,
|
|
21
21
|
configure_command,
|
|
22
|
-
load_config,
|
|
23
22
|
)
|
|
24
23
|
from glaip_sdk.cli.commands.mcps import mcps_group
|
|
25
24
|
from glaip_sdk.cli.commands.models import models_group
|
|
26
25
|
from glaip_sdk.cli.commands.tools import tools_group
|
|
26
|
+
from glaip_sdk.cli.config import load_config
|
|
27
27
|
from glaip_sdk.cli.update_notifier import maybe_notify_update
|
|
28
28
|
from glaip_sdk.cli.utils import spinner_context, update_spinner
|
|
29
29
|
from glaip_sdk.config.constants import (
|
|
@@ -73,7 +73,6 @@ def main(
|
|
|
73
73
|
aip tools create my_tool.py # Create a new tool
|
|
74
74
|
aip agents run my-agent "Hello world" # Run an agent
|
|
75
75
|
"""
|
|
76
|
-
|
|
77
76
|
# Store configuration in context
|
|
78
77
|
ctx.ensure_object(dict)
|
|
79
78
|
ctx.obj["api_url"] = api_url
|
|
@@ -116,7 +115,6 @@ main.add_command(configure_command)
|
|
|
116
115
|
|
|
117
116
|
def _should_launch_slash(ctx: click.Context) -> bool:
|
|
118
117
|
"""Determine whether to open the command palette automatically."""
|
|
119
|
-
|
|
120
118
|
ctx_obj = ctx.obj or {}
|
|
121
119
|
if not bool(ctx_obj.get("tty", True)):
|
|
122
120
|
return False
|
|
@@ -127,6 +125,137 @@ def _should_launch_slash(ctx: click.Context) -> bool:
|
|
|
127
125
|
return True
|
|
128
126
|
|
|
129
127
|
|
|
128
|
+
def _load_and_merge_config(ctx: click.Context) -> dict:
|
|
129
|
+
"""Load configuration from multiple sources and merge them."""
|
|
130
|
+
# Load config from file and merge with context
|
|
131
|
+
file_config = load_config()
|
|
132
|
+
context_config = ctx.obj or {}
|
|
133
|
+
|
|
134
|
+
# Load environment variables (middle priority)
|
|
135
|
+
env_config = {}
|
|
136
|
+
if os.getenv("AIP_API_URL"):
|
|
137
|
+
env_config["api_url"] = os.getenv("AIP_API_URL")
|
|
138
|
+
if os.getenv("AIP_API_KEY"):
|
|
139
|
+
env_config["api_key"] = os.getenv("AIP_API_KEY")
|
|
140
|
+
|
|
141
|
+
# Filter out None values from context config to avoid overriding other configs
|
|
142
|
+
filtered_context = {k: v for k, v in context_config.items() if v is not None}
|
|
143
|
+
|
|
144
|
+
# Merge configs: file (low) -> env (mid) -> CLI args (high)
|
|
145
|
+
return {**file_config, **env_config, **filtered_context}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_config_and_show_error(config: dict, console: Console) -> None:
|
|
149
|
+
"""Validate configuration and show error if incomplete."""
|
|
150
|
+
if not config.get("api_url") or not config.get("api_key"):
|
|
151
|
+
console.print(
|
|
152
|
+
AIPPanel(
|
|
153
|
+
"[bold red]❌ Configuration incomplete[/bold red]\n\n"
|
|
154
|
+
f"🔍 Current config:\n"
|
|
155
|
+
f" • API URL: {config.get('api_url', 'Not set')}\n"
|
|
156
|
+
f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
|
|
157
|
+
f"💡 To fix this:\n"
|
|
158
|
+
f" • Run 'aip configure' to set up credentials\n"
|
|
159
|
+
f" • Or run 'aip config list' to see current config",
|
|
160
|
+
title="❌ Configuration Error",
|
|
161
|
+
border_style="red",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
console.print(
|
|
165
|
+
f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION}) - Configure to connect"
|
|
166
|
+
)
|
|
167
|
+
sys.exit(1)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _create_and_test_client(config: dict, console: Console) -> Client:
|
|
171
|
+
"""Create client and test connection by fetching resources."""
|
|
172
|
+
# Try to create client
|
|
173
|
+
client = Client(
|
|
174
|
+
api_url=config["api_url"],
|
|
175
|
+
api_key=config["api_key"],
|
|
176
|
+
timeout=config.get("timeout", 30.0),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Test connection by listing resources
|
|
180
|
+
try:
|
|
181
|
+
with spinner_context(
|
|
182
|
+
None, # We'll pass ctx later
|
|
183
|
+
"[bold blue]Checking GL AIP status…[/bold blue]",
|
|
184
|
+
console_override=console,
|
|
185
|
+
spinner_style="cyan",
|
|
186
|
+
) as status_indicator:
|
|
187
|
+
update_spinner(status_indicator, "[bold blue]Fetching agents…[/bold blue]")
|
|
188
|
+
agents = client.list_agents()
|
|
189
|
+
|
|
190
|
+
update_spinner(status_indicator, "[bold blue]Fetching tools…[/bold blue]")
|
|
191
|
+
tools = client.list_tools()
|
|
192
|
+
|
|
193
|
+
update_spinner(status_indicator, "[bold blue]Fetching MCPs…[/bold blue]")
|
|
194
|
+
mcps = client.list_mcps()
|
|
195
|
+
|
|
196
|
+
# Create status table
|
|
197
|
+
table = AIPTable(title="🔗 GL AIP Status")
|
|
198
|
+
table.add_column("Resource", style="cyan", width=15)
|
|
199
|
+
table.add_column("Count", style="green", width=10)
|
|
200
|
+
table.add_column("Status", style="green", width=15)
|
|
201
|
+
|
|
202
|
+
table.add_row("Agents", str(len(agents)), "✅ Available")
|
|
203
|
+
table.add_row("Tools", str(len(tools)), "✅ Available")
|
|
204
|
+
table.add_row("MCPs", str(len(mcps)), "✅ Available")
|
|
205
|
+
|
|
206
|
+
console.print(
|
|
207
|
+
AIPPanel(
|
|
208
|
+
f"[bold green]✅ Connected to GL AIP[/bold green]\n"
|
|
209
|
+
f"🔗 API URL: {client.api_url}\n"
|
|
210
|
+
f"🤖 Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
|
|
211
|
+
title="🚀 Connection Status",
|
|
212
|
+
border_style="green",
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
console.print(table)
|
|
217
|
+
|
|
218
|
+
except Exception as e:
|
|
219
|
+
# Show AIP Ready status even if connection fails
|
|
220
|
+
console.print(
|
|
221
|
+
f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
console.print(
|
|
225
|
+
AIPPanel(
|
|
226
|
+
f"[bold yellow]⚠️ Connection established but API call failed[/bold yellow]\n"
|
|
227
|
+
f"🔗 API URL: {client.api_url}\n"
|
|
228
|
+
f"❌ Error: {e}\n\n"
|
|
229
|
+
f"💡 This usually means:\n"
|
|
230
|
+
f" • Network connectivity issues\n"
|
|
231
|
+
f" • API permissions problems\n"
|
|
232
|
+
f" • Backend service issues",
|
|
233
|
+
title="⚠️ Partial Connection",
|
|
234
|
+
border_style="yellow",
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
return client
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _handle_connection_error(config: dict, console: Console, error: Exception) -> None:
|
|
242
|
+
"""Handle connection errors and show troubleshooting information."""
|
|
243
|
+
console.print(
|
|
244
|
+
AIPPanel(
|
|
245
|
+
f"[bold red]❌ Connection failed[/bold red]\n\n"
|
|
246
|
+
f"🔍 Error: {error}\n\n"
|
|
247
|
+
f"💡 Troubleshooting steps:\n"
|
|
248
|
+
f" • Verify your API URL and key are correct\n"
|
|
249
|
+
f" • Check network connectivity to {config.get('api_url', 'your API')}\n"
|
|
250
|
+
f" • Run 'aip configure' to update credentials\n"
|
|
251
|
+
f" • Run 'aip config list' to check configuration",
|
|
252
|
+
title="❌ Connection Error",
|
|
253
|
+
border_style="red",
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
sys.exit(1)
|
|
257
|
+
|
|
258
|
+
|
|
130
259
|
@main.command()
|
|
131
260
|
@click.pass_context
|
|
132
261
|
def status(ctx: Any) -> None:
|
|
@@ -146,132 +275,20 @@ def status(ctx: Any) -> None:
|
|
|
146
275
|
f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
|
|
147
276
|
)
|
|
148
277
|
|
|
149
|
-
# Load
|
|
150
|
-
|
|
151
|
-
context_config = ctx.obj or {}
|
|
152
|
-
|
|
153
|
-
# Load environment variables (middle priority)
|
|
154
|
-
|
|
155
|
-
env_config = {}
|
|
156
|
-
if os.getenv("AIP_API_URL"):
|
|
157
|
-
env_config["api_url"] = os.getenv("AIP_API_URL")
|
|
158
|
-
if os.getenv("AIP_API_KEY"):
|
|
159
|
-
env_config["api_key"] = os.getenv("AIP_API_KEY")
|
|
160
|
-
|
|
161
|
-
# Filter out None values from context config to avoid overriding other configs
|
|
162
|
-
filtered_context = {k: v for k, v in context_config.items() if v is not None}
|
|
163
|
-
|
|
164
|
-
# Merge configs: file (low) -> env (mid) -> CLI args (high)
|
|
165
|
-
config = {**file_config, **env_config, **filtered_context}
|
|
166
|
-
|
|
167
|
-
if not config.get("api_url") or not config.get("api_key"):
|
|
168
|
-
console.print(
|
|
169
|
-
AIPPanel(
|
|
170
|
-
"[bold red]❌ Configuration incomplete[/bold red]\n\n"
|
|
171
|
-
f"🔍 Current config:\n"
|
|
172
|
-
f" • API URL: {config.get('api_url', 'Not set')}\n"
|
|
173
|
-
f" • API Key: {'***' + config.get('api_key', '')[-4:] if config.get('api_key') else 'Not set'}\n\n"
|
|
174
|
-
f"💡 To fix this:\n"
|
|
175
|
-
f" • Run 'aip configure' to set up credentials\n"
|
|
176
|
-
f" • Or run 'aip config list' to see current config",
|
|
177
|
-
title="❌ Configuration Error",
|
|
178
|
-
border_style="red",
|
|
179
|
-
)
|
|
180
|
-
)
|
|
181
|
-
console.print(
|
|
182
|
-
f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION}) - Configure to connect"
|
|
183
|
-
)
|
|
184
|
-
sys.exit(1)
|
|
185
|
-
|
|
186
|
-
# Try to create client
|
|
187
|
-
client = Client(
|
|
188
|
-
api_url=config["api_url"],
|
|
189
|
-
api_key=config["api_key"],
|
|
190
|
-
timeout=config.get("timeout", 30.0),
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
# Test connection by listing resources
|
|
194
|
-
try:
|
|
195
|
-
with spinner_context(
|
|
196
|
-
ctx,
|
|
197
|
-
"[bold blue]Checking GL AIP status…[/bold blue]",
|
|
198
|
-
console_override=console,
|
|
199
|
-
spinner_style="cyan",
|
|
200
|
-
) as status_indicator:
|
|
201
|
-
update_spinner(
|
|
202
|
-
status_indicator, "[bold blue]Fetching agents…[/bold blue]"
|
|
203
|
-
)
|
|
204
|
-
agents = client.list_agents()
|
|
278
|
+
# Load and merge configuration
|
|
279
|
+
config = _load_and_merge_config(ctx)
|
|
205
280
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
tools = client.list_tools()
|
|
210
|
-
|
|
211
|
-
update_spinner(
|
|
212
|
-
status_indicator, "[bold blue]Fetching MCPs…[/bold blue]"
|
|
213
|
-
)
|
|
214
|
-
mcps = client.list_mcps()
|
|
215
|
-
|
|
216
|
-
# Create status table
|
|
217
|
-
table = AIPTable(title="🔗 GL AIP Status")
|
|
218
|
-
table.add_column("Resource", style="cyan", width=15)
|
|
219
|
-
table.add_column("Count", style="green", width=10)
|
|
220
|
-
table.add_column("Status", style="green", width=15)
|
|
221
|
-
|
|
222
|
-
table.add_row("Agents", str(len(agents)), "✅ Available")
|
|
223
|
-
table.add_row("Tools", str(len(tools)), "✅ Available")
|
|
224
|
-
table.add_row("MCPs", str(len(mcps)), "✅ Available")
|
|
225
|
-
|
|
226
|
-
console.print(
|
|
227
|
-
AIPPanel(
|
|
228
|
-
f"[bold green]✅ Connected to GL AIP[/bold green]\n"
|
|
229
|
-
f"🔗 API URL: {client.api_url}\n"
|
|
230
|
-
f"🤖 Agent Run Timeout: {DEFAULT_AGENT_RUN_TIMEOUT}s",
|
|
231
|
-
title="🚀 Connection Status",
|
|
232
|
-
border_style="green",
|
|
233
|
-
)
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
console.print(table)
|
|
237
|
-
|
|
238
|
-
except Exception as e:
|
|
239
|
-
# Show AIP Ready status even if connection fails
|
|
240
|
-
console.print(
|
|
241
|
-
f"\n[bold green]✅ AIP - Ready[/bold green] (SDK v{_SDK_VERSION})"
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
console.print(
|
|
245
|
-
AIPPanel(
|
|
246
|
-
f"[bold yellow]⚠️ Connection established but API call failed[/bold yellow]\n"
|
|
247
|
-
f"🔗 API URL: {client.api_url}\n"
|
|
248
|
-
f"❌ Error: {e}\n\n"
|
|
249
|
-
f"💡 This usually means:\n"
|
|
250
|
-
f" • Network connectivity issues\n"
|
|
251
|
-
f" • API permissions problems\n"
|
|
252
|
-
f" • Backend service issues",
|
|
253
|
-
title="⚠️ Partial Connection",
|
|
254
|
-
border_style="yellow",
|
|
255
|
-
)
|
|
256
|
-
)
|
|
281
|
+
# Validate configuration
|
|
282
|
+
_validate_config_and_show_error(config, console)
|
|
257
283
|
|
|
284
|
+
# Create and test client connection
|
|
285
|
+
client = _create_and_test_client(config, console)
|
|
258
286
|
client.close()
|
|
259
287
|
|
|
260
288
|
except Exception as e:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
f"🔍 Error: {e}\n\n"
|
|
265
|
-
f"💡 Troubleshooting steps:\n"
|
|
266
|
-
f" • Run 'aip config list' to check configuration\n"
|
|
267
|
-
f" • Run 'aip configure' to update credentials\n"
|
|
268
|
-
f" • Verify your API URL and key are correct\n"
|
|
269
|
-
f" • Check network connectivity to {config.get('api_url', 'your API')}",
|
|
270
|
-
title="❌ Connection Error",
|
|
271
|
-
border_style="red",
|
|
272
|
-
)
|
|
273
|
-
)
|
|
274
|
-
sys.exit(1)
|
|
289
|
+
# Handle any unexpected errors during the process
|
|
290
|
+
console = Console()
|
|
291
|
+
_handle_connection_error(config if "config" in locals() else {}, console, e)
|
|
275
292
|
|
|
276
293
|
|
|
277
294
|
@main.command()
|
glaip_sdk/cli/masking.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Masking helpers for CLI output.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"mask_payload",
|
|
14
|
+
"mask_rows",
|
|
15
|
+
"_mask_value",
|
|
16
|
+
"_mask_any",
|
|
17
|
+
"_maybe_mask_row",
|
|
18
|
+
"_resolve_mask_fields",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
_DEFAULT_MASK_FIELDS = {
|
|
22
|
+
"api_key",
|
|
23
|
+
"apikey",
|
|
24
|
+
"token",
|
|
25
|
+
"access_token",
|
|
26
|
+
"secret",
|
|
27
|
+
"client_secret",
|
|
28
|
+
"password",
|
|
29
|
+
"private_key",
|
|
30
|
+
"bearer",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _mask_value(raw: Any) -> str:
|
|
35
|
+
"""Return a masked representation of the provided value.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
raw: The raw value to mask, converted to string.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str: A masked representation showing first 4 and last 4 characters
|
|
42
|
+
separated by dots, or "••••" for strings ≤ 8 characters.
|
|
43
|
+
"""
|
|
44
|
+
text = str(raw)
|
|
45
|
+
if len(text) <= 8:
|
|
46
|
+
return "••••"
|
|
47
|
+
return f"{text[:4]}••••••••{text[-4:]}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _mask_any(value: Any, mask_fields: set[str]) -> Any:
|
|
51
|
+
"""Recursively mask sensitive fields in mappings and iterables.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
value: The value to process - can be dict, list, or any other type.
|
|
55
|
+
mask_fields: Set of field names (lowercase) that should be masked.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Any: The processed value with sensitive fields masked. Dicts and lists
|
|
59
|
+
are processed recursively, other values are returned unchanged.
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
masked: dict[Any, Any] = {}
|
|
63
|
+
for key, raw in value.items():
|
|
64
|
+
if isinstance(key, str) and key.lower() in mask_fields and raw is not None:
|
|
65
|
+
masked[key] = _mask_value(raw)
|
|
66
|
+
else:
|
|
67
|
+
masked[key] = _mask_any(raw, mask_fields)
|
|
68
|
+
return masked
|
|
69
|
+
|
|
70
|
+
if isinstance(value, list):
|
|
71
|
+
return [_mask_any(item, mask_fields) for item in value]
|
|
72
|
+
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _maybe_mask_row(row: dict[str, Any], mask_fields: set[str]) -> dict[str, Any]:
|
|
77
|
+
"""Mask a single row when masking is enabled.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
row: A dictionary representing a single row of data.
|
|
81
|
+
mask_fields: Set of field names to mask. If empty, returns row unchanged.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
dict[str, Any]: The row with sensitive fields masked, or the original
|
|
85
|
+
row if no mask_fields are provided.
|
|
86
|
+
"""
|
|
87
|
+
if not mask_fields:
|
|
88
|
+
return row
|
|
89
|
+
return _mask_any(row, mask_fields)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _resolve_mask_fields() -> set[str]:
|
|
93
|
+
"""Resolve the set of sensitive fields to mask based on environment.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
set[str]: Set of field names to mask. Empty set if masking is disabled
|
|
97
|
+
via AIP_MASK_OFF environment variable, custom fields from
|
|
98
|
+
AIP_MASK_FIELDS, or default fields if neither is set.
|
|
99
|
+
"""
|
|
100
|
+
if os.getenv("AIP_MASK_OFF", "0") in {"1", "true", "on", "yes"}:
|
|
101
|
+
return set()
|
|
102
|
+
|
|
103
|
+
env_fields = (os.getenv("AIP_MASK_FIELDS") or "").strip()
|
|
104
|
+
if env_fields:
|
|
105
|
+
parts = [part.strip().lower() for part in env_fields.split(",") if part.strip()]
|
|
106
|
+
return set(parts)
|
|
107
|
+
|
|
108
|
+
return set(_DEFAULT_MASK_FIELDS)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def mask_payload(payload: Any) -> Any:
|
|
112
|
+
"""Mask sensitive values in an arbitrary payload when masking is enabled.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
payload: Any data structure (dict, list, or primitive) to mask.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Any: The payload with sensitive fields masked based on environment
|
|
119
|
+
configuration. Returns original payload if masking is disabled
|
|
120
|
+
or if an error occurs during masking.
|
|
121
|
+
"""
|
|
122
|
+
mask_fields = _resolve_mask_fields()
|
|
123
|
+
if not mask_fields:
|
|
124
|
+
return payload
|
|
125
|
+
try:
|
|
126
|
+
return _mask_any(payload, mask_fields)
|
|
127
|
+
except Exception:
|
|
128
|
+
return payload
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def mask_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
132
|
+
"""Mask sensitive values in row-oriented data when masking is enabled.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
rows: List of dictionaries representing rows of tabular data.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
list[dict[str, Any]]: List of rows with sensitive fields masked based
|
|
139
|
+
on environment configuration. Returns original
|
|
140
|
+
rows if masking is disabled or if an error occurs.
|
|
141
|
+
"""
|
|
142
|
+
mask_fields = _resolve_mask_fields()
|
|
143
|
+
if not mask_fields:
|
|
144
|
+
return rows
|
|
145
|
+
try:
|
|
146
|
+
return [_maybe_mask_row(row, mask_fields) for row in rows]
|
|
147
|
+
except Exception:
|
|
148
|
+
return rows
|
glaip_sdk/cli/mcp_validators.py
CHANGED
|
@@ -15,7 +15,7 @@ import click
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def format_validation_error(prefix: str, detail: str | None = None) -> str:
|
|
18
|
-
"""Format a validation error message with optional detail.
|
|
18
|
+
r"""Format a validation error message with optional detail.
|
|
19
19
|
|
|
20
20
|
Args:
|
|
21
21
|
prefix: Main error message
|
|
@@ -26,7 +26,7 @@ def format_validation_error(prefix: str, detail: str | None = None) -> str:
|
|
|
26
26
|
|
|
27
27
|
Examples:
|
|
28
28
|
>>> format_validation_error("Invalid config", "Missing 'url' field")
|
|
29
|
-
"Invalid config
|
|
29
|
+
"Invalid config\nMissing 'url' field"
|
|
30
30
|
"""
|
|
31
31
|
parts = [prefix]
|
|
32
32
|
if detail:
|