glaip-sdk 0.2.2__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/cli/commands/agents.py +50 -35
- glaip_sdk/cli/commands/mcps.py +28 -18
- glaip_sdk/cli/commands/models.py +3 -5
- glaip_sdk/cli/commands/tools.py +27 -16
- glaip_sdk/cli/constants.py +3 -0
- glaip_sdk/cli/main.py +1 -3
- glaip_sdk/cli/slash/agent_session.py +3 -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 +138 -47
- 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 +6 -32
- glaip_sdk/cli/utils.py +183 -9
- 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 +42 -22
- glaip_sdk/client/main.py +18 -6
- glaip_sdk/client/mcps.py +2 -4
- 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-0.2.2.dist-info → glaip_sdk-0.3.0.dist-info}/METADATA +24 -2
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.3.0.dist-info}/RECORD +38 -31
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.3.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.2.2.dist-info → glaip_sdk-0.3.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/utils.py
CHANGED
|
@@ -12,8 +12,9 @@ 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
|
|
|
@@ -39,10 +40,11 @@ from glaip_sdk.cli.context import (
|
|
|
39
40
|
detect_export_format as _detect_export_format,
|
|
40
41
|
get_ctx_value,
|
|
41
42
|
)
|
|
42
|
-
from glaip_sdk.cli.
|
|
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
|
|
43
45
|
from glaip_sdk.icons import ICON_AGENT
|
|
44
46
|
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
45
|
-
from glaip_sdk.utils import is_uuid
|
|
47
|
+
from glaip_sdk.utils import format_datetime, is_uuid
|
|
46
48
|
from glaip_sdk.utils.rendering.renderer import (
|
|
47
49
|
CapturingConsole,
|
|
48
50
|
RendererConfig,
|
|
@@ -50,6 +52,160 @@ from glaip_sdk.utils.rendering.renderer import (
|
|
|
50
52
|
)
|
|
51
53
|
|
|
52
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
|
+
|
|
53
209
|
class _LiteralYamlDumper(yaml.SafeDumper):
|
|
54
210
|
"""YAML dumper that emits literal scalars for multiline strings."""
|
|
55
211
|
|
|
@@ -160,8 +316,6 @@ def format_datetime_fields(
|
|
|
160
316
|
Returns:
|
|
161
317
|
New dictionary with formatted datetime fields
|
|
162
318
|
"""
|
|
163
|
-
from glaip_sdk.utils import format_datetime
|
|
164
|
-
|
|
165
319
|
formatted = data.copy()
|
|
166
320
|
for field in fields:
|
|
167
321
|
if field in formatted:
|
|
@@ -224,10 +378,6 @@ def handle_resource_export(
|
|
|
224
378
|
get_by_id_func: Function to fetch resource by ID
|
|
225
379
|
console_override: Optional console override
|
|
226
380
|
"""
|
|
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
381
|
active_console = console_override or console
|
|
232
382
|
|
|
233
383
|
# Auto-detect format from file extension
|
|
@@ -251,6 +401,8 @@ def handle_resource_export(
|
|
|
251
401
|
):
|
|
252
402
|
export_resource_to_file_with_validation(full_resource, export_path, detected_format)
|
|
253
403
|
except Exception:
|
|
404
|
+
from glaip_sdk.cli.display import handle_rich_output # noqa: E402 - avoid circular import
|
|
405
|
+
|
|
254
406
|
handle_rich_output(
|
|
255
407
|
ctx,
|
|
256
408
|
markup_text(f"[{WARNING_STYLE}]⚠️ Failed to fetch full details, using available data[/]"),
|
|
@@ -345,6 +497,28 @@ def sdk_version() -> str:
|
|
|
345
497
|
return "0.0.0"
|
|
346
498
|
|
|
347
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
|
+
|
|
348
522
|
def spinner_context(
|
|
349
523
|
ctx: Any | None,
|
|
350
524
|
message: str,
|
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)
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -5,9 +5,11 @@ Authors:
|
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import asyncio
|
|
8
9
|
import json
|
|
9
10
|
import logging
|
|
10
11
|
from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
11
13
|
from os import PathLike
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from typing import Any, BinaryIO
|
|
@@ -20,6 +22,7 @@ from glaip_sdk.client._agent_payloads import (
|
|
|
20
22
|
AgentListResult,
|
|
21
23
|
AgentUpdateRequest,
|
|
22
24
|
)
|
|
25
|
+
from glaip_sdk.client.agent_runs import AgentRunsClient
|
|
23
26
|
from glaip_sdk.client.base import BaseClient
|
|
24
27
|
from glaip_sdk.client.mcps import MCPClient
|
|
25
28
|
from glaip_sdk.client.run_rendering import (
|
|
@@ -28,6 +31,7 @@ from glaip_sdk.client.run_rendering import (
|
|
|
28
31
|
)
|
|
29
32
|
from glaip_sdk.client.tools import ToolClient
|
|
30
33
|
from glaip_sdk.config.constants import (
|
|
34
|
+
AGENT_CONFIG_FIELDS,
|
|
31
35
|
DEFAULT_AGENT_FRAMEWORK,
|
|
32
36
|
DEFAULT_AGENT_RUN_TIMEOUT,
|
|
33
37
|
DEFAULT_AGENT_TYPE,
|
|
@@ -67,6 +71,19 @@ _MERGED_SEQUENCE_FIELDS = ("tools", "agents", "mcps")
|
|
|
67
71
|
_DEFAULT_METADATA_TYPE = "custom"
|
|
68
72
|
|
|
69
73
|
|
|
74
|
+
@asynccontextmanager
|
|
75
|
+
async def _async_timeout_guard(timeout_seconds: float | None) -> AsyncGenerator[None, None]:
|
|
76
|
+
"""Apply an asyncio timeout when a custom timeout is provided."""
|
|
77
|
+
if timeout_seconds is None:
|
|
78
|
+
yield
|
|
79
|
+
return
|
|
80
|
+
try:
|
|
81
|
+
async with asyncio.timeout(timeout_seconds):
|
|
82
|
+
yield
|
|
83
|
+
except asyncio.TimeoutError as exc:
|
|
84
|
+
raise httpx.TimeoutException(f"Request timed out after {timeout_seconds}s") from exc
|
|
85
|
+
|
|
86
|
+
|
|
70
87
|
def _normalise_sequence(value: Any) -> list[Any] | None:
|
|
71
88
|
"""Normalise optional sequence inputs to plain lists."""
|
|
72
89
|
if value is None:
|
|
@@ -193,19 +210,7 @@ def _extract_original_refs(raw_definition: dict) -> dict[str, list]:
|
|
|
193
210
|
|
|
194
211
|
def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
|
|
195
212
|
"""Build CLI args from overrides, filtering out None values."""
|
|
196
|
-
cli_args = {
|
|
197
|
-
key: overrides_dict.get(key)
|
|
198
|
-
for key in (
|
|
199
|
-
"name",
|
|
200
|
-
"instruction",
|
|
201
|
-
"model",
|
|
202
|
-
"tools",
|
|
203
|
-
"agents",
|
|
204
|
-
"mcps",
|
|
205
|
-
"timeout",
|
|
206
|
-
)
|
|
207
|
-
if overrides_dict.get(key) is not None
|
|
208
|
-
}
|
|
213
|
+
cli_args = {key: overrides_dict.get(key) for key in AGENT_CONFIG_FIELDS if overrides_dict.get(key) is not None}
|
|
209
214
|
|
|
210
215
|
# Normalize sequence fields
|
|
211
216
|
for field in _MERGED_SEQUENCE_FIELDS:
|
|
@@ -254,6 +259,7 @@ class AgentClient(BaseClient):
|
|
|
254
259
|
self._renderer_manager = AgentRunRenderingManager(logger)
|
|
255
260
|
self._tool_client: ToolClient | None = None
|
|
256
261
|
self._mcp_client: MCPClient | None = None
|
|
262
|
+
self._runs_client: "AgentRunsClient | None" = None
|
|
257
263
|
|
|
258
264
|
def list_agents(
|
|
259
265
|
self,
|
|
@@ -1172,7 +1178,7 @@ class AgentClient(BaseClient):
|
|
|
1172
1178
|
message: str,
|
|
1173
1179
|
files: list[str | BinaryIO] | None = None,
|
|
1174
1180
|
*,
|
|
1175
|
-
|
|
1181
|
+
request_timeout: float | None = None,
|
|
1176
1182
|
**kwargs,
|
|
1177
1183
|
) -> AsyncGenerator[dict, None]:
|
|
1178
1184
|
"""Async run an agent with a message, yielding streaming JSON chunks.
|
|
@@ -1181,7 +1187,7 @@ class AgentClient(BaseClient):
|
|
|
1181
1187
|
agent_id: ID of the agent to run
|
|
1182
1188
|
message: Message to send to the agent
|
|
1183
1189
|
files: Optional list of files to include
|
|
1184
|
-
|
|
1190
|
+
request_timeout: Optional request timeout in seconds (defaults to client timeout)
|
|
1185
1191
|
**kwargs: Additional arguments (chat_history, pii_mapping, etc.)
|
|
1186
1192
|
|
|
1187
1193
|
Yields:
|
|
@@ -1192,18 +1198,22 @@ class AgentClient(BaseClient):
|
|
|
1192
1198
|
httpx.TimeoutException: When general timeout occurs
|
|
1193
1199
|
Exception: For other unexpected errors
|
|
1194
1200
|
"""
|
|
1201
|
+
# Derive timeout values for request/control flow
|
|
1202
|
+
legacy_timeout = kwargs.get("timeout")
|
|
1203
|
+
http_timeout_override = request_timeout if request_timeout is not None else legacy_timeout
|
|
1204
|
+
http_timeout = http_timeout_override or self.timeout
|
|
1205
|
+
|
|
1195
1206
|
# Prepare request data
|
|
1196
1207
|
payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
|
|
1197
1208
|
|
|
1198
1209
|
# Create async client configuration
|
|
1199
|
-
async_client_config = self._create_async_client_config(
|
|
1210
|
+
async_client_config = self._create_async_client_config(http_timeout_override, headers)
|
|
1200
1211
|
|
|
1201
1212
|
# Get execution timeout for streaming control
|
|
1202
1213
|
timeout_seconds = kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
1203
1214
|
agent_name = kwargs.get("agent_name")
|
|
1204
1215
|
|
|
1205
|
-
|
|
1206
|
-
# Create async client and stream response
|
|
1216
|
+
async def _chunk_stream() -> AsyncGenerator[dict, None]:
|
|
1207
1217
|
async with httpx.AsyncClient(**async_client_config) as async_client:
|
|
1208
1218
|
async for chunk in self._stream_agent_response(
|
|
1209
1219
|
async_client,
|
|
@@ -1217,7 +1227,17 @@ class AgentClient(BaseClient):
|
|
|
1217
1227
|
):
|
|
1218
1228
|
yield chunk
|
|
1219
1229
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1230
|
+
async with _async_timeout_guard(http_timeout):
|
|
1231
|
+
async for chunk in _chunk_stream():
|
|
1232
|
+
yield chunk
|
|
1233
|
+
|
|
1234
|
+
@property
|
|
1235
|
+
def runs(self) -> "AgentRunsClient":
|
|
1236
|
+
"""Get the agent runs client."""
|
|
1237
|
+
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)
|
|
1242
|
+
self._runs_client = AgentRunsClient(**shared_config)
|
|
1243
|
+
return self._runs_client
|
glaip_sdk/client/main.py
CHANGED
|
@@ -14,6 +14,23 @@ from glaip_sdk.client.tools import ToolClient
|
|
|
14
14
|
from glaip_sdk.models import MCP, Agent, Tool
|
|
15
15
|
|
|
16
16
|
|
|
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
|
+
|
|
17
34
|
class Client(BaseClient):
|
|
18
35
|
"""Main client that composes all specialized clients and shares one HTTP session."""
|
|
19
36
|
|
|
@@ -25,12 +42,7 @@ class Client(BaseClient):
|
|
|
25
42
|
"""
|
|
26
43
|
super().__init__(**kwargs)
|
|
27
44
|
# Share the single httpx.Client + config with sub-clients
|
|
28
|
-
shared_config =
|
|
29
|
-
"parent_client": self,
|
|
30
|
-
"api_url": self.api_url,
|
|
31
|
-
"api_key": self.api_key,
|
|
32
|
-
"timeout": self._timeout,
|
|
33
|
-
}
|
|
45
|
+
shared_config = _build_shared_config(self)
|
|
34
46
|
self.agents = AgentClient(**shared_config)
|
|
35
47
|
self.tools = ToolClient(**shared_config)
|
|
36
48
|
self.mcps = MCPClient(**shared_config)
|
glaip_sdk/client/mcps.py
CHANGED
|
@@ -14,7 +14,7 @@ from glaip_sdk.config.constants import (
|
|
|
14
14
|
DEFAULT_MCP_TYPE,
|
|
15
15
|
)
|
|
16
16
|
from glaip_sdk.models import MCP
|
|
17
|
-
from glaip_sdk.utils.client_utils import create_model_instances, find_by_name
|
|
17
|
+
from glaip_sdk.utils.client_utils import add_kwargs_to_payload, create_model_instances, find_by_name
|
|
18
18
|
|
|
19
19
|
# API endpoints
|
|
20
20
|
MCPS_ENDPOINT = "/mcps/"
|
|
@@ -147,9 +147,7 @@ class MCPClient(BaseClient):
|
|
|
147
147
|
|
|
148
148
|
# Add any other kwargs (excluding already handled ones)
|
|
149
149
|
excluded_keys = {"type"} # type is handled above
|
|
150
|
-
|
|
151
|
-
if key not in excluded_keys:
|
|
152
|
-
payload[key] = value
|
|
150
|
+
add_kwargs_to_payload(payload, kwargs, excluded_keys)
|
|
153
151
|
|
|
154
152
|
return payload
|
|
155
153
|
|
glaip_sdk/client/tools.py
CHANGED
|
@@ -18,6 +18,7 @@ from glaip_sdk.config.constants import (
|
|
|
18
18
|
)
|
|
19
19
|
from glaip_sdk.models import Tool
|
|
20
20
|
from glaip_sdk.utils.client_utils import (
|
|
21
|
+
add_kwargs_to_payload,
|
|
21
22
|
create_model_instances,
|
|
22
23
|
find_by_name,
|
|
23
24
|
)
|
|
@@ -200,9 +201,7 @@ class ToolClient(BaseClient):
|
|
|
200
201
|
|
|
201
202
|
# Add any other kwargs (excluding already handled ones)
|
|
202
203
|
excluded_keys = {"tags", "version"}
|
|
203
|
-
|
|
204
|
-
if key not in excluded_keys:
|
|
205
|
-
payload[key] = value
|
|
204
|
+
add_kwargs_to_payload(payload, kwargs, excluded_keys)
|
|
206
205
|
|
|
207
206
|
return payload
|
|
208
207
|
|
glaip_sdk/config/constants.py
CHANGED
|
@@ -39,3 +39,14 @@ DEFAULT_MCP_TRANSPORT = "stdio"
|
|
|
39
39
|
|
|
40
40
|
# Default error messages
|
|
41
41
|
DEFAULT_ERROR_MESSAGE = "Unknown error"
|
|
42
|
+
|
|
43
|
+
# Agent configuration fields used for CLI args and payload building
|
|
44
|
+
AGENT_CONFIG_FIELDS = (
|
|
45
|
+
"name",
|
|
46
|
+
"instruction",
|
|
47
|
+
"model",
|
|
48
|
+
"tools",
|
|
49
|
+
"agents",
|
|
50
|
+
"mcps",
|
|
51
|
+
"timeout",
|
|
52
|
+
)
|