glaip-sdk 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. glaip_sdk/cli/account_store.py +522 -0
  2. glaip_sdk/cli/auth.py +224 -8
  3. glaip_sdk/cli/commands/accounts.py +414 -0
  4. glaip_sdk/cli/commands/agents.py +2 -2
  5. glaip_sdk/cli/commands/common_config.py +65 -0
  6. glaip_sdk/cli/commands/configure.py +153 -87
  7. glaip_sdk/cli/commands/mcps.py +191 -44
  8. glaip_sdk/cli/commands/transcripts.py +1 -1
  9. glaip_sdk/cli/config.py +31 -3
  10. glaip_sdk/cli/display.py +1 -1
  11. glaip_sdk/cli/hints.py +57 -0
  12. glaip_sdk/cli/io.py +6 -3
  13. glaip_sdk/cli/main.py +181 -79
  14. glaip_sdk/cli/masking.py +14 -1
  15. glaip_sdk/cli/slash/agent_session.py +2 -1
  16. glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
  17. glaip_sdk/cli/slash/session.py +11 -9
  18. glaip_sdk/cli/slash/tui/remote_runs_app.py +2 -3
  19. glaip_sdk/cli/transcript/capture.py +12 -18
  20. glaip_sdk/cli/transcript/viewer.py +13 -646
  21. glaip_sdk/cli/update_notifier.py +2 -1
  22. glaip_sdk/cli/utils.py +95 -139
  23. glaip_sdk/client/agents.py +2 -4
  24. glaip_sdk/client/main.py +2 -18
  25. glaip_sdk/client/mcps.py +11 -1
  26. glaip_sdk/client/run_rendering.py +90 -111
  27. glaip_sdk/client/shared.py +21 -0
  28. glaip_sdk/models.py +8 -7
  29. glaip_sdk/utils/display.py +23 -15
  30. glaip_sdk/utils/rendering/__init__.py +6 -13
  31. glaip_sdk/utils/rendering/formatting.py +5 -30
  32. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  33. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  34. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  35. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  36. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  37. glaip_sdk/utils/rendering/models.py +1 -0
  38. glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
  39. glaip_sdk/utils/rendering/renderer/base.py +214 -1469
  40. glaip_sdk/utils/rendering/renderer/debug.py +24 -0
  41. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  42. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  43. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  44. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  45. glaip_sdk/utils/rendering/state.py +204 -0
  46. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  47. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  48. glaip_sdk/utils/rendering/steps/format.py +176 -0
  49. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  50. glaip_sdk/utils/rendering/timing.py +36 -0
  51. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  52. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  53. glaip_sdk/utils/validation.py +13 -21
  54. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/METADATA +1 -1
  55. glaip_sdk-0.5.0.dist-info/RECORD +113 -0
  56. glaip_sdk-0.3.0.dist-info/RECORD +0 -94
  57. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/WHEEL +0 -0
  58. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -26,7 +26,8 @@ from glaip_sdk.branding import (
26
26
  )
27
27
  from glaip_sdk.cli.commands.update import update_command
28
28
  from glaip_sdk.cli.constants import UPDATE_CHECK_ENABLED
29
- from glaip_sdk.cli.utils import command_hint, format_command_hint
29
+ from glaip_sdk.cli.hints import format_command_hint
30
+ from glaip_sdk.cli.utils import command_hint
30
31
  from glaip_sdk.rich_components import AIPPanel
31
32
 
32
33
  FetchLatestVersion = Callable[[], str | None]
glaip_sdk/cli/utils.py CHANGED
@@ -7,12 +7,12 @@ Authors:
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import asyncio
10
11
  import importlib
11
12
  import json
12
13
  import logging
13
14
  import os
14
15
  import sys
15
- import asyncio
16
16
  from collections.abc import Callable, Iterable
17
17
  from contextlib import AbstractContextManager, contextmanager, nullcontext
18
18
  from pathlib import Path
@@ -27,19 +27,21 @@ from rich.syntax import Syntax
27
27
  from glaip_sdk import _version as _version_module
28
28
  from glaip_sdk.branding import (
29
29
  ACCENT_STYLE,
30
- HINT_COMMAND_STYLE,
31
- HINT_DESCRIPTION_COLOR,
32
30
  SUCCESS_STYLE,
33
31
  WARNING_STYLE,
34
32
  )
