glaip-sdk 0.0.7__py3-none-any.whl → 0.0.8__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 CHANGED
@@ -31,8 +31,8 @@ except Exception: # pragma: no cover
31
31
 
32
32
 
33
33
  # ---- minimal, readable styles (light blue + white theme) -----------------
34
- PRIMARY = "bright_blue" # Light blue for main elements
35
- BORDER = "bright_blue" # Light blue borders
34
+ PRIMARY = "#15a2d8" # GDP Labs brand blue
35
+ BORDER = PRIMARY # Keep borders aligned with brand tone
36
36
  TITLE_STYLE = f"bold {PRIMARY}"
37
37
  LABEL = "bold"
38
38
 
@@ -95,7 +95,7 @@ GDP Labs AI Agents Package
95
95
  # ---- public API -----------------------------------------------------------
96
96
  def get_welcome_banner(self) -> str:
97
97
  """Get AIP banner with version info."""
98
- banner = self.AIP_LOGO
98
+ banner = f"[{PRIMARY}]{self.AIP_LOGO}[/{PRIMARY}]"
99
99
  line = f"Version: {self.version}"
100
100
  banner = f"{banner}\n{line}"
101
101
  return banner
@@ -6,6 +6,7 @@ Authors:
6
6
 
7
7
  import json
8
8
  import os
9
+ from collections.abc import Mapping
9
10
  from pathlib import Path
10
11
  from typing import Any
11
12
 
@@ -53,6 +54,7 @@ from glaip_sdk.cli.utils import (
53
54
  output_flags,
54
55
  output_list,
55
56
  output_result,
57
+ spinner_context,
56
58
  )
57
59
  from glaip_sdk.cli.validators import (
58
60
  validate_agent_instruction_cli as validate_agent_instruction,
@@ -76,6 +78,91 @@ console = Console()
76
78
  AGENT_NOT_FOUND_ERROR = "Agent not found"
77
79
 
78
80
 
81
+ def _safe_agent_attribute(agent: Any, name: str) -> Any:
82
+ """Return attribute value for ``name`` while filtering Mock sentinels."""
83
+
84
+ try:
85
+ value = getattr(agent, name)
86
+ except Exception:
87
+ return None
88
+
89
+ if hasattr(value, "_mock_name"):
90
+ return None
91
+ return value
92
+
93
+
94
+ def _coerce_mapping_candidate(candidate: Any) -> dict[str, Any] | None:
95
+ """Convert a mapping-like candidate to a plain dict when possible."""
96
+
97
+ if candidate is None:
98
+ return None
99
+ if isinstance(candidate, Mapping):
100
+ return dict(candidate)
101
+ return None
102
+
103
+
104
+ def _call_agent_method(agent: Any, method_name: str) -> dict[str, Any] | None:
105
+ """Attempt to call the named method and coerce its output to a dict."""
106
+
107
+ method = getattr(agent, method_name, None)
108
+ if not callable(method):
109
+ return None
110
+ try:
111
+ candidate = method()
112
+ except Exception:
113
+ return None
114
+ return _coerce_mapping_candidate(candidate)
115
+
116
+
117
+ def _coerce_agent_via_methods(agent: Any) -> dict[str, Any] | None:
118
+ """Try standard serialisation helpers to produce a mapping."""
119
+
120
+ for attr in ("model_dump", "dict", "to_dict"):
121
+ mapping = _call_agent_method(agent, attr)
122
+ if mapping is not None:
123
+ return mapping
124
+ return None
125
+
126
+
127
+ def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
128
+ """Construct a minimal mapping from well-known agent attributes."""
129
+
130
+ fallback_fields = (
131
+ "id",
132
+ "name",
133
+ "instruction",
134
+ "description",
135
+ "model",
136
+ "agent_config",
137
+ "tools",
138
+ "agents",
139
+ "mcps",
140
+ "timeout",
141
+ )
142
+
143
+ fallback: dict[str, Any] = {}
144
+ for field in fallback_fields:
145
+ value = _safe_agent_attribute(agent, field)
146
+ if value is not None:
147
+ fallback[field] = value
148
+
149
+ return fallback or {"name": str(agent)}
150
+
151
+
152
+ def _prepare_agent_output(agent: Any) -> dict[str, Any]:
153
+ """Build a JSON-serialisable mapping for CLI output."""
154
+
155
+ method_mapping = _coerce_agent_via_methods(agent)
156
+ if method_mapping is not None:
157
+ return method_mapping
158
+
159
+ intrinsic = _coerce_mapping_candidate(agent)
160
+ if intrinsic is not None:
161
+ return intrinsic
162
+
163
+ return _build_fallback_agent_mapping(agent)
164
+
165
+
79
166
  def _fetch_full_agent_details(client: Any, agent: Any) -> Any | None:
80
167
  """Fetch full agent details by ID to ensure all fields are populated."""
81
168
  try:
@@ -199,7 +286,12 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
199
286
  return
200
287
 
201
288
  # Try to fetch and format raw agent data first
202
- formatted_data = _fetch_and_format_raw_agent_data(client, agent)
289
+ with spinner_context(
290
+ ctx,
291
+ "[bold blue]Loading agent details…[/bold blue]",
292
+ console_override=console,
293
+ ):
294
+ formatted_data = _fetch_and_format_raw_agent_data(client, agent)
203
295
 
204
296
  if formatted_data:
205
297
  # Use raw API data - this preserves ALL fields including account_id
@@ -216,7 +308,12 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
216
308
  ctx, Text("[yellow]Falling back to Pydantic model data[/yellow]")
217
309
  )
