glaip-sdk 0.0.18__py3-none-any.whl → 0.0.20__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/_version.py +2 -2
- glaip_sdk/branding.py +27 -2
- glaip_sdk/cli/auth.py +93 -28
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/agents.py +108 -21
- glaip_sdk/cli/commands/configure.py +141 -90
- glaip_sdk/cli/commands/mcps.py +371 -48
- glaip_sdk/cli/commands/models.py +4 -3
- glaip_sdk/cli/commands/tools.py +27 -14
- glaip_sdk/cli/commands/update.py +66 -0
- glaip_sdk/cli/config.py +13 -2
- glaip_sdk/cli/display.py +35 -26
- glaip_sdk/cli/io.py +14 -5
- glaip_sdk/cli/main.py +185 -73
- glaip_sdk/cli/pager.py +2 -1
- glaip_sdk/cli/parsers/json_input.py +62 -14
- glaip_sdk/cli/resolution.py +4 -1
- glaip_sdk/cli/slash/__init__.py +3 -4
- glaip_sdk/cli/slash/agent_session.py +88 -36
- glaip_sdk/cli/slash/prompt.py +20 -48
- glaip_sdk/cli/slash/session.py +440 -189
- glaip_sdk/cli/transcript/__init__.py +71 -0
- glaip_sdk/cli/transcript/cache.py +338 -0
- glaip_sdk/cli/transcript/capture.py +278 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/launcher.py +79 -0
- glaip_sdk/cli/transcript/viewer.py +624 -0
- glaip_sdk/cli/update_notifier.py +29 -5
- glaip_sdk/cli/utils.py +256 -74
- glaip_sdk/client/agents.py +3 -1
- glaip_sdk/client/run_rendering.py +2 -2
- glaip_sdk/icons.py +19 -0
- glaip_sdk/models.py +6 -0
- glaip_sdk/rich_components.py +29 -1
- glaip_sdk/utils/__init__.py +1 -1
- glaip_sdk/utils/client_utils.py +6 -4
- glaip_sdk/utils/display.py +61 -32
- glaip_sdk/utils/rendering/formatting.py +6 -5
- glaip_sdk/utils/rendering/renderer/base.py +213 -66
- glaip_sdk/utils/rendering/renderer/debug.py +73 -16
- glaip_sdk/utils/rendering/renderer/panels.py +27 -15
- glaip_sdk/utils/rendering/renderer/progress.py +61 -38
- glaip_sdk/utils/serialization.py +5 -2
- glaip_sdk/utils/validation.py +1 -2
- {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/METADATA +1 -1
- glaip_sdk-0.0.20.dist-info/RECORD +80 -0
- glaip_sdk/utils/rich_utils.py +0 -29
- glaip_sdk-0.0.18.dist-info/RECORD +0 -73
- {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/utils.py
CHANGED
|
@@ -7,29 +7,44 @@ Authors:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import importlib
|
|
10
11
|
import json
|
|
11
12
|
import logging
|
|
12
13
|
import os
|
|
13
14
|
import sys
|
|
14
|
-
from collections.abc import Callable
|
|
15
|
+
from collections.abc import Callable, Iterable
|
|
15
16
|
from contextlib import AbstractContextManager, nullcontext
|
|
16
|
-
from typing import TYPE_CHECKING, Any
|
|
17
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
17
18
|
|
|
18
19
|
import click
|
|
19
20
|
from rich.console import Console, Group
|
|
20
21
|
from rich.markdown import Markdown
|
|
21
22
|
from rich.pretty import Pretty
|
|
22
23
|
|
|
24
|
+
from glaip_sdk.branding import (
|
|
25
|
+
ACCENT_STYLE,
|
|
26
|
+
HINT_COMMAND_STYLE,
|
|
27
|
+
HINT_DESCRIPTION_COLOR,
|
|
28
|
+
SUCCESS_STYLE,
|
|
29
|
+
WARNING_STYLE,
|
|
30
|
+
)
|
|
23
31
|
from glaip_sdk.cli.rich_helpers import markup_text
|
|
32
|
+
from glaip_sdk.icons import ICON_AGENT
|
|
24
33
|
from glaip_sdk.rich_components import AIPPanel
|
|
25
34
|
|
|
26
35
|
# Optional interactive deps (fuzzy palette)
|
|
27
36
|
try:
|
|
37
|
+
from prompt_toolkit.buffer import Buffer
|
|
28
38
|
from prompt_toolkit.completion import Completion
|
|
29
|
-
from prompt_toolkit.
|
|
39
|
+
from prompt_toolkit.selection import SelectionType
|
|
40
|
+
from prompt_toolkit.shortcuts import PromptSession, prompt
|
|
30
41
|
|
|
31
42
|
_HAS_PTK = True
|
|
32
43
|
except Exception: # pragma: no cover - optional dependency
|
|
44
|
+
Buffer = None # type: ignore[assignment]
|
|
45
|
+
SelectionType = None # type: ignore[assignment]
|
|
46
|
+
PromptSession = None # type: ignore[assignment]
|
|
47
|
+
prompt = None # type: ignore[assignment]
|
|
33
48
|
_HAS_PTK = False
|
|
34
49
|
|
|
35
50
|
try:
|
|
@@ -114,25 +129,54 @@ def command_hint(
|
|
|
114
129
|
return f"aip {cli_command}"
|
|
115
130
|
|
|
116
131
|
|
|
132
|
+
def format_command_hint(
|
|
133
|
+
command: str | None,
|
|
134
|
+
description: str | None = None,
|
|
135
|
+
) -> str | None:
|
|
136
|
+
"""Return a Rich markup string that highlights a command hint.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
command: Command text to highlight (already formatted for the active mode).
|
|
140
|
+
description: Optional short description to display alongside the command.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Markup string suitable for Rich rendering, or ``None`` when ``command`` is falsy.
|
|
144
|
+
"""
|
|
145
|
+
if not command:
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
|
|
149
|
+
if description:
|
|
150
|
+
highlighted += (
|
|
151
|
+
f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
|
|
152
|
+
)
|
|
153
|
+
return highlighted
|
|
154
|
+
|
|
155
|
+
|
|
117
156
|
def spinner_context(
|
|
118
157
|
ctx: Any | None,
|
|
119
158
|
message: str,
|
|
120
159
|
*,
|
|
121
160
|
console_override: Console | None = None,
|
|
122
161
|
spinner: str = "dots",
|
|
123
|
-
spinner_style: str =
|
|
162
|
+
spinner_style: str = ACCENT_STYLE,
|
|
124
163
|
) -> AbstractContextManager[Any]:
|
|
125
164
|
"""Return a context manager that renders a spinner when appropriate."""
|
|
126
165
|
active_console = console_override or console
|
|
127
166
|
if not _can_use_spinner(ctx, active_console):
|
|
128
167
|
return nullcontext()
|
|
129
168
|
|
|
130
|
-
|
|
169
|
+
status = active_console.status(
|
|
131
170
|
message,
|
|
132
171
|
spinner=spinner,
|
|
133
172
|
spinner_style=spinner_style,
|
|
134
173
|
)
|
|
135
174
|
|
|
175
|
+
if not hasattr(status, "__enter__") or not hasattr(status, "__exit__"):
|
|
176
|
+
return nullcontext()
|
|
177
|
+
|
|
178
|
+
return status
|
|
179
|
+
|
|
136
180
|
|
|
137
181
|
def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
|
|
138
182
|
"""Check if spinner output is allowed in the current environment."""
|
|
@@ -189,8 +233,8 @@ _spinner_stop = stop_spinner
|
|
|
189
233
|
|
|
190
234
|
def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
191
235
|
"""Get configured client from context, env, and config file (ctx > env > file)."""
|
|
192
|
-
|
|
193
|
-
|
|
236
|
+
module = importlib.import_module("glaip_sdk")
|
|
237
|
+
client_class = cast("type[Client]", getattr(module, "Client"))
|
|
194
238
|
file_config = load_config() or {}
|
|
195
239
|
context_config_obj = getattr(ctx, "obj", None)
|
|
196
240
|
context_config = context_config_obj or {}
|
|
@@ -223,7 +267,7 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
|
223
267
|
actions.append("set AIP_* env vars")
|
|
224
268
|
raise click.ClickException(f"Missing api_url/api_key. {' or '.join(actions)}.")
|
|
225
269
|
|
|
226
|
-
return
|
|
270
|
+
return client_class(
|
|
227
271
|
api_url=config.get("api_url"),
|
|
228
272
|
api_key=config.get("api_key"),
|
|
229
273
|
timeout=float(config.get("timeout") or 30.0),
|
|
@@ -335,6 +379,86 @@ def _build_unique_labels(
|
|
|
335
379
|
return labels, by_label
|
|
336
380
|
|
|
337
381
|
|
|
382
|
+
def _basic_prompt(
|
|
383
|
+
message: str,
|
|
384
|
+
completer: Any,
|
|
385
|
+
) -> str | None:
|
|
386
|
+
"""Fallback prompt handler when PromptSession is unavailable or fails."""
|
|
387
|
+
if prompt is None: # pragma: no cover - optional dependency path
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
return prompt(
|
|
392
|
+
message=message,
|
|
393
|
+
completer=completer,
|
|
394
|
+
complete_in_thread=True,
|
|
395
|
+
complete_while_typing=True,
|
|
396
|
+
)
|
|
397
|
+
except (KeyboardInterrupt, EOFError):
|
|
398
|
+
return None
|
|
399
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
400
|
+
logger.debug("Fallback prompt failed: %s", exc)
|
|
401
|
+
return None
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _prompt_with_auto_select(
|
|
405
|
+
message: str,
|
|
406
|
+
completer: Any,
|
|
407
|
+
choices: Iterable[str],
|
|
408
|
+
) -> str | None:
|
|
409
|
+
"""Prompt with fuzzy completer that auto-selects suggested matches."""
|
|
410
|
+
if not _HAS_PTK or PromptSession is None or Buffer is None or SelectionType is None:
|
|
411
|
+
return _basic_prompt(message, completer)
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
session = PromptSession(
|
|
415
|
+
message,
|
|
416
|
+
completer=completer,
|
|
417
|
+
complete_in_thread=True,
|
|
418
|
+
complete_while_typing=True,
|
|
419
|
+
reserve_space_for_menu=8,
|
|
420
|
+
)
|
|
421
|
+
except Exception as exc: # pragma: no cover - depends on prompt_toolkit
|
|
422
|
+
logger.debug(
|
|
423
|
+
"PromptSession init failed (%s); falling back to basic prompt.", exc
|
|
424
|
+
)
|
|
425
|
+
return _basic_prompt(message, completer)
|
|
426
|
+
|
|
427
|
+
buffer = session.default_buffer
|
|
428
|
+
valid_choices = set(choices)
|
|
429
|
+
|
|
430
|
+
def _auto_select(_: Buffer) -> None:
|
|
431
|
+
text = buffer.text
|
|
432
|
+
if not text or text not in valid_choices:
|
|
433
|
+
return
|
|
434
|
+
buffer.cursor_position = 0
|
|
435
|
+
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
|
436
|
+
buffer.cursor_position = len(text)
|
|
437
|
+
|
|
438
|
+
handler_attached = False
|
|
439
|
+
try:
|
|
440
|
+
buffer.on_text_changed += _auto_select
|
|
441
|
+
handler_attached = True
|
|
442
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
443
|
+
logger.debug("Failed to attach auto-select handler: %s", exc)
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
return session.prompt()
|
|
447
|
+
except (KeyboardInterrupt, EOFError):
|
|
448
|
+
return None
|
|
449
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
450
|
+
logger.debug(
|
|
451
|
+
"PromptSession prompt failed (%s); falling back to basic prompt.", exc
|
|
452
|
+
)
|
|
453
|
+
return _basic_prompt(message, completer)
|
|
454
|
+
finally:
|
|
455
|
+
if handler_attached:
|
|
456
|
+
try:
|
|
457
|
+
buffer.on_text_changed -= _auto_select
|
|
458
|
+
except Exception: # pragma: no cover - defensive
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
|
|
338
462
|
class _FuzzyCompleter:
|
|
339
463
|
"""Fuzzy completer for prompt_toolkit."""
|
|
340
464
|
|
|
@@ -404,17 +528,12 @@ def _fuzzy_pick(
|
|
|
404
528
|
|
|
405
529
|
# Create fuzzy completer
|
|
406
530
|
completer = _FuzzyCompleter(labels)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
complete_while_typing=True,
|
|
414
|
-
)
|
|
415
|
-
except (KeyboardInterrupt, EOFError): # pragma: no cover - user cancelled input
|
|
416
|
-
return None
|
|
417
|
-
except Exception: # pragma: no cover - prompt_toolkit not available in headless env
|
|
531
|
+
answer = _prompt_with_auto_select(
|
|
532
|
+
f"Find {title.rstrip('s')}: ",
|
|
533
|
+
completer,
|
|
534
|
+
labels,
|
|
535
|
+
)
|
|
536
|
+
if answer is None:
|
|
418
537
|
return None
|
|
419
538
|
|
|
420
539
|
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
@@ -557,7 +676,7 @@ def output_result(
|
|
|
557
676
|
if panel_title:
|
|
558
677
|
console.print(AIPPanel(renderable, title=panel_title))
|
|
559
678
|
else:
|
|
560
|
-
console.print(markup_text(f"[
|
|
679
|
+
console.print(markup_text(f"[{ACCENT_STYLE}]{title}:[/]"))
|
|
561
680
|
console.print(renderable)
|
|
562
681
|
|
|
563
682
|
|
|
@@ -665,7 +784,7 @@ def _handle_markdown_output(
|
|
|
665
784
|
|
|
666
785
|
def _handle_empty_items(title: str) -> None:
|
|
667
786
|
"""Handle case when no items are found."""
|
|
668
|
-
console.print(markup_text(f"[
|
|
787
|
+
console.print(markup_text(f"[{WARNING_STYLE}]No {title.lower()} found.[/]"))
|
|
669
788
|
|
|
670
789
|
|
|
671
790
|
def _should_use_fuzzy_picker() -> bool:
|
|
@@ -816,6 +935,18 @@ def coerce_to_row(item: Any, keys: list[str]) -> dict[str, Any]:
|
|
|
816
935
|
return result
|
|
817
936
|
|
|
818
937
|
|
|
938
|
+
def _register_renderer_with_session(ctx: Any, renderer: RichStreamRenderer) -> None:
|
|
939
|
+
"""Attach renderer to an active slash session when present."""
|
|
940
|
+
try:
|
|
941
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
942
|
+
session = ctx_obj.get("_slash_session") if isinstance(ctx_obj, dict) else None
|
|
943
|
+
if session and hasattr(session, "register_active_renderer"):
|
|
944
|
+
session.register_active_renderer(renderer)
|
|
945
|
+
except Exception:
|
|
946
|
+
# Never let session bookkeeping break renderer creation
|
|
947
|
+
pass
|
|
948
|
+
|
|
949
|
+
|
|
819
950
|
def build_renderer(
|
|
820
951
|
_ctx: Any,
|
|
821
952
|
*,
|
|
@@ -841,21 +972,17 @@ def build_renderer(
|
|
|
841
972
|
Tuple of (renderer, capturing_console) for streaming output.
|
|
842
973
|
"""
|
|
843
974
|
# Use capturing console if saving output
|
|
844
|
-
working_console = console
|
|
845
|
-
if save_path:
|
|
846
|
-
working_console = CapturingConsole(console, capture=True)
|
|
975
|
+
working_console = CapturingConsole(console, capture=True) if save_path else console
|
|
847
976
|
|
|
848
977
|
# Configure renderer based on verbose mode and explicit overrides
|
|
849
|
-
if live is None
|
|
850
|
-
|
|
851
|
-
else:
|
|
852
|
-
live_enabled = bool(live)
|
|
978
|
+
live_enabled = bool(live) if live is not None else not verbose
|
|
979
|
+
style = "debug" if verbose else "pretty"
|
|
853
980
|
|
|
854
981
|
renderer_cfg = RendererConfig(
|
|
855
982
|
theme=theme,
|
|
856
|
-
style=
|
|
983
|
+
style=style,
|
|
857
984
|
live=live_enabled,
|
|
858
|
-
show_delegate_tool_panels=
|
|
985
|
+
show_delegate_tool_panels=False,
|
|
859
986
|
append_finished_snapshots=bool(snapshots)
|
|
860
987
|
if snapshots is not None
|
|
861
988
|
else RendererConfig.append_finished_snapshots,
|
|
@@ -871,15 +998,7 @@ def build_renderer(
|
|
|
871
998
|
)
|
|
872
999
|
|
|
873
1000
|
# Link the renderer back to the slash session when running from the palette.
|
|
874
|
-
|
|
875
|
-
ctx_obj = getattr(_ctx, "obj", None)
|
|
876
|
-
if isinstance(ctx_obj, dict):
|
|
877
|
-
session = ctx_obj.get("_slash_session")
|
|
878
|
-
if session and hasattr(session, "register_active_renderer"):
|
|
879
|
-
session.register_active_renderer(renderer)
|
|
880
|
-
except Exception:
|
|
881
|
-
# Never let session bookkeeping break renderer creation
|
|
882
|
-
pass
|
|
1001
|
+
_register_renderer_with_session(_ctx, renderer)
|
|
883
1002
|
|
|
884
1003
|
return renderer, working_console
|
|
885
1004
|
|
|
@@ -935,15 +1054,12 @@ def _fuzzy_pick_for_resources(
|
|
|
935
1054
|
|
|
936
1055
|
# Create fuzzy completer
|
|
937
1056
|
completer = _FuzzyCompleter(labels)
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
complete_while_typing=True,
|
|
945
|
-
)
|
|
946
|
-
except (KeyboardInterrupt, EOFError):
|
|
1057
|
+
answer = _prompt_with_auto_select(
|
|
1058
|
+
f"Find {ICON_AGENT} {resource_type.title()}: ",
|
|
1059
|
+
completer,
|
|
1060
|
+
labels,
|
|
1061
|
+
)
|
|
1062
|
+
if answer is None:
|
|
947
1063
|
return None
|
|
948
1064
|
|
|
949
1065
|
return _perform_fuzzy_search(answer, labels, by_label) if answer else None
|
|
@@ -967,19 +1083,19 @@ def _resolve_by_name_multiple_with_select(matches: list[Any], select: int) -> An
|
|
|
967
1083
|
def _resolve_by_name_multiple_fuzzy(
|
|
968
1084
|
ctx: Any, ref: str, matches: list[Any], label: str
|
|
969
1085
|
) -> Any:
|
|
970
|
-
"""Resolve multiple matches
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
# Fallback to original ambiguity handler if fuzzy picker fails
|
|
975
|
-
return handle_ambiguous_resource(ctx, label.lower(), ref, matches)
|
|
1086
|
+
"""Resolve multiple matches preferring the fuzzy picker interface."""
|
|
1087
|
+
return handle_ambiguous_resource(
|
|
1088
|
+
ctx, label.lower(), ref, matches, interface_preference="fuzzy"
|
|
1089
|
+
)
|
|
976
1090
|
|
|
977
1091
|
|
|
978
1092
|
def _resolve_by_name_multiple_questionary(
|
|
979
1093
|
ctx: Any, ref: str, matches: list[Any], label: str
|
|
980
1094
|
) -> Any:
|
|
981
|
-
"""Resolve multiple matches
|
|
982
|
-
return handle_ambiguous_resource(
|
|
1095
|
+
"""Resolve multiple matches preferring the questionary interface."""
|
|
1096
|
+
return handle_ambiguous_resource(
|
|
1097
|
+
ctx, label.lower(), ref, matches, interface_preference="questionary"
|
|
1098
|
+
)
|
|
983
1099
|
|
|
984
1100
|
|
|
985
1101
|
def resolve_resource(
|
|
@@ -1015,7 +1131,7 @@ def resolve_resource(
|
|
|
1015
1131
|
_spinner_update(spinner, f"[bold blue]Fetching {label} by ID…[/bold blue]")
|
|
1016
1132
|
result = _resolve_by_id(ref, get_by_id)
|
|
1017
1133
|
if result is not None:
|
|
1018
|
-
_spinner_update(spinner, f"[
|
|
1134
|
+
_spinner_update(spinner, f"[{SUCCESS_STYLE}]{label} found[/]")
|
|
1019
1135
|
return result
|
|
1020
1136
|
|
|
1021
1137
|
# If get_by_id returned None, the resource doesn't exist
|
|
@@ -1033,7 +1149,7 @@ def resolve_resource(
|
|
|
1033
1149
|
raise click.ClickException(f"{label} '{ref}' not found")
|
|
1034
1150
|
|
|
1035
1151
|
if len(matches) == 1:
|
|
1036
|
-
_spinner_update(spinner, f"[
|
|
1152
|
+
_spinner_update(spinner, f"[{SUCCESS_STYLE}]{label} found[/]")
|
|
1037
1153
|
return matches[0]
|
|
1038
1154
|
|
|
1039
1155
|
# Multiple matches found, handle ambiguity
|
|
@@ -1043,7 +1159,10 @@ def resolve_resource(
|
|
|
1043
1159
|
|
|
1044
1160
|
# Choose interface based on preference
|
|
1045
1161
|
_spinner_stop(spinner)
|
|
1046
|
-
|
|
1162
|
+
preference = (interface_preference or "fuzzy").lower()
|
|
1163
|
+
if preference not in {"fuzzy", "questionary"}:
|
|
1164
|
+
preference = "fuzzy"
|
|
1165
|
+
if preference == "fuzzy":
|
|
1047
1166
|
return _resolve_by_name_multiple_fuzzy(ctx, ref, matches, label)
|
|
1048
1167
|
else:
|
|
1049
1168
|
return _resolve_by_name_multiple_questionary(ctx, ref, matches, label)
|
|
@@ -1093,7 +1212,7 @@ def _handle_fallback_numeric_ambiguity(
|
|
|
1093
1212
|
|
|
1094
1213
|
console.print(
|
|
1095
1214
|
markup_text(
|
|
1096
|
-
f"[
|
|
1215
|
+
f"[{WARNING_STYLE}]Multiple {safe_resource_type}s found matching '{safe_ref}':[/]"
|
|
1097
1216
|
)
|
|
1098
1217
|
)
|
|
1099
1218
|
table = AIPTable(
|
|
@@ -1101,7 +1220,7 @@ def _handle_fallback_numeric_ambiguity(
|
|
|
1101
1220
|
)
|
|
1102
1221
|
table.add_column("#", style="dim", width=3)
|
|
1103
1222
|
table.add_column("ID", style="dim", width=36)
|
|
1104
|
-
table.add_column("Name", style=
|
|
1223
|
+
table.add_column("Name", style=ACCENT_STYLE)
|
|
1105
1224
|
for i, m in enumerate(matches, 1):
|
|
1106
1225
|
table.add_row(str(i), str(getattr(m, "id", "")), str(getattr(m, "name", "")))
|
|
1107
1226
|
console.print(table)
|
|
@@ -1127,22 +1246,85 @@ def _should_fallback_to_numeric_prompt(exception: Exception) -> bool:
|
|
|
1127
1246
|
return True
|
|
1128
1247
|
|
|
1129
1248
|
|
|
1249
|
+
def _normalize_interface_preference(preference: str) -> str:
|
|
1250
|
+
"""Normalize and validate interface preference."""
|
|
1251
|
+
normalized = (preference or "questionary").lower()
|
|
1252
|
+
return normalized if normalized in {"fuzzy", "questionary"} else "questionary"
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def _get_interface_order(preference: str) -> tuple[str, str]:
|
|
1256
|
+
"""Get the ordered interface preferences."""
|
|
1257
|
+
interface_orders = {
|
|
1258
|
+
"fuzzy": ("fuzzy", "questionary"),
|
|
1259
|
+
"questionary": ("questionary", "fuzzy"),
|
|
1260
|
+
}
|
|
1261
|
+
return interface_orders.get(preference, ("questionary", "fuzzy"))
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def _try_fuzzy_selection(
|
|
1265
|
+
resource_type: str,
|
|
1266
|
+
ref: str,
|
|
1267
|
+
matches: list[Any],
|
|
1268
|
+
) -> Any | None:
|
|
1269
|
+
"""Try fuzzy interface selection."""
|
|
1270
|
+
picked = _fuzzy_pick_for_resources(matches, resource_type, ref)
|
|
1271
|
+
return picked if picked else None
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
def _try_questionary_selection(
|
|
1275
|
+
resource_type: str,
|
|
1276
|
+
ref: str,
|
|
1277
|
+
matches: list[Any],
|
|
1278
|
+
) -> Any | None:
|
|
1279
|
+
"""Try questionary interface selection."""
|
|
1280
|
+
try:
|
|
1281
|
+
return _handle_questionary_ambiguity(resource_type, ref, matches)
|
|
1282
|
+
except Exception as exc:
|
|
1283
|
+
if not _should_fallback_to_numeric_prompt(exc):
|
|
1284
|
+
raise
|
|
1285
|
+
return None
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def _try_interface_selection(
|
|
1289
|
+
interface_order: tuple[str, str],
|
|
1290
|
+
resource_type: str,
|
|
1291
|
+
ref: str,
|
|
1292
|
+
matches: list[Any],
|
|
1293
|
+
) -> Any | None:
|
|
1294
|
+
"""Try interface selection in order, return result or None if all failed."""
|
|
1295
|
+
interface_handlers = {
|
|
1296
|
+
"fuzzy": _try_fuzzy_selection,
|
|
1297
|
+
"questionary": _try_questionary_selection,
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
for interface in interface_order:
|
|
1301
|
+
handler = interface_handlers.get(interface)
|
|
1302
|
+
if handler:
|
|
1303
|
+
result = handler(resource_type, ref, matches)
|
|
1304
|
+
if result:
|
|
1305
|
+
return result
|
|
1306
|
+
|
|
1307
|
+
return None
|
|
1308
|
+
|
|
1309
|
+
|
|
1130
1310
|
def handle_ambiguous_resource(
|
|
1131
|
-
ctx: Any,
|
|
1311
|
+
ctx: Any,
|
|
1312
|
+
resource_type: str,
|
|
1313
|
+
ref: str,
|
|
1314
|
+
matches: list[Any],
|
|
1315
|
+
*,
|
|
1316
|
+
interface_preference: str = "questionary",
|
|
1132
1317
|
) -> Any:
|
|
1133
1318
|
"""Handle multiple resource matches gracefully."""
|
|
1134
1319
|
if _get_view(ctx) == "json":
|
|
1135
1320
|
return _handle_json_view_ambiguity(matches)
|
|
1136
1321
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
else:
|
|
1147
|
-
# Re-raise cancellation exceptions
|
|
1148
|
-
raise
|
|
1322
|
+
preference = _normalize_interface_preference(interface_preference)
|
|
1323
|
+
interface_order = _get_interface_order(preference)
|
|
1324
|
+
|
|
1325
|
+
result = _try_interface_selection(interface_order, resource_type, ref, matches)
|
|
1326
|
+
|
|
1327
|
+
if result is not None:
|
|
1328
|
+
return result
|
|
1329
|
+
|
|
1330
|
+
return _handle_fallback_numeric_ambiguity(resource_type, ref, matches)
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -928,6 +928,8 @@ class AgentClient(BaseClient):
|
|
|
928
928
|
started_monotonic: float | None = None
|
|
929
929
|
finished_monotonic: float | None = None
|
|
930
930
|
|
|
931
|
+
timeout_seconds = compute_timeout_seconds(kwargs)
|
|
932
|
+
|
|
931
933
|
try:
|
|
932
934
|
response = self.http_client.stream(
|
|
933
935
|
"POST",
|
|
@@ -936,12 +938,12 @@ class AgentClient(BaseClient):
|
|
|
936
938
|
data=data_payload,
|
|
937
939
|
files=files_payload,
|
|
938
940
|
headers=headers,
|
|
941
|
+
timeout=timeout_seconds,
|
|
939
942
|
)
|
|
940
943
|
|
|
941
944
|
with response as stream_response:
|
|
942
945
|
stream_response.raise_for_status()
|
|
943
946
|
|
|
944
|
-
timeout_seconds = compute_timeout_seconds(kwargs)
|
|
945
947
|
agent_name = kwargs.get("agent_name")
|
|
946
948
|
|
|
947
949
|
(
|
|
@@ -106,7 +106,7 @@ class AgentRunRenderingManager:
|
|
|
106
106
|
theme="dark",
|
|
107
107
|
style="debug",
|
|
108
108
|
live=False,
|
|
109
|
-
show_delegate_tool_panels=
|
|
109
|
+
show_delegate_tool_panels=False,
|
|
110
110
|
append_finished_snapshots=False,
|
|
111
111
|
)
|
|
112
112
|
return RichStreamRenderer(
|
|
@@ -118,7 +118,7 @@ class AgentRunRenderingManager:
|
|
|
118
118
|
def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
|
|
119
119
|
if verbose:
|
|
120
120
|
return self._create_verbose_renderer()
|
|
121
|
-
default_config = RendererConfig(
|
|
121
|
+
default_config = RendererConfig()
|
|
122
122
|
return RichStreamRenderer(console=_Console(), cfg=default_config)
|
|
123
123
|
|
|
124
124
|
# --------------------------------------------------------------------- #
|
glaip_sdk/icons.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Lightweight icon definitions used across the CLI.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
ICON_AGENT = "🤖"
|
|
8
|
+
ICON_AGENT_STEP = "🧠"
|
|
9
|
+
ICON_TOOL = "🔧"
|
|
10
|
+
ICON_TOOL_STEP = "⚙️"
|
|
11
|
+
ICON_DELEGATE = ICON_AGENT
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ICON_AGENT",
|
|
15
|
+
"ICON_AGENT_STEP",
|
|
16
|
+
"ICON_TOOL",
|
|
17
|
+
"ICON_TOOL_STEP",
|
|
18
|
+
"ICON_DELEGATE",
|
|
19
|
+
]
|
glaip_sdk/models.py
CHANGED
|
@@ -58,6 +58,9 @@ class Agent(BaseModel):
|
|
|
58
58
|
)
|
|
59
59
|
# Automatically pass the agent name for better renderer display
|
|
60
60
|
kwargs.setdefault("agent_name", self.name)
|
|
61
|
+
# Pass the agent's configured timeout if not explicitly overridden
|
|
62
|
+
if "timeout" not in kwargs:
|
|
63
|
+
kwargs["timeout"] = self.timeout
|
|
61
64
|
# Pass verbose flag through to enable event JSON output
|
|
62
65
|
return self._client.run_agent(self.id, message, verbose=verbose, **kwargs)
|
|
63
66
|
|
|
@@ -82,6 +85,9 @@ class Agent(BaseModel):
|
|
|
82
85
|
)
|
|
83
86
|
# Automatically pass the agent name for better context
|
|
84
87
|
kwargs.setdefault("agent_name", self.name)
|
|
88
|
+
# Pass the agent's configured timeout if not explicitly overridden
|
|
89
|
+
if "timeout" not in kwargs:
|
|
90
|
+
kwargs["timeout"] = self.timeout
|
|
85
91
|
|
|
86
92
|
async for chunk in self._client.arun_agent(self.id, message, **kwargs):
|
|
87
93
|
yield chunk
|
glaip_sdk/rich_components.py
CHANGED
|
@@ -38,4 +38,32 @@ class AIPTable(Table):
|
|
|
38
38
|
super().__init__(*args, **kwargs)
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
class AIPGrid(Table):
|
|
42
|
+
"""Table-based grid with GL AIP defaults for layout blocks."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
expand: bool = True,
|
|
48
|
+
padding: tuple[int, int] = (0, 1),
|
|
49
|
+
collapse_padding: bool = True,
|
|
50
|
+
):
|
|
51
|
+
"""Initialize AIPGrid with zero-edge borders and optional expansion.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
expand: Whether the grid should expand to fill available width.
|
|
55
|
+
padding: Cell padding for the grid (row, column).
|
|
56
|
+
collapse_padding: Collapse padding between renderables.
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(
|
|
59
|
+
show_header=False,
|
|
60
|
+
show_edge=False,
|
|
61
|
+
pad_edge=False,
|
|
62
|
+
box=None,
|
|
63
|
+
expand=expand,
|
|
64
|
+
padding=padding,
|
|
65
|
+
collapse_padding=collapse_padding,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["AIPPanel", "AIPTable", "AIPGrid"]
|
glaip_sdk/utils/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ Authors:
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from glaip_sdk.utils.display import (
|
|
8
|
+
RICH_AVAILABLE,
|
|
8
9
|
print_agent_created,
|
|
9
10
|
print_agent_deleted,
|
|
10
11
|
print_agent_output,
|
|
@@ -19,7 +20,6 @@ from glaip_sdk.utils.general import (
|
|
|
19
20
|
)
|
|
20
21
|
from glaip_sdk.utils.rendering.models import RunStats, Step
|
|
21
22
|
from glaip_sdk.utils.rendering.steps import StepManager
|
|
22
|
-
from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
|
|
23
23
|
from glaip_sdk.utils.run_renderer import RichStreamRenderer
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
glaip_sdk/utils/client_utils.py
CHANGED
|
@@ -17,6 +17,12 @@ from typing import Any, BinaryIO, NoReturn
|
|
|
17
17
|
import httpx
|
|
18
18
|
|
|
19
19
|
from glaip_sdk.exceptions import AgentTimeoutError
|
|
20
|
+
from glaip_sdk.utils.resource_refs import (
|
|
21
|
+
extract_ids as extract_ids_new,
|
|
22
|
+
)
|
|
23
|
+
from glaip_sdk.utils.resource_refs import (
|
|
24
|
+
find_by_name as find_by_name_new,
|
|
25
|
+
)
|
|
20
26
|
|
|
21
27
|
# Set up module-level logger
|
|
22
28
|
logger = logging.getLogger("glaip_sdk.client_utils")
|
|
@@ -77,8 +83,6 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
|
|
|
77
83
|
This function maintains backward compatibility by returning None for empty input.
|
|
78
84
|
New code should use glaip_sdk.utils.resource_refs.extract_ids which returns [].
|
|
79
85
|
"""
|
|
80
|
-
from .resource_refs import extract_ids as extract_ids_new
|
|
81
|
-
|
|
82
86
|
if not items:
|
|
83
87
|
return None
|
|
84
88
|
|
|
@@ -127,8 +131,6 @@ def find_by_name(
|
|
|
127
131
|
Note:
|
|
128
132
|
This function now delegates to glaip_sdk.utils.resource_refs.find_by_name.
|
|
129
133
|
"""
|
|
130
|
-
from .resource_refs import find_by_name as find_by_name_new
|
|
131
|
-
|
|
132
134
|
return find_by_name_new(items, name, case_sensitive)
|
|
133
135
|
|
|
134
136
|
|