glaip-sdk 0.0.15__py3-none-any.whl → 0.0.17__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/__init__.py +1 -1
- glaip_sdk/branding.py +28 -2
- glaip_sdk/cli/commands/agents.py +36 -27
- glaip_sdk/cli/commands/configure.py +46 -52
- glaip_sdk/cli/commands/mcps.py +19 -22
- glaip_sdk/cli/commands/tools.py +19 -13
- glaip_sdk/cli/config.py +42 -0
- glaip_sdk/cli/display.py +97 -30
- glaip_sdk/cli/main.py +141 -124
- glaip_sdk/cli/mcp_validators.py +2 -2
- glaip_sdk/cli/pager.py +3 -2
- glaip_sdk/cli/parsers/json_input.py +2 -2
- glaip_sdk/cli/resolution.py +12 -10
- glaip_sdk/cli/rich_helpers.py +29 -0
- glaip_sdk/cli/slash/agent_session.py +7 -0
- glaip_sdk/cli/slash/prompt.py +21 -2
- glaip_sdk/cli/slash/session.py +15 -21
- glaip_sdk/cli/update_notifier.py +8 -2
- glaip_sdk/cli/utils.py +115 -58
- glaip_sdk/client/_agent_payloads.py +504 -0
- glaip_sdk/client/agents.py +633 -559
- glaip_sdk/client/base.py +92 -20
- glaip_sdk/client/main.py +14 -0
- glaip_sdk/client/run_rendering.py +275 -0
- glaip_sdk/config/constants.py +4 -1
- glaip_sdk/exceptions.py +15 -0
- glaip_sdk/models.py +5 -0
- glaip_sdk/payload_schemas/__init__.py +19 -0
- glaip_sdk/payload_schemas/agent.py +87 -0
- glaip_sdk/rich_components.py +12 -0
- glaip_sdk/utils/client_utils.py +12 -0
- glaip_sdk/utils/import_export.py +2 -2
- glaip_sdk/utils/rendering/formatting.py +5 -0
- glaip_sdk/utils/rendering/models.py +22 -0
- glaip_sdk/utils/rendering/renderer/base.py +9 -1
- glaip_sdk/utils/rendering/renderer/panels.py +0 -1
- glaip_sdk/utils/rendering/steps.py +59 -0
- glaip_sdk/utils/serialization.py +24 -3
- {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/METADATA +2 -2
- glaip_sdk-0.0.17.dist-info/RECORD +73 -0
- glaip_sdk-0.0.15.dist-info/RECORD +0 -67
- {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.15.dist-info → glaip_sdk-0.0.17.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/base.py
CHANGED
|
@@ -14,7 +14,7 @@ from dotenv import load_dotenv
|
|
|
14
14
|
|
|
15
15
|
import glaip_sdk
|
|
16
16
|
from glaip_sdk._version import __version__ as SDK_VERSION
|
|
17
|
-
from glaip_sdk.config.constants import SDK_NAME
|
|
17
|
+
from glaip_sdk.config.constants import DEFAULT_ERROR_MESSAGE, SDK_NAME
|
|
18
18
|
from glaip_sdk.exceptions import (
|
|
19
19
|
AuthenticationError,
|
|
20
20
|
ConflictError,
|
|
@@ -230,8 +230,13 @@ class BaseClient:
|
|
|
230
230
|
else:
|
|
231
231
|
raise
|
|
232
232
|
|
|
233
|
-
def
|
|
234
|
-
|
|
233
|
+
def _perform_request(
|
|
234
|
+
self,
|
|
235
|
+
method: str,
|
|
236
|
+
endpoint: str,
|
|
237
|
+
**kwargs: Any,
|
|
238
|
+
) -> httpx.Response:
|
|
239
|
+
"""Execute a raw HTTP request with retry handling."""
|
|
235
240
|
# Ensure client is alive before making request
|
|
236
241
|
self._ensure_client_alive()
|
|
237
242
|
|
|
@@ -239,7 +244,7 @@ class BaseClient:
|
|
|
239
244
|
try:
|
|
240
245
|
response = self.http_client.request(method, endpoint, **kwargs)
|
|
241
246
|
client_log.debug(f"Response status: {response.status_code}")
|
|
242
|
-
return
|
|
247
|
+
return response
|
|
243
248
|
except httpx.ConnectError as e:
|
|
244
249
|
client_log.warning(
|
|
245
250
|
f"Connection error on {method} {endpoint}, retrying once: {e}"
|
|
@@ -249,11 +254,26 @@ class BaseClient:
|
|
|
249
254
|
client_log.debug(
|
|
250
255
|
f"Retry successful, response status: {response.status_code}"
|
|
251
256
|
)
|
|
252
|
-
return
|
|
257
|
+
return response
|
|
253
258
|
except httpx.ConnectError:
|
|
254
259
|
client_log.error(f"Retry failed for {method} {endpoint}: {e}")
|
|
255
260
|
raise e
|
|
256
261
|
|
|
262
|
+
def _request(self, method: str, endpoint: str, **kwargs) -> Any:
|
|
263
|
+
"""Make HTTP request with error handling and unwrap success envelopes."""
|
|
264
|
+
response = self._perform_request(method, endpoint, **kwargs)
|
|
265
|
+
return self._handle_response(response, unwrap=True)
|
|
266
|
+
|
|
267
|
+
def _request_with_envelope(
|
|
268
|
+
self,
|
|
269
|
+
method: str,
|
|
270
|
+
endpoint: str,
|
|
271
|
+
**kwargs: Any,
|
|
272
|
+
) -> Any:
|
|
273
|
+
"""Make HTTP request but return the full success envelope."""
|
|
274
|
+
response = self._perform_request(method, endpoint, **kwargs)
|
|
275
|
+
return self._handle_response(response, unwrap=False)
|
|
276
|
+
|
|
257
277
|
def _parse_response_content(self, response: httpx.Response) -> Any | None:
|
|
258
278
|
"""Parse response content based on content type."""
|
|
259
279
|
if response.status_code == 204:
|
|
@@ -271,14 +291,14 @@ class BaseClient:
|
|
|
271
291
|
else:
|
|
272
292
|
return None # Let _handle_response deal with error status codes
|
|
273
293
|
|
|
274
|
-
def _handle_success_response(self, parsed: Any) -> Any:
|
|
294
|
+
def _handle_success_response(self, parsed: Any, *, unwrap: bool) -> Any:
|
|
275
295
|
"""Handle successful response with success flag."""
|
|
276
296
|
if isinstance(parsed, dict) and "success" in parsed:
|
|
277
297
|
if parsed.get("success"):
|
|
278
|
-
return parsed.get("data", parsed)
|
|
298
|
+
return parsed.get("data", parsed) if unwrap else parsed
|
|
279
299
|
else:
|
|
280
300
|
error_type = parsed.get("error", "UnknownError")
|
|
281
|
-
message = parsed.get("message",
|
|
301
|
+
message = parsed.get("message", DEFAULT_ERROR_MESSAGE)
|
|
282
302
|
self._raise_api_error(
|
|
283
303
|
400,
|
|
284
304
|
message,
|
|
@@ -290,19 +310,67 @@ class BaseClient:
|
|
|
290
310
|
|
|
291
311
|
def _get_error_message(self, response: httpx.Response) -> str:
|
|
292
312
|
"""Extract error message from response, preferring parsed content."""
|
|
293
|
-
|
|
294
|
-
|
|
313
|
+
parsed = self._parse_error_json(response)
|
|
314
|
+
if parsed is None:
|
|
315
|
+
return response.text
|
|
316
|
+
|
|
317
|
+
formatted = self._format_parsed_error(parsed)
|
|
318
|
+
return formatted if formatted is not None else response.text
|
|
319
|
+
|
|
320
|
+
def _parse_error_json(self, response: httpx.Response) -> Any | None:
|
|
321
|
+
"""Safely parse JSON from an error response."""
|
|
295
322
|
try:
|
|
296
|
-
|
|
297
|
-
if isinstance(parsed, dict) and "message" in parsed:
|
|
298
|
-
error_message = parsed["message"]
|
|
299
|
-
elif isinstance(parsed, str):
|
|
300
|
-
error_message = parsed
|
|
323
|
+
return response.json()
|
|
301
324
|
except (ValueError, TypeError):
|
|
302
|
-
|
|
303
|
-
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
def _format_parsed_error(self, parsed: Any) -> str | None:
|
|
328
|
+
"""Build a readable error message from parsed JSON payloads."""
|
|
329
|
+
if isinstance(parsed, dict):
|
|
330
|
+
return self._format_error_dict(parsed)
|
|
331
|
+
if isinstance(parsed, str):
|
|
332
|
+
return parsed
|
|
333
|
+
return str(parsed) if parsed else None
|
|
334
|
+
|
|
335
|
+
def _format_error_dict(self, parsed: dict[str, Any]) -> str:
|
|
336
|
+
"""Format structured API error payloads."""
|
|
337
|
+
detail = parsed.get("detail")
|
|
338
|
+
if isinstance(detail, list):
|
|
339
|
+
validation_message = self._format_validation_errors(detail)
|
|
340
|
+
if validation_message:
|
|
341
|
+
return validation_message
|
|
342
|
+
return f"Validation error: {parsed}"
|
|
343
|
+
|
|
344
|
+
message = parsed.get("message")
|
|
345
|
+
if message:
|
|
346
|
+
return message
|
|
347
|
+
|
|
348
|
+
return str(parsed) if parsed else DEFAULT_ERROR_MESSAGE
|
|
349
|
+
|
|
350
|
+
def _format_validation_errors(self, errors: list[Any]) -> str | None:
|
|
351
|
+
"""Render validation errors into a human-readable string."""
|
|
352
|
+
entries: list[str] = []
|
|
353
|
+
for error in errors:
|
|
354
|
+
if isinstance(error, dict):
|
|
355
|
+
loc = " -> ".join(str(x) for x in error.get("loc", []))
|
|
356
|
+
msg = error.get("msg", DEFAULT_ERROR_MESSAGE)
|
|
357
|
+
error_type = error.get("type", "unknown")
|
|
358
|
+
prefix = loc if loc else "Field"
|
|
359
|
+
entries.append(f" {prefix}: {msg} ({error_type})")
|
|
360
|
+
else:
|
|
361
|
+
entries.append(f" {error}")
|
|
362
|
+
|
|
363
|
+
if not entries:
|
|
364
|
+
return None
|
|
304
365
|
|
|
305
|
-
|
|
366
|
+
return "Validation errors:\n" + "\n".join(entries)
|
|
367
|
+
|
|
368
|
+
def _handle_response(
|
|
369
|
+
self,
|
|
370
|
+
response: httpx.Response,
|
|
371
|
+
*,
|
|
372
|
+
unwrap: bool = True,
|
|
373
|
+
) -> Any:
|
|
306
374
|
"""Handle HTTP response with proper error handling."""
|
|
307
375
|
# Handle no-content success before general error handling
|
|
308
376
|
if response.status_code == 204:
|
|
@@ -311,14 +379,18 @@ class BaseClient:
|
|
|
311
379
|
# Handle error status codes
|
|
312
380
|
if not (200 <= response.status_code < 300):
|
|
313
381
|
error_message = self._get_error_message(response)
|
|
314
|
-
|
|
382
|
+
# Try to parse response content for payload
|
|
383
|
+
parsed_content = self._parse_response_content(response)
|
|
384
|
+
self._raise_api_error(
|
|
385
|
+
response.status_code, error_message, payload=parsed_content
|
|
386
|
+
)
|
|
315
387
|
return None # Won't be reached but helps with type checking
|
|
316
388
|
|
|
317
389
|
parsed = self._parse_response_content(response)
|
|
318
390
|
if parsed is None:
|
|
319
391
|
return None
|
|
320
392
|
|
|
321
|
-
return self._handle_success_response(parsed)
|
|
393
|
+
return self._handle_success_response(parsed, unwrap=unwrap)
|
|
322
394
|
|
|
323
395
|
def _raise_api_error(
|
|
324
396
|
self,
|
glaip_sdk/client/main.py
CHANGED
|
@@ -18,6 +18,11 @@ class Client(BaseClient):
|
|
|
18
18
|
"""Main client that composes all specialized clients and shares one HTTP session."""
|
|
19
19
|
|
|
20
20
|
def __init__(self, **kwargs):
|
|
21
|
+
"""Initialize the main client.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
**kwargs: Client configuration arguments (api_url, api_key, timeout, etc.)
|
|
25
|
+
"""
|
|
21
26
|
super().__init__(**kwargs)
|
|
22
27
|
# Share the single httpx.Client + config with sub-clients
|
|
23
28
|
shared_config = {
|
|
@@ -37,6 +42,10 @@ class Client(BaseClient):
|
|
|
37
42
|
"""Create a new agent."""
|
|
38
43
|
return self.agents.create_agent(**kwargs)
|
|
39
44
|
|
|
45
|
+
def create_agent_from_file(self, *args, **kwargs) -> Agent:
|
|
46
|
+
"""Create a new agent from a JSON or YAML configuration file."""
|
|
47
|
+
return self.agents.create_agent_from_file(*args, **kwargs)
|
|
48
|
+
|
|
40
49
|
def list_agents(
|
|
41
50
|
self,
|
|
42
51
|
agent_type: str | None = None,
|
|
@@ -81,6 +90,10 @@ class Client(BaseClient):
|
|
|
81
90
|
"""Update an existing agent."""
|
|
82
91
|
return self.agents.update_agent(agent_id, **kwargs)
|
|
83
92
|
|
|
93
|
+
def update_agent_from_file(self, agent_id: str, *args, **kwargs) -> Agent:
|
|
94
|
+
"""Update an existing agent using a JSON or YAML configuration file."""
|
|
95
|
+
return self.agents.update_agent_from_file(agent_id, *args, **kwargs)
|
|
96
|
+
|
|
84
97
|
def delete_agent(self, agent_id: str) -> bool:
|
|
85
98
|
"""Delete an agent."""
|
|
86
99
|
return self.agents.delete_agent(agent_id)
|
|
@@ -199,6 +212,7 @@ class Client(BaseClient):
|
|
|
199
212
|
# ---- Timeout propagation ----
|
|
200
213
|
@property
|
|
201
214
|
def timeout(self) -> float: # type: ignore[override]
|
|
215
|
+
"""Get the client timeout value."""
|
|
202
216
|
return super().timeout
|
|
203
217
|
|
|
204
218
|
@timeout.setter
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Rendering helpers for agent streaming flows.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from time import monotonic
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
from rich.console import Console as _Console
|
|
18
|
+
|
|
19
|
+
from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
|
|
20
|
+
from glaip_sdk.utils.client_utils import iter_sse_events
|
|
21
|
+
from glaip_sdk.utils.rendering.models import RunStats
|
|
22
|
+
from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
|
|
23
|
+
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AgentRunRenderingManager:
|
|
27
|
+
"""Coordinate renderer creation and streaming event handling."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, logger: logging.Logger | None = None) -> None:
|
|
30
|
+
"""Initialize the rendering manager.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
logger: Optional logger instance, creates default if None
|
|
34
|
+
"""
|
|
35
|
+
self._logger = logger or logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# --------------------------------------------------------------------- #
|
|
38
|
+
# Renderer setup helpers
|
|
39
|
+
# --------------------------------------------------------------------- #
|
|
40
|
+
def create_renderer(
|
|
41
|
+
self,
|
|
42
|
+
renderer_spec: RichStreamRenderer | str | None,
|
|
43
|
+
*,
|
|
44
|
+
verbose: bool = False,
|
|
45
|
+
) -> RichStreamRenderer:
|
|
46
|
+
"""Create an appropriate renderer based on the supplied spec."""
|
|
47
|
+
if isinstance(renderer_spec, RichStreamRenderer):
|
|
48
|
+
return renderer_spec
|
|
49
|
+
|
|
50
|
+
if isinstance(renderer_spec, str):
|
|
51
|
+
if renderer_spec == "silent":
|
|
52
|
+
return self._create_silent_renderer()
|
|
53
|
+
if renderer_spec == "minimal":
|
|
54
|
+
return self._create_minimal_renderer()
|
|
55
|
+
return self._create_default_renderer(verbose)
|
|
56
|
+
|
|
57
|
+
return self._create_default_renderer(verbose)
|
|
58
|
+
|
|
59
|
+
def build_initial_metadata(
|
|
60
|
+
self,
|
|
61
|
+
agent_id: str,
|
|
62
|
+
message: str,
|
|
63
|
+
kwargs: dict[str, Any],
|
|
64
|
+
) -> dict[str, Any]:
|
|
65
|
+
"""Construct the initial renderer metadata payload."""
|
|
66
|
+
return {
|
|
67
|
+
"agent_name": kwargs.get("agent_name", agent_id),
|
|
68
|
+
"model": kwargs.get("model"),
|
|
69
|
+
"run_id": None,
|
|
70
|
+
"input_message": message,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def start_renderer(renderer: RichStreamRenderer, meta: dict[str, Any]) -> None:
|
|
75
|
+
"""Notify renderer that streaming is starting."""
|
|
76
|
+
renderer.on_start(meta)
|
|
77
|
+
|
|
78
|
+
def _create_silent_renderer(self) -> RichStreamRenderer:
|
|
79
|
+
silent_config = RendererConfig(
|
|
80
|
+
live=False,
|
|
81
|
+
persist_live=False,
|
|
82
|
+
show_delegate_tool_panels=False,
|
|
83
|
+
render_thinking=False,
|
|
84
|
+
)
|
|
85
|
+
return RichStreamRenderer(
|
|
86
|
+
console=_Console(file=io.StringIO(), force_terminal=False),
|
|
87
|
+
cfg=silent_config,
|
|
88
|
+
verbose=False,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _create_minimal_renderer(self) -> RichStreamRenderer:
|
|
92
|
+
minimal_config = RendererConfig(
|
|
93
|
+
live=False,
|
|
94
|
+
persist_live=False,
|
|
95
|
+
show_delegate_tool_panels=False,
|
|
96
|
+
render_thinking=False,
|
|
97
|
+
)
|
|
98
|
+
return RichStreamRenderer(
|
|
99
|
+
console=_Console(),
|
|
100
|
+
cfg=minimal_config,
|
|
101
|
+
verbose=False,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _create_verbose_renderer(self) -> RichStreamRenderer:
|
|
105
|
+
verbose_config = RendererConfig(
|
|
106
|
+
theme="dark",
|
|
107
|
+
style="debug",
|
|
108
|
+
live=False,
|
|
109
|
+
show_delegate_tool_panels=True,
|
|
110
|
+
append_finished_snapshots=False,
|
|
111
|
+
)
|
|
112
|
+
return RichStreamRenderer(
|
|
113
|
+
console=_Console(),
|
|
114
|
+
cfg=verbose_config,
|
|
115
|
+
verbose=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
|
|
119
|
+
if verbose:
|
|
120
|
+
return self._create_verbose_renderer()
|
|
121
|
+
default_config = RendererConfig(show_delegate_tool_panels=True)
|
|
122
|
+
return RichStreamRenderer(console=_Console(), cfg=default_config)
|
|
123
|
+
|
|
124
|
+
# --------------------------------------------------------------------- #
|
|
125
|
+
# Streaming event handling
|
|
126
|
+
# --------------------------------------------------------------------- #
|
|
127
|
+
def process_stream_events(
|
|
128
|
+
self,
|
|
129
|
+
stream_response: httpx.Response,
|
|
130
|
+
renderer: RichStreamRenderer,
|
|
131
|
+
timeout_seconds: float,
|
|
132
|
+
agent_name: str | None,
|
|
133
|
+
meta: dict[str, Any],
|
|
134
|
+
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
135
|
+
"""Process streaming events and accumulate response."""
|
|
136
|
+
final_text = ""
|
|
137
|
+
stats_usage: dict[str, Any] = {}
|
|
138
|
+
started_monotonic: float | None = None
|
|
139
|
+
|
|
140
|
+
self._capture_request_id(stream_response, meta, renderer)
|
|
141
|
+
|
|
142
|
+
for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
|
|
143
|
+
if started_monotonic is None:
|
|
144
|
+
started_monotonic = self._maybe_start_timer(event)
|
|
145
|
+
|
|
146
|
+
final_text, stats_usage = self._process_single_event(
|
|
147
|
+
event,
|
|
148
|
+
renderer,
|
|
149
|
+
final_text,
|
|
150
|
+
stats_usage,
|
|
151
|
+
meta,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
finished_monotonic = monotonic()
|
|
155
|
+
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
156
|
+
|
|
157
|
+
def _capture_request_id(
|
|
158
|
+
self,
|
|
159
|
+
stream_response: httpx.Response,
|
|
160
|
+
meta: dict[str, Any],
|
|
161
|
+
renderer: RichStreamRenderer,
|
|
162
|
+
) -> None:
|
|
163
|
+
req_id = stream_response.headers.get(
|
|
164
|
+
"x-request-id"
|
|
165
|
+
) or stream_response.headers.get("x-run-id")
|
|
166
|
+
if req_id:
|
|
167
|
+
meta["run_id"] = req_id
|
|
168
|
+
renderer.on_start(meta)
|
|
169
|
+
|
|
170
|
+
def _maybe_start_timer(self, event: dict[str, Any]) -> float | None:
|
|
171
|
+
try:
|
|
172
|
+
ev = json.loads(event["data"])
|
|
173
|
+
except json.JSONDecodeError:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
if "content" in ev or "status" in ev or ev.get("metadata"):
|
|
177
|
+
return monotonic()
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def _process_single_event(
|
|
181
|
+
self,
|
|
182
|
+
event: dict[str, Any],
|
|
183
|
+
renderer: RichStreamRenderer,
|
|
184
|
+
final_text: str,
|
|
185
|
+
stats_usage: dict[str, Any],
|
|
186
|
+
meta: dict[str, Any],
|
|
187
|
+
) -> tuple[str, dict[str, Any]]:
|
|
188
|
+
try:
|
|
189
|
+
ev = json.loads(event["data"])
|
|
190
|
+
except json.JSONDecodeError:
|
|
191
|
+
self._logger.debug("Non-JSON SSE fragment skipped")
|
|
192
|
+
return final_text, stats_usage
|
|
193
|
+
|
|
194
|
+
kind = (ev.get("metadata") or {}).get("kind")
|
|
195
|
+
renderer.on_event(ev)
|
|
196
|
+
|
|
197
|
+
if kind == "artifact":
|
|
198
|
+
return final_text, stats_usage
|
|
199
|
+
|
|
200
|
+
if kind == "final_response" and ev.get("content"):
|
|
201
|
+
final_text = ev.get("content", "")
|
|
202
|
+
elif ev.get("content"):
|
|
203
|
+
final_text = self._handle_content_event(ev, final_text)
|
|
204
|
+
elif kind == "usage":
|
|
205
|
+
stats_usage.update(ev.get("usage") or {})
|
|
206
|
+
elif kind == "run_info":
|
|
207
|
+
self._handle_run_info_event(ev, meta, renderer)
|
|
208
|
+
|
|
209
|
+
return final_text, stats_usage
|
|
210
|
+
|
|
211
|
+
def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
|
|
212
|
+
content = ev.get("content", "")
|
|
213
|
+
if not content.startswith("Artifact received:"):
|
|
214
|
+
return content
|
|
215
|
+
return final_text
|
|
216
|
+
|
|
217
|
+
def _handle_run_info_event(
|
|
218
|
+
self,
|
|
219
|
+
ev: dict[str, Any],
|
|
220
|
+
meta: dict[str, Any],
|
|
221
|
+
renderer: RichStreamRenderer,
|
|
222
|
+
) -> None:
|
|
223
|
+
if ev.get("model"):
|
|
224
|
+
meta["model"] = ev["model"]
|
|
225
|
+
renderer.on_start(meta)
|
|
226
|
+
if ev.get("run_id"):
|
|
227
|
+
meta["run_id"] = ev["run_id"]
|
|
228
|
+
renderer.on_start(meta)
|
|
229
|
+
|
|
230
|
+
# --------------------------------------------------------------------- #
|
|
231
|
+
# Finalisation helpers
|
|
232
|
+
# --------------------------------------------------------------------- #
|
|
233
|
+
def finalize_renderer(
|
|
234
|
+
self,
|
|
235
|
+
renderer: RichStreamRenderer,
|
|
236
|
+
final_text: str,
|
|
237
|
+
stats_usage: dict[str, Any],
|
|
238
|
+
started_monotonic: float | None,
|
|
239
|
+
finished_monotonic: float | None,
|
|
240
|
+
) -> str:
|
|
241
|
+
"""Complete rendering and return the textual result."""
|
|
242
|
+
st = RunStats()
|
|
243
|
+
st.started_at = started_monotonic or st.started_at
|
|
244
|
+
st.finished_at = finished_monotonic or st.started_at
|
|
245
|
+
st.usage = stats_usage
|
|
246
|
+
|
|
247
|
+
rendered_text = ""
|
|
248
|
+
buffer_values: Any | None = None
|
|
249
|
+
|
|
250
|
+
if hasattr(renderer, "state") and hasattr(renderer.state, "buffer"):
|
|
251
|
+
buffer_values = renderer.state.buffer
|
|
252
|
+
elif hasattr(renderer, "buffer"):
|
|
253
|
+
buffer_values = getattr(renderer, "buffer")
|
|
254
|
+
|
|
255
|
+
if buffer_values is not None:
|
|
256
|
+
try:
|
|
257
|
+
rendered_text = "".join(buffer_values)
|
|
258
|
+
except TypeError:
|
|
259
|
+
rendered_text = ""
|
|
260
|
+
|
|
261
|
+
renderer.on_complete(st)
|
|
262
|
+
return final_text or rendered_text or "No response content received."
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
|
|
266
|
+
"""Determine the execution timeout for agent runs.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
kwargs: Dictionary containing execution parameters, including timeout.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The timeout value in seconds, defaulting to DEFAULT_AGENT_RUN_TIMEOUT
|
|
273
|
+
if not specified in kwargs.
|
|
274
|
+
"""
|
|
275
|
+
return kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
glaip_sdk/config/constants.py
CHANGED
|
@@ -5,7 +5,7 @@ Authors:
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
# Default language model configuration
|
|
8
|
-
DEFAULT_MODEL = "gpt-
|
|
8
|
+
DEFAULT_MODEL = "gpt-5-nano"
|
|
9
9
|
DEFAULT_AGENT_RUN_TIMEOUT = 300
|
|
10
10
|
|
|
11
11
|
# User agent and version
|
|
@@ -36,3 +36,6 @@ DEFAULT_TOOL_VERSION = "1.0"
|
|
|
36
36
|
# MCP creation/update constants
|
|
37
37
|
DEFAULT_MCP_TYPE = "server"
|
|
38
38
|
DEFAULT_MCP_TRANSPORT = "stdio"
|
|
39
|
+
|
|
40
|
+
# Default error messages
|
|
41
|
+
DEFAULT_ERROR_MESSAGE = "Unknown error"
|
glaip_sdk/exceptions.py
CHANGED
|
@@ -26,6 +26,15 @@ class APIError(AIPError):
|
|
|
26
26
|
payload: Any = None,
|
|
27
27
|
request_id: str | None = None,
|
|
28
28
|
):
|
|
29
|
+
"""Initialize the API error.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
message: The error message
|
|
33
|
+
status_code: HTTP status code
|
|
34
|
+
error_type: Type of error
|
|
35
|
+
payload: Additional error payload
|
|
36
|
+
request_id: Request identifier
|
|
37
|
+
"""
|
|
29
38
|
super().__init__(message)
|
|
30
39
|
self.status_code = status_code
|
|
31
40
|
self.error_type = error_type
|
|
@@ -91,6 +100,12 @@ class AgentTimeoutError(TimeoutError):
|
|
|
91
100
|
"""Agent execution timeout with specific duration information."""
|
|
92
101
|
|
|
93
102
|
def __init__(self, timeout_seconds: float, agent_name: str = None):
|
|
103
|
+
"""Initialize the agent timeout error.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
timeout_seconds: The timeout duration in seconds
|
|
107
|
+
agent_name: Optional name of the agent that timed out
|
|
108
|
+
"""
|
|
94
109
|
agent_info = f" for agent '{agent_name}'" if agent_name else ""
|
|
95
110
|
message = (
|
|
96
111
|
f"Agent execution timed out after {timeout_seconds} seconds{agent_info}"
|
glaip_sdk/models.py
CHANGED
|
@@ -236,6 +236,11 @@ class TTYRenderer:
|
|
|
236
236
|
"""Simple TTY renderer for non-Rich environments."""
|
|
237
237
|
|
|
238
238
|
def __init__(self, use_color: bool = True):
|
|
239
|
+
"""Initialize the TTY renderer.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
use_color: Whether to use color output
|
|
243
|
+
"""
|
|
239
244
|
self.use_color = use_color
|
|
240
245
|
|
|
241
246
|
def render_message(self, message: str, event_type: str = "message") -> None:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Payload schema metadata for AIP resources.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from glaip_sdk.payload_schemas.agent import (
|
|
8
|
+
AgentImportOperation,
|
|
9
|
+
ImportFieldPlan,
|
|
10
|
+
get_import_field_plan,
|
|
11
|
+
list_server_only_fields,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AgentImportOperation",
|
|
16
|
+
"ImportFieldPlan",
|
|
17
|
+
"get_import_field_plan",
|
|
18
|
+
"list_server_only_fields",
|
|
19
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Agent payload schema metadata derived from agent_payloads.md.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
|
|
6
|
+
This module encodes which agent fields are mutable, server-managed, or require
|
|
7
|
+
additional sanitisation so that CLI and SDK flows can share the same rules.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Collection, Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
AgentImportOperation = Literal["create", "update"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class FieldRule:
|
|
19
|
+
"""Schema rule defining how a field should be treated."""
|
|
20
|
+
|
|
21
|
+
server_only: bool = False
|
|
22
|
+
cli_managed_create: bool = False
|
|
23
|
+
cli_managed_update: bool = False
|
|
24
|
+
requires_sanitization: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class ImportFieldPlan:
|
|
29
|
+
"""Plan for how an import pipeline should treat a specific field."""
|
|
30
|
+
|
|
31
|
+
copy: bool
|
|
32
|
+
sanitize: bool = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_DEFAULT_RULE = FieldRule()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
AGENT_FIELD_RULES: Mapping[str, FieldRule] = {
|
|
39
|
+
# Server-provided metadata (never send back)
|
|
40
|
+
"id": FieldRule(server_only=True),
|
|
41
|
+
"created_at": FieldRule(server_only=True),
|
|
42
|
+
"updated_at": FieldRule(server_only=True),
|
|
43
|
+
"deleted_at": FieldRule(server_only=True),
|
|
44
|
+
"success": FieldRule(server_only=True),
|
|
45
|
+
"message": FieldRule(server_only=True),
|
|
46
|
+
# Fields handled explicitly by CLI/SDK helpers for language model selection
|
|
47
|
+
"language_model_id": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
48
|
+
"provider": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
49
|
+
"model_name": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
50
|
+
"model": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
51
|
+
# Fields collected via CLI flags / explicit logic
|
|
52
|
+
"name": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
53
|
+
"instruction": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
54
|
+
"tools": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
55
|
+
"agents": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
56
|
+
"mcps": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
57
|
+
"timeout": FieldRule(cli_managed_create=True, cli_managed_update=True),
|
|
58
|
+
# Fields requiring sanitisation before sending to the API
|
|
59
|
+
"agent_config": FieldRule(requires_sanitization=True),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_import_field_plan(
|
|
64
|
+
field_name: str, operation: AgentImportOperation
|
|
65
|
+
) -> ImportFieldPlan:
|
|
66
|
+
"""Return the import handling plan for ``field_name`` under ``operation``.
|
|
67
|
+
|
|
68
|
+
Unknown fields default to being copied as-is so new API fields propagate
|
|
69
|
+
without additional code changes.
|
|
70
|
+
"""
|
|
71
|
+
rule = AGENT_FIELD_RULES.get(field_name, _DEFAULT_RULE)
|
|
72
|
+
|
|
73
|
+
if rule.server_only:
|
|
74
|
+
return ImportFieldPlan(copy=False)
|
|
75
|
+
|
|
76
|
+
if operation == "create" and rule.cli_managed_create:
|
|
77
|
+
return ImportFieldPlan(copy=False)
|
|
78
|
+
|
|
79
|
+
if operation == "update" and rule.cli_managed_update:
|
|
80
|
+
return ImportFieldPlan(copy=False)
|
|
81
|
+
|
|
82
|
+
return ImportFieldPlan(copy=True, sanitize=rule.requires_sanitization)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def list_server_only_fields() -> Collection[str]:
|
|
86
|
+
"""Expose the set of server-only fields for other tooling."""
|
|
87
|
+
return {name for name, rule in AGENT_FIELD_RULES.items() if rule.server_only}
|
glaip_sdk/rich_components.py
CHANGED
|
@@ -11,6 +11,12 @@ class AIPPanel(Panel):
|
|
|
11
11
|
"""Rich Panel configured without vertical borders by default."""
|
|
12
12
|
|
|
13
13
|
def __init__(self, *args, **kwargs):
|
|
14
|
+
"""Initialize AIPPanel with default settings for horizontal borders and padding.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
*args: Positional arguments passed to Panel
|
|
18
|
+
**kwargs: Keyword arguments passed to Panel
|
|
19
|
+
"""
|
|
14
20
|
kwargs.setdefault("box", box.HORIZONTALS)
|
|
15
21
|
kwargs.setdefault("padding", (0, 1))
|
|
16
22
|
super().__init__(*args, **kwargs)
|
|
@@ -20,6 +26,12 @@ class AIPTable(Table):
|
|
|
20
26
|
"""Rich Table configured without vertical borders by default."""
|
|
21
27
|
|
|
22
28
|
def __init__(self, *args, **kwargs):
|
|
29
|
+
"""Initialize AIPTable with default settings for horizontal borders and no edge padding.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
*args: Positional arguments passed to Table
|
|
33
|
+
**kwargs: Keyword arguments passed to Table
|
|
34
|
+
"""
|
|
23
35
|
kwargs.setdefault("box", box.HORIZONTALS)
|
|
24
36
|
kwargs.setdefault("show_edge", False)
|
|
25
37
|
kwargs.setdefault("pad_edge", False)
|