33
+ from glaip_sdk.cli import display as cli_display
35
34
  from glaip_sdk.cli import masking, pager
36
- from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
37
35
  from glaip_sdk.cli.config import load_config
36
+ from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
38
37
  from glaip_sdk.cli.context import (
39
38
  _get_view,
40
- detect_export_format as _detect_export_format,
41
39
  get_ctx_value,
42
40
  )
41
+ from glaip_sdk.cli.context import (
42
+ detect_export_format as _detect_export_format,
43
+ )
44
+ from glaip_sdk.cli.hints import command_hint
43
45
  from glaip_sdk.cli.io import export_resource_to_file_with_validation
44
46
  from glaip_sdk.cli.rich_helpers import markup_text, print_markup
45
47
  from glaip_sdk.icons import ICON_AGENT
@@ -47,10 +49,35 @@ from glaip_sdk.rich_components import AIPPanel, AIPTable
47
49
  from glaip_sdk.utils import format_datetime, is_uuid
48
50
  from glaip_sdk.utils.rendering.renderer import (
49
51
  CapturingConsole,
50
- RendererConfig,
52
+ RendererFactoryOptions,
51
53
  RichStreamRenderer,
54
+ make_default_renderer,
55
+ make_verbose_renderer,
52
56
  )
53
57
 
58
+ questionary = None # type: ignore[assignment]
59
+
60
+
61
+ def _load_questionary_module() -> tuple[Any | None, Any | None]:
62
+ """Return the questionary module and Choice class if available."""
63
+ module = questionary
64
+ if module is not None:
65
+ return module, getattr(module, "Choice", None)
66
+
67
+ try: # pragma: no cover - optional dependency
68
+ module = __import__("questionary")
69
+ except ImportError:
70
+ return None, None
71
+
72
+ return module, getattr(module, "Choice", None)
73
+
74
+
75
+ def _make_questionary_choice(choice_cls: Any | None, **kwargs: Any) -> Any:
76
+ """Create a questionary Choice instance or lightweight fallback."""
77
+ if choice_cls is None:
78
+ return kwargs
79
+ return choice_cls(**kwargs)
80
+
54
81
 
55
82
  @contextmanager
56
83
  def bind_slash_session_context(ctx: Any, session: Any) -> Any:
