glaip-sdk 0.2.1__py3-none-any.whl → 0.3.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/_version.py +8 -0
- glaip_sdk/branding.py +13 -0
- glaip_sdk/cli/commands/agents.py +180 -39
- glaip_sdk/cli/commands/mcps.py +44 -18
- glaip_sdk/cli/commands/models.py +11 -5
- glaip_sdk/cli/commands/tools.py +35 -16
- glaip_sdk/cli/commands/transcripts.py +8 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/display.py +34 -19
- glaip_sdk/cli/main.py +14 -7
- glaip_sdk/cli/masking.py +8 -33
- glaip_sdk/cli/pager.py +9 -10
- glaip_sdk/cli/slash/agent_session.py +57 -20
- glaip_sdk/cli/slash/prompt.py +8 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +341 -46
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +632 -0
- glaip_sdk/cli/transcript/viewer.py +232 -32
- glaip_sdk/cli/update_notifier.py +2 -2
- glaip_sdk/cli/utils.py +266 -35
- glaip_sdk/cli/validators.py +5 -6
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +30 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +186 -22
- glaip_sdk/client/main.py +23 -6
- glaip_sdk/client/mcps.py +2 -4
- glaip_sdk/client/run_rendering.py +66 -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/rich_components.py +58 -2
- glaip_sdk/utils/client_utils.py +13 -0
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/import_export.py +6 -9
- glaip_sdk/utils/rendering/__init__.py +122 -1
- glaip_sdk/utils/rendering/renderer/base.py +3 -7
- glaip_sdk/utils/rendering/renderer/debug.py +0 -1
- glaip_sdk/utils/rendering/renderer/stream.py +4 -12
- glaip_sdk/utils/rendering/steps.py +1 -0
- glaip_sdk/utils/resource_refs.py +26 -15
- glaip_sdk/utils/serialization.py +16 -0
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
- glaip_sdk-0.3.0.dist-info/RECORD +94 -0
- glaip_sdk-0.2.1.dist-info/RECORD +0 -86
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.2.1.dist-info → glaip_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/utils.py
CHANGED
|
@@ -12,15 +12,17 @@ import json
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
|
+
import asyncio
|
|
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
|
|
|
20
21
|
import click
|
|
22
|
+
import yaml
|
|
21
23
|
from rich.console import Console, Group
|
|
22
24
|
from rich.markdown import Markdown
|
|
23
|
-
from rich.
|
|
25
|
+
from rich.syntax import Syntax
|
|
24
26
|
|
|
25
27
|
from glaip_sdk import _version as _version_module
|
|
26
28
|
from glaip_sdk.branding import (
|
|
@@ -30,9 +32,195 @@ from glaip_sdk.branding import (
|
|
|
30
32
|
SUCCESS_STYLE,
|
|
31
33
|
WARNING_STYLE,
|
|
32
34
|
)
|
|
33
|
-
from glaip_sdk.cli
|
|
35
|
+
from glaip_sdk.cli import masking, pager
|
|
36
|
+
from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
|
|
37
|
+
from glaip_sdk.cli.config import load_config
|
|
38
|
+
from glaip_sdk.cli.context import (
|
|
39
|
+
_get_view,
|
|
40
|
+
detect_export_format as _detect_export_format,
|
|
41
|
+
get_ctx_value,
|
|
42
|
+
)
|
|
43
|
+
from glaip_sdk.cli.io import export_resource_to_file_with_validation
|
|
44
|
+
from glaip_sdk.cli.rich_helpers import markup_text, print_markup
|
|
34
45
|
from glaip_sdk.icons import ICON_AGENT
|
|
35
|
-
from glaip_sdk.rich_components import AIPPanel
|
|
46
|
+
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
47
|
+
from glaip_sdk.utils import format_datetime, is_uuid
|
|
48
|
+
from glaip_sdk.utils.rendering.renderer import (
|
|
49
|
+
CapturingConsole,
|
|
50
|
+
RendererConfig,
|
|
51
|
+
RichStreamRenderer,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@contextmanager
|
|
56
|
+
def bind_slash_session_context(ctx: Any, session: Any) -> Any:
|
|
57
|
+
"""Temporarily attach a slash session to the Click context.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
ctx: Click context object.
|
|
61
|
+
session: SlashSession instance to bind.
|
|
62
|
+
|
|
63
|
+
Yields:
|
|
64
|
+
None - context manager for use in with statement.
|
|
65
|
+
"""
|
|
66
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
67
|
+
has_context = isinstance(ctx_obj, dict)
|
|
68
|
+
previous_session = ctx_obj.get("_slash_session") if has_context else None
|
|
69
|
+
if has_context:
|
|
70
|
+
ctx_obj["_slash_session"] = session
|
|
71
|
+
try:
|
|
72
|
+
yield
|
|
73
|
+
finally:
|
|
74
|
+
if has_context:
|
|
75
|
+
if previous_session is None:
|
|
76
|
+
ctx_obj.pop("_slash_session", None)
|
|
77
|
+
else:
|
|
78
|
+
ctx_obj["_slash_session"] = previous_session
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def restore_slash_session_context(ctx_obj: dict[str, Any], previous_session: Any | None) -> None:
|
|
82
|
+
"""Restore slash session context after operation.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
ctx_obj: Click context obj dictionary.
|
|
86
|
+
previous_session: Previous session to restore, or None to remove.
|
|
87
|
+
"""
|
|
88
|
+
if previous_session is None:
|
|
89
|
+
ctx_obj.pop("_slash_session", None)
|
|
90
|
+
else:
|
|
91
|
+
ctx_obj["_slash_session"] = previous_session
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def handle_best_effort_check(
|
|
95
|
+
check_func: Callable[[], None],
|
|
96
|
+
) -> None:
|
|
97
|
+
"""Handle best-effort duplicate/existence checks with proper exception handling.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
check_func: Function that performs the check and raises ClickException if duplicate found.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
check_func()
|
|
104
|
+
except click.ClickException:
|
|
105
|
+
raise
|
|
106
|
+
except Exception:
|
|
107
|
+
# Non-fatal: best-effort duplicate check
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def prompt_export_choice_questionary(
|
|
112
|
+
default_path: Path,
|
|
113
|
+
default_display: str,
|
|
114
|
+
) -> tuple[str, Path | None] | None:
|
|
115
|
+
"""Prompt user for export destination using questionary with numeric shortcuts.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
default_path: Default export path.
|
|
119
|
+
default_display: Formatted display string for default path.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Tuple of (choice, path) or None if cancelled/unavailable.
|
|
123
|
+
Choice can be "default", "custom", or "cancel".
|
|
124
|
+
"""
|
|
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:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
question = questionary.select(
|
|
137
|
+
"Export transcript",
|
|
138
|
+
choices=[
|
|
139
|
+
Choice(
|
|
140
|
+
title=f"Save to default ({default_display})",
|
|
141
|
+
value=("default", default_path),
|
|
142
|
+
shortcut_key="1",
|
|
143
|
+
),
|
|
144
|
+
Choice(
|
|
145
|
+
title="Choose a different path",
|
|
146
|
+
value=("custom", None),
|
|
147
|
+
shortcut_key="2",
|
|
148
|
+
),
|
|
149
|
+
Choice(
|
|
150
|
+
title="Cancel",
|
|
151
|
+
value=("cancel", None),
|
|
152
|
+
shortcut_key="3",
|
|
153
|
+
),
|
|
154
|
+
],
|
|
155
|
+
use_shortcuts=True,
|
|
156
|
+
instruction="Press 1-3 (or arrows) then Enter.",
|
|
157
|
+
)
|
|
158
|
+
answer = questionary_safe_ask(question)
|
|
159
|
+
except Exception:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
if answer is None:
|
|
163
|
+
return ("cancel", None)
|
|
164
|
+
return answer
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def questionary_safe_ask(question: Any, *, patch_stdout: bool = False) -> Any:
|
|
168
|
+
"""Run `questionary.Question` safely even when an asyncio loop is active."""
|
|
169
|
+
ask_fn = getattr(question, "unsafe_ask", None)
|
|
170
|
+
if not callable(ask_fn):
|
|
171
|
+
raise RuntimeError("Questionary prompt is missing unsafe_ask()")
|
|
172
|
+
|
|
173
|
+
if not _asyncio_loop_running():
|
|
174
|
+
return ask_fn(patch_stdout=patch_stdout)
|
|
175
|
+
|
|
176
|
+
return _run_questionary_in_thread(question, patch_stdout=patch_stdout)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _asyncio_loop_running() -> bool:
|
|
180
|
+
"""Return True when an asyncio event loop is already running."""
|
|
181
|
+
try:
|
|
182
|
+
asyncio.get_running_loop()
|
|
183
|
+
except RuntimeError:
|
|
184
|
+
return False
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _run_questionary_in_thread(question: Any, *, patch_stdout: bool = False) -> Any:
|
|
189
|
+
"""Execute a questionary prompt in a background thread."""
|
|
190
|
+
if getattr(question, "should_skip_question", False):
|
|
191
|
+
return getattr(question, "default", None)
|
|
192
|
+
|
|
193
|
+
application = getattr(question, "application", None)
|
|
194
|
+
run_callable = getattr(application, "run", None) if application is not None else None
|
|
195
|
+
if callable(run_callable):
|
|
196
|
+
try:
|
|
197
|
+
if patch_stdout:
|
|
198
|
+
from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
|
|
199
|
+
|
|
200
|
+
with pt_patch_stdout():
|
|
201
|
+
return run_callable(in_thread=True)
|
|
202
|
+
return run_callable(in_thread=True)
|
|
203
|
+
except TypeError:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
return question.unsafe_ask(patch_stdout=patch_stdout)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class _LiteralYamlDumper(yaml.SafeDumper):
|
|
210
|
+
"""YAML dumper that emits literal scalars for multiline strings."""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _literal_str_representer(dumper: yaml.Dumper, data: str) -> yaml.nodes.ScalarNode:
|
|
214
|
+
"""Represent strings in YAML, using literal blocks for verbose values."""
|
|
215
|
+
needs_literal = "\n" in data or "\r" in data
|
|
216
|
+
if not needs_literal and LITERAL_STRING_THRESHOLD and len(data) >= LITERAL_STRING_THRESHOLD: # pragma: no cover
|
|
217
|
+
needs_literal = True
|
|
218
|
+
|
|
219
|
+
style = "|" if needs_literal else None
|
|
220
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
_LiteralYamlDumper.add_representer(str, _literal_str_representer)
|
|
36
224
|
|
|
37
225
|
# Optional interactive deps (fuzzy palette)
|
|
38
226
|
try:
|
|
@@ -56,22 +244,6 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
56
244
|
|
|
57
245
|
if TYPE_CHECKING: # pragma: no cover - import-only during type checking
|
|
58
246
|
from glaip_sdk import Client
|
|
59
|
-
from glaip_sdk.cli import masking, pager
|
|
60
|
-
from glaip_sdk.cli.config import load_config
|
|
61
|
-
from glaip_sdk.cli.context import (
|
|
62
|
-
_get_view,
|
|
63
|
-
get_ctx_value,
|
|
64
|
-
)
|
|
65
|
-
from glaip_sdk.cli.context import (
|
|
66
|
-
detect_export_format as _detect_export_format,
|
|
67
|
-
)
|
|
68
|
-
from glaip_sdk.rich_components import AIPTable
|
|
69
|
-
from glaip_sdk.utils import is_uuid
|
|
70
|
-
from glaip_sdk.utils.rendering.renderer import (
|
|
71
|
-
CapturingConsole,
|
|
72
|
-
RendererConfig,
|
|
73
|
-
RichStreamRenderer,
|
|
74
|
-
)
|
|
75
247
|
|
|
76
248
|
console = Console()
|
|
77
249
|
pager.console = console
|
|
@@ -144,8 +316,6 @@ def format_datetime_fields(
|
|
|
144
316
|
Returns:
|
|
145
317
|
New dictionary with formatted datetime fields
|
|
146
318
|
"""
|
|
147
|
-
from glaip_sdk.utils import format_datetime
|
|
148
|
-
|
|
149
319
|
formatted = data.copy()
|
|
150
320
|
for field in fields:
|
|
151
321
|
if field in formatted:
|
|
@@ -208,10 +378,6 @@ def handle_resource_export(
|
|
|
208
378
|
get_by_id_func: Function to fetch resource by ID
|
|
209
379
|
console_override: Optional console override
|
|
210
380
|
"""
|
|
211
|
-
from glaip_sdk.cli.display import handle_rich_output
|
|
212
|
-
from glaip_sdk.cli.io import export_resource_to_file_with_validation
|
|
213
|
-
from glaip_sdk.cli.rich_helpers import print_markup
|
|
214
|
-
|
|
215
381
|
active_console = console_override or console
|
|
216
382
|
|
|
217
383
|
# Auto-detect format from file extension
|
|
@@ -235,6 +401,8 @@ def handle_resource_export(
|
|
|
235
401
|
):
|
|
236
402
|
export_resource_to_file_with_validation(full_resource, export_path, detected_format)
|
|
237
403
|
except Exception:
|
|
404
|
+
from glaip_sdk.cli.display import handle_rich_output # noqa: E402 - avoid circular import
|
|
405
|
+
|
|
238
406
|
handle_rich_output(
|
|
239
407
|
ctx,
|
|
240
408
|
markup_text(f"[{WARNING_STYLE}]⚠️ Failed to fetch full details, using available data[/]"),
|
|
@@ -329,6 +497,28 @@ def sdk_version() -> str:
|
|
|
329
497
|
return "0.0.0"
|
|
330
498
|
|
|
331
499
|
|
|
500
|
+
@contextmanager
|
|
501
|
+
def with_client_and_spinner(
|
|
502
|
+
ctx: Any,
|
|
503
|
+
spinner_message: str,
|
|
504
|
+
*,
|
|
505
|
+
console_override: Console | None = None,
|
|
506
|
+
) -> Any:
|
|
507
|
+
"""Context manager for commands that need client and spinner.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
ctx: Click context.
|
|
511
|
+
spinner_message: Message to display in spinner.
|
|
512
|
+
console_override: Optional console override.
|
|
513
|
+
|
|
514
|
+
Yields:
|
|
515
|
+
Client instance.
|
|
516
|
+
"""
|
|
517
|
+
client = get_client(ctx)
|
|
518
|
+
with spinner_context(ctx, spinner_message, console_override=console_override):
|
|
519
|
+
yield client
|
|
520
|
+
|
|
521
|
+
|
|
332
522
|
def spinner_context(
|
|
333
523
|
ctx: Any | None,
|
|
334
524
|
message: str,
|
|
@@ -602,6 +792,7 @@ def _prompt_with_auto_select(
|
|
|
602
792
|
valid_choices = set(choices)
|
|
603
793
|
|
|
604
794
|
def _auto_select(_: Buffer) -> None:
|
|
795
|
+
"""Auto-select text when a valid choice is entered."""
|
|
605
796
|
text = buffer.text
|
|
606
797
|
if not text or text not in valid_choices:
|
|
607
798
|
return
|
|
@@ -635,9 +826,23 @@ class _FuzzyCompleter:
|
|
|
635
826
|
"""Fuzzy completer for prompt_toolkit."""
|
|
636
827
|
|
|
637
828
|
def __init__(self, words: list[str]) -> None:
|
|
829
|
+
"""Initialize fuzzy completer with word list.
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
words: List of words to complete from.
|
|
833
|
+
"""
|
|
638
834
|
self.words = words
|
|
639
835
|
|
|
640
836
|
def get_completions(self, document: Any, _complete_event: Any) -> Any: # pragma: no cover
|
|
837
|
+
"""Get fuzzy completions for the current word.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
document: Document object from prompt_toolkit.
|
|
841
|
+
_complete_event: Completion event (unused).
|
|
842
|
+
|
|
843
|
+
Yields:
|
|
844
|
+
Completion objects matching the current word.
|
|
845
|
+
"""
|
|
641
846
|
word = document.get_word_before_cursor()
|
|
642
847
|
if not word:
|
|
643
848
|
return
|
|
@@ -769,7 +974,7 @@ def _fuzzy_score(search: str, target: str) -> int:
|
|
|
769
974
|
return score
|
|
770
975
|
|
|
771
976
|
|
|
772
|
-
#
|
|
977
|
+
# ----------------------- Structured renderer helpers -------------------- #
|
|
773
978
|
|
|
774
979
|
|
|
775
980
|
def _coerce_result_payload(result: Any) -> Any:
|
|
@@ -811,6 +1016,37 @@ def _render_markdown_output(data: Any) -> None:
|
|
|
811
1016
|
click.echo(str(data))
|
|
812
1017
|
|
|
813
1018
|
|
|
1019
|
+
def _format_yaml_text(data: Any) -> str:
|
|
1020
|
+
"""Convert structured payloads to YAML for readability."""
|
|
1021
|
+
try:
|
|
1022
|
+
yaml_text = yaml.dump(
|
|
1023
|
+
data,
|
|
1024
|
+
sort_keys=False,
|
|
1025
|
+
default_flow_style=False,
|
|
1026
|
+
allow_unicode=True,
|
|
1027
|
+
Dumper=_LiteralYamlDumper,
|
|
1028
|
+
)
|
|
1029
|
+
except Exception: # pragma: no cover - defensive YAML fallback
|
|
1030
|
+
try:
|
|
1031
|
+
return str(data)
|
|
1032
|
+
except Exception: # pragma: no cover - defensive str fallback
|
|
1033
|
+
return repr(data)
|
|
1034
|
+
|
|
1035
|
+
yaml_text = yaml_text.rstrip()
|
|
1036
|
+
if yaml_text.endswith("..."): # pragma: no cover - defensive YAML cleanup
|
|
1037
|
+
yaml_text = yaml_text[:-3].rstrip()
|
|
1038
|
+
return yaml_text
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
def _build_yaml_renderable(data: Any) -> Any:
|
|
1042
|
+
"""Return a syntax-highlighted YAML renderable when possible."""
|
|
1043
|
+
yaml_text = _format_yaml_text(data) or "# No data"
|
|
1044
|
+
try:
|
|
1045
|
+
return Syntax(yaml_text, "yaml", word_wrap=False)
|
|
1046
|
+
except Exception: # pragma: no cover - defensive syntax highlighting fallback
|
|
1047
|
+
return yaml_text
|
|
1048
|
+
|
|
1049
|
+
|
|
814
1050
|
def output_result(
|
|
815
1051
|
ctx: Any,
|
|
816
1052
|
result: Any,
|
|
@@ -843,7 +1079,7 @@ def output_result(
|
|
|
843
1079
|
_render_markdown_output(data)
|
|
844
1080
|
return
|
|
845
1081
|
|
|
846
|
-
renderable =
|
|
1082
|
+
renderable = _build_yaml_renderable(data)
|
|
847
1083
|
if panel_title:
|
|
848
1084
|
console.print(AIPPanel(renderable, title=panel_title))
|
|
849
1085
|
else:
|
|
@@ -854,7 +1090,7 @@ def output_result(
|
|
|
854
1090
|
# ----------------------------- List rendering ---------------------------- #
|
|
855
1091
|
|
|
856
1092
|
# Threshold no longer used - fuzzy palette is always default for TTY
|
|
857
|
-
# _PICK_THRESHOLD =
|
|
1093
|
+
# _PICK_THRESHOLD = 5
|
|
858
1094
|
|
|
859
1095
|
|
|
860
1096
|
def _normalise_rows(items: list[Any], transform_func: Callable[[Any], dict[str, Any]] | None) -> list[dict[str, Any]]:
|
|
@@ -902,12 +1138,7 @@ def _render_markdown_list(rows: list[dict[str, Any]], title: str, columns: list[
|
|
|
902
1138
|
|
|
903
1139
|
def _should_sort_rows(rows: list[dict[str, Any]]) -> bool:
|
|
904
1140
|
"""Return True when rows should be name-sorted prior to rendering."""
|
|
905
|
-
return (
|
|
906
|
-
os.getenv("AIP_TABLE_NO_SORT", "0") not in ("1", "true", "on")
|
|
907
|
-
and rows
|
|
908
|
-
and isinstance(rows[0], dict)
|
|
909
|
-
and "name" in rows[0]
|
|
910
|
-
)
|
|
1141
|
+
return TABLE_SORT_ENABLED and rows and isinstance(rows[0], dict) and "name" in rows[0]
|
|
911
1142
|
|
|
912
1143
|
|
|
913
1144
|
def _create_table(columns: list[tuple[str, str, str, int | None]], title: str) -> Any:
|
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
|
@@ -209,6 +209,11 @@ class AgentListParams:
|
|
|
209
209
|
return params
|
|
210
210
|
|
|
211
211
|
def _base_filter_params(self) -> dict[str, Any]:
|
|
212
|
+
"""Build base filter parameters from non-None fields.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dictionary of filter parameters with non-None values.
|
|
216
|
+
"""
|
|
212
217
|
return {
|
|
213
218
|
key: value
|
|
214
219
|
for key, value in (
|
|
@@ -221,6 +226,11 @@ class AgentListParams:
|
|
|
221
226
|
}
|
|
222
227
|
|
|
223
228
|
def _apply_pagination_params(self, params: dict[str, Any]) -> None:
|
|
229
|
+
"""Apply pagination parameters to the params dictionary.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
params: Dictionary to update with pagination parameters.
|
|
233
|
+
"""
|
|
224
234
|
if self.limit is not None:
|
|
225
235
|
if not 1 <= self.limit <= 100:
|
|
226
236
|
raise ValueError("limit must be between 1 and 100 inclusive")
|
|
@@ -238,6 +248,11 @@ class AgentListParams:
|
|
|
238
248
|
params["sync_langflow_agents"] = str(self.sync_langflow_agents).lower()
|
|
239
249
|
|
|
240
250
|
def _apply_timestamp_filters(self, params: dict[str, Any]) -> None:
|
|
251
|
+
"""Apply timestamp filter parameters to the params dictionary.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
params: Dictionary to update with timestamp filter parameters.
|
|
255
|
+
"""
|
|
241
256
|
timestamp_filters = {
|
|
242
257
|
"created_at_start": self.created_at_start,
|
|
243
258
|
"created_at_end": self.created_at_end,
|
|
@@ -249,6 +264,11 @@ class AgentListParams:
|
|
|
249
264
|
params[key] = value
|
|
250
265
|
|
|
251
266
|
def _apply_metadata_filters(self, params: dict[str, Any]) -> None:
|
|
267
|
+
"""Apply metadata filter parameters to the params dictionary.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
params: Dictionary to update with metadata filter parameters.
|
|
271
|
+
"""
|
|
252
272
|
if not self.metadata:
|
|
253
273
|
return
|
|
254
274
|
for key, value in self.metadata.items():
|
|
@@ -269,12 +289,22 @@ class AgentListResult:
|
|
|
269
289
|
message: str | None = None
|
|
270
290
|
|
|
271
291
|
def __len__(self) -> int: # pragma: no cover - simple delegation
|
|
292
|
+
"""Return the number of items in the result list."""
|
|
272
293
|
return len(self.items)
|
|
273
294
|
|
|
274
295
|
def __iter__(self): # pragma: no cover - simple delegation
|
|
296
|
+
"""Return an iterator over the items in the result list."""
|
|
275
297
|
return iter(self.items)
|
|
276
298
|
|
|
277
299
|
def __getitem__(self, index: int) -> Any: # pragma: no cover - simple delegation
|
|
300
|
+
"""Get an item from the result list by index.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
index: Index of the item to retrieve.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
The item at the specified index.
|
|
307
|
+
"""
|
|
278
308
|
return self.items[index]
|
|
279
309
|
|
|
280
310
|
|
|
@@ -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)
|