218
310
 
219
- result_data = _format_fallback_agent_data(client, agent)
311
+ with spinner_context(
312
+ ctx,
313
+ "[bold blue]Preparing fallback agent details…[/bold blue]",
314
+ console_override=console,
315
+ ):
316
+ result_data = _format_fallback_agent_data(client, agent)
220
317
 
221
318
  # Display using output_result
222
319
  output_result(
@@ -289,13 +386,18 @@ def list_agents(
289
386
  """List agents with optional filtering."""
290
387
  try:
291
388
  client = get_client(ctx)
292
- agents = client.agents.list_agents(
293
- agent_type=agent_type,
294
- framework=framework,
295
- name=name,
296
- version=version,
297
- sync_langflow_agents=sync_langflow,
298
- )
389
+ with spinner_context(
390
+ ctx,
391
+ "[bold blue]Fetching agents…[/bold blue]",
392
+ console_override=console,
393
+ ):
394
+ agents = client.agents.list_agents(
395
+ agent_type=agent_type,
396
+ framework=framework,
397
+ name=name,
398
+ version=version,
399
+ sync_langflow_agents=sync_langflow,
400
+ )
299
401
 
300
402
  # Define table columns: (data_key, header, style, width)
301
403
  columns = [
@@ -364,7 +466,12 @@ def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> Non
364
466
 
365
467
  # Always export comprehensive data - re-fetch agent with full details
366
468
  try:
367
- agent = client.agents.get_agent_by_id(agent.id)
469
+ with spinner_context(
470
+ ctx,
471
+ "[bold blue]Fetching complete agent data…[/bold blue]",
472
+ console_override=console,
473
+ ):
474
+ agent = client.agents.get_agent_by_id(agent.id)
368
475
  except Exception as e:
369
476
  handle_rich_output(
370
477
  ctx,
@@ -776,7 +883,7 @@ def _get_language_model_display_name(agent: Any, model: str | None) -> str:
776
883
 
777
884
  def _handle_successful_creation(ctx: Any, agent: Any, model: str | None) -> None:
778
885
  """Handle successful agent creation output."""
779
- handle_json_output(ctx, agent.model_dump())
886
+ handle_json_output(ctx, _prepare_agent_output(agent))
780
887
 
781
888
  lm_display = _get_language_model_display_name(agent, model)
782
889
 
@@ -1080,7 +1187,7 @@ def update(
1080
1187
 
1081
1188
  updated_agent = client.agents.update_agent(agent.id, **update_data)
1082
1189
 
1083
- handle_json_output(ctx, updated_agent.model_dump())
1190
+ handle_json_output(ctx, _prepare_agent_output(updated_agent))
1084
1191
  handle_rich_output(ctx, display_update_success("Agent", updated_agent.name))
1085
1192
  handle_rich_output(ctx, display_agent_run_suggestions(updated_agent))
1086
1193
 
glaip_sdk/cli/display.py CHANGED
@@ -247,9 +247,10 @@ def display_agent_run_suggestions(agent: Any) -> Panel:
247
247
 
248
248
  return AIPPanel(
249
249
  f"[bold blue]💡 Next Steps:[/bold blue]\n\n"
250
- f"🚀 Run this agent:\n"
251
- f' [green]aip agents run {agent.id} "Your message here"[/green]\n\n'
252
- f"📋 Or use the agent name:\n"
250
+ f"🚀 Start chatting with [bold]{agent.name}[/bold] right here:\n"
251
+ 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'
253
254
  f' [green]aip agents run "{agent.name}" "Your message here"[/green]\n\n'
254
255
  f"🔧 Available options:\n"
255
256
  f" [dim]--chat-history[/dim] Include previous conversation\n"
glaip_sdk/cli/main.py CHANGED
@@ -24,11 +24,21 @@ from glaip_sdk.cli.commands.configure import (
24
24
  from glaip_sdk.cli.commands.mcps import mcps_group
25
25
  from glaip_sdk.cli.commands.models import models_group
26
26
  from glaip_sdk.cli.commands.tools import tools_group
27
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
27
+ from glaip_sdk.cli.utils import spinner_context
28
+ from glaip_sdk.config.constants import (
29
+ DEFAULT_AGENT_RUN_TIMEOUT,
30
+ )
28
31
  from glaip_sdk.rich_components import AIPPanel, AIPTable
29
32
 
33
+ # Import SlashSession for potential mocking in tests
34
+ try:
35
+ from glaip_sdk.cli.slash import SlashSession
36
+ except ImportError: # pragma: no cover - optional slash dependencies
37
+ # Slash dependencies might not be available in all environments
38
+ SlashSession = None
39
+
30
40
 
31
- @click.group()
41
+ @click.group(invoke_without_command=True)
32
42
  @click.version_option(version=_SDK_VERSION, prog_name="aip")
33
43
  @click.option("--api-url", envvar="AIP_API_URL", help="AIP API URL")
34
44
  @click.option("--api-key", envvar="AIP_API_KEY", help="AIP API Key")
@@ -72,6 +82,15 @@ def main(
72
82
 
73
83
  ctx.obj["tty"] = not no_tty
74
84
 
85
+ if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
86
+ if _should_launch_slash(ctx) and SlashSession is not None:
87
+ session = SlashSession(ctx)
88
+ session.run()
89
+ ctx.exit()
90
+ else:
91
+ click.echo(ctx.get_help())
92
+ ctx.exit()
93
+
75
94
 
76
95
  # Add command groups
77
96
  main.add_command(agents_group)
@@ -87,6 +106,19 @@ main.add_command(configure_command)
87
106
  # Tip: `--version` is provided by click.version_option above.
88
107
 
89
108
 
109
+ def _should_launch_slash(ctx: click.Context) -> bool:
110
+ """Determine whether to open the command palette automatically."""
111
+
112
+ ctx_obj = ctx.obj or {}
113
+ if not bool(ctx_obj.get("tty", True)):
114
+ return False
115
+
116
+ if not (sys.stdin.isatty() and sys.stdout.isatty()):
117
+ return False
118
+
119
+ return True
120
+
121
+
90
122
  @main.command()
91
123
  @click.pass_context
92
124
  def status(ctx: Any) -> None:
@@ -152,9 +184,23 @@ def status(ctx: Any) -> None:
152
184
 
153
185
  # Test connection by listing resources
154
186
  try:
155
- agents = client.list_agents()
156
- tools = client.list_tools()
157
- mcps = client.list_mcps()
187
+ with spinner_context(
188
+ ctx,
189
+ "[bold blue]Checking GL AIP status…[/bold blue]",
190
+ console_override=console,
191
+ spinner_style="cyan",
192
+ ) as status_indicator:
193
+ if status_indicator is not None:
194
+ status_indicator.update("[bold blue]Fetching agents…[/bold blue]")
195
+ agents = client.list_agents()
196
+
197
+ if status_indicator is not None:
198
+ status_indicator.update("[bold blue]Fetching tools…[/bold blue]")
199
+ tools = client.list_tools()
200
+
201
+ if status_indicator is not None:
202
+ status_indicator.update("[bold blue]Fetching MCPs…[/bold blue]")
203
+ mcps = client.list_mcps()
158
204
 
159
205
  # Create status table
160
206
  table = AIPTable(title="🔗 GL AIP Status")
@@ -12,7 +12,7 @@ from typing import Any
12
12
 
13
13
  import click
14
14
 
15
- from glaip_sdk.cli.utils import resolve_resource
15
+ from glaip_sdk.cli.utils import resolve_resource, spinner_context
16
16
 
17
17
 
18
18
  def resolve_resource_reference(
@@ -25,6 +25,7 @@ def resolve_resource_reference(
25
25
  label: str,
26
26
  select: int | None = None,
27
27
  interface_preference: str | None = None,
28
+ spinner_message: str | None = None,
28
29
  ) -> Any | None:
29
30
  """Resolve resource reference (ID or name) with ambiguity handling.
30
31
 
@@ -47,14 +48,21 @@ def resolve_resource_reference(
47
48
  click.ClickException: If resolution fails
48
49
  """
49
50
  try:
50
- return resolve_resource(
51
- ctx,
52
- reference,
53
- get_by_id=get_by_id_func,
54
- find_by_name=find_by_name_func,
55
- label=label,
56
- select=select,
57
- interface_preference=interface_preference,
51
+ message = (
52
+ spinner_message
53
+ if spinner_message is not None
54
+ else f"[bold blue]Fetching {label}…[/bold blue]"
58
55
  )
56
+ with spinner_context(ctx, message, spinner_style="cyan") as status_indicator:
57
+ return resolve_resource(
58
+ ctx,
59
+ reference,
60
+ get_by_id=get_by_id_func,
61
+ find_by_name=find_by_name_func,
62
+ label=label,
63
+ select=select,
64
+ interface_preference=interface_preference,
65
+ status_indicator=status_indicator,
66
+ )
59
67
  except Exception as e:
60
68
  raise click.ClickException(f"Failed to resolve {resource_type.lower()}: {e}")
@@ -0,0 +1,25 @@
1
+ """Slash command palette entrypoints.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from glaip_sdk.cli.commands.agents import get as agents_get_command
8
+ from glaip_sdk.cli.commands.agents import run as agents_run_command
9
+ from glaip_sdk.cli.commands.configure import configure_command, load_config
10
+ from glaip_sdk.cli.utils import get_client
11
+
12
+ from .agent_session import AgentRunSession
13
+ from .prompt import _HAS_PROMPT_TOOLKIT
14
+ from .session import SlashSession
15
+
16
+ __all__ = [
17
+ "AgentRunSession",
18
+ "SlashSession",
19
+ "_HAS_PROMPT_TOOLKIT",
20
+ "agents_get_command",
21
+ "agents_run_command",
22
+ "configure_command",
23
+ "get_client",
24
+ "load_config",
25
+ ]
@@ -0,0 +1,146 @@
1
+ """Agent-specific interaction loop for the command palette.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ import click
12
+
13
+ from glaip_sdk.cli.commands.agents import get as agents_get_command
14
+ from glaip_sdk.cli.commands.agents import run as agents_run_command
15
+
16
+ if TYPE_CHECKING: # pragma: no cover - type checking only
17
+ from .session import SlashSession
18
+
19
+
20
+ class AgentRunSession:
21
+ """Per-agent execution context for the command palette."""
22
+
23
+ def __init__(self, session: SlashSession, agent: Any) -> None:
24
+ self.session = session
25
+ self.agent = agent
26
+ self.console = session.console
27
+ self._agent_id = str(getattr(agent, "id", ""))
28
+ self._agent_name = getattr(agent, "name", "") or self._agent_id
29
+ self._prompt_placeholder: str = (
30
+ "Chat with this agent here; use / for shortcuts."
31
+ )
32
+ self._contextual_completion_help: dict[str, str] = {
33
+ "details": "Show this agent's full configuration.",
34
+ "help": "Display this context-aware menu.",
35
+ "exit": "Return to the command palette.",
36
+ "q": "Return to the command palette.",
37
+ }
38
+
39
+ def run(self) -> None:
40
+ self.session.set_contextual_commands(
41
+ self._contextual_completion_help, include_global=False
42
+ )
43
+ try:
44
+ self._display_agent_info()
45
+ self._run_agent_loop()
46
+ finally:
47
+ self.session.set_contextual_commands(None)
48
+
49
+ def _display_agent_info(self) -> None:
50
+ """Display agent information and summary."""
51
+ self.session._render_header(self.agent, focus_agent=True)
52
+
53
+ def _run_agent_loop(self) -> None:
54
+ """Run the main agent interaction loop."""
55
+ while True:
56
+ raw = self._get_user_input()
57
+ if raw is None:
58
+ return
59
+
60
+ raw = raw.strip()
61
+ if not raw:
62
+ continue
63
+
64
+ if raw.startswith("/"):
65
+ if not self._handle_slash_command(raw, self._agent_id):
66
+ return
67
+ continue
68
+
69
+ self._run_agent(self._agent_id, raw)
70
+
71
+ def _get_user_input(self) -> str | None:
72
+ """Get user input with proper error handling."""
73
+ try:
74
+ raw = self.session._prompt(
75
+ f"{self._agent_name} ({self._agent_id})\n› ",
76
+ placeholder=self._prompt_placeholder,
77
+ )
78
+ if self._prompt_placeholder:
79
+ # Show the guidance once, then fall back to a clean prompt.
80
+ self._prompt_placeholder = ""
81
+ return raw
82
+ except EOFError:
83
+ self.console.print("\nExiting agent context.")
84
+ return None
85
+ except KeyboardInterrupt:
86
+ self.console.print("")
87
+ return ""
88
+
89
+ def _handle_slash_command(self, raw: str, agent_id: str) -> bool:
90
+ """Handle slash commands in agent context. Returns False if should exit."""
91
+ # Handle simple commands first
92
+ if raw == "/":
93
+ return self._handle_help_command()
94
+
95
+ if raw in {"/exit", "/back", "/q"}:
96
+ return self._handle_exit_command()
97
+
98
+ if raw in {"/details", "/detail"}:
99
+ return self._handle_details_command(agent_id)
100
+
101
+ if raw in {"/help", "/?"}:
102
+ return self._handle_help_command()
103
+
104
+ # Handle other commands through the main session
105
+ return self._handle_other_command(raw)
106
+
107
+ def _handle_help_command(self) -> bool:
108
+ """Handle help command."""
109
+ self.session._cmd_help([], True)
110
+ return True
111
+
112
+ def _handle_exit_command(self) -> bool:
113
+ """Handle exit command."""
114
+ self.console.print("[dim]Returning to the main prompt.[/dim]")
115
+ return False
116
+
117
+ def _handle_details_command(self, agent_id: str) -> bool:
118
+ """Handle details command."""
119
+ self._show_details(agent_id)
120
+ return True
121
+
122
+ def _handle_other_command(self, raw: str) -> bool:
123
+ """Handle other commands through the main session."""
124
+ self.session.handle_command(raw, invoked_from_agent=True)
125
+ return not self.session._should_exit
126
+
127
+ def _show_details(self, agent_id: str) -> None:
128
+ try:
129
+ self.session.ctx.invoke(agents_get_command, agent_ref=agent_id)
130
+ self.console.print(
131
+ "[dim]Tip: Continue the conversation in this prompt, or use /help for shortcuts."
132
+ )
133
+ except click.ClickException as exc:
134
+ self.console.print(f"[red]{exc}[/red]")
135
+
136
+ def _run_agent(self, agent_id: str, message: str) -> None:
137
+ if not message:
138
+ return
139
+
140
+ try:
141
+ self.session.ctx.invoke(
142
+ agents_run_command, agent_ref=agent_id, input_text=message
143
+ )
144
+ self.session.last_run_input = message
145
+ except click.ClickException as exc:
146
+ self.console.print(f"[red]{exc}[/red]")