glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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 +5 -2
- glaip_sdk/_version.py +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1191 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +265 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +251 -173
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +735 -143
- glaip_sdk/cli/commands/mcps.py +266 -134
- glaip_sdk/cli/commands/models.py +13 -9
- glaip_sdk/cli/commands/tools.py +67 -88
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +49 -7
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +45 -32
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +232 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +12 -19
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +65 -29
- glaip_sdk/cli/slash/prompt.py +24 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +807 -225
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +79 -499
- glaip_sdk/cli/update_notifier.py +177 -24
- glaip_sdk/cli/utils.py +242 -1308
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +53 -37
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +320 -92
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +123 -15
- glaip_sdk/client/run_rendering.py +136 -101
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +163 -34
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +706 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +7 -35
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +3 -6
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
- glaip_sdk/utils/rendering/renderer/base.py +258 -1577
- glaip_sdk/utils/rendering/renderer/config.py +1 -5
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +10 -51
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
- 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/step_tree_state.py +1 -3
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
- 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 +29 -26
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +32 -46
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +20 -28
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
- glaip_sdk-0.6.10.dist-info/RECORD +159 -0
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.1.0.dist-info/RECORD +0 -82
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
|
@@ -7,9 +7,9 @@ Authors:
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import io
|
|
11
10
|
import json
|
|
12
11
|
import logging
|
|
12
|
+
from collections.abc import Callable
|
|
13
13
|
from time import monotonic
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
@@ -19,8 +19,17 @@ from rich.console import Console as _Console
|
|
|
19
19
|
from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
|
|
20
20
|
from glaip_sdk.utils.client_utils import iter_sse_events
|
|
21
21
|
from glaip_sdk.utils.rendering.models import RunStats
|
|
22
|
-
from glaip_sdk.utils.rendering.renderer import
|
|
23
|
-
|
|
22
|
+
from glaip_sdk.utils.rendering.renderer import (
|
|
23
|
+
RendererFactoryOptions,
|
|
24
|
+
RichStreamRenderer,
|
|
25
|
+
make_default_renderer,
|
|
26
|
+
make_minimal_renderer,
|
|
27
|
+
make_silent_renderer,
|
|
28
|
+
make_verbose_renderer,
|
|
29
|
+
)
|
|
30
|
+
from glaip_sdk.utils.rendering.state import TranscriptBuffer
|
|
31
|
+
|
|
32
|
+
NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
|
|
24
33
|
|
|
25
34
|
|
|
26
35
|
def _coerce_to_string(value: Any) -> str:
|
|
@@ -36,45 +45,6 @@ def _has_visible_text(value: Any) -> bool:
|
|
|
36
45
|
return isinstance(value, str) and bool(value.strip())
|
|
37
46
|
|
|
38
47
|
|
|
39
|
-
def _update_state_transcript(state: Any, text_value: str) -> bool:
|
|
40
|
-
"""Inject transcript text into renderer state if possible."""
|
|
41
|
-
if state is None:
|
|
42
|
-
return False
|
|
43
|
-
|
|
44
|
-
updated = False
|
|
45
|
-
|
|
46
|
-
if hasattr(state, "final_text") and not _has_visible_text(
|
|
47
|
-
getattr(state, "final_text", "")
|
|
48
|
-
):
|
|
49
|
-
try:
|
|
50
|
-
state.final_text = text_value
|
|
51
|
-
updated = True
|
|
52
|
-
except Exception:
|
|
53
|
-
pass
|
|
54
|
-
|
|
55
|
-
buffer = getattr(state, "buffer", None)
|
|
56
|
-
if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
|
|
57
|
-
buffer.append(text_value)
|
|
58
|
-
updated = True
|
|
59
|
-
|
|
60
|
-
return updated
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
|
|
64
|
-
"""Populate the renderer (or its state) with the supplied text."""
|
|
65
|
-
state = getattr(renderer, "state", None)
|
|
66
|
-
if _update_state_transcript(state, text_value):
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
if hasattr(renderer, "final_text") and not _has_visible_text(
|
|
70
|
-
getattr(renderer, "final_text", "")
|
|
71
|
-
):
|
|
72
|
-
try:
|
|
73
|
-
setattr(renderer, "final_text", text_value)
|
|
74
|
-
except Exception:
|
|
75
|
-
pass
|
|
76
|
-
|
|
77
|
-
|
|
78
48
|
class AgentRunRenderingManager:
|
|
79
49
|
"""Coordinate renderer creation and streaming event handling."""
|
|
80
50
|
|
|
@@ -85,6 +55,7 @@ class AgentRunRenderingManager:
|
|
|
85
55
|
logger: Optional logger instance, creates default if None
|
|
86
56
|
"""
|
|
87
57
|
self._logger = logger or logging.getLogger(__name__)
|
|
58
|
+
self._buffer_factory = TranscriptBuffer
|
|
88
59
|
|
|
89
60
|
# --------------------------------------------------------------------- #
|
|
90
61
|
# Renderer setup helpers
|
|
@@ -96,17 +67,38 @@ class AgentRunRenderingManager:
|
|
|
96
67
|
verbose: bool = False,
|
|
97
68
|
) -> RichStreamRenderer:
|
|
98
69
|
"""Create an appropriate renderer based on the supplied spec."""
|
|
70
|
+
transcript_buffer = self._buffer_factory()
|
|
71
|
+
base_options = RendererFactoryOptions(console=_Console(), transcript_buffer=transcript_buffer)
|
|
99
72
|
if isinstance(renderer_spec, RichStreamRenderer):
|
|
100
73
|
return renderer_spec
|
|
101
74
|
|
|
102
75
|
if isinstance(renderer_spec, str):
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
76
|
+
lowered = renderer_spec.lower()
|
|
77
|
+
if lowered == "silent":
|
|
78
|
+
return self._attach_buffer(base_options.build(make_silent_renderer), transcript_buffer)
|
|
79
|
+
if lowered == "minimal":
|
|
80
|
+
return self._attach_buffer(base_options.build(make_minimal_renderer), transcript_buffer)
|
|
81
|
+
if lowered == "verbose":
|
|
82
|
+
return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
|
|
83
|
+
|
|
84
|
+
if verbose:
|
|
85
|
+
return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
|
|
86
|
+
|
|
87
|
+
default_options = RendererFactoryOptions(
|
|
88
|
+
console=_Console(),
|
|
89
|
+
transcript_buffer=transcript_buffer,
|
|
90
|
+
verbose=verbose,
|
|
91
|
+
)
|
|
92
|
+
return self._attach_buffer(default_options.build(make_default_renderer), transcript_buffer)
|
|
108
93
|
|
|
109
|
-
|
|
94
|
+
@staticmethod
|
|
95
|
+
def _attach_buffer(renderer: RichStreamRenderer, buffer: TranscriptBuffer) -> RichStreamRenderer:
|
|
96
|
+
"""Attach a captured transcript buffer to a renderer for later inspection."""
|
|
97
|
+
try:
|
|
98
|
+
renderer._captured_transcript_buffer = buffer # type: ignore[attr-defined]
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
return renderer
|
|
110
102
|
|
|
111
103
|
def build_initial_metadata(
|
|
112
104
|
self,
|
|
@@ -127,49 +119,6 @@ class AgentRunRenderingManager:
|
|
|
127
119
|
"""Notify renderer that streaming is starting."""
|
|
128
120
|
renderer.on_start(meta)
|
|
129
121
|
|
|
130
|
-
def _create_silent_renderer(self) -> RichStreamRenderer:
|
|
131
|
-
silent_config = RendererConfig(
|
|
132
|
-
live=False,
|
|
133
|
-
persist_live=False,
|
|
134
|
-
render_thinking=False,
|
|
135
|
-
)
|
|
136
|
-
return RichStreamRenderer(
|
|
137
|
-
console=_Console(file=io.StringIO(), force_terminal=False),
|
|
138
|
-
cfg=silent_config,
|
|
139
|
-
verbose=False,
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
def _create_minimal_renderer(self) -> RichStreamRenderer:
|
|
143
|
-
minimal_config = RendererConfig(
|
|
144
|
-
live=False,
|
|
145
|
-
persist_live=False,
|
|
146
|
-
render_thinking=False,
|
|
147
|
-
)
|
|
148
|
-
return RichStreamRenderer(
|
|
149
|
-
console=_Console(),
|
|
150
|
-
cfg=minimal_config,
|
|
151
|
-
verbose=False,
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
def _create_verbose_renderer(self) -> RichStreamRenderer:
|
|
155
|
-
verbose_config = RendererConfig(
|
|
156
|
-
theme="dark",
|
|
157
|
-
style="debug",
|
|
158
|
-
live=False,
|
|
159
|
-
append_finished_snapshots=False,
|
|
160
|
-
)
|
|
161
|
-
return RichStreamRenderer(
|
|
162
|
-
console=_Console(),
|
|
163
|
-
cfg=verbose_config,
|
|
164
|
-
verbose=True,
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
|
|
168
|
-
if verbose:
|
|
169
|
-
return self._create_verbose_renderer()
|
|
170
|
-
default_config = RendererConfig()
|
|
171
|
-
return RichStreamRenderer(console=_Console(), cfg=default_config)
|
|
172
|
-
|
|
173
122
|
# --------------------------------------------------------------------- #
|
|
174
123
|
# Streaming event handling
|
|
175
124
|
# --------------------------------------------------------------------- #
|
|
@@ -220,14 +169,27 @@ class AgentRunRenderingManager:
|
|
|
220
169
|
meta: dict[str, Any],
|
|
221
170
|
renderer: RichStreamRenderer,
|
|
222
171
|
) -> None:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
172
|
+
"""Capture request ID from response headers and update metadata.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
stream_response: HTTP response stream.
|
|
176
|
+
meta: Metadata dictionary to update.
|
|
177
|
+
renderer: Renderer instance.
|
|
178
|
+
"""
|
|
179
|
+
req_id = stream_response.headers.get("x-request-id") or stream_response.headers.get("x-run-id")
|
|
226
180
|
if req_id:
|
|
227
181
|
meta["run_id"] = req_id
|
|
228
182
|
renderer.on_start(meta)
|
|
229
183
|
|
|
230
184
|
def _maybe_start_timer(self, event: dict[str, Any]) -> float | None:
|
|
185
|
+
"""Start timing if this is a content-bearing event.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
event: Event dictionary.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Monotonic time if timer should start, None otherwise.
|
|
192
|
+
"""
|
|
231
193
|
try:
|
|
232
194
|
ev = json.loads(event["data"])
|
|
233
195
|
except json.JSONDecodeError:
|
|
@@ -245,6 +207,18 @@ class AgentRunRenderingManager:
|
|
|
245
207
|
stats_usage: dict[str, Any],
|
|
246
208
|
meta: dict[str, Any],
|
|
247
209
|
) -> tuple[str, dict[str, Any]]:
|
|
210
|
+
"""Process a single streaming event.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
event: Event dictionary.
|
|
214
|
+
renderer: Renderer instance.
|
|
215
|
+
final_text: Accumulated text so far.
|
|
216
|
+
stats_usage: Usage statistics dictionary.
|
|
217
|
+
meta: Metadata dictionary.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Tuple of (updated_final_text, updated_stats_usage).
|
|
221
|
+
"""
|
|
248
222
|
try:
|
|
249
223
|
ev = json.loads(event["data"])
|
|
250
224
|
except json.JSONDecodeError:
|
|
@@ -300,6 +274,15 @@ class AgentRunRenderingManager:
|
|
|
300
274
|
return None
|
|
301
275
|
|
|
302
276
|
def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
|
|
277
|
+
"""Handle a content event and update final text.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
ev: Event dictionary.
|
|
281
|
+
final_text: Current accumulated text.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Updated final text.
|
|
285
|
+
"""
|
|
303
286
|
content = ev.get("content", "")
|
|
304
287
|
if not content.startswith("Artifact received:"):
|
|
305
288
|
return content
|
|
@@ -311,6 +294,13 @@ class AgentRunRenderingManager:
|
|
|
311
294
|
meta: dict[str, Any],
|
|
312
295
|
renderer: RichStreamRenderer,
|
|
313
296
|
) -> None:
|
|
297
|
+
"""Handle a run_info event and update metadata.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
ev: Event dictionary.
|
|
301
|
+
meta: Metadata dictionary to update.
|
|
302
|
+
renderer: Renderer instance.
|
|
303
|
+
"""
|
|
314
304
|
if ev.get("model"):
|
|
315
305
|
meta["model"] = ev["model"]
|
|
316
306
|
renderer.on_start(meta)
|
|
@@ -318,15 +308,58 @@ class AgentRunRenderingManager:
|
|
|
318
308
|
meta["run_id"] = ev["run_id"]
|
|
319
309
|
renderer.on_start(meta)
|
|
320
310
|
|
|
321
|
-
def _ensure_renderer_final_content(
|
|
322
|
-
self, renderer: RichStreamRenderer, text: str
|
|
323
|
-
) -> None:
|
|
311
|
+
def _ensure_renderer_final_content(self, renderer: RichStreamRenderer, text: str) -> None:
|
|
324
312
|
"""Populate renderer state with final output when the stream omits it."""
|
|
325
313
|
if not text:
|
|
326
314
|
return
|
|
327
315
|
|
|
328
316
|
text_value = _coerce_to_string(text)
|
|
329
|
-
|
|
317
|
+
state = getattr(renderer, "state", None)
|
|
318
|
+
if state is None:
|
|
319
|
+
self._ensure_renderer_text(renderer, text_value)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
self._ensure_state_final_text(state, text_value)
|
|
323
|
+
self._ensure_state_buffer(state, text_value)
|
|
324
|
+
|
|
325
|
+
def _ensure_renderer_text(self, renderer: RichStreamRenderer, text_value: str) -> None:
|
|
326
|
+
"""Best-effort assignment for renderer.final_text."""
|
|
327
|
+
if not hasattr(renderer, "final_text"):
|
|
328
|
+
return
|
|
329
|
+
current_text = getattr(renderer, "final_text", "")
|
|
330
|
+
if _has_visible_text(current_text):
|
|
331
|
+
return
|
|
332
|
+
self._safe_set_attr(renderer, "final_text", text_value)
|
|
333
|
+
|
|
334
|
+
def _ensure_state_final_text(self, state: Any, text_value: str) -> None:
|
|
335
|
+
"""Best-effort assignment for renderer.state.final_text."""
|
|
336
|
+
current_text = getattr(state, "final_text", "")
|
|
337
|
+
if _has_visible_text(current_text):
|
|
338
|
+
return
|
|
339
|
+
self._safe_set_attr(state, "final_text", text_value)
|
|
340
|
+
|
|
341
|
+
def _ensure_state_buffer(self, state: Any, text_value: str) -> None:
|
|
342
|
+
"""Append fallback text to the state buffer when available."""
|
|
343
|
+
buffer = getattr(state, "buffer", None)
|
|
344
|
+
if not hasattr(buffer, "append"):
|
|
345
|
+
return
|
|
346
|
+
self._safe_append(buffer.append, text_value)
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _safe_set_attr(target: Any, attr: str, value: str) -> None:
|
|
350
|
+
"""Assign attribute while masking renderer-specific failures."""
|
|
351
|
+
try:
|
|
352
|
+
setattr(target, attr, value)
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _safe_append(appender: Callable[[str], Any], value: str) -> None:
|
|
358
|
+
"""Invoke append-like functions without leaking renderer errors."""
|
|
359
|
+
try:
|
|
360
|
+
appender(value)
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
330
363
|
|
|
331
364
|
# --------------------------------------------------------------------- #
|
|
332
365
|
# Finalisation helpers
|
|
@@ -351,9 +384,11 @@ class AgentRunRenderingManager:
|
|
|
351
384
|
if hasattr(renderer, "state") and hasattr(renderer.state, "buffer"):
|
|
352
385
|
buffer_values = renderer.state.buffer
|
|
353
386
|
elif hasattr(renderer, "buffer"):
|
|
354
|
-
buffer_values =
|
|
387
|
+
buffer_values = renderer.buffer
|
|
355
388
|
|
|
356
|
-
if buffer_values
|
|
389
|
+
if isinstance(buffer_values, TranscriptBuffer):
|
|
390
|
+
rendered_text = buffer_values.render()
|
|
391
|
+
elif buffer_values is not None:
|
|
357
392
|
try:
|
|
358
393
|
rendered_text = "".join(buffer_values)
|
|
359
394
|
except TypeError:
|
|
@@ -364,7 +399,7 @@ class AgentRunRenderingManager:
|
|
|
364
399
|
self._ensure_renderer_final_content(renderer, fallback_text)
|
|
365
400
|
|
|
366
401
|
renderer.on_complete(st)
|
|
367
|
-
return final_text or rendered_text or
|
|
402
|
+
return final_text or rendered_text or NO_AGENT_RESPONSE_FALLBACK
|
|
368
403
|
|
|
369
404
|
|
|
370
405
|
def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared helpers for client configuration wiring.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from glaip_sdk.client.base import BaseClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_shared_config(client: BaseClient) -> dict[str, Any]:
|
|
15
|
+
"""Return the keyword arguments used to initialize sub-clients."""
|
|
16
|
+
return {
|
|
17
|
+
"parent_client": client,
|
|
18
|
+
"api_url": client.api_url,
|
|
19
|
+
"api_key": client.api_key,
|
|
20
|
+
"timeout": client._timeout,
|
|
21
|
+
}
|
glaip_sdk/client/tools.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
@@ -16,11 +17,14 @@ from glaip_sdk.config.constants import (
|
|
|
16
17
|
DEFAULT_TOOL_TYPE,
|
|
17
18
|
DEFAULT_TOOL_VERSION,
|
|
18
19
|
)
|
|
19
|
-
from glaip_sdk.models import
|
|
20
|
+
from glaip_sdk.models import ToolResponse
|
|
21
|
+
from glaip_sdk.tools import Tool
|
|
20
22
|
from glaip_sdk.utils.client_utils import (
|
|
23
|
+
add_kwargs_to_payload,
|
|
21
24
|
create_model_instances,
|
|
22
25
|
find_by_name,
|
|
23
26
|
)
|
|
27
|
+
from glaip_sdk.utils.resource_refs import is_uuid
|
|
24
28
|
|
|
25
29
|
# API endpoints
|
|
26
30
|
TOOLS_ENDPOINT = "/tools/"
|
|
@@ -58,11 +62,11 @@ class ToolClient(BaseClient):
|
|
|
58
62
|
def get_tool_by_id(self, tool_id: str) -> Tool:
|
|
59
63
|
"""Get tool by ID."""
|
|
60
64
|
data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
|
|
61
|
-
|
|
65
|
+
response = ToolResponse(**data)
|
|
66
|
+
return Tool.from_response(response, client=self)
|
|
62
67
|
|
|
63
68
|
def find_tools(self, name: str | None = None) -> list[Tool]:
|
|
64
69
|
"""Find tools by name."""
|
|
65
|
-
# Backend doesn't support name query parameter, so we fetch all and filter client-side
|
|
66
70
|
data = self._request("GET", TOOLS_ENDPOINT)
|
|
67
71
|
tools = create_model_instances(data, Tool, self)
|
|
68
72
|
return find_by_name(tools, name, case_sensitive=False)
|
|
@@ -96,9 +100,7 @@ class ToolClient(BaseClient):
|
|
|
96
100
|
"""
|
|
97
101
|
return os.path.splitext(os.path.basename(file_path))[0]
|
|
98
102
|
|
|
99
|
-
def _prepare_upload_data(
|
|
100
|
-
self, name: str, framework: str, description: str | None = None, **kwargs
|
|
101
|
-
) -> dict:
|
|
103
|
+
def _prepare_upload_data(self, name: str, framework: str, description: str | None = None, **kwargs) -> dict:
|
|
102
104
|
"""Prepare upload data dictionary.
|
|
103
105
|
|
|
104
106
|
Args:
|
|
@@ -113,6 +115,7 @@ class ToolClient(BaseClient):
|
|
|
113
115
|
data = {
|
|
114
116
|
"name": name,
|
|
115
117
|
"framework": framework,
|
|
118
|
+
"type": kwargs.pop("tool_type", DEFAULT_TOOL_TYPE), # Default to custom
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
if description:
|
|
@@ -154,7 +157,8 @@ class ToolClient(BaseClient):
|
|
|
154
157
|
data=upload_data,
|
|
155
158
|
)
|
|
156
159
|
|
|
157
|
-
|
|
160
|
+
tool_response = ToolResponse(**response)
|
|
161
|
+
return Tool.from_response(tool_response, client=self)
|
|
158
162
|
|
|
159
163
|
def _build_create_payload(
|
|
160
164
|
self,
|
|
@@ -202,9 +206,7 @@ class ToolClient(BaseClient):
|
|
|
202
206
|
|
|
203
207
|
# Add any other kwargs (excluding already handled ones)
|
|
204
208
|
excluded_keys = {"tags", "version"}
|
|
205
|
-
|
|
206
|
-
if key not in excluded_keys:
|
|
207
|
-
payload[key] = value
|
|
209
|
+
add_kwargs_to_payload(payload, kwargs, excluded_keys)
|
|
208
210
|
|
|
209
211
|
return payload
|
|
210
212
|
|
|
@@ -217,29 +219,21 @@ class ToolClient(BaseClient):
|
|
|
217
219
|
elif hasattr(current_tool, "description") and current_tool.description:
|
|
218
220
|
update_data["description"] = current_tool.description
|
|
219
221
|
|
|
220
|
-
def _handle_tags_update(
|
|
221
|
-
self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool
|
|
222
|
-
) -> None:
|
|
222
|
+
def _handle_tags_update(self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool) -> None:
|
|
223
223
|
"""Handle tags field in update payload."""
|
|
224
224
|
if kwargs.get("tags"):
|
|
225
225
|
if isinstance(kwargs["tags"], list):
|
|
226
|
-
update_data["tags"] = ",".join(
|
|
227
|
-
str(tag).strip() for tag in kwargs["tags"]
|
|
228
|
-
)
|
|
226
|
+
update_data["tags"] = ",".join(str(tag).strip() for tag in kwargs["tags"])
|
|
229
227
|
else:
|
|
230
228
|
update_data["tags"] = str(kwargs["tags"])
|
|
231
229
|
elif hasattr(current_tool, "tags") and current_tool.tags:
|
|
232
230
|
# Preserve existing tags if present
|
|
233
231
|
if isinstance(current_tool.tags, list):
|
|
234
|
-
update_data["tags"] = ",".join(
|
|
235
|
-
str(tag).strip() for tag in current_tool.tags
|
|
236
|
-
)
|
|
232
|
+
update_data["tags"] = ",".join(str(tag).strip() for tag in current_tool.tags)
|
|
237
233
|
else:
|
|
238
234
|
update_data["tags"] = str(current_tool.tags)
|
|
239
235
|
|
|
240
|
-
def _handle_additional_kwargs(
|
|
241
|
-
self, update_data: dict[str, Any], kwargs: dict[str, Any]
|
|
242
|
-
) -> None:
|
|
236
|
+
def _handle_additional_kwargs(self, update_data: dict[str, Any], kwargs: dict[str, Any]) -> None:
|
|
243
237
|
"""Handle additional kwargs in update payload."""
|
|
244
238
|
excluded_keys = {
|
|
245
239
|
"tags",
|
|
@@ -286,16 +280,15 @@ class ToolClient(BaseClient):
|
|
|
286
280
|
or getattr(current_tool, "type", None)
|
|
287
281
|
or DEFAULT_TOOL_TYPE
|
|
288
282
|
)
|
|
283
|
+
# Convert enum to string value for API payload
|
|
284
|
+
if hasattr(current_type, "value"):
|
|
285
|
+
current_type = current_type.value
|
|
289
286
|
|
|
290
287
|
update_data = {
|
|
291
288
|
"name": name if name is not None else current_tool.name,
|
|
292
289
|
"type": current_type,
|
|
293
|
-
"framework": kwargs.get(
|
|
294
|
-
|
|
295
|
-
),
|
|
296
|
-
"version": kwargs.get(
|
|
297
|
-
"version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)
|
|
298
|
-
),
|
|
290
|
+
"framework": kwargs.get("framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)),
|
|
291
|
+
"version": kwargs.get("version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)),
|
|
299
292
|
}
|
|
300
293
|
|
|
301
294
|
# Handle description update
|
|
@@ -355,9 +348,7 @@ class ToolClient(BaseClient):
|
|
|
355
348
|
|
|
356
349
|
try:
|
|
357
350
|
# Prepare upload data
|
|
358
|
-
upload_data = self._prepare_upload_data(
|
|
359
|
-
name=name, framework=framework, description=description, **kwargs
|
|
360
|
-
)
|
|
351
|
+
upload_data = self._prepare_upload_data(name=name, framework=framework, description=description, **kwargs)
|
|
361
352
|
|
|
362
353
|
# Upload file
|
|
363
354
|
return self._upload_tool_file(temp_file_path, upload_data)
|
|
@@ -451,12 +442,149 @@ class ToolClient(BaseClient):
|
|
|
451
442
|
def update_tool(self, tool_id: str, **kwargs) -> Tool:
|
|
452
443
|
"""Update an existing tool."""
|
|
453
444
|
data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
|
|
454
|
-
|
|
445
|
+
response = ToolResponse(**data)
|
|
446
|
+
return Tool.from_response(response, client=self)
|
|
455
447
|
|
|
456
448
|
def delete_tool(self, tool_id: str) -> None:
|
|
457
449
|
"""Delete a tool."""
|
|
458
450
|
self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
|
|
459
451
|
|
|
452
|
+
def upsert_tool(
|
|
453
|
+
self,
|
|
454
|
+
identifier: str | Tool,
|
|
455
|
+
code: str | None = None,
|
|
456
|
+
description: str | None = None,
|
|
457
|
+
framework: str = "langchain",
|
|
458
|
+
**kwargs,
|
|
459
|
+
) -> Tool:
|
|
460
|
+
"""Create or update a tool by instance, ID, or name.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
identifier: Tool instance, ID (UUID string), or name
|
|
464
|
+
code: Python code containing the tool plugin (required for create)
|
|
465
|
+
description: Tool description
|
|
466
|
+
framework: Tool framework (defaults to "langchain")
|
|
467
|
+
**kwargs: Additional parameters (tags, version, etc.)
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
The created or updated tool.
|
|
471
|
+
|
|
472
|
+
Example:
|
|
473
|
+
>>> # By name with code (creates if not exists)
|
|
474
|
+
>>> tool = client.tools.upsert_tool(
|
|
475
|
+
... "greeting",
|
|
476
|
+
... code=bundled_source,
|
|
477
|
+
... description="A greeting tool",
|
|
478
|
+
... )
|
|
479
|
+
>>> # By instance
|
|
480
|
+
>>> tool = client.tools.upsert_tool(existing_tool, code=new_code)
|
|
481
|
+
>>> # By ID
|
|
482
|
+
>>> tool = client.tools.upsert_tool("uuid-here", code=new_code)
|
|
483
|
+
"""
|
|
484
|
+
# Handle Tool instance
|
|
485
|
+
if isinstance(identifier, Tool):
|
|
486
|
+
if identifier.id:
|
|
487
|
+
logger.info("Updating tool by instance: %s", identifier.name)
|
|
488
|
+
return self._do_tool_upsert_update(
|
|
489
|
+
identifier.id,
|
|
490
|
+
identifier.name,
|
|
491
|
+
code,
|
|
492
|
+
description,
|
|
493
|
+
framework,
|
|
494
|
+
**kwargs,
|
|
495
|
+
)
|
|
496
|
+
identifier = identifier.name
|
|
497
|
+
|
|
498
|
+
# Handle string (ID or name)
|
|
499
|
+
if isinstance(identifier, str):
|
|
500
|
+
if is_uuid(identifier):
|
|
501
|
+
logger.info("Updating tool by ID: %s", identifier)
|
|
502
|
+
existing = self.get_tool_by_id(identifier)
|
|
503
|
+
return self._do_tool_upsert_update(identifier, existing.name, code, description, framework, **kwargs)
|
|
504
|
+
|
|
505
|
+
# It's a name - find or create
|
|
506
|
+
return self._upsert_tool_by_name(identifier, code, description, framework, **kwargs)
|
|
507
|
+
|
|
508
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
509
|
+
|
|
510
|
+
def _do_tool_upsert_update(
|
|
511
|
+
self,
|
|
512
|
+
tool_id: str,
|
|
513
|
+
name: str | None,
|
|
514
|
+
code: str | None,
|
|
515
|
+
description: str | None,
|
|
516
|
+
framework: str,
|
|
517
|
+
**kwargs,
|
|
518
|
+
) -> Tool:
|
|
519
|
+
"""Perform the update part of tool upsert."""
|
|
520
|
+
if code:
|
|
521
|
+
# Update via file upload
|
|
522
|
+
with tempfile.NamedTemporaryFile(
|
|
523
|
+
mode="w",
|
|
524
|
+
suffix=".py",
|
|
525
|
+
prefix=f"{name or 'tool'}_",
|
|
526
|
+
delete=False,
|
|
527
|
+
encoding="utf-8",
|
|
528
|
+
) as temp_file:
|
|
529
|
+
temp_file.write(code)
|
|
530
|
+
temp_file_path = temp_file.name
|
|
531
|
+
|
|
532
|
+
try:
|
|
533
|
+
return self.update_tool_via_file(
|
|
534
|
+
tool_id,
|
|
535
|
+
temp_file_path,
|
|
536
|
+
name=name,
|
|
537
|
+
description=description,
|
|
538
|
+
framework=framework,
|
|
539
|
+
**kwargs,
|
|
540
|
+
)
|
|
541
|
+
finally:
|
|
542
|
+
try:
|
|
543
|
+
os.unlink(temp_file_path)
|
|
544
|
+
except OSError:
|
|
545
|
+
pass
|
|
546
|
+
else:
|
|
547
|
+
# Metadata-only update
|
|
548
|
+
update_kwargs = {"framework": framework, **kwargs}
|
|
549
|
+
if name:
|
|
550
|
+
update_kwargs["name"] = name
|
|
551
|
+
if description:
|
|
552
|
+
update_kwargs["description"] = description
|
|
553
|
+
return self.update_tool(tool_id, **update_kwargs)
|
|
554
|
+
|
|
555
|
+
def _upsert_tool_by_name(
|
|
556
|
+
self,
|
|
557
|
+
name: str,
|
|
558
|
+
code: str | None,
|
|
559
|
+
description: str | None,
|
|
560
|
+
framework: str,
|
|
561
|
+
**kwargs,
|
|
562
|
+
) -> Tool:
|
|
563
|
+
"""Find tool by name and update, or create if not found."""
|
|
564
|
+
existing = self.find_tools(name)
|
|
565
|
+
name_lower = name.lower()
|
|
566
|
+
exact_matches = [tool for tool in existing if tool.name and tool.name.lower() == name_lower]
|
|
567
|
+
|
|
568
|
+
if len(exact_matches) == 1:
|
|
569
|
+
logger.info("Updating existing tool: %s", name)
|
|
570
|
+
return self._do_tool_upsert_update(exact_matches[0].id, name, code, description, framework, **kwargs)
|
|
571
|
+
|
|
572
|
+
if len(exact_matches) > 1:
|
|
573
|
+
raise ValueError(f"Multiple tools found with name '{name}'")
|
|
574
|
+
|
|
575
|
+
# Create new tool - code is required
|
|
576
|
+
if not code:
|
|
577
|
+
raise ValueError(f"Tool '{name}' not found and no code provided for creation")
|
|
578
|
+
|
|
579
|
+
logger.info("Creating new tool: %s", name)
|
|
580
|
+
return self.create_tool_from_code(
|
|
581
|
+
name=name,
|
|
582
|
+
code=code,
|
|
583
|
+
framework=framework,
|
|
584
|
+
description=description,
|
|
585
|
+
**kwargs,
|
|
586
|
+
)
|
|
587
|
+
|
|
460
588
|
def get_tool_script(self, tool_id: str) -> str:
|
|
461
589
|
"""Get the tool script content.
|
|
462
590
|
|
|
@@ -525,8 +653,9 @@ class ToolClient(BaseClient):
|
|
|
525
653
|
data=update_payload,
|
|
526
654
|
)
|
|
527
655
|
|
|
528
|
-
|
|
656
|
+
tool_response = ToolResponse(**response)
|
|
657
|
+
return Tool.from_response(tool_response, client=self)
|
|
529
658
|
|
|
530
659
|
except Exception as e:
|
|
531
|
-
logger.error(
|
|
660
|
+
logger.error("Failed to update tool %s via file: %s", tool_id, e)
|
|
532
661
|
raise
|