glaip-sdk 0.2.2__py3-none-any.whl → 0.4.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.
- glaip_sdk/cli/auth.py +2 -1
- glaip_sdk/cli/commands/agents.py +51 -36
- glaip_sdk/cli/commands/configure.py +2 -1
- glaip_sdk/cli/commands/mcps.py +219 -62
- glaip_sdk/cli/commands/models.py +3 -5
- glaip_sdk/cli/commands/tools.py +27 -16
- glaip_sdk/cli/commands/transcripts.py +1 -1
- glaip_sdk/cli/constants.py +3 -0
- glaip_sdk/cli/display.py +1 -1
- glaip_sdk/cli/hints.py +58 -0
- glaip_sdk/cli/io.py +6 -3
- glaip_sdk/cli/main.py +3 -4
- glaip_sdk/cli/slash/agent_session.py +4 -13
- glaip_sdk/cli/slash/prompt.py +3 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +139 -48
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
- glaip_sdk/cli/transcript/capture.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +19 -678
- glaip_sdk/cli/update_notifier.py +2 -1
- glaip_sdk/cli/utils.py +228 -101
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +40 -22
- glaip_sdk/client/main.py +2 -6
- glaip_sdk/client/mcps.py +13 -5
- glaip_sdk/client/run_rendering.py +90 -111
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +2 -3
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/models/__init__.py +56 -0
- glaip_sdk/models/agent_runs.py +117 -0
- glaip_sdk/models.py +8 -7
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/utils/client_utils.py +13 -0
- glaip_sdk/utils/display.py +23 -15
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/import_export.py +6 -9
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +5 -30
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +1 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
- glaip_sdk/utils/rendering/renderer/base.py +217 -1476
- glaip_sdk/utils/rendering/renderer/debug.py +24 -1
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -439
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +26 -15
- glaip_sdk/utils/validation.py +13 -21
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/METADATA +24 -2
- glaip_sdk-0.4.0.dist-info/RECORD +110 -0
- glaip_sdk-0.2.2.dist-info/RECORD +0 -87
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.4.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -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.
|
|
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,13 +7,14 @@ 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
16
|
from collections.abc import Callable, Iterable
|
|
16
|
-
from contextlib import AbstractContextManager, nullcontext
|
|
17
|
+
from contextlib import AbstractContextManager, contextmanager, nullcontext
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import TYPE_CHECKING, Any, cast
|
|
19
20
|
|
|
@@ -26,29 +27,206 @@ from rich.syntax import Syntax
|
|
|
26
27
|
from glaip_sdk import _version as _version_module
|
|
27
28
|
from glaip_sdk.branding import (
|
|
28
29
|
ACCENT_STYLE,
|
|
29
|
-
HINT_COMMAND_STYLE,
|
|
30
|
-
HINT_DESCRIPTION_COLOR,
|
|
31
30
|
SUCCESS_STYLE,
|
|
32
31
|
WARNING_STYLE,
|
|
33
32
|
)
|
|
34
33
|
from glaip_sdk.cli import masking, pager
|
|
35
|
-
from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
|
|
36
34
|
from glaip_sdk.cli.config import load_config
|
|
35
|
+
from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
|
|
37
36
|
from glaip_sdk.cli.context import (
|
|
38
37
|
_get_view,
|
|
39
|
-
detect_export_format as _detect_export_format,
|
|
40
38
|
get_ctx_value,
|
|
41
39
|
)
|
|
42
|
-
from glaip_sdk.cli.
|
|
40
|
+
from glaip_sdk.cli.context import (
|
|
41
|
+
detect_export_format as _detect_export_format,
|
|
42
|
+
)
|
|
43
|
+
from glaip_sdk.cli import display as cli_display
|
|
44
|
+
from glaip_sdk.cli.hints import command_hint
|
|
45
|
+
from glaip_sdk.cli.io import export_resource_to_file_with_validation
|
|
46
|
+
from glaip_sdk.cli.rich_helpers import markup_text, print_markup
|
|
43
47
|
from glaip_sdk.icons import ICON_AGENT
|
|
44
48
|
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
45
|
-
from glaip_sdk.utils import is_uuid
|
|
49
|
+
from glaip_sdk.utils import format_datetime, is_uuid
|
|
46
50
|
from glaip_sdk.utils.rendering.renderer import (
|
|
47
51
|
CapturingConsole,
|
|
48
|
-
|
|
52
|
+
RendererFactoryOptions,
|
|
49
53
|
RichStreamRenderer,
|
|
54
|
+
make_default_renderer,
|
|
55
|
+
make_verbose_renderer,
|
|
50
56
|
)
|
|
51
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
|
+
|
|
81
|
+
|
|
82
|
+
@contextmanager
|
|
83
|
+
def bind_slash_session_context(ctx: Any, session: Any) -> Any:
|
|
84
|
+
"""Temporarily attach a slash session to the Click context.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
ctx: Click context object.
|
|
88
|
+
session: SlashSession instance to bind.
|
|
89
|
+
|
|
90
|
+
Yields:
|
|
91
|
+
None - context manager for use in with statement.
|
|
92
|
+
"""
|
|
93
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
94
|
+
has_context = isinstance(ctx_obj, dict)
|
|
95
|
+
previous_session = ctx_obj.get("_slash_session") if has_context else None
|
|
96
|
+
if has_context:
|
|
97
|
+
ctx_obj["_slash_session"] = session
|
|
98
|
+
try:
|
|
99
|
+
yield
|
|
100
|
+
finally:
|
|
101
|
+
if has_context:
|
|
102
|
+
if previous_session is None:
|
|
103
|
+
ctx_obj.pop("_slash_session", None)
|
|
104
|
+
else:
|
|
105
|
+
ctx_obj["_slash_session"] = previous_session
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def restore_slash_session_context(ctx_obj: dict[str, Any], previous_session: Any | None) -> None:
|
|
109
|
+
"""Restore slash session context after operation.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
ctx_obj: Click context obj dictionary.
|
|
113
|
+
previous_session: Previous session to restore, or None to remove.
|
|
114
|
+
"""
|
|
115
|
+
if previous_session is None:
|
|
116
|
+
ctx_obj.pop("_slash_session", None)
|
|
117
|
+
else:
|
|
118
|
+
ctx_obj["_slash_session"] = previous_session
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def handle_best_effort_check(
|
|
122
|
+
check_func: Callable[[], None],
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Handle best-effort duplicate/existence checks with proper exception handling.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
check_func: Function that performs the check and raises ClickException if duplicate found.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
check_func()
|
|
131
|
+
except click.ClickException:
|
|
132
|
+
raise
|
|
133
|
+
except Exception:
|
|
134
|
+
# Non-fatal: best-effort duplicate check
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def prompt_export_choice_questionary(
|
|
139
|
+
default_path: Path,
|
|
140
|
+
default_display: str,
|
|
141
|
+
) -> tuple[str, Path | None] | None:
|
|
142
|
+
"""Prompt user for export destination using questionary with numeric shortcuts.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
default_path: Default export path.
|
|
146
|
+
default_display: Formatted display string for default path.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Tuple of (choice, path) or None if cancelled/unavailable.
|
|
150
|
+
Choice can be "default", "custom", or "cancel".
|
|
151
|
+
"""
|
|
152
|
+
questionary_module, choice_cls = _load_questionary_module()
|
|
153
|
+
if questionary_module is None or choice_cls is None:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
question = questionary_module.select(
|
|
158
|
+
"Export transcript",
|
|
159
|
+
choices=[
|
|
160
|
+
_make_questionary_choice(
|
|
161
|
+
choice_cls,
|
|
162
|
+
title=f"Save to default ({default_display})",
|
|
163
|
+
value=("default", default_path),
|
|
164
|
+
shortcut_key="1",
|
|
165
|
+
),
|
|
166
|
+
_make_questionary_choice(
|
|
167
|
+
choice_cls,
|
|
168
|
+
title="Choose a different path",
|
|
169
|
+
value=("custom", None),
|
|
170
|
+
shortcut_key="2",
|
|
171
|
+
),
|
|
172
|
+
_make_questionary_choice(
|
|
173
|
+
choice_cls,
|
|
174
|
+
title="Cancel",
|
|
175
|
+
value=("cancel", None),
|
|
176
|
+
shortcut_key="3",
|
|
177
|
+
),
|
|
178
|
+
],
|
|
179
|
+
use_shortcuts=True,
|
|
180
|
+
instruction="Press 1-3 (or arrows) then Enter.",
|
|
181
|
+
)
|
|
182
|
+
answer = questionary_safe_ask(question)
|
|
183
|
+
except Exception:
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
if answer is None:
|
|
187
|
+
return ("cancel", None)
|
|
188
|
+
return answer
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def questionary_safe_ask(question: Any, *, patch_stdout: bool = False) -> Any:
|
|
192
|
+
"""Run `questionary.Question` safely even when an asyncio loop is active."""
|
|
193
|
+
ask_fn = getattr(question, "unsafe_ask", None)
|
|
194
|
+
if not callable(ask_fn):
|
|
195
|
+
raise RuntimeError("Questionary prompt is missing unsafe_ask()")
|
|
196
|
+
|
|
197
|
+
if not _asyncio_loop_running():
|
|
198
|
+
return ask_fn(patch_stdout=patch_stdout)
|
|
199
|
+
|
|
200
|
+
return _run_questionary_in_thread(question, patch_stdout=patch_stdout)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _asyncio_loop_running() -> bool:
|
|
204
|
+
"""Return True when an asyncio event loop is already running."""
|
|
205
|
+
try:
|
|
206
|
+
asyncio.get_running_loop()
|
|
207
|
+
except RuntimeError:
|
|
208
|
+
return False
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _run_questionary_in_thread(question: Any, *, patch_stdout: bool = False) -> Any:
|
|
213
|
+
"""Execute a questionary prompt in a background thread."""
|
|
214
|
+
if getattr(question, "should_skip_question", False):
|
|
215
|
+
return getattr(question, "default", None)
|
|
216
|
+
|
|
217
|
+
application = getattr(question, "application", None)
|
|
218
|
+
run_callable = getattr(application, "run", None) if application is not None else None
|
|
219
|
+
if callable(run_callable):
|
|
220
|
+
try:
|
|
221
|
+
if patch_stdout and pt_patch_stdout is not None:
|
|
222
|
+
with pt_patch_stdout():
|
|
223
|
+
return run_callable(in_thread=True)
|
|
224
|
+
return run_callable(in_thread=True)
|
|
225
|
+
except TypeError:
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
return question.unsafe_ask(patch_stdout=patch_stdout)
|
|
229
|
+
|
|
52
230
|
|
|
53
231
|
class _LiteralYamlDumper(yaml.SafeDumper):
|
|
54
232
|
"""YAML dumper that emits literal scalars for multiline strings."""
|
|
@@ -70,6 +248,7 @@ _LiteralYamlDumper.add_representer(str, _literal_str_representer)
|
|
|
70
248
|
try:
|
|
71
249
|
from prompt_toolkit.buffer import Buffer
|
|
72
250
|
from prompt_toolkit.completion import Completion
|
|
251
|
+
from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
|
|
73
252
|
from prompt_toolkit.selection import SelectionType
|
|
74
253
|
from prompt_toolkit.shortcuts import PromptSession, prompt
|
|
75
254
|
|
|
@@ -79,13 +258,9 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
79
258
|
SelectionType = None # type: ignore[assignment]
|
|
80
259
|
PromptSession = None # type: ignore[assignment]
|
|
81
260
|
prompt = None # type: ignore[assignment]
|
|
261
|
+
pt_patch_stdout = None # type: ignore[assignment]
|
|
82
262
|
_HAS_PTK = False
|
|
83
263
|
|
|
84
|
-
try:
|
|
85
|
-
import questionary
|
|
86
|
-
except Exception: # pragma: no cover - optional dependency
|
|
87
|
-
questionary = None
|
|
88
|
-
|
|
89
264
|
if TYPE_CHECKING: # pragma: no cover - import-only during type checking
|
|
90
265
|
from glaip_sdk import Client
|
|
91
266
|
|
|
@@ -160,8 +335,6 @@ def format_datetime_fields(
|
|
|
160
335
|
Returns:
|
|
161
336
|
New dictionary with formatted datetime fields
|
|
162
337
|
"""
|
|
163
|
-
from glaip_sdk.utils import format_datetime
|
|
164
|
-
|
|
165
338
|
formatted = data.copy()
|
|
166
339
|
for field in fields:
|
|
167
340
|
if field in formatted:
|
|
@@ -224,10 +397,6 @@ def handle_resource_export(
|
|
|
224
397
|
get_by_id_func: Function to fetch resource by ID
|
|
225
398
|
console_override: Optional console override
|
|
226
399
|
"""
|
|
227
|
-
from glaip_sdk.cli.display import handle_rich_output
|
|
228
|
-
from glaip_sdk.cli.io import export_resource_to_file_with_validation
|
|
229
|
-
from glaip_sdk.cli.rich_helpers import print_markup
|
|
230
|
-
|
|
231
400
|
active_console = console_override or console
|
|
232
401
|
|
|
233
402
|
# Auto-detect format from file extension
|
|
@@ -251,7 +420,7 @@ def handle_resource_export(
|
|
|
251
420
|
):
|
|
252
421
|
export_resource_to_file_with_validation(full_resource, export_path, detected_format)
|
|
253
422
|
except Exception:
|
|
254
|
-
handle_rich_output(
|
|
423
|
+
cli_display.handle_rich_output(
|
|
255
424
|
ctx,
|
|
256
425
|
markup_text(f"[{WARNING_STYLE}]⚠️ Failed to fetch full details, using available data[/]"),
|
|
257
426
|
)
|
|
@@ -264,73 +433,6 @@ def handle_resource_export(
|
|
|
264
433
|
)
|
|
265
434
|
|
|
266
435
|
|
|
267
|
-
def in_slash_mode(ctx: click.Context | None = None) -> bool:
|
|
268
|
-
"""Return True when running inside the slash command palette."""
|
|
269
|
-
if ctx is None:
|
|
270
|
-
try:
|
|
271
|
-
ctx = click.get_current_context(silent=True)
|
|
272
|
-
except RuntimeError:
|
|
273
|
-
ctx = None
|
|
274
|
-
|
|
275
|
-
if ctx is None:
|
|
276
|
-
return False
|
|
277
|
-
|
|
278
|
-
obj = getattr(ctx, "obj", None)
|
|
279
|
-
if isinstance(obj, dict):
|
|
280
|
-
return bool(obj.get("_slash_session"))
|
|
281
|
-
|
|
282
|
-
return bool(getattr(obj, "_slash_session", False))
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def command_hint(
|
|
286
|
-
cli_command: str | None,
|
|
287
|
-
slash_command: str | None = None,
|
|
288
|
-
*,
|
|
289
|
-
ctx: click.Context | None = None,
|
|
290
|
-
) -> str | None:
|
|
291
|
-
"""Return the appropriate command string for the current mode.
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
cli_command: Command string without the ``aip`` prefix (e.g., ``"status"``).
|
|
295
|
-
slash_command: Slash command counterpart (e.g., ``"status"`` or ``"/status"``).
|
|
296
|
-
ctx: Optional Click context override.
|
|
297
|
-
|
|
298
|
-
Returns:
|
|
299
|
-
The formatted command string for the active mode, or ``None`` when no
|
|
300
|
-
equivalent command exists in that mode.
|
|
301
|
-
"""
|
|
302
|
-
if in_slash_mode(ctx):
|
|
303
|
-
if not slash_command:
|
|
304
|
-
return None
|
|
305
|
-
return slash_command if slash_command.startswith("/") else f"/{slash_command}"
|
|
306
|
-
|
|
307
|
-
if not cli_command:
|
|
308
|
-
return None
|
|
309
|
-
return f"aip {cli_command}"
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
def format_command_hint(
|
|
313
|
-
command: str | None,
|
|
314
|
-
description: str | None = None,
|
|
315
|
-
) -> str | None:
|
|
316
|
-
"""Return a Rich markup string that highlights a command hint.
|
|
317
|
-
|
|
318
|
-
Args:
|
|
319
|
-
command: Command text to highlight (already formatted for the active mode).
|
|
320
|
-
description: Optional short description to display alongside the command.
|
|
321
|
-
|
|
322
|
-
Returns:
|
|
323
|
-
Markup string suitable for Rich rendering, or ``None`` when ``command`` is falsy.
|
|
324
|
-
"""
|
|
325
|
-
if not command:
|
|
326
|
-
return None
|
|
327
|
-
|
|
328
|
-
highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
|
|
329
|
-
if description:
|
|
330
|
-
highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
|
|
331
|
-
return highlighted
|
|
332
|
-
|
|
333
|
-
|
|
334
436
|
def sdk_version() -> str:
|
|
335
437
|
"""Return the current SDK version, warning if metadata is unavailable."""
|
|
336
438
|
version = getattr(_version_module, "__version__", None)
|
|
@@ -345,6 +447,28 @@ def sdk_version() -> str:
|
|
|
345
447
|
return "0.0.0"
|
|
346
448
|
|
|
347
449
|
|
|
450
|
+
@contextmanager
|
|
451
|
+
def with_client_and_spinner(
|
|
452
|
+
ctx: Any,
|
|
453
|
+
spinner_message: str,
|
|
454
|
+
*,
|
|
455
|
+
console_override: Console | None = None,
|
|
456
|
+
) -> Any:
|
|
457
|
+
"""Context manager for commands that need client and spinner.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
ctx: Click context.
|
|
461
|
+
spinner_message: Message to display in spinner.
|
|
462
|
+
console_override: Optional console override.
|
|
463
|
+
|
|
464
|
+
Yields:
|
|
465
|
+
Client instance.
|
|
466
|
+
"""
|
|
467
|
+
client = get_client(ctx)
|
|
468
|
+
with spinner_context(ctx, spinner_message, console_override=console_override):
|
|
469
|
+
yield client
|
|
470
|
+
|
|
471
|
+
|
|
348
472
|
def spinner_context(
|
|
349
473
|
ctx: Any | None,
|
|
350
474
|
message: str,
|
|
@@ -1184,19 +1308,20 @@ def build_renderer(
|
|
|
1184
1308
|
|
|
1185
1309
|
# Configure renderer based on verbose mode and explicit overrides
|
|
1186
1310
|
live_enabled = bool(live) if live is not None else not verbose
|
|
1187
|
-
|
|
1188
|
-
live
|
|
1189
|
-
append_finished_snapshots
|
|
1190
|
-
|
|
1191
|
-
|
|
1311
|
+
cfg_overrides = {
|
|
1312
|
+
"live": live_enabled,
|
|
1313
|
+
"append_finished_snapshots": bool(snapshots) if snapshots is not None else False,
|
|
1314
|
+
}
|
|
1315
|
+
renderer_console = (
|
|
1316
|
+
working_console.original_console if isinstance(working_console, CapturingConsole) else working_console
|
|
1192
1317
|
)
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
verbose=verbose,
|
|
1318
|
+
factory = make_verbose_renderer if verbose else make_default_renderer
|
|
1319
|
+
factory_options = RendererFactoryOptions(
|
|
1320
|
+
console=renderer_console,
|
|
1321
|
+
cfg_overrides=cfg_overrides,
|
|
1322
|
+
verbose=verbose if factory is make_default_renderer else None,
|
|
1199
1323
|
)
|
|
1324
|
+
renderer = factory_options.build(factory)
|
|
1200
1325
|
|
|
1201
1326
|
# Link the renderer back to the slash session when running from the palette.
|
|
1202
1327
|
_register_renderer_with_session(_ctx, renderer)
|
|
@@ -1366,17 +1491,19 @@ def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
|
|
|
1366
1491
|
|
|
1367
1492
|
def _handle_questionary_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
|
|
1368
1493
|
"""Handle ambiguity using questionary interactive interface."""
|
|
1369
|
-
|
|
1494
|
+
questionary_module, choice_cls = _load_questionary_module()
|
|
1495
|
+
if not (questionary_module and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
|
|
1370
1496
|
raise click.ClickException("Interactive selection not available")
|
|
1371
1497
|
|
|
1372
1498
|
# Escape special characters for questionary
|
|
1373
1499
|
safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
|
|
1374
1500
|
safe_ref = ref.replace("{", "{{").replace("}", "}}")
|
|
1375
1501
|
|
|
1376
|
-
picked_idx =
|
|
1502
|
+
picked_idx = questionary_module.select(
|
|
1377
1503
|
f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
|
|
1378
1504
|
choices=[
|
|
1379
|
-
|
|
1505
|
+
_make_questionary_choice(
|
|
1506
|
+
choice_cls,
|
|
1380
1507
|
title=(
|
|
1381
1508
|
f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — "
|
|
1382
1509
|
f"{getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}"
|
glaip_sdk/cli/validators.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
import click
|
|
15
15
|
|
|
16
|
+
from glaip_sdk.cli.utils import handle_best_effort_check
|
|
16
17
|
from glaip_sdk.utils.validation import (
|
|
17
18
|
coerce_timeout,
|
|
18
19
|
validate_agent_instruction,
|
|
@@ -226,14 +227,12 @@ def validate_name_uniqueness_cli(
|
|
|
226
227
|
Raises:
|
|
227
228
|
click.ClickException: If name is not unique
|
|
228
229
|
"""
|
|
229
|
-
|
|
230
|
+
|
|
231
|
+
def _check_duplicate() -> None:
|
|
230
232
|
existing = finder_func(name=name)
|
|
231
233
|
if existing:
|
|
232
234
|
raise click.ClickException(
|
|
233
235
|
f"A {resource_type.lower()} named '{name}' already exists. Please choose a unique name."
|
|
234
236
|
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
except Exception:
|
|
238
|
-
# Non-fatal: best-effort duplicate check
|
|
239
|
-
pass
|
|
237
|
+
|
|
238
|
+
handle_best_effort_check(_check_duplicate)
|
glaip_sdk/client/__init__.py
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Agent runs client for AIP SDK.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from glaip_sdk.client.base import BaseClient
|
|
13
|
+
from glaip_sdk.exceptions import TimeoutError, ValidationError
|
|
14
|
+
from glaip_sdk.models.agent_runs import RunSummary, RunsPage, RunWithOutput
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AgentRunsClient(BaseClient):
|
|
18
|
+
"""Client for agent run operations."""
|
|
19
|
+
|
|
20
|
+
def list_runs(
|
|
21
|
+
self,
|
|
22
|
+
agent_id: str,
|
|
23
|
+
*,
|
|
24
|
+
limit: int = 20,
|
|
25
|
+
page: int = 1,
|
|
26
|
+
) -> RunsPage:
|
|
27
|
+
"""List agent runs with pagination.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
agent_id: UUID of the agent
|
|
31
|
+
limit: Number of runs per page (1-100, default 20)
|
|
32
|
+
page: Page number (1-based, default 1)
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
RunsPage containing paginated run summaries
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
ValidationError: If pagination parameters are invalid
|
|
39
|
+
NotFoundError: If agent is not found
|
|
40
|
+
AuthenticationError: If authentication fails
|
|
41
|
+
TimeoutError: If request times out (30s default)
|
|
42
|
+
"""
|
|
43
|
+
self._validate_pagination_params(limit, page)
|
|
44
|
+
envelope = self._fetch_runs_envelope(agent_id, limit, page)
|
|
45
|
+
normalized_data = self._normalize_runs_payload(envelope.get("data"))
|
|
46
|
+
runs = [RunSummary(**item) for item in normalized_data]
|
|
47
|
+
return self._build_runs_page(envelope, runs, limit, page)
|
|
48
|
+
|
|
49
|
+
def _fetch_runs_envelope(self, agent_id: str, limit: int, page: int) -> dict[str, Any]:
|
|
50
|
+
params = {"limit": limit, "page": page}
|
|
51
|
+
try:
|
|
52
|
+
envelope = self._request_with_envelope(
|
|
53
|
+
"GET",
|
|
54
|
+
f"/agents/{agent_id}/runs",
|
|
55
|
+
params=params,
|
|
56
|
+
)
|
|
57
|
+
except httpx.TimeoutException as e:
|
|
58
|
+
raise TimeoutError(f"Request timed out after {self._timeout}s while fetching agent runs") from e
|
|
59
|
+
|
|
60
|
+
if isinstance(envelope, dict):
|
|
61
|
+
return envelope
|
|
62
|
+
return {"data": envelope}
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _validate_pagination_params(limit: int, page: int) -> None:
|
|
66
|
+
if limit < 1 or limit > 100:
|
|
67
|
+
raise ValidationError("limit must be between 1 and 100")
|
|
68
|
+
if page < 1:
|
|
69
|
+
raise ValidationError("page must be >= 1")
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _normalize_runs_payload(data_payload: Any) -> list[Any]:
|
|
73
|
+
if not data_payload:
|
|
74
|
+
return []
|
|
75
|
+
normalized_data: list[Any] = []
|
|
76
|
+
for item in data_payload:
|
|
77
|
+
normalized_data.append(AgentRunsClient._normalize_run_item(item))
|
|
78
|
+
return normalized_data
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _normalize_run_item(item: Any) -> Any:
|
|
82
|
+
if isinstance(item, dict):
|
|
83
|
+
if item.get("config") is None:
|
|
84
|
+
item["config"] = {}
|
|
85
|
+
schedule_id = item.get("schedule_id")
|
|
86
|
+
if schedule_id == "None" or schedule_id == "":
|
|
87
|
+
item["schedule_id"] = None
|
|
88
|
+
return item
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def _build_runs_page(
|
|
92
|
+
envelope: dict[str, Any],
|
|
93
|
+
runs: list[RunSummary],
|
|
94
|
+
limit: int,
|
|
95
|
+
page: int,
|
|
96
|
+
) -> RunsPage:
|
|
97
|
+
return RunsPage(
|
|
98
|
+
data=runs,
|
|
99
|
+
total=envelope.get("total", 0),
|
|
100
|
+
page=envelope.get("page", page),
|
|
101
|
+
limit=envelope.get("limit", limit),
|
|
102
|
+
has_next=envelope.get("has_next", False),
|
|
103
|
+
has_prev=envelope.get("has_prev", False),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def get_run(
|
|
107
|
+
self,
|
|
108
|
+
agent_id: str,
|
|
109
|
+
run_id: str,
|
|
110
|
+
) -> RunWithOutput:
|
|
111
|
+
"""Get detailed run information including SSE event stream.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
agent_id: UUID of the agent
|
|
115
|
+
run_id: UUID of the run
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
RunWithOutput containing complete run details and event stream
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
NotFoundError: If run or agent is not found
|
|
122
|
+
AuthenticationError: If authentication fails
|
|
123
|
+
TimeoutError: If request times out (30s default)
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
envelope = self._request_with_envelope(
|
|
127
|
+
"GET",
|
|
128
|
+
f"/agents/{agent_id}/runs/{run_id}",
|
|
129
|
+
)
|
|
130
|
+
except httpx.TimeoutException as e:
|
|
131
|
+
raise TimeoutError(f"Request timed out after {self._timeout}s while fetching run detail") from e
|
|
132
|
+
|
|
133
|
+
if not isinstance(envelope, dict):
|
|
134
|
+
envelope = {"data": envelope}
|
|
135
|
+
|
|
136
|
+
data = envelope.get("data") or {}
|
|
137
|
+
# Normalize config, output, and schedule_id fields
|
|
138
|
+
if data.get("config") is None:
|
|
139
|
+
data["config"] = {}
|
|
140
|
+
if data.get("output") is None:
|
|
141
|
+
data["output"] = []
|
|
142
|
+
# Normalize schedule_id: convert string "None" to None
|
|
143
|
+
schedule_id = data.get("schedule_id")
|
|
144
|
+
if schedule_id == "None" or schedule_id == "":
|
|
145
|
+
data["schedule_id"] = None
|
|
146
|
+
|
|
147
|
+
return RunWithOutput(**data)
|