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/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/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:
|
glaip_sdk/cli/pager.py
CHANGED
|
@@ -57,8 +57,9 @@ def _get_console() -> Console:
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def _prepare_pager_env(clear_on_exit: bool = True) -> None:
|
|
60
|
-
"""
|
|
61
|
-
|
|
60
|
+
"""Configure LESS flags for a predictable, high-quality UX.
|
|
61
|
+
|
|
62
|
+
Sets sensible defaults for the system pager:
|
|
62
63
|
-R : pass ANSI color escapes
|
|
63
64
|
-S : chop long lines (horizontal scroll with ←/→)
|
|
64
65
|
(No -F, no -X) so we open a full-screen pager and clear on exit.
|
|
@@ -17,7 +17,7 @@ import click
|
|
|
17
17
|
def _format_file_error(
|
|
18
18
|
prefix: str, file_path_str: str, resolved_path: Path, *, detail: str | None = None
|
|
19
19
|
) -> str:
|
|
20
|
-
"""Format a file-related error message with path context.
|
|
20
|
+
r"""Format a file-related error message with path context.
|
|
21
21
|
|
|
22
22
|
Args:
|
|
23
23
|
prefix: Main error message
|
|
@@ -31,7 +31,7 @@ def _format_file_error(
|
|
|
31
31
|
Examples:
|
|
32
32
|
>>> from pathlib import Path
|
|
33
33
|
>>> _format_file_error("File not found", "config.json", Path("/abs/config.json"))
|
|
34
|
-
'File not found: config.json
|
|
34
|
+
'File not found: config.json\nResolved path: /abs/config.json'
|
|
35
35
|
"""
|
|
36
36
|
parts = [f"{prefix}: {file_path_str}", f"Resolved path: {resolved_path}"]
|
|
37
37
|
if detail:
|
glaip_sdk/cli/resolution.py
CHANGED
|
@@ -32,20 +32,22 @@ def resolve_resource_reference(
|
|
|
32
32
|
This is a common pattern used across all resource types.
|
|
33
33
|
|
|
34
34
|
Args:
|
|
35
|
-
ctx: Click context
|
|
36
|
-
|
|
37
|
-
reference: Resource ID or name
|
|
38
|
-
resource_type: Type of resource
|
|
39
|
-
get_by_id_func: Function to get resource by ID
|
|
40
|
-
find_by_name_func: Function to find resources by name
|
|
41
|
-
label: Label for error messages
|
|
42
|
-
select: Selection index for ambiguous matches
|
|
35
|
+
ctx: Click context for CLI operations.
|
|
36
|
+
_client: API client instance for backend operations.
|
|
37
|
+
reference: Resource ID or name to resolve.
|
|
38
|
+
resource_type: Type of resource being resolved.
|
|
39
|
+
get_by_id_func: Function to get resource by ID.
|
|
40
|
+
find_by_name_func: Function to find resources by name.
|
|
41
|
+
label: Label for error messages and user feedback.
|
|
42
|
+
select: Selection index for ambiguous matches in non-interactive mode.
|
|
43
|
+
interface_preference: Interface preference for user interaction ("fuzzy" or "questionary").
|
|
44
|
+
spinner_message: Custom message to show during resolution process.
|
|
43
45
|
|
|
44
46
|
Returns:
|
|
45
|
-
Resolved resource object
|
|
47
|
+
Resolved resource object or None if not found.
|
|
46
48
|
|
|
47
49
|
Raises:
|
|
48
|
-
click.ClickException: If resolution fails
|
|
50
|
+
click.ClickException: If resolution fails.
|
|
49
51
|
"""
|
|
50
52
|
try:
|
|
51
53
|
message = (
|
|
@@ -22,6 +22,12 @@ class AgentRunSession:
|
|
|
22
22
|
"""Per-agent execution context for the command palette."""
|
|
23
23
|
|
|
24
24
|
def __init__(self, session: SlashSession, agent: Any) -> None:
|
|
25
|
+
"""Initialize the agent run session.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
session: The slash session context
|
|
29
|
+
agent: The agent to interact with
|
|
30
|
+
"""
|
|
25
31
|
self.session = session
|
|
26
32
|
self.agent = agent
|
|
27
33
|
self.console = session.console
|
|
@@ -39,6 +45,7 @@ class AgentRunSession:
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
def run(self) -> None:
|
|
48
|
+
"""Run the interactive agent session loop."""
|
|
42
49
|
self.session.set_contextual_commands(
|
|
43
50
|
self._contextual_completion_help, include_global=False
|
|
44
51
|
)
|
glaip_sdk/cli/slash/prompt.py
CHANGED
|
@@ -46,6 +46,11 @@ if _HAS_PROMPT_TOOLKIT:
|
|
|
46
46
|
"""Provide slash command completions inside the prompt."""
|
|
47
47
|
|
|
48
48
|
def __init__(self, session: SlashSession) -> None:
|
|
49
|
+
"""Initialize the slash completer.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
session: The slash session context
|
|
53
|
+
"""
|
|
49
54
|
self._session = session
|
|
50
55
|
|
|
51
56
|
def get_completions(
|
|
@@ -53,6 +58,15 @@ if _HAS_PROMPT_TOOLKIT:
|
|
|
53
58
|
document: Any,
|
|
54
59
|
_complete_event: Any, # type: ignore[no-any-return]
|
|
55
60
|
) -> Iterable[Completion]: # pragma: no cover - UI
|
|
61
|
+
"""Get completions for slash commands.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
document: The document being edited
|
|
65
|
+
_complete_event: The completion event
|
|
66
|
+
|
|
67
|
+
Yields:
|
|
68
|
+
Completion objects for matching commands
|
|
69
|
+
"""
|
|
56
70
|
if Completion is None:
|
|
57
71
|
return
|
|
58
72
|
|
|
@@ -66,7 +80,14 @@ if _HAS_PROMPT_TOOLKIT:
|
|
|
66
80
|
else: # pragma: no cover - fallback when prompt_toolkit is missing
|
|
67
81
|
|
|
68
82
|
class SlashCompleter: # type: ignore[too-many-ancestors]
|
|
83
|
+
"""Fallback slash completer when prompt_toolkit is not available."""
|
|
84
|
+
|
|
69
85
|
def __init__(self, session: SlashSession) -> None: # noqa: D401 - stub
|
|
86
|
+
"""Initialize the fallback slash completer.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
session: The slash session context
|
|
90
|
+
"""
|
|
70
91
|
self._session = session
|
|
71
92
|
|
|
72
93
|
|
|
@@ -76,7 +97,6 @@ def setup_prompt_toolkit(
|
|
|
76
97
|
interactive: bool,
|
|
77
98
|
) -> tuple[Any | None, Any | None]:
|
|
78
99
|
"""Configure prompt_toolkit session and style for interactive mode."""
|
|
79
|
-
|
|
80
100
|
if not (interactive and _HAS_PROMPT_TOOLKIT):
|
|
81
101
|
return None, None
|
|
82
102
|
|
|
@@ -105,7 +125,6 @@ def setup_prompt_toolkit(
|
|
|
105
125
|
|
|
106
126
|
def _create_key_bindings(session: SlashSession) -> Any:
|
|
107
127
|
"""Create prompt_toolkit key bindings for the command palette."""
|
|
108
|
-
|
|
109
128
|
if KeyBindings is None:
|
|
110
129
|
return None
|
|
111
130
|
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -19,7 +19,7 @@ from rich.table import Table
|
|
|
19
19
|
|
|
20
20
|
from glaip_sdk.branding import AIPBranding
|
|
21
21
|
from glaip_sdk.cli.commands.configure import configure_command, load_config
|
|
22
|
-
from glaip_sdk.cli.utils import _fuzzy_pick_for_resources, get_client
|
|
22
|
+
from glaip_sdk.cli.utils import _fuzzy_pick_for_resources, command_hint, get_client
|
|
23
23
|
from glaip_sdk.rich_components import AIPPanel
|
|
24
24
|
|
|
25
25
|
from .agent_session import AgentRunSession
|
|
@@ -49,6 +49,12 @@ class SlashSession:
|
|
|
49
49
|
"""Interactive command palette controller."""
|
|
50
50
|
|
|
51
51
|
def __init__(self, ctx: click.Context, *, console: Console | None = None) -> None:
|
|
52
|
+
"""Initialize the slash session.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
ctx: The Click context
|
|
56
|
+
console: Optional console instance, creates default if None
|
|
57
|
+
"""
|
|
52
58
|
self.ctx = ctx
|
|
53
59
|
self.console = console or Console()
|
|
54
60
|
self._commands: dict[str, SlashCommand] = {}
|
|
@@ -90,7 +96,6 @@ class SlashSession:
|
|
|
90
96
|
|
|
91
97
|
def run(self, initial_commands: Iterable[str] | None = None) -> None:
|
|
92
98
|
"""Start the command palette session loop."""
|
|
93
|
-
|
|
94
99
|
if not self._interactive:
|
|
95
100
|
self._run_non_interactive(initial_commands)
|
|
96
101
|
return
|
|
@@ -153,7 +158,6 @@ class SlashSession:
|
|
|
153
158
|
|
|
154
159
|
def _ensure_configuration(self) -> bool:
|
|
155
160
|
"""Ensure the CLI has both API URL and credentials before continuing."""
|
|
156
|
-
|
|
157
161
|
while not self._configuration_ready():
|
|
158
162
|
self.console.print(
|
|
159
163
|
"[yellow]Configuration required.[/] Launching `/login` wizard..."
|
|
@@ -173,7 +177,6 @@ class SlashSession:
|
|
|
173
177
|
|
|
174
178
|
def _configuration_ready(self) -> bool:
|
|
175
179
|
"""Check whether API URL and credentials are available."""
|
|
176
|
-
|
|
177
180
|
config = self._load_config()
|
|
178
181
|
api_url = self._get_api_url(config)
|
|
179
182
|
if not api_url:
|
|
@@ -188,7 +191,6 @@ class SlashSession:
|
|
|
188
191
|
|
|
189
192
|
def handle_command(self, raw: str, *, invoked_from_agent: bool = False) -> bool:
|
|
190
193
|
"""Parse and execute a single slash command string."""
|
|
191
|
-
|
|
192
194
|
verb, args = self._parse(raw)
|
|
193
195
|
if not verb:
|
|
194
196
|
self.console.print("[red]Unrecognised command[/red]")
|
|
@@ -308,9 +310,13 @@ class SlashSession:
|
|
|
308
310
|
return True
|
|
309
311
|
|
|
310
312
|
if not agents:
|
|
311
|
-
self.
|
|
312
|
-
|
|
313
|
-
|
|
313
|
+
hint = command_hint("agents create", slash_command=None, ctx=self.ctx)
|
|
314
|
+
if hint:
|
|
315
|
+
self.console.print(
|
|
316
|
+
f"[yellow]No agents available. Use `{hint}` to add one.[/yellow]"
|
|
317
|
+
)
|
|
318
|
+
else:
|
|
319
|
+
self.console.print("[yellow]No agents available.[/yellow]")
|
|
314
320
|
return True
|
|
315
321
|
|
|
316
322
|
if args:
|
|
@@ -373,7 +379,7 @@ class SlashSession:
|
|
|
373
379
|
self._register(
|
|
374
380
|
SlashCommand(
|
|
375
381
|
name="login",
|
|
376
|
-
help="Run `
|
|
382
|
+
help="Run `/login` (alias `/configure`) to set credentials.",
|
|
377
383
|
handler=SlashSession._cmd_login,
|
|
378
384
|
aliases=("configure",),
|
|
379
385
|
)
|
|
@@ -419,12 +425,10 @@ class SlashSession:
|
|
|
419
425
|
@property
|
|
420
426
|
def verbose_enabled(self) -> bool:
|
|
421
427
|
"""Return whether verbose agent runs are enabled."""
|
|
422
|
-
|
|
423
428
|
return self._verbose_enabled
|
|
424
429
|
|
|
425
430
|
def set_verbose(self, enabled: bool, *, announce: bool = True) -> None:
|
|
426
431
|
"""Enable or disable verbose mode with optional announcement."""
|
|
427
|
-
|
|
428
432
|
if self._verbose_enabled == enabled:
|
|
429
433
|
if announce:
|
|
430
434
|
self._print_verbose_status(context="already")
|
|
@@ -437,12 +441,10 @@ class SlashSession:
|
|
|
437
441
|
|
|
438
442
|
def toggle_verbose(self, *, announce: bool = True) -> None:
|
|
439
443
|
"""Flip verbose mode state."""
|
|
440
|
-
|
|
441
444
|
self.set_verbose(not self._verbose_enabled, announce=announce)
|
|
442
445
|
|
|
443
446
|
def _cmd_verbose(self, args: list[str], _invoked_from_agent: bool) -> bool:
|
|
444
447
|
"""Slash handler for `/verbose` command."""
|
|
445
|
-
|
|
446
448
|
if args:
|
|
447
449
|
self.console.print(
|
|
448
450
|
"Usage: `/verbose` toggles verbose streaming output. Press Ctrl+T as a shortcut."
|
|
@@ -470,30 +472,25 @@ class SlashSession:
|
|
|
470
472
|
# ------------------------------------------------------------------
|
|
471
473
|
def register_active_renderer(self, renderer: Any) -> None:
|
|
472
474
|
"""Register the renderer currently streaming an agent run."""
|
|
473
|
-
|
|
474
475
|
self._active_renderer = renderer
|
|
475
476
|
self._sync_active_renderer()
|
|
476
477
|
|
|
477
478
|
def clear_active_renderer(self, renderer: Any | None = None) -> None:
|
|
478
479
|
"""Clear the active renderer if it matches the provided instance."""
|
|
479
|
-
|
|
480
480
|
if renderer is not None and renderer is not self._active_renderer:
|
|
481
481
|
return
|
|
482
482
|
self._active_renderer = None
|
|
483
483
|
|
|
484
484
|
def notify_agent_run_started(self) -> None:
|
|
485
485
|
"""Mark that an agent run is in progress."""
|
|
486
|
-
|
|
487
486
|
self.clear_active_renderer()
|
|
488
487
|
|
|
489
488
|
def notify_agent_run_finished(self) -> None:
|
|
490
489
|
"""Mark that the active agent run has completed."""
|
|
491
|
-
|
|
492
490
|
self.clear_active_renderer()
|
|
493
491
|
|
|
494
492
|
def _sync_active_renderer(self) -> None:
|
|
495
493
|
"""Ensure the active renderer reflects the current verbose state."""
|
|
496
|
-
|
|
497
494
|
renderer = self._active_renderer
|
|
498
495
|
if renderer is None:
|
|
499
496
|
return
|
|
@@ -622,18 +619,15 @@ class SlashSession:
|
|
|
622
619
|
self, commands: dict[str, str] | None, *, include_global: bool = True
|
|
623
620
|
) -> None:
|
|
624
621
|
"""Set context-specific commands that should appear in completions."""
|
|
625
|
-
|
|
626
622
|
self._contextual_commands = dict(commands or {})
|
|
627
623
|
self._contextual_include_global = include_global if commands else True
|
|
628
624
|
|
|
629
625
|
def get_contextual_commands(self) -> dict[str, str]: # type: ignore[no-any-return]
|
|
630
626
|
"""Return a copy of the currently active contextual commands."""
|
|
631
|
-
|
|
632
627
|
return dict(self._contextual_commands)
|
|
633
628
|
|
|
634
629
|
def should_include_global_commands(self) -> bool:
|
|
635
630
|
"""Return whether global slash commands should appear in completions."""
|
|
636
|
-
|
|
637
631
|
return self._contextual_include_global
|
|
638
632
|
|
|
639
633
|
def _remember_agent(self, agent: Any) -> None: # type: ignore[no-any-return]
|
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -13,6 +13,7 @@ import httpx
|
|
|
13
13
|
from packaging.version import InvalidVersion, Version
|
|
14
14
|
from rich.console import Console
|
|
15
15
|
|
|
16
|
+
from glaip_sdk.cli.utils import command_hint
|
|
16
17
|
from glaip_sdk.rich_components import AIPPanel
|
|
17
18
|
|
|
18
19
|
FetchLatestVersion = Callable[[], str | None]
|
|
@@ -59,6 +60,7 @@ def _should_check_for_updates() -> bool:
|
|
|
59
60
|
def _build_update_panel(
|
|
60
61
|
current_version: str,
|
|
61
62
|
latest_version: str,
|
|
63
|
+
command_text: str,
|
|
62
64
|
) -> AIPPanel:
|
|
63
65
|
"""Create a Rich panel that prompts the user to update."""
|
|
64
66
|
message = (
|
|
@@ -66,7 +68,7 @@ def _build_update_panel(
|
|
|
66
68
|
f"{current_version} → {latest_version}\n\n"
|
|
67
69
|
"See the latest release notes:\n"
|
|
68
70
|
f"https://pypi.org/project/glaip-sdk/{latest_version}/\n\n"
|
|
69
|
-
"[cyan]Run[/cyan] [bold]
|
|
71
|
+
f"[cyan]Run[/cyan] [bold]{command_text}[/bold] to install."
|
|
70
72
|
)
|
|
71
73
|
return AIPPanel(
|
|
72
74
|
message,
|
|
@@ -99,8 +101,12 @@ def maybe_notify_update(
|
|
|
99
101
|
if current is None or latest is None or latest <= current:
|
|
100
102
|
return
|
|
101
103
|
|
|
104
|
+
command_text = command_hint("update")
|
|
105
|
+
if command_text is None:
|
|
106
|
+
return
|
|
107
|
+
|
|
102
108
|
active_console = console or Console()
|
|
103
|
-
panel = _build_update_panel(current_version, latest_version)
|
|
109
|
+
panel = _build_update_panel(current_version, latest_version, command_text)
|
|
104
110
|
active_console.print(panel)
|
|
105
111
|
|
|
106
112
|
|