glaip-sdk 0.6.10__py3-none-any.whl → 0.6.19__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 +42 -5
- glaip_sdk/agents/base.py +12 -7
- glaip_sdk/cli/commands/common_config.py +14 -11
- glaip_sdk/cli/core/output.py +12 -7
- glaip_sdk/cli/main.py +15 -4
- glaip_sdk/client/agents.py +4 -6
- glaip_sdk/client/run_rendering.py +334 -2
- glaip_sdk/hitl/__init__.py +15 -0
- glaip_sdk/hitl/local.py +151 -0
- glaip_sdk/runner/deps.py +5 -8
- glaip_sdk/runner/langgraph.py +184 -20
- glaip_sdk/utils/rendering/renderer/base.py +58 -0
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- {glaip_sdk-0.6.10.dist-info → glaip_sdk-0.6.19.dist-info}/METADATA +35 -38
- {glaip_sdk-0.6.10.dist-info → glaip_sdk-0.6.19.dist-info}/RECORD +49 -45
- {glaip_sdk-0.6.10.dist-info → glaip_sdk-0.6.19.dist-info}/WHEEL +2 -1
- glaip_sdk-0.6.19.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.6.19.dist-info/top_level.txt +1 -0
- glaip_sdk-0.6.10.dist-info/entry_points.txt +0 -3
glaip_sdk/__init__.py
CHANGED
|
@@ -4,12 +4,49 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
7
12
|
from glaip_sdk._version import __version__
|
|
8
|
-
from glaip_sdk.client import Client
|
|
9
|
-
from glaip_sdk.exceptions import AIPError
|
|
10
|
-
from glaip_sdk.agents import Agent
|
|
11
|
-
from glaip_sdk.tools import Tool
|
|
12
|
-
from glaip_sdk.mcps import MCP
|
|
13
13
|
|
|
14
|
+
if TYPE_CHECKING: # pragma: no cover - import only for type checking
|
|
15
|
+
from glaip_sdk.agents import Agent
|
|
16
|
+
from glaip_sdk.client import Client
|
|
17
|
+
from glaip_sdk.exceptions import AIPError
|
|
18
|
+
from glaip_sdk.mcps import MCP
|
|
19
|
+
from glaip_sdk.tools import Tool
|
|
14
20
|
|
|
15
21
|
__all__ = ["Client", "Agent", "Tool", "MCP", "AIPError", "__version__"]
|
|
22
|
+
|
|
23
|
+
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
24
|
+
"Client": ("glaip_sdk.client", "Client"),
|
|
25
|
+
"Agent": ("glaip_sdk.agents", "Agent"),
|
|
26
|
+
"Tool": ("glaip_sdk.tools", "Tool"),
|
|
27
|
+
"MCP": ("glaip_sdk.mcps", "MCP"),
|
|
28
|
+
"AIPError": ("glaip_sdk.exceptions", "AIPError"),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def __getattr__(name: str) -> Any:
|
|
33
|
+
"""Lazy attribute access for public SDK symbols to defer heavy imports."""
|
|
34
|
+
if name == "__version__":
|
|
35
|
+
# Import __version__ when accessed via __getattr__
|
|
36
|
+
# This ensures coverage even if __version__ was removed from __dict__ for testing
|
|
37
|
+
from glaip_sdk._version import __version__ as version # noqa: PLC0415
|
|
38
|
+
|
|
39
|
+
globals()["__version__"] = version
|
|
40
|
+
return version
|
|
41
|
+
if name in _LAZY_IMPORTS:
|
|
42
|
+
module_path, attr_name = _LAZY_IMPORTS[name]
|
|
43
|
+
module = importlib.import_module(module_path)
|
|
44
|
+
attr = getattr(module, attr_name)
|
|
45
|
+
globals()[name] = attr
|
|
46
|
+
return attr
|
|
47
|
+
raise AttributeError(f"module 'glaip_sdk' has no attribute {name!r}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def __dir__() -> list[str]:
|
|
51
|
+
"""Return module attributes for dir()."""
|
|
52
|
+
return sorted(__all__)
|
glaip_sdk/agents/base.py
CHANGED
|
@@ -52,14 +52,7 @@ from pathlib import Path
|
|
|
52
52
|
from typing import TYPE_CHECKING, Any
|
|
53
53
|
|
|
54
54
|
from glaip_sdk.registry import get_agent_registry, get_mcp_registry, get_tool_registry
|
|
55
|
-
from glaip_sdk.runner import get_default_runner
|
|
56
|
-
from glaip_sdk.runner.deps import (
|
|
57
|
-
check_local_runtime_available,
|
|
58
|
-
get_local_runtime_missing_message,
|
|
59
|
-
)
|
|
60
|
-
from glaip_sdk.utils.discovery import find_agent
|
|
61
55
|
from glaip_sdk.utils.resource_refs import is_uuid
|
|
62
|
-
from glaip_sdk.utils.runtime_config import normalize_runtime_config_keys
|
|
63
56
|
|
|
64
57
|
if TYPE_CHECKING:
|
|
65
58
|
from glaip_sdk.models import AgentResponse
|
|
@@ -520,6 +513,8 @@ class Agent:
|
|
|
520
513
|
from glaip_sdk.utils.client import get_client # noqa: PLC0415
|
|
521
514
|
|
|
522
515
|
client = get_client()
|
|
516
|
+
from glaip_sdk.utils.discovery import find_agent # noqa: PLC0415
|
|
517
|
+
|
|
523
518
|
response = self._create_or_update_agent(config, client, find_agent)
|
|
524
519
|
|
|
525
520
|
# Update self with deployed info
|
|
@@ -885,6 +880,10 @@ class Agent:
|
|
|
885
880
|
}
|
|
886
881
|
|
|
887
882
|
if runtime_config is not None:
|
|
883
|
+
from glaip_sdk.utils.runtime_config import ( # noqa: PLC0415
|
|
884
|
+
normalize_runtime_config_keys,
|
|
885
|
+
)
|
|
886
|
+
|
|
888
887
|
call_kwargs["runtime_config"] = normalize_runtime_config_keys(
|
|
889
888
|
runtime_config,
|
|
890
889
|
tool_registry=get_tool_registry(),
|
|
@@ -904,6 +903,12 @@ class Agent:
|
|
|
904
903
|
Raises:
|
|
905
904
|
ValueError: If local runtime is not available.
|
|
906
905
|
"""
|
|
906
|
+
from glaip_sdk.runner import get_default_runner # noqa: PLC0415
|
|
907
|
+
from glaip_sdk.runner.deps import ( # noqa: PLC0415
|
|
908
|
+
check_local_runtime_available,
|
|
909
|
+
get_local_runtime_missing_message,
|
|
910
|
+
)
|
|
911
|
+
|
|
907
912
|
if check_local_runtime_available():
|
|
908
913
|
return get_default_runner()
|
|
909
914
|
raise ValueError(f"{_AGENT_NOT_DEPLOYED_MSG}\n\n{get_local_runtime_missing_message()}")
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
"""Shared helpers for configuration/account flows."""
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import click
|
|
5
9
|
from rich.console import Console
|
|
6
10
|
from rich.text import Text
|
|
7
|
-
|
|
8
|
-
from glaip_sdk import Client
|
|
9
11
|
from glaip_sdk.branding import PRIMARY, SUCCESS_STYLE, WARNING_STYLE, AIPBranding
|
|
10
12
|
from glaip_sdk.cli.utils import sdk_version
|
|
11
13
|
|
|
14
|
+
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
15
|
+
from glaip_sdk import Client
|
|
16
|
+
|
|
12
17
|
|
|
13
18
|
def render_branding_header(console: Console, rule_text: str) -> None:
|
|
14
19
|
"""Render the standard CLI branding header with a custom rule text."""
|
|
@@ -36,11 +41,10 @@ def check_connection(
|
|
|
36
41
|
console.print("\n🔌 Testing connection...")
|
|
37
42
|
client: Client | None = None
|
|
38
43
|
try:
|
|
39
|
-
# Import lazily
|
|
40
|
-
from
|
|
44
|
+
# Import lazily to avoid pulling in SDK dependencies during CLI startup.
|
|
45
|
+
from glaip_sdk import Client # noqa: PLC0415
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
client = client_module.Client(api_url=api_url, api_key=api_key)
|
|
47
|
+
client = Client(api_url=api_url, api_key=api_key)
|
|
44
48
|
try:
|
|
45
49
|
agents = client.list_agents()
|
|
46
50
|
console.print(Text(f"✅ Connection successful! Found {len(agents)} agents", style=SUCCESS_STYLE))
|
|
@@ -75,11 +79,10 @@ def check_connection_with_reason(
|
|
|
75
79
|
"""Test connectivity and return structured reason."""
|
|
76
80
|
client: Client | None = None
|
|
77
81
|
try:
|
|
78
|
-
# Import lazily
|
|
79
|
-
from
|
|
82
|
+
# Import lazily to avoid pulling in SDK dependencies during CLI startup.
|
|
83
|
+
from glaip_sdk import Client # noqa: PLC0415
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
client = client_module.Client(api_url=api_url, api_key=api_key)
|
|
85
|
+
client = Client(api_url=api_url, api_key=api_key)
|
|
83
86
|
try:
|
|
84
87
|
client.list_agents()
|
|
85
88
|
return True, ""
|
glaip_sdk/cli/core/output.py
CHANGED
|
@@ -29,12 +29,6 @@ from glaip_sdk.cli.io import export_resource_to_file_with_validation
|
|
|
29
29
|
from glaip_sdk.cli.rich_helpers import markup_text, print_markup
|
|
30
30
|
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
31
31
|
from glaip_sdk.utils import format_datetime, is_uuid
|
|
32
|
-
|
|
33
|
-
try:
|
|
34
|
-
from glaip_sdk import _version as _version_module
|
|
35
|
-
except ImportError: # pragma: no cover - defensive import
|
|
36
|
-
_version_module = None
|
|
37
|
-
|
|
38
32
|
from .prompting import (
|
|
39
33
|
_fuzzy_pick,
|
|
40
34
|
_fuzzy_pick_for_resources,
|
|
@@ -43,6 +37,9 @@ from .prompting import (
|
|
|
43
37
|
)
|
|
44
38
|
from .rendering import _spinner_stop, _spinner_update, spinner_context
|
|
45
39
|
|
|
40
|
+
_VERSION_MODULE_MISSING = object()
|
|
41
|
+
_version_module: Any | None = _VERSION_MODULE_MISSING
|
|
42
|
+
|
|
46
43
|
console = Console()
|
|
47
44
|
pager.console = console
|
|
48
45
|
logger = logging.getLogger("glaip_sdk.cli.core.output")
|
|
@@ -236,7 +233,15 @@ def handle_resource_export(
|
|
|
236
233
|
|
|
237
234
|
def sdk_version() -> str:
|
|
238
235
|
"""Return the current SDK version, warning if metadata is unavailable."""
|
|
239
|
-
global _WARNED_SDK_VERSION_FALLBACK
|
|
236
|
+
global _WARNED_SDK_VERSION_FALLBACK, _version_module
|
|
237
|
+
|
|
238
|
+
if _version_module is _VERSION_MODULE_MISSING:
|
|
239
|
+
try:
|
|
240
|
+
from importlib import import_module # noqa: PLC0415
|
|
241
|
+
|
|
242
|
+
_version_module = import_module("glaip_sdk._version")
|
|
243
|
+
except Exception: # pragma: no cover - defensive fallback
|
|
244
|
+
_version_module = None
|
|
240
245
|
|
|
241
246
|
if _version_module is None:
|
|
242
247
|
if not _WARNED_SDK_VERSION_FALLBACK:
|
glaip_sdk/cli/main.py
CHANGED
|
@@ -11,8 +11,6 @@ from typing import Any
|
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
13
|
from rich.console import Console
|
|
14
|
-
|
|
15
|
-
from glaip_sdk import Client
|
|
16
14
|
from glaip_sdk.branding import (
|
|
17
15
|
ERROR,
|
|
18
16
|
ERROR_STYLE,
|
|
@@ -49,6 +47,18 @@ from glaip_sdk.config.constants import (
|
|
|
49
47
|
from glaip_sdk.icons import ICON_AGENT
|
|
50
48
|
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
51
49
|
|
|
50
|
+
Client: type[Any] | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _resolve_client_class() -> type[Any]:
|
|
54
|
+
"""Resolve the Client class lazily to avoid heavy imports at CLI startup."""
|
|
55
|
+
global Client
|
|
56
|
+
if Client is None:
|
|
57
|
+
from glaip_sdk import Client as ClientClass # noqa: PLC0415
|
|
58
|
+
|
|
59
|
+
Client = ClientClass
|
|
60
|
+
return Client
|
|
61
|
+
|
|
52
62
|
|
|
53
63
|
def _suppress_chatty_loggers() -> None:
|
|
54
64
|
"""Silence noisy SDK/httpx logs for CLI output."""
|
|
@@ -339,14 +349,15 @@ def _safe_list_call(obj: Any, attr: str) -> list[Any]:
|
|
|
339
349
|
|
|
340
350
|
def _get_client_from_config(config: dict) -> Any:
|
|
341
351
|
"""Return a Client instance built from config."""
|
|
342
|
-
|
|
352
|
+
client_class = _resolve_client_class()
|
|
353
|
+
return client_class(
|
|
343
354
|
api_url=config["api_url"],
|
|
344
355
|
api_key=config["api_key"],
|
|
345
356
|
timeout=config.get("timeout", 30.0),
|
|
346
357
|
)
|
|
347
358
|
|
|
348
359
|
|
|
349
|
-
def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) ->
|
|
360
|
+
def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Any:
|
|
350
361
|
"""Create client and test connection by fetching resources."""
|
|
351
362
|
client: Any = _get_client_from_config(config)
|
|
352
363
|
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -453,13 +453,11 @@ class AgentClient(BaseClient):
|
|
|
453
453
|
Returns:
|
|
454
454
|
Final text string.
|
|
455
455
|
"""
|
|
456
|
+
from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
|
|
457
|
+
|
|
456
458
|
manager = self._get_renderer_manager()
|
|
457
|
-
return
|
|
458
|
-
renderer,
|
|
459
|
-
final_text,
|
|
460
|
-
stats_usage,
|
|
461
|
-
started_monotonic,
|
|
462
|
-
finished_monotonic,
|
|
459
|
+
return finalize_render_manager(
|
|
460
|
+
manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
|
|
463
461
|
)
|
|
464
462
|
|
|
465
463
|
def _get_tool_client(self) -> ToolClient:
|
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
-
from collections.abc import Callable
|
|
12
|
+
from collections.abc import AsyncIterable, Callable
|
|
13
13
|
from time import monotonic
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
@@ -30,6 +30,7 @@ from glaip_sdk.utils.rendering.renderer import (
|
|
|
30
30
|
from glaip_sdk.utils.rendering.state import TranscriptBuffer
|
|
31
31
|
|
|
32
32
|
NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
|
|
33
|
+
_FINAL_EVENT_TYPES = {"final_response", "error", "step_limit_exceeded"}
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
def _coerce_to_string(value: Any) -> str:
|
|
@@ -156,6 +157,9 @@ class AgentRunRenderingManager:
|
|
|
156
157
|
|
|
157
158
|
if controller and getattr(controller, "enabled", False):
|
|
158
159
|
controller.poll(renderer)
|
|
160
|
+
parsed_event = self._parse_event(event)
|
|
161
|
+
if parsed_event and self._is_final_event(parsed_event):
|
|
162
|
+
break
|
|
159
163
|
finally:
|
|
160
164
|
if controller and getattr(controller, "enabled", False):
|
|
161
165
|
controller.on_stream_complete()
|
|
@@ -163,6 +167,300 @@ class AgentRunRenderingManager:
|
|
|
163
167
|
finished_monotonic = monotonic()
|
|
164
168
|
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
165
169
|
|
|
170
|
+
async def async_process_stream_events(
|
|
171
|
+
self,
|
|
172
|
+
event_stream: AsyncIterable[dict[str, Any]],
|
|
173
|
+
renderer: RichStreamRenderer,
|
|
174
|
+
meta: dict[str, Any],
|
|
175
|
+
*,
|
|
176
|
+
skip_final_render: bool = True,
|
|
177
|
+
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
178
|
+
"""Process streaming events from an async event source.
|
|
179
|
+
|
|
180
|
+
This method provides unified stream processing for both remote (HTTP)
|
|
181
|
+
and local (LangGraph) agent execution, ensuring consistent behavior.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
event_stream: Async iterable yielding SSE-like event dicts.
|
|
185
|
+
Each event should have a "data" key with JSON string, or be
|
|
186
|
+
a pre-parsed dict with "content", "metadata", etc.
|
|
187
|
+
renderer: Renderer to use for displaying events.
|
|
188
|
+
meta: Metadata dictionary for renderer context.
|
|
189
|
+
skip_final_render: If True, skip rendering final_response events
|
|
190
|
+
(they are rendered separately via finalize_renderer).
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
|
|
194
|
+
"""
|
|
195
|
+
final_text = ""
|
|
196
|
+
stats_usage: dict[str, Any] = {}
|
|
197
|
+
started_monotonic: float | None = None
|
|
198
|
+
last_rendered_content: str | None = None
|
|
199
|
+
|
|
200
|
+
controller = getattr(renderer, "transcript_controller", None)
|
|
201
|
+
if controller and getattr(controller, "enabled", False):
|
|
202
|
+
controller.on_stream_start(renderer)
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
async for event in event_stream:
|
|
206
|
+
if started_monotonic is None:
|
|
207
|
+
started_monotonic = monotonic()
|
|
208
|
+
|
|
209
|
+
# Parse event if needed (handles both raw SSE and pre-parsed dicts)
|
|
210
|
+
parsed_event = self._parse_event(event)
|
|
211
|
+
if parsed_event is None:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
# Process the event and update accumulators
|
|
215
|
+
final_text, stats_usage = self._handle_parsed_event(
|
|
216
|
+
parsed_event,
|
|
217
|
+
renderer,
|
|
218
|
+
final_text,
|
|
219
|
+
stats_usage,
|
|
220
|
+
meta,
|
|
221
|
+
skip_final_render=skip_final_render,
|
|
222
|
+
last_rendered_content=last_rendered_content,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Track last rendered content to avoid duplicates
|
|
226
|
+
content_str = self._extract_content_string(parsed_event)
|
|
227
|
+
if content_str:
|
|
228
|
+
last_rendered_content = content_str
|
|
229
|
+
|
|
230
|
+
if controller and getattr(controller, "enabled", False):
|
|
231
|
+
controller.poll(renderer)
|
|
232
|
+
if parsed_event and self._is_final_event(parsed_event):
|
|
233
|
+
break
|
|
234
|
+
finally:
|
|
235
|
+
if controller and getattr(controller, "enabled", False):
|
|
236
|
+
controller.on_stream_complete()
|
|
237
|
+
|
|
238
|
+
finished_monotonic = monotonic()
|
|
239
|
+
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
240
|
+
|
|
241
|
+
def _parse_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
242
|
+
"""Parse an SSE event dict into a usable format.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
event: Raw event dict, either with "data" key (SSE format) or
|
|
246
|
+
pre-parsed with "content", "metadata", etc.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Parsed event dict, or None if parsing fails.
|
|
250
|
+
"""
|
|
251
|
+
if "data" in event:
|
|
252
|
+
try:
|
|
253
|
+
return json.loads(event["data"])
|
|
254
|
+
except json.JSONDecodeError:
|
|
255
|
+
self._logger.debug("Non-JSON SSE fragment skipped")
|
|
256
|
+
return None
|
|
257
|
+
# Already parsed (e.g., from local runner)
|
|
258
|
+
return event if event else None
|
|
259
|
+
|
|
260
|
+
def _handle_parsed_event(
|
|
261
|
+
self,
|
|
262
|
+
ev: dict[str, Any],
|
|
263
|
+
renderer: RichStreamRenderer,
|
|
264
|
+
final_text: str,
|
|
265
|
+
stats_usage: dict[str, Any],
|
|
266
|
+
meta: dict[str, Any],
|
|
267
|
+
*,
|
|
268
|
+
skip_final_render: bool = True,
|
|
269
|
+
last_rendered_content: str | None = None,
|
|
270
|
+
) -> tuple[str, dict[str, Any]]:
|
|
271
|
+
"""Handle a parsed event and update accumulators.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
ev: Parsed event dictionary.
|
|
275
|
+
renderer: Renderer instance.
|
|
276
|
+
final_text: Current accumulated final text.
|
|
277
|
+
stats_usage: Usage statistics dictionary.
|
|
278
|
+
meta: Metadata dictionary.
|
|
279
|
+
skip_final_render: If True, skip rendering final_response events.
|
|
280
|
+
last_rendered_content: Last rendered content to avoid duplicates.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Tuple of (updated_final_text, updated_stats_usage).
|
|
284
|
+
"""
|
|
285
|
+
kind = self._get_event_kind(ev)
|
|
286
|
+
|
|
287
|
+
# Dispatch to specialized handlers based on event kind
|
|
288
|
+
handler = self._get_event_handler(kind, ev)
|
|
289
|
+
if handler:
|
|
290
|
+
return handler(ev, renderer, final_text, stats_usage, meta, skip_final_render)
|
|
291
|
+
|
|
292
|
+
# Default: handle content events
|
|
293
|
+
return self._handle_content_event_async(ev, renderer, final_text, stats_usage, last_rendered_content)
|
|
294
|
+
|
|
295
|
+
def _get_event_handler(
|
|
296
|
+
self,
|
|
297
|
+
kind: str | None,
|
|
298
|
+
ev: dict[str, Any],
|
|
299
|
+
) -> Callable[..., tuple[str, dict[str, Any]]] | None:
|
|
300
|
+
"""Get the appropriate handler for an event kind.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
kind: Event kind string.
|
|
304
|
+
ev: Event dictionary (for checking is_final flag).
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Handler function or None for default content handling.
|
|
308
|
+
"""
|
|
309
|
+
if kind == "usage":
|
|
310
|
+
return self._handle_usage_event
|
|
311
|
+
if kind == "final_response" or ev.get("is_final"):
|
|
312
|
+
return self._handle_final_response_event
|
|
313
|
+
if kind == "run_info":
|
|
314
|
+
return self._handle_run_info_event_wrapper
|
|
315
|
+
if kind in ("artifact", "status_update"):
|
|
316
|
+
return self._handle_render_only_event
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
def _handle_usage_event(
|
|
320
|
+
self,
|
|
321
|
+
ev: dict[str, Any],
|
|
322
|
+
_renderer: RichStreamRenderer,
|
|
323
|
+
final_text: str,
|
|
324
|
+
stats_usage: dict[str, Any],
|
|
325
|
+
_meta: dict[str, Any],
|
|
326
|
+
_skip_final_render: bool,
|
|
327
|
+
) -> tuple[str, dict[str, Any]]:
|
|
328
|
+
"""Handle usage events."""
|
|
329
|
+
stats_usage.update(ev.get("usage") or {})
|
|
330
|
+
return final_text, stats_usage
|
|
331
|
+
|
|
332
|
+
def _handle_final_response_event(
|
|
333
|
+
self,
|
|
334
|
+
ev: dict[str, Any],
|
|
335
|
+
renderer: RichStreamRenderer,
|
|
336
|
+
final_text: str,
|
|
337
|
+
stats_usage: dict[str, Any],
|
|
338
|
+
_meta: dict[str, Any],
|
|
339
|
+
skip_final_render: bool,
|
|
340
|
+
) -> tuple[str, dict[str, Any]]:
|
|
341
|
+
"""Handle final_response events."""
|
|
342
|
+
content = ev.get("content")
|
|
343
|
+
if content:
|
|
344
|
+
final_text = str(content)
|
|
345
|
+
if not skip_final_render:
|
|
346
|
+
renderer.on_event(ev)
|
|
347
|
+
return final_text, stats_usage
|
|
348
|
+
|
|
349
|
+
def _handle_run_info_event_wrapper(
|
|
350
|
+
self,
|
|
351
|
+
ev: dict[str, Any],
|
|
352
|
+
renderer: RichStreamRenderer,
|
|
353
|
+
final_text: str,
|
|
354
|
+
stats_usage: dict[str, Any],
|
|
355
|
+
meta: dict[str, Any],
|
|
356
|
+
_skip_final_render: bool,
|
|
357
|
+
) -> tuple[str, dict[str, Any]]:
|
|
358
|
+
"""Handle run_info events."""
|
|
359
|
+
self._handle_run_info_event(ev, meta, renderer)
|
|
360
|
+
return final_text, stats_usage
|
|
361
|
+
|
|
362
|
+
def _handle_render_only_event(
|
|
363
|
+
self,
|
|
364
|
+
ev: dict[str, Any],
|
|
365
|
+
renderer: RichStreamRenderer,
|
|
366
|
+
final_text: str,
|
|
367
|
+
stats_usage: dict[str, Any],
|
|
368
|
+
_meta: dict[str, Any],
|
|
369
|
+
_skip_final_render: bool,
|
|
370
|
+
) -> tuple[str, dict[str, Any]]:
|
|
371
|
+
"""Handle events that only need rendering (artifact, status_update)."""
|
|
372
|
+
renderer.on_event(ev)
|
|
373
|
+
return final_text, stats_usage
|
|
374
|
+
|
|
375
|
+
def _handle_content_event_async(
|
|
376
|
+
self,
|
|
377
|
+
ev: dict[str, Any],
|
|
378
|
+
renderer: RichStreamRenderer,
|
|
379
|
+
final_text: str,
|
|
380
|
+
stats_usage: dict[str, Any],
|
|
381
|
+
last_rendered_content: str | None,
|
|
382
|
+
) -> tuple[str, dict[str, Any]]:
|
|
383
|
+
"""Handle content events with deduplication."""
|
|
384
|
+
content = ev.get("content")
|
|
385
|
+
if content:
|
|
386
|
+
content_str = str(content)
|
|
387
|
+
if not content_str.startswith("Artifact received:"):
|
|
388
|
+
kind = self._get_event_kind(ev)
|
|
389
|
+
# Skip accumulating content for status updates and agent steps
|
|
390
|
+
if kind in ("agent_step", "status_update"):
|
|
391
|
+
renderer.on_event(ev)
|
|
392
|
+
return final_text, stats_usage
|
|
393
|
+
|
|
394
|
+
if self._is_token_event(ev):
|
|
395
|
+
renderer.on_event(ev)
|
|
396
|
+
final_text = f"{final_text}{content_str}"
|
|
397
|
+
else:
|
|
398
|
+
if content_str != last_rendered_content:
|
|
399
|
+
renderer.on_event(ev)
|
|
400
|
+
final_text = content_str
|
|
401
|
+
else:
|
|
402
|
+
renderer.on_event(ev)
|
|
403
|
+
return final_text, stats_usage
|
|
404
|
+
|
|
405
|
+
def _get_event_kind(self, ev: dict[str, Any]) -> str | None:
|
|
406
|
+
"""Extract normalized event kind from parsed event.
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
ev: Parsed event dictionary.
|
|
410
|
+
|
|
411
|
+
Returns:
|
|
412
|
+
Event kind string or None.
|
|
413
|
+
"""
|
|
414
|
+
metadata = ev.get("metadata") or {}
|
|
415
|
+
kind = metadata.get("kind")
|
|
416
|
+
if kind:
|
|
417
|
+
return str(kind)
|
|
418
|
+
event_type = ev.get("event_type")
|
|
419
|
+
return str(event_type) if event_type else None
|
|
420
|
+
|
|
421
|
+
def _is_token_event(self, ev: dict[str, Any]) -> bool:
|
|
422
|
+
"""Return True when the event represents token streaming output.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
ev: Parsed event dictionary.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
True when the event is a token chunk, otherwise False.
|
|
429
|
+
"""
|
|
430
|
+
metadata = ev.get("metadata") or {}
|
|
431
|
+
kind = metadata.get("kind")
|
|
432
|
+
return str(kind).lower() == "token"
|
|
433
|
+
|
|
434
|
+
def _is_final_event(self, ev: dict[str, Any]) -> bool:
|
|
435
|
+
"""Return True when the event marks stream termination.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
ev: Parsed event dictionary.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
True when the event is terminal, otherwise False.
|
|
442
|
+
"""
|
|
443
|
+
if ev.get("is_final") is True or ev.get("final") is True:
|
|
444
|
+
return True
|
|
445
|
+
kind = self._get_event_kind(ev)
|
|
446
|
+
return kind in _FINAL_EVENT_TYPES
|
|
447
|
+
|
|
448
|
+
def _extract_content_string(self, event: dict[str, Any]) -> str | None:
|
|
449
|
+
"""Extract textual content from a parsed event.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
event: Parsed event dictionary.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Content string or None.
|
|
456
|
+
"""
|
|
457
|
+
if not event:
|
|
458
|
+
return None
|
|
459
|
+
content = event.get("content")
|
|
460
|
+
if content:
|
|
461
|
+
return str(content)
|
|
462
|
+
return None
|
|
463
|
+
|
|
166
464
|
def _capture_request_id(
|
|
167
465
|
self,
|
|
168
466
|
stream_response: httpx.Response,
|
|
@@ -239,7 +537,9 @@ class AgentRunRenderingManager:
|
|
|
239
537
|
if handled is not None:
|
|
240
538
|
return handled
|
|
241
539
|
|
|
242
|
-
|
|
540
|
+
# Only accumulate content for actual content events, not status updates or agent steps
|
|
541
|
+
# Status updates (agent_step) should be rendered but not accumulated in final_text
|
|
542
|
+
if ev.get("content") and kind not in ("agent_step", "status_update"):
|
|
243
543
|
final_text = self._handle_content_event(ev, final_text)
|
|
244
544
|
|
|
245
545
|
return final_text, stats_usage
|
|
@@ -285,6 +585,8 @@ class AgentRunRenderingManager:
|
|
|
285
585
|
"""
|
|
286
586
|
content = ev.get("content", "")
|
|
287
587
|
if not content.startswith("Artifact received:"):
|
|
588
|
+
if self._is_token_event(ev):
|
|
589
|
+
return f"{final_text}{content}"
|
|
288
590
|
return content
|
|
289
591
|
return final_text
|
|
290
592
|
|
|
@@ -413,3 +715,33 @@ def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
|
|
|
413
715
|
if not specified in kwargs.
|
|
414
716
|
"""
|
|
415
717
|
return kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def finalize_render_manager(
|
|
721
|
+
manager: AgentRunRenderingManager,
|
|
722
|
+
renderer: RichStreamRenderer,
|
|
723
|
+
final_text: str,
|
|
724
|
+
stats_usage: dict[str, Any],
|
|
725
|
+
started_monotonic: float | None,
|
|
726
|
+
finished_monotonic: float | None,
|
|
727
|
+
) -> str:
|
|
728
|
+
"""Helper to finalize renderer via manager and return final text.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
manager: The rendering manager instance.
|
|
732
|
+
renderer: Renderer to finalize.
|
|
733
|
+
final_text: Final text content.
|
|
734
|
+
stats_usage: Usage statistics dictionary.
|
|
735
|
+
started_monotonic: Start time (monotonic).
|
|
736
|
+
finished_monotonic: Finish time (monotonic).
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
Final text string.
|
|
740
|
+
"""
|
|
741
|
+
return manager.finalize_renderer(
|
|
742
|
+
renderer,
|
|
743
|
+
final_text,
|
|
744
|
+
stats_usage,
|
|
745
|
+
started_monotonic,
|
|
746
|
+
finished_monotonic,
|
|
747
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Human-in-the-Loop (HITL) utilities for glaip-sdk.
|
|
2
|
+
|
|
3
|
+
This package provides utilities for HITL approval workflows in both local
|
|
4
|
+
and remote agent execution modes.
|
|
5
|
+
|
|
6
|
+
For local development, LocalPromptHandler is automatically injected when
|
|
7
|
+
agent_config.hitl_enabled is True. No manual setup required.
|
|
8
|
+
|
|
9
|
+
Authors:
|
|
10
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from glaip_sdk.hitl.local import LocalPromptHandler, PauseResumeCallback
|
|
14
|
+
|
|
15
|
+
__all__ = ["LocalPromptHandler", "PauseResumeCallback"]
|