@@ -122,31 +149,28 @@ def prompt_export_choice_questionary(
122
149
  Tuple of (choice, path) or None if cancelled/unavailable.
123
150
  Choice can be "default", "custom", or "cancel".
124
151
  """
125
- # Import here for optional dependency (questionary may not be installed)
126
- try: # pragma: no cover - optional dependency
127
- import questionary
128
- from questionary import Choice
129
- except Exception: # pragma: no cover - optional dependency
130
- return None
131
-
132
- if questionary is None or Choice is None:
152
+ questionary_module, choice_cls = _load_questionary_module()
153
+ if questionary_module is None or choice_cls is None:
133
154
  return None
134
155
 
135
156
  try:
136
- question = questionary.select(
157
+ question = questionary_module.select(
137
158
  "Export transcript",
138
159
  choices=[
139
- Choice(
160
+ _make_questionary_choice(
161
+ choice_cls,
140
162
  title=f"Save to default ({default_display})",
141
163
  value=("default", default_path),
142
164
  shortcut_key="1",
143
165
  ),
144
- Choice(
166
+ _make_questionary_choice(
167
+ choice_cls,
145
168
  title="Choose a different path",
146
169
  value=("custom", None),
147
170
  shortcut_key="2",
148
171
  ),
149
- Choice(
172
+ _make_questionary_choice(
173
+ choice_cls,
150
174
  title="Cancel",
151
175
  value=("cancel", None),
152
176
  shortcut_key="3",
@@ -194,9 +218,7 @@ def _run_questionary_in_thread(question: Any, *, patch_stdout: bool = False) ->
194
218
  run_callable = getattr(application, "run", None) if application is not None else None
195
219
  if callable(run_callable):
196
220
  try:
197
- if patch_stdout:
198
- from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
199
-
221
+ if patch_stdout and pt_patch_stdout is not None:
200
222
  with pt_patch_stdout():
201
223
  return run_callable(in_thread=True)
202
224
  return run_callable(in_thread=True)
@@ -226,6 +248,7 @@ _LiteralYamlDumper.add_representer(str, _literal_str_representer)
226
248
  try:
227
249
  from prompt_toolkit.buffer import Buffer
228
250
  from prompt_toolkit.completion import Completion
251
+ from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
229
252
  from prompt_toolkit.selection import SelectionType
230
253
  from prompt_toolkit.shortcuts import PromptSession, prompt
231
254
 
@@ -235,13 +258,9 @@ except Exception: # pragma: no cover - optional dependency
235
258
  SelectionType = None # type: ignore[assignment]
236
259
  PromptSession = None # type: ignore[assignment]
237
260
  prompt = None # type: ignore[assignment]
261
+ pt_patch_stdout = None # type: ignore[assignment]
238
262
  _HAS_PTK = False
239
263
 
240
- try:
241
- import questionary
242
- except Exception: # pragma: no cover - optional dependency
243
- questionary = None
244
-
245
264
  if TYPE_CHECKING: # pragma: no cover - import-only during type checking
246
265
  from glaip_sdk import Client
247
266
 
@@ -401,9 +420,7 @@ def handle_resource_export(
401
420
  ):
402
421
  export_resource_to_file_with_validation(full_resource, export_path, detected_format)
403
422
  except Exception:
404
- from glaip_sdk.cli.display import handle_rich_output # noqa: E402 - avoid circular import
405
-
406
- handle_rich_output(
423
+ cli_display.handle_rich_output(
407
424
  ctx,
408
425
  markup_text(f"[{WARNING_STYLE}]⚠️ Failed to fetch full details, using available data[/]"),
409
426
  )
@@ -416,73 +433,6 @@ def handle_resource_export(
416
433
  )
417
434
 
418
435
 
419
- def in_slash_mode(ctx: click.Context | None = None) -> bool:
420
- """Return True when running inside the slash command palette."""
421
- if ctx is None:
422
- try:
423
- ctx = click.get_current_context(silent=True)
424
- except RuntimeError:
425
- ctx = None
426
-
427
- if ctx is None:
428
- return False
429
-
430
- obj = getattr(ctx, "obj", None)
431
- if isinstance(obj, dict):
432
- return bool(obj.get("_slash_session"))
433
-
434
- return bool(getattr(obj, "_slash_session", False))
435
-
436
-
437
- def command_hint(
438
- cli_command: str | None,
439
- slash_command: str | None = None,
440
- *,
441
- ctx: click.Context | None = None,
442
- ) -> str | None:
443
- """Return the appropriate command string for the current mode.
444
-
445
- Args:
446
- cli_command: Command string without the ``aip`` prefix (e.g., ``"status"``).
447
- slash_command: Slash command counterpart (e.g., ``"status"`` or ``"/status"``).
448
- ctx: Optional Click context override.
449
-
450
- Returns:
451
- The formatted command string for the active mode, or ``None`` when no
452
- equivalent command exists in that mode.
453
- """
454
- if in_slash_mode(ctx):
455
- if not slash_command:
456
- return None
457
- return slash_command if slash_command.startswith("/") else f"/{slash_command}"
458
-
459
- if not cli_command:
460
- return None
461
- return f"aip {cli_command}"
462
-
463
-
464
- def format_command_hint(
465
- command: str | None,
466
- description: str | None = None,
467
- ) -> str | None:
468
- """Return a Rich markup string that highlights a command hint.
469
-
470
- Args:
471
- command: Command text to highlight (already formatted for the active mode).
472
- description: Optional short description to display alongside the command.
473
-
474
- Returns:
475
- Markup string suitable for Rich rendering, or ``None`` when ``command`` is falsy.
476
- """
477
- if not command:
478
- return None
479
-
480
- highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
481
- if description:
482
- highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
483
- return highlighted
484
-
485
-
486
436
  def sdk_version() -> str:
487
437
  """Return the current SDK version, warning if metadata is unavailable."""
488
438
  version = getattr(_version_module, "__version__", None)
@@ -598,45 +548,48 @@ _spinner_stop = stop_spinner
598
548
 
599
549
 
600
550
  def get_client(ctx: Any) -> Client: # pragma: no cover
601
- """Get configured client from context, env, and config file (ctx > env > file)."""
551
+ """Get configured client from context and account store (ctx > account)."""
552
+ # Import here to avoid circular import
553
+ from glaip_sdk.cli.auth import resolve_credentials # noqa: PLC0415
554
+
602
555
  module = importlib.import_module("glaip_sdk")
603
556
  client_class = cast("type[Client]", module.Client)
604
- file_config = load_config() or {}
605
557
  context_config_obj = getattr(ctx, "obj", None)
606
558
  context_config = context_config_obj or {}
607
559
 
608
- raw_timeout = os.getenv("AIP_TIMEOUT", "0") or "0"
609
- try:
610
- timeout_value = float(raw_timeout)
611
- except ValueError:
612
- timeout_value = None
613
-
614
- env_config = {
615
- "api_url": os.getenv("AIP_API_URL"),
616
- "api_key": os.getenv("AIP_API_KEY"),
617
- "timeout": timeout_value if timeout_value else None,
618
- }
619
- env_config = {k: v for k, v in env_config.items() if v not in (None, "", 0)}
620
-
621
- # Merge config sources: context > env > file
622
- config = {
623
- **file_config,
624
- **env_config,
625
- **{k: v for k, v in context_config.items() if v is not None},
626
- }
560
+ account_name = context_config.get("account_name")
561
+ api_url, api_key, _ = resolve_credentials(
562
+ account_name=account_name,
563
+ api_url=context_config.get("api_url"),
564
+ api_key=context_config.get("api_key"),
565
+ )
627
566
 
628
- if not config.get("api_url") or not config.get("api_key"):
629
- configure_hint = command_hint("configure", slash_command="login", ctx=ctx)
630
- actions = []
567
+ if not api_url or not api_key:
568
+ configure_hint = command_hint("accounts add", slash_command="login", ctx=ctx)
569
+ actions: list[str] = []
631
570
  if configure_hint:
632
- actions.append(f"Run `{configure_hint}`")
633
- actions.append("set AIP_* env vars")
571
+ actions.append(f"Run `{configure_hint}` to add an account profile")
572
+ else:
573
+ actions.append("add an account with 'aip accounts add'")
634
574
  raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
635
575
 
576
+ # Get timeout from context or config
577
+ timeout = context_config.get("timeout")
578
+ if timeout is None:
579
+ raw_timeout = os.getenv("AIP_TIMEOUT", "0") or "0"
580
+ try:
581
+ timeout = float(raw_timeout) if raw_timeout != "0" else None
582
+ except ValueError:
583
+ timeout = None
584
+ if timeout is None:
585
+ # Fallback to legacy config
586
+ file_config = load_config() or {}
587
+ timeout = file_config.get("timeout")
588
+
636
589
  return client_class(
637
- api_url=config.get("api_url"),
638
- api_key=config.get("api_key"),
639
- timeout=float(config.get("timeout") or 30.0),
590
+ api_url=api_url,
591
+ api_key=api_key,
592
+ timeout=float(timeout or 30.0),
640
593
  )
641
594
 
642
595
 
@@ -1358,19 +1311,20 @@ def build_renderer(
1358
1311
 
1359
1312
  # Configure renderer based on verbose mode and explicit overrides
1360
1313
  live_enabled = bool(live) if live is not None else not verbose
1361
- renderer_cfg = RendererConfig(
1362
- live=live_enabled,
1363
- append_finished_snapshots=bool(snapshots)
1364
- if snapshots is not None
1365
- else RendererConfig.append_finished_snapshots,
1314
+ cfg_overrides = {
1315
+ "live": live_enabled,
1316
+ "append_finished_snapshots": bool(snapshots) if snapshots is not None else False,
1317
+ }
1318
+ renderer_console = (
1319
+ working_console.original_console if isinstance(working_console, CapturingConsole) else working_console
1366
1320
  )
1367
-
1368
- # Create the renderer instance
1369
- renderer = RichStreamRenderer(
1370
- working_console.original_console if isinstance(working_console, CapturingConsole) else working_console,
1371
- cfg=renderer_cfg,
1372
- verbose=verbose,
1321
+ factory = make_verbose_renderer if verbose else make_default_renderer
1322
+ factory_options = RendererFactoryOptions(
1323
+ console=renderer_console,
1324
+ cfg_overrides=cfg_overrides,
1325
+ verbose=verbose if factory is make_default_renderer else None,
1373
1326
  )
1327
+ renderer = factory_options.build(factory)
1374
1328
 
1375
1329
  # Link the renderer back to the slash session when running from the palette.
1376
1330
  _register_renderer_with_session(_ctx, renderer)
@@ -1540,17 +1494,19 @@ def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
1540
1494
 
1541
1495
  def _handle_questionary_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
1542
1496
  """Handle ambiguity using questionary interactive interface."""
1543
- if not (questionary and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
1497
+ questionary_module, choice_cls = _load_questionary_module()
1498
+ if not (questionary_module and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
1544
1499
  raise click.ClickException("Interactive selection not available")
1545
1500
 
1546
1501
  # Escape special characters for questionary
1547
1502
  safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
1548
1503
  safe_ref = ref.replace("{", "{{").replace("}", "}}")
1549
1504
 
1550
- picked_idx = questionary.select(
1505
+ picked_idx = questionary_module.select(
1551
1506
  f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
1552
1507
  choices=[
1553
- questionary.Choice(
1508
+ _make_questionary_choice(
1509
+ choice_cls,
1554
1510
  title=(
1555
1511
  f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — "
1556
1512
  f"{getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}"
@@ -29,6 +29,7 @@ from glaip_sdk.client.run_rendering import (
29
29
  AgentRunRenderingManager,
30
30
  compute_timeout_seconds,
31
31
  )
32
+ from glaip_sdk.client.shared import build_shared_config
32
33
  from glaip_sdk.client.tools import ToolClient
33
34
  from glaip_sdk.config.constants import (
34
35
  AGENT_CONFIG_FIELDS,
@@ -1235,9 +1236,6 @@ class AgentClient(BaseClient):
1235
1236
  def runs(self) -> "AgentRunsClient":
1236
1237
  """Get the agent runs client."""
1237
1238
  if self._runs_client is None:
1238
- # Import here to avoid circular dependency with client.main
1239
- from glaip_sdk.client.main import _build_shared_config
1240
-
1241
- shared_config = _build_shared_config(self)
1239
+ shared_config = build_shared_config(self)
1242
1240
  self._runs_client = AgentRunsClient(**shared_config)
1243
1241
  return self._runs_client
glaip_sdk/client/main.py CHANGED
@@ -10,27 +10,11 @@ from typing import Any
10
10
  from glaip_sdk.client.agents import AgentClient
11
11
  from glaip_sdk.client.base import BaseClient
12
12
  from glaip_sdk.client.mcps import MCPClient
13
+ from glaip_sdk.client.shared import build_shared_config
13
14
  from glaip_sdk.client.tools import ToolClient
14
15
  from glaip_sdk.models import MCP, Agent, Tool
15
16
 
16
17
 
17
- def _build_shared_config(client: BaseClient) -> dict[str, Any]:
18
- """Build shared configuration dictionary for sub-clients.
19
-
20
- Args:
21
- client: Base client instance.
22
-
23
- Returns:
24
- Dictionary with shared configuration.
25
- """
26
- return {
27
- "parent_client": client,
28
- "api_url": client.api_url,
29
- "api_key": client.api_key,
30
- "timeout": client._timeout,
31
- }
32
-
33
-
34
18
  class Client(BaseClient):
35
19
  """Main client that composes all specialized clients and shares one HTTP session."""
36
20
 
@@ -42,7 +26,7 @@ class Client(BaseClient):
42
26
  """
43
27
  super().__init__(**kwargs)
44
28
  # Share the single httpx.Client + config with sub-clients
45
- shared_config = _build_shared_config(self)
29
+ shared_config = build_shared_config(self)
46
30
  self.agents = AgentClient(**shared_config)
47
31
  self.tools = ToolClient(**shared_config)
48
32
  self.mcps = MCPClient(**shared_config)
glaip_sdk/client/mcps.py CHANGED
@@ -204,7 +204,17 @@ class MCPClient(BaseClient):
204
204
  def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
205
205
  """Get tools available from an MCP."""
206
206
  data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
207
- return data or []
207
+ if data is None:
208
+ return []
209
+ if isinstance(data, list):
210
+ return data
211
+ if isinstance(data, dict):
212
+ if "tools" in data:
213
+ return data.get("tools", []) or []
214
+ logger.warning("Unexpected MCP tools response keys %s; returning empty list", list(data.keys()))
215
+ return []
216
+ logger.warning("Unexpected MCP tools response type %s; returning empty list", type(data).__name__)
217
+ return []
208
218
 
209
219
  def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
210
220
  """Test MCP connection using configuration.