glaip-sdk 0.7.0__py3-none-any.whl → 0.7.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/cli/slash/accounts_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +19 -0
- glaip_sdk/cli/slash/tui/__init__.py +16 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +7 -5
- glaip_sdk/cli/slash/tui/accounts_app.py +66 -9
- glaip_sdk/cli/slash/tui/clipboard.py +147 -0
- glaip_sdk/cli/slash/tui/context.py +11 -3
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/terminal.py +2 -2
- glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
- glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
- glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +123 -0
- glaip_sdk/client/agents.py +26 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +2 -0
- glaip_sdk/client/run_rendering.py +33 -1
- glaip_sdk/hitl/__init__.py +14 -1
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/remote.py +523 -0
- {glaip_sdk-0.7.0.dist-info → glaip_sdk-0.7.2.dist-info}/METADATA +1 -1
- {glaip_sdk-0.7.0.dist-info → glaip_sdk-0.7.2.dist-info}/RECORD +26 -16
- {glaip_sdk-0.7.0.dist-info → glaip_sdk-0.7.2.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.7.0.dist-info → glaip_sdk-0.7.2.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.7.0.dist-info → glaip_sdk-0.7.2.dist-info}/top_level.txt +0 -0
|
@@ -89,7 +89,7 @@ class TerminalCapabilities:
|
|
|
89
89
|
|
|
90
90
|
# Basic capability detection
|
|
91
91
|
ansi = tty_available and term not in ("dumb", "")
|
|
92
|
-
osc52 =
|
|
92
|
+
osc52 = detect_osc52_support()
|
|
93
93
|
mouse = tty_available and term not in ("dumb", "")
|
|
94
94
|
truecolor = colorterm in ("truecolor", "24bit")
|
|
95
95
|
|
|
@@ -366,7 +366,7 @@ def _check_terminal_in_env(env_value: str, terminals: list[str]) -> bool:
|
|
|
366
366
|
return any(terminal in env_value for terminal in terminals)
|
|
367
367
|
|
|
368
368
|
|
|
369
|
-
def
|
|
369
|
+
def detect_osc52_support() -> bool:
|
|
370
370
|
"""Check if terminal likely supports OSC 52 (clipboard).
|
|
371
371
|
|
|
372
372
|
Returns:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Theme system primitives for Textual TUIs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from glaip_sdk.cli.slash.tui.theme.catalog import get_builtin_theme, list_builtin_themes
|
|
6
|
+
from glaip_sdk.cli.slash.tui.theme.manager import ThemeManager, ThemeMode
|
|
7
|
+
from glaip_sdk.cli.slash.tui.theme.tokens import ThemeTokens
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"ThemeManager",
|
|
11
|
+
"ThemeMode",
|
|
12
|
+
"ThemeTokens",
|
|
13
|
+
"get_builtin_theme",
|
|
14
|
+
"list_builtin_themes",
|
|
15
|
+
]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Built-in theme catalog for TUI applications.
|
|
2
|
+
|
|
3
|
+
This module implements Phase 2 of the TUI Theme System spec, providing a foundational
|
|
4
|
+
set of 12 color tokens (primary, secondary, accent, background, background_panel, text,
|
|
5
|
+
text_muted, success, warning, error, info). Additional tokens (e.g., diff.added,
|
|
6
|
+
syntax.*, backgroundElevated, textDim) will be added in future phases per the spec's
|
|
7
|
+
"100+ color tokens" requirement.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from glaip_sdk.cli.slash.tui.theme.tokens import ThemeModeLiteral, ThemeTokens
|
|
13
|
+
|
|
14
|
+
_BUILTIN_THEMES: dict[str, ThemeTokens] = {
|
|
15
|
+
"gl-dark": ThemeTokens(
|
|
16
|
+
name="gl-dark",
|
|
17
|
+
mode="dark",
|
|
18
|
+
primary="#6EA8FE",
|
|
19
|
+
secondary="#ADB5BD",
|
|
20
|
+
accent="#C77DFF",
|
|
21
|
+
background="#0B0F19",
|
|
22
|
+
background_panel="#111827",
|
|
23
|
+
text="#E5E7EB",
|
|
24
|
+
text_muted="#9CA3AF",
|
|
25
|
+
success="#34D399",
|
|
26
|
+
warning="#FBBF24",
|
|
27
|
+
error="#F87171",
|
|
28
|
+
info="#60A5FA",
|
|
29
|
+
),
|
|
30
|
+
"gl-light": ThemeTokens(
|
|
31
|
+
name="gl-light",
|
|
32
|
+
mode="light",
|
|
33
|
+
primary="#1D4ED8",
|
|
34
|
+
secondary="#4B5563",
|
|
35
|
+
accent="#7C3AED",
|
|
36
|
+
background="#FFFFFF",
|
|
37
|
+
background_panel="#F3F4F6",
|
|
38
|
+
text="#111827",
|
|
39
|
+
text_muted="#4B5563",
|
|
40
|
+
success="#059669",
|
|
41
|
+
warning="#B45309",
|
|
42
|
+
error="#B91C1C",
|
|
43
|
+
info="#1D4ED8",
|
|
44
|
+
),
|
|
45
|
+
"gl-high-contrast": ThemeTokens(
|
|
46
|
+
name="gl-high-contrast",
|
|
47
|
+
mode="dark",
|
|
48
|
+
# High-contrast theme uses uniform colors (#FFFFFF on #000000) to maximize
|
|
49
|
+
# contrast for accessibility. Semantic distinctions (success/warning/error)
|
|
50
|
+
# are intentionally uniform to prioritize maximum readability over color
|
|
51
|
+
# coding, per accessibility best practices for high-contrast modes.
|
|
52
|
+
primary="#FFFFFF",
|
|
53
|
+
secondary="#FFFFFF",
|
|
54
|
+
accent="#FFFFFF",
|
|
55
|
+
background="#000000",
|
|
56
|
+
background_panel="#000000",
|
|
57
|
+
text="#FFFFFF",
|
|
58
|
+
text_muted="#FFFFFF",
|
|
59
|
+
success="#FFFFFF",
|
|
60
|
+
warning="#FFFFFF",
|
|
61
|
+
error="#FFFFFF",
|
|
62
|
+
info="#FFFFFF",
|
|
63
|
+
),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_builtin_theme(name: str) -> ThemeTokens | None:
|
|
68
|
+
"""Return a built-in theme by name."""
|
|
69
|
+
return _BUILTIN_THEMES.get(name)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def list_builtin_themes() -> list[str]:
|
|
73
|
+
"""List available built-in theme names."""
|
|
74
|
+
return sorted(_BUILTIN_THEMES)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def default_theme_name_for_mode(mode: ThemeModeLiteral) -> str:
|
|
78
|
+
"""Return the default theme name for the given light/dark mode."""
|
|
79
|
+
return "gl-light" if mode == "light" else "gl-dark"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Theme manager for TUI applications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
|
|
10
|
+
from glaip_sdk.cli.slash.tui.theme.catalog import default_theme_name_for_mode, get_builtin_theme
|
|
11
|
+
from glaip_sdk.cli.slash.tui.theme.tokens import ThemeTokens
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ThemeMode(str, Enum):
|
|
17
|
+
"""User-selectable theme mode."""
|
|
18
|
+
|
|
19
|
+
AUTO = "auto"
|
|
20
|
+
LIGHT = "light"
|
|
21
|
+
DARK = "dark"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ThemeManager:
|
|
25
|
+
"""Resolve active theme tokens from terminal state and user preferences."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
terminal: TerminalCapabilities,
|
|
30
|
+
*,
|
|
31
|
+
mode: ThemeMode | str = ThemeMode.AUTO,
|
|
32
|
+
theme: str | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize the theme manager."""
|
|
35
|
+
self._terminal = terminal
|
|
36
|
+
self._mode = self._coerce_mode(mode)
|
|
37
|
+
self._theme = theme
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def mode(self) -> ThemeMode:
|
|
41
|
+
"""Return configured mode (auto/light/dark)."""
|
|
42
|
+
return self._mode
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def effective_mode(self) -> Literal["light", "dark"]:
|
|
46
|
+
"""Return resolved light/dark mode."""
|
|
47
|
+
if self._mode == ThemeMode.AUTO:
|
|
48
|
+
return self._terminal.background_mode
|
|
49
|
+
return "light" if self._mode == ThemeMode.LIGHT else "dark"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def theme_name(self) -> str:
|
|
53
|
+
"""Return resolved theme name."""
|
|
54
|
+
return self._theme or default_theme_name_for_mode(self.effective_mode)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def tokens(self) -> ThemeTokens:
|
|
58
|
+
"""Return tokens for the resolved theme."""
|
|
59
|
+
chosen = get_builtin_theme(self.theme_name)
|
|
60
|
+
if chosen is not None:
|
|
61
|
+
return chosen
|
|
62
|
+
|
|
63
|
+
fallback_name = default_theme_name_for_mode(self.effective_mode)
|
|
64
|
+
fallback = get_builtin_theme(fallback_name)
|
|
65
|
+
if fallback is None:
|
|
66
|
+
raise RuntimeError(f"Missing default theme: {fallback_name}")
|
|
67
|
+
|
|
68
|
+
return fallback
|
|
69
|
+
|
|
70
|
+
def set_mode(self, mode: ThemeMode | str) -> None:
|
|
71
|
+
"""Set auto/light/dark mode."""
|
|
72
|
+
self._mode = self._coerce_mode(mode)
|
|
73
|
+
|
|
74
|
+
def set_theme(self, theme: str | None) -> None:
|
|
75
|
+
"""Set explicit theme name (or None to use the default)."""
|
|
76
|
+
self._theme = theme
|
|
77
|
+
|
|
78
|
+
def _coerce_mode(self, mode: ThemeMode | str) -> ThemeMode:
|
|
79
|
+
"""Coerce a mode value to ThemeMode enum, defaulting to AUTO on invalid input."""
|
|
80
|
+
if isinstance(mode, ThemeMode):
|
|
81
|
+
return mode
|
|
82
|
+
try:
|
|
83
|
+
return ThemeMode(mode)
|
|
84
|
+
except ValueError:
|
|
85
|
+
logger.warning(f"Invalid theme mode '{mode}', defaulting to AUTO")
|
|
86
|
+
return ThemeMode.AUTO
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Theme token definitions for TUI applications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
ThemeModeLiteral = Literal["light", "dark"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class ThemeTokens:
|
|
13
|
+
"""Color token set for a built-in theme."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
mode: ThemeModeLiteral
|
|
17
|
+
|
|
18
|
+
primary: str
|
|
19
|
+
secondary: str
|
|
20
|
+
accent: str
|
|
21
|
+
|
|
22
|
+
background: str
|
|
23
|
+
background_panel: str
|
|
24
|
+
|
|
25
|
+
text: str
|
|
26
|
+
text_muted: str
|
|
27
|
+
|
|
28
|
+
success: str
|
|
29
|
+
warning: str
|
|
30
|
+
error: str
|
|
31
|
+
info: str
|
|
32
|
+
|
|
33
|
+
def as_dict(self) -> dict[str, str]:
|
|
34
|
+
"""Return color tokens as a plain dictionary.
|
|
35
|
+
|
|
36
|
+
Returns only color tokens (primary, secondary, accent, background, etc.),
|
|
37
|
+
excluding metadata fields (name, mode). This is intentional for use cases
|
|
38
|
+
like Textual TCSS mapping where only color values are needed.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dictionary mapping color token names to hex color strings.
|
|
42
|
+
"""
|
|
43
|
+
return {
|
|
44
|
+
"primary": self.primary,
|
|
45
|
+
"secondary": self.secondary,
|
|
46
|
+
"accent": self.accent,
|
|
47
|
+
"background": self.background,
|
|
48
|
+
"background_panel": self.background_panel,
|
|
49
|
+
"text": self.text,
|
|
50
|
+
"text_muted": self.text_muted,
|
|
51
|
+
"success": self.success,
|
|
52
|
+
"warning": self.warning,
|
|
53
|
+
"error": self.error,
|
|
54
|
+
"info": self.info,
|
|
55
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Toast notification helpers for Textual TUIs.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ToastVariant(str, Enum):
|
|
15
|
+
"""Toast message variant."""
|
|
16
|
+
|
|
17
|
+
INFO = "info"
|
|
18
|
+
SUCCESS = "success"
|
|
19
|
+
WARNING = "warning"
|
|
20
|
+
ERROR = "error"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
DEFAULT_TOAST_DURATIONS_SECONDS: dict[ToastVariant, float] = {
|
|
24
|
+
ToastVariant.SUCCESS: 2.0,
|
|
25
|
+
ToastVariant.INFO: 3.0,
|
|
26
|
+
ToastVariant.WARNING: 3.0,
|
|
27
|
+
ToastVariant.ERROR: 5.0,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class ToastState:
|
|
33
|
+
"""Immutable toast payload."""
|
|
34
|
+
|
|
35
|
+
message: str
|
|
36
|
+
variant: ToastVariant
|
|
37
|
+
duration_seconds: float
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ToastBus:
|
|
41
|
+
"""Single-toast state holder with auto-dismiss."""
|
|
42
|
+
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
"""Initialize the bus."""
|
|
45
|
+
self._state: ToastState | None = None
|
|
46
|
+
self._dismiss_task: asyncio.Task[None] | None = None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def state(self) -> ToastState | None:
|
|
50
|
+
"""Return the current toast state."""
|
|
51
|
+
return self._state
|
|
52
|
+
|
|
53
|
+
def show(
|
|
54
|
+
self,
|
|
55
|
+
message: str,
|
|
56
|
+
variant: ToastVariant | str = ToastVariant.INFO,
|
|
57
|
+
*,
|
|
58
|
+
duration_seconds: float | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Set toast state and schedule auto-dismiss."""
|
|
61
|
+
resolved_variant = self._coerce_variant(variant)
|
|
62
|
+
resolved_duration = (
|
|
63
|
+
DEFAULT_TOAST_DURATIONS_SECONDS[resolved_variant] if duration_seconds is None else float(duration_seconds)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self._state = ToastState(
|
|
67
|
+
message=message,
|
|
68
|
+
variant=resolved_variant,
|
|
69
|
+
duration_seconds=resolved_duration,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self._cancel_dismiss_task()
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
loop = asyncio.get_running_loop()
|
|
76
|
+
except RuntimeError:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
"Cannot schedule toast auto-dismiss: no running event loop. "
|
|
79
|
+
"ToastBus.show() must be called from within an async context."
|
|
80
|
+
) from None
|
|
81
|
+
|
|
82
|
+
self._dismiss_task = loop.create_task(self._auto_dismiss(resolved_duration))
|
|
83
|
+
|
|
84
|
+
def clear(self) -> None:
|
|
85
|
+
"""Clear the current toast."""
|
|
86
|
+
self._cancel_dismiss_task()
|
|
87
|
+
self._state = None
|
|
88
|
+
|
|
89
|
+
def copy_success(self, label: str | None = None) -> None:
|
|
90
|
+
"""Show clipboard success toast."""
|
|
91
|
+
message = "Copied to clipboard" if not label else f"Copied {label} to clipboard"
|
|
92
|
+
self.show(message=message, variant=ToastVariant.SUCCESS)
|
|
93
|
+
|
|
94
|
+
def copy_failed(self) -> None:
|
|
95
|
+
"""Show clipboard failure toast."""
|
|
96
|
+
self.show(
|
|
97
|
+
message="Clipboard unavailable. Text printed below",
|
|
98
|
+
variant=ToastVariant.WARNING,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _coerce_variant(self, variant: ToastVariant | str) -> ToastVariant:
|
|
102
|
+
if isinstance(variant, ToastVariant):
|
|
103
|
+
return variant
|
|
104
|
+
try:
|
|
105
|
+
return ToastVariant(variant)
|
|
106
|
+
except ValueError:
|
|
107
|
+
return ToastVariant.INFO
|
|
108
|
+
|
|
109
|
+
def _cancel_dismiss_task(self) -> None:
|
|
110
|
+
if self._dismiss_task is None:
|
|
111
|
+
return
|
|
112
|
+
if not self._dismiss_task.done():
|
|
113
|
+
self._dismiss_task.cancel()
|
|
114
|
+
self._dismiss_task = None
|
|
115
|
+
|
|
116
|
+
async def _auto_dismiss(self, duration_seconds: float) -> None:
|
|
117
|
+
try:
|
|
118
|
+
await asyncio.sleep(duration_seconds)
|
|
119
|
+
except asyncio.CancelledError:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
self._state = None
|
|
123
|
+
self._dismiss_task = None
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, BinaryIO
|
|
|
17
17
|
|
|
18
18
|
if TYPE_CHECKING:
|
|
19
19
|
from glaip_sdk.client.schedules import ScheduleClient
|
|
20
|
+
from glaip_sdk.hitl.remote import RemoteHITLHandler
|
|
20
21
|
|
|
21
22
|
import httpx
|
|
22
23
|
from glaip_sdk.agents import Agent
|
|
@@ -415,6 +416,7 @@ class AgentClient(BaseClient):
|
|
|
415
416
|
timeout_seconds: float,
|
|
416
417
|
agent_name: str | None,
|
|
417
418
|
meta: dict[str, Any],
|
|
419
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
418
420
|
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
419
421
|
"""Process stream events from an HTTP response.
|
|
420
422
|
|
|
@@ -424,6 +426,7 @@ class AgentClient(BaseClient):
|
|
|
424
426
|
timeout_seconds: Timeout in seconds.
|
|
425
427
|
agent_name: Optional agent name.
|
|
426
428
|
meta: Metadata dictionary.
|
|
429
|
+
hitl_handler: Optional HITL handler for approval callbacks.
|
|
427
430
|
|
|
428
431
|
Returns:
|
|
429
432
|
Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
|
|
@@ -435,6 +438,7 @@ class AgentClient(BaseClient):
|
|
|
435
438
|
timeout_seconds,
|
|
436
439
|
agent_name,
|
|
437
440
|
meta,
|
|
441
|
+
hitl_handler=hitl_handler,
|
|
438
442
|
)
|
|
439
443
|
|
|
440
444
|
def _finalize_renderer(
|
|
@@ -1112,6 +1116,7 @@ class AgentClient(BaseClient):
|
|
|
1112
1116
|
*,
|
|
1113
1117
|
renderer: RichStreamRenderer | str | None = "auto",
|
|
1114
1118
|
runtime_config: dict[str, Any] | None = None,
|
|
1119
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
1115
1120
|
**kwargs,
|
|
1116
1121
|
) -> str:
|
|
1117
1122
|
"""Run an agent with a message, streaming via a renderer.
|
|
@@ -1129,6 +1134,8 @@ class AgentClient(BaseClient):
|
|
|
1129
1134
|
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
1130
1135
|
"agent_config": {"planning": True},
|
|
1131
1136
|
}
|
|
1137
|
+
hitl_handler: Optional RemoteHITLHandler for approval callbacks.
|
|
1138
|
+
Set GLAIP_HITL_AUTO_APPROVE=true for auto-approval without handler.
|
|
1132
1139
|
**kwargs: Additional arguments to pass to the run API.
|
|
1133
1140
|
|
|
1134
1141
|
Returns:
|
|
@@ -1185,6 +1192,7 @@ class AgentClient(BaseClient):
|
|
|
1185
1192
|
timeout_seconds,
|
|
1186
1193
|
agent_name,
|
|
1187
1194
|
meta,
|
|
1195
|
+
hitl_handler=hitl_handler,
|
|
1188
1196
|
)
|
|
1189
1197
|
|
|
1190
1198
|
except KeyboardInterrupt:
|
|
@@ -1201,6 +1209,13 @@ class AgentClient(BaseClient):
|
|
|
1201
1209
|
if multipart_data:
|
|
1202
1210
|
multipart_data.close()
|
|
1203
1211
|
|
|
1212
|
+
# Wait for pending HITL decisions before returning
|
|
1213
|
+
if hitl_handler and hasattr(hitl_handler, "wait_for_pending_decisions"):
|
|
1214
|
+
try:
|
|
1215
|
+
hitl_handler.wait_for_pending_decisions(timeout=30)
|
|
1216
|
+
except Exception as e:
|
|
1217
|
+
logger.warning(f"Error waiting for HITL decisions: {e}")
|
|
1218
|
+
|
|
1204
1219
|
return self._finalize_renderer(
|
|
1205
1220
|
r,
|
|
1206
1221
|
final_text,
|
|
@@ -1282,6 +1297,7 @@ class AgentClient(BaseClient):
|
|
|
1282
1297
|
*,
|
|
1283
1298
|
request_timeout: float | None = None,
|
|
1284
1299
|
runtime_config: dict[str, Any] | None = None,
|
|
1300
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
1285
1301
|
**kwargs,
|
|
1286
1302
|
) -> AsyncGenerator[dict, None]:
|
|
1287
1303
|
"""Async run an agent with a message, yielding streaming JSON chunks.
|
|
@@ -1298,16 +1314,26 @@ class AgentClient(BaseClient):
|
|
|
1298
1314
|
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
1299
1315
|
"agent_config": {"planning": True},
|
|
1300
1316
|
}
|
|
1317
|
+
hitl_handler: Optional HITL handler for remote approval requests.
|
|
1318
|
+
Note: Async HITL support is currently deferred. This parameter
|
|
1319
|
+
is accepted for API consistency but will raise NotImplementedError
|
|
1320
|
+
if provided.
|
|
1301
1321
|
**kwargs: Additional arguments (chat_history, pii_mapping, etc.)
|
|
1302
1322
|
|
|
1303
1323
|
Yields:
|
|
1304
1324
|
Dictionary containing parsed JSON chunks from the streaming response
|
|
1305
1325
|
|
|
1306
1326
|
Raises:
|
|
1327
|
+
NotImplementedError: If hitl_handler is provided (async HITL not yet supported)
|
|
1307
1328
|
AgentTimeoutError: When agent execution times out
|
|
1308
1329
|
httpx.TimeoutException: When general timeout occurs
|
|
1309
1330
|
Exception: For other unexpected errors
|
|
1310
1331
|
"""
|
|
1332
|
+
if hitl_handler is not None:
|
|
1333
|
+
raise NotImplementedError(
|
|
1334
|
+
"Async HITL support is currently deferred. "
|
|
1335
|
+
"Please use the synchronous run_agent() method with hitl_handler."
|
|
1336
|
+
)
|
|
1311
1337
|
# Include runtime_config in kwargs only when caller hasn't already provided it
|
|
1312
1338
|
if runtime_config is not None and "runtime_config" not in kwargs:
|
|
1313
1339
|
kwargs["runtime_config"] = runtime_config
|
glaip_sdk/client/hitl.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""HITL REST client for manual approval operations.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
GLAIP SDK Team
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.client.base import BaseClient
|
|
11
|
+
from glaip_sdk.hitl.base import HITLDecision
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HITLClient(BaseClient):
|
|
15
|
+
"""Client for HITL REST endpoints.
|
|
16
|
+
|
|
17
|
+
Use for manual approval workflows separate from agent runs.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> # List pending approvals
|
|
21
|
+
>>> pending = client.hitl.list_pending()
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Approve a request
|
|
24
|
+
>>> client.hitl.approve(
|
|
25
|
+
... request_id="bc4d0a77-7800-470e-a91c-7fd663a66b4d",
|
|
26
|
+
... operator_input="Verified and approved",
|
|
27
|
+
... )
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def approve(
|
|
31
|
+
self,
|
|
32
|
+
request_id: str,
|
|
33
|
+
operator_input: str | None = None,
|
|
34
|
+
run_id: str | None = None,
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
"""Approve a HITL request.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
request_id: HITL request ID from SSE stream
|
|
40
|
+
operator_input: Optional notes/reason for approval
|
|
41
|
+
run_id: Optional client-side run correlation ID
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Response dict: {"status": "ok", "message": "..."}
|
|
45
|
+
"""
|
|
46
|
+
return self._post_decision(
|
|
47
|
+
request_id,
|
|
48
|
+
HITLDecision.APPROVED,
|
|
49
|
+
operator_input,
|
|
50
|
+
run_id,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def reject(
|
|
54
|
+
self,
|
|
55
|
+
request_id: str,
|
|
56
|
+
operator_input: str | None = None,
|
|
57
|
+
run_id: str | None = None,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
"""Reject a HITL request.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
request_id: HITL request ID
|
|
63
|
+
operator_input: Optional reason for rejection
|
|
64
|
+
run_id: Optional run correlation ID
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Response dict
|
|
68
|
+
"""
|
|
69
|
+
return self._post_decision(
|
|
70
|
+
request_id,
|
|
71
|
+
HITLDecision.REJECTED,
|
|
72
|
+
operator_input,
|
|
73
|
+
run_id,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def skip(
|
|
77
|
+
self,
|
|
78
|
+
request_id: str,
|
|
79
|
+
operator_input: str | None = None,
|
|
80
|
+
run_id: str | None = None,
|
|
81
|
+
) -> dict[str, Any]:
|
|
82
|
+
"""Skip a HITL request.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
request_id: HITL request ID
|
|
86
|
+
operator_input: Optional notes
|
|
87
|
+
run_id: Optional run correlation ID
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Response dict
|
|
91
|
+
"""
|
|
92
|
+
return self._post_decision(
|
|
93
|
+
request_id,
|
|
94
|
+
HITLDecision.SKIPPED,
|
|
95
|
+
operator_input,
|
|
96
|
+
run_id,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _post_decision(
|
|
100
|
+
self,
|
|
101
|
+
request_id: str,
|
|
102
|
+
decision: HITLDecision,
|
|
103
|
+
operator_input: str | None,
|
|
104
|
+
run_id: str | None,
|
|
105
|
+
) -> dict[str, Any]:
|
|
106
|
+
"""Post HITL decision to backend."""
|
|
107
|
+
payload = {
|
|
108
|
+
"request_id": request_id,
|
|
109
|
+
"decision": decision.value,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if operator_input:
|
|
113
|
+
payload["operator_input"] = operator_input
|
|
114
|
+
if run_id:
|
|
115
|
+
payload["run_id"] = run_id
|
|
116
|
+
|
|
117
|
+
return self._request("POST", "/agents/hitl/decision", json=payload)
|
|
118
|
+
|
|
119
|
+
def list_pending(self) -> list[dict[str, Any]]:
|
|
120
|
+
"""List all pending HITL requests.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of pending request dicts with metadata:
|
|
124
|
+
[
|
|
125
|
+
{
|
|
126
|
+
"request_id": "...",
|
|
127
|
+
"tool": "...",
|
|
128
|
+
"arguments": {...},
|
|
129
|
+
"created_at": "...",
|
|
130
|
+
"agent_id": "...",
|
|
131
|
+
"hitl_metadata": {...},
|
|
132
|
+
},
|
|
133
|
+
...
|
|
134
|
+
]
|
|
135
|
+
"""
|
|
136
|
+
return self._request("GET", "/agents/hitl/pending")
|
glaip_sdk/client/main.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
12
12
|
|
|
13
13
|
from glaip_sdk.client.agents import AgentClient
|
|
14
14
|
from glaip_sdk.client.base import BaseClient
|
|
15
|
+
from glaip_sdk.client.hitl import HITLClient
|
|
15
16
|
from glaip_sdk.client.mcps import MCPClient
|
|
16
17
|
from glaip_sdk.client.schedules import ScheduleClient
|
|
17
18
|
from glaip_sdk.client.shared import build_shared_config
|
|
@@ -40,6 +41,7 @@ class Client(BaseClient):
|
|
|
40
41
|
self.tools = ToolClient(**shared_config)
|
|
41
42
|
self.mcps = MCPClient(**shared_config)
|
|
42
43
|
self.schedules = ScheduleClient(**shared_config)
|
|
44
|
+
self.hitl = HITLClient(**shared_config)
|
|
43
45
|
|
|
44
46
|
# ---- Core API Methods (Public Interface) ----
|
|
45
47
|
|