glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.12__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 +217 -42
- glaip_sdk/branding.py +113 -2
- glaip_sdk/cli/account_store.py +15 -0
- glaip_sdk/cli/auth.py +14 -8
- glaip_sdk/cli/commands/accounts.py +1 -1
- glaip_sdk/cli/commands/agents/__init__.py +119 -0
- glaip_sdk/cli/commands/agents/_common.py +561 -0
- glaip_sdk/cli/commands/agents/create.py +151 -0
- glaip_sdk/cli/commands/agents/delete.py +64 -0
- glaip_sdk/cli/commands/agents/get.py +89 -0
- glaip_sdk/cli/commands/agents/list.py +129 -0
- glaip_sdk/cli/commands/agents/run.py +264 -0
- glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
- glaip_sdk/cli/commands/agents/update.py +112 -0
- glaip_sdk/cli/commands/common_config.py +15 -12
- glaip_sdk/cli/commands/configure.py +2 -3
- glaip_sdk/cli/commands/mcps/__init__.py +94 -0
- glaip_sdk/cli/commands/mcps/_common.py +459 -0
- glaip_sdk/cli/commands/mcps/connect.py +82 -0
- glaip_sdk/cli/commands/mcps/create.py +152 -0
- glaip_sdk/cli/commands/mcps/delete.py +73 -0
- glaip_sdk/cli/commands/mcps/get.py +212 -0
- glaip_sdk/cli/commands/mcps/list.py +69 -0
- glaip_sdk/cli/commands/mcps/tools.py +235 -0
- glaip_sdk/cli/commands/mcps/update.py +190 -0
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/shared/__init__.py +21 -0
- glaip_sdk/cli/commands/shared/formatters.py +91 -0
- glaip_sdk/cli/commands/tools/__init__.py +69 -0
- glaip_sdk/cli/commands/tools/_common.py +80 -0
- glaip_sdk/cli/commands/tools/create.py +228 -0
- glaip_sdk/cli/commands/tools/delete.py +61 -0
- glaip_sdk/cli/commands/tools/get.py +103 -0
- glaip_sdk/cli/commands/tools/list.py +69 -0
- glaip_sdk/cli/commands/tools/script.py +49 -0
- glaip_sdk/cli/commands/tools/update.py +102 -0
- glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
- glaip_sdk/cli/commands/transcripts/_common.py +9 -0
- glaip_sdk/cli/commands/transcripts/clear.py +5 -0
- glaip_sdk/cli/commands/transcripts/detail.py +5 -0
- glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
- glaip_sdk/cli/commands/update.py +163 -17
- glaip_sdk/cli/config.py +1 -0
- glaip_sdk/cli/core/output.py +12 -7
- glaip_sdk/cli/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +127 -39
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +112 -32
- glaip_sdk/cli/slash/agent_session.py +5 -2
- glaip_sdk/cli/slash/prompt.py +11 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +369 -23
- glaip_sdk/cli/slash/tui/__init__.py +26 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +79 -5
- glaip_sdk/cli/slash/tui/accounts_app.py +1027 -88
- glaip_sdk/cli/slash/tui/clipboard.py +195 -0
- glaip_sdk/cli/slash/tui/context.py +87 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
- glaip_sdk/cli/slash/tui/layouts/harlequin.py +160 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +119 -12
- glaip_sdk/cli/slash/tui/terminal.py +407 -0
- 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 +112 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +374 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +5 -3
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/cli/update_notifier.py +215 -7
- glaip_sdk/cli/validators.py +1 -1
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_schedule_payloads.py +89 -0
- glaip_sdk/client/agents.py +50 -8
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -1
- glaip_sdk/client/mcps.py +44 -13
- glaip_sdk/client/payloads/agent/__init__.py +23 -0
- glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +414 -3
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +57 -26
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +89 -0
- glaip_sdk/hitl/__init__.py +48 -0
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +121 -0
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +17 -0
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/schedule.py +224 -0
- glaip_sdk/payload_schemas/agent.py +1 -0
- glaip_sdk/payload_schemas/guardrails.py +34 -0
- glaip_sdk/registry/tool.py +273 -59
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +5 -8
- glaip_sdk/runner/langgraph.py +318 -42
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
- glaip_sdk/schedules/__init__.py +22 -0
- glaip_sdk/schedules/base.py +291 -0
- glaip_sdk/tools/base.py +67 -14
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +43 -11
- glaip_sdk/utils/rendering/renderer/base.py +58 -0
- glaip_sdk/utils/runtime_config.py +15 -12
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- glaip_sdk/utils/tool_storage_provider.py +140 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/METADATA +49 -37
- glaip_sdk-0.7.12.dist-info/RECORD +219 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.12.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.12.dist-info/top_level.txt +1 -0
- glaip_sdk/cli/commands/agents.py +0 -1509
- glaip_sdk/cli/commands/mcps.py +0 -1356
- glaip_sdk/cli/commands/tools.py +0 -576
- glaip_sdk/cli/utils.py +0 -263
- glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
- glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Typed loader/saver for TUI preferences stored in config.yaml.
|
|
2
|
+
|
|
3
|
+
This module implements the TUI preferences store as defined in
|
|
4
|
+
`specs/architecture/tui-preferences-store/spec.md`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Literal, cast
|
|
11
|
+
|
|
12
|
+
from glaip_sdk.cli.account_store import AccountStore, get_account_store
|
|
13
|
+
|
|
14
|
+
ThemeModeValue = Literal["auto", "light", "dark"]
|
|
15
|
+
|
|
16
|
+
_DEFAULT_THEME_MODE: ThemeModeValue = "auto"
|
|
17
|
+
_DEFAULT_LEADER = "ctrl+x"
|
|
18
|
+
_DEFAULT_MOUSE_CAPTURE = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class TUISettings:
|
|
23
|
+
"""Resolved TUI preferences from config.yaml."""
|
|
24
|
+
|
|
25
|
+
theme_mode: ThemeModeValue = _DEFAULT_THEME_MODE
|
|
26
|
+
theme_name: str | None = None
|
|
27
|
+
leader: str = _DEFAULT_LEADER
|
|
28
|
+
keybind_overrides: dict[str, str] = field(default_factory=dict)
|
|
29
|
+
mouse_capture: bool = _DEFAULT_MOUSE_CAPTURE
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_tui_settings(*, store: AccountStore | None = None) -> TUISettings:
|
|
33
|
+
"""Load TUI preferences from the CLI config file."""
|
|
34
|
+
store = store or get_account_store()
|
|
35
|
+
config = store.load_config()
|
|
36
|
+
|
|
37
|
+
tui = _as_dict(config.get("tui"))
|
|
38
|
+
theme = _as_dict(tui.get("theme"))
|
|
39
|
+
mode = _coerce_theme_mode(theme.get("mode"))
|
|
40
|
+
name = _normalize_theme_name(theme.get("name"))
|
|
41
|
+
|
|
42
|
+
keybinds = _as_dict(tui.get("keybinds"))
|
|
43
|
+
leader = keybinds.get("leader")
|
|
44
|
+
if not isinstance(leader, str) or not leader.strip():
|
|
45
|
+
leader = _DEFAULT_LEADER
|
|
46
|
+
|
|
47
|
+
overrides = _coerce_keybind_overrides(keybinds.get("overrides"))
|
|
48
|
+
|
|
49
|
+
mouse_capture = tui.get("mouse_capture")
|
|
50
|
+
if not isinstance(mouse_capture, bool):
|
|
51
|
+
mouse_capture = _DEFAULT_MOUSE_CAPTURE
|
|
52
|
+
|
|
53
|
+
return TUISettings(
|
|
54
|
+
theme_mode=mode,
|
|
55
|
+
theme_name=name,
|
|
56
|
+
leader=leader,
|
|
57
|
+
keybind_overrides=overrides,
|
|
58
|
+
mouse_capture=mouse_capture,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def update_tui_settings(patch: dict[str, Any], *, store: AccountStore | None = None) -> None:
|
|
63
|
+
"""Update TUI preferences, preserving unrelated config keys."""
|
|
64
|
+
store = store or get_account_store()
|
|
65
|
+
config = store.load_config()
|
|
66
|
+
|
|
67
|
+
existing = _as_dict(config.get("tui"))
|
|
68
|
+
config["tui"] = _merge_dict(existing, patch)
|
|
69
|
+
|
|
70
|
+
store.save_config_updates(config)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def persist_tui_theme(*, mode: ThemeModeValue, name: str | None, store: AccountStore | None = None) -> None:
|
|
74
|
+
"""Persist theme preferences in the tui.theme namespace."""
|
|
75
|
+
update_tui_settings(
|
|
76
|
+
{"theme": {"mode": _coerce_theme_mode(mode), "name": _serialize_theme_name(name)}},
|
|
77
|
+
store=store,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _as_dict(value: Any) -> dict[str, Any]:
|
|
82
|
+
if isinstance(value, dict):
|
|
83
|
+
return dict(value)
|
|
84
|
+
return {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _coerce_theme_mode(value: Any) -> ThemeModeValue:
|
|
88
|
+
if isinstance(value, str):
|
|
89
|
+
lowered = value.strip().lower()
|
|
90
|
+
if lowered in {"auto", "light", "dark"}:
|
|
91
|
+
return cast(ThemeModeValue, lowered)
|
|
92
|
+
return _DEFAULT_THEME_MODE
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalize_theme_name(value: Any) -> str | None:
|
|
96
|
+
if not isinstance(value, str):
|
|
97
|
+
return None
|
|
98
|
+
cleaned = value.strip()
|
|
99
|
+
if not cleaned or cleaned.lower() == "default":
|
|
100
|
+
return None
|
|
101
|
+
return cleaned
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _serialize_theme_name(name: str | None) -> str:
|
|
105
|
+
if isinstance(name, str):
|
|
106
|
+
cleaned = name.strip()
|
|
107
|
+
if cleaned:
|
|
108
|
+
return cleaned
|
|
109
|
+
return "default"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _coerce_keybind_overrides(value: Any) -> dict[str, str]:
|
|
113
|
+
if not isinstance(value, dict):
|
|
114
|
+
return {}
|
|
115
|
+
return {key: val for key, val in value.items() if isinstance(key, str) and isinstance(val, str)}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _merge_dict(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
119
|
+
merged = dict(base)
|
|
120
|
+
for key, value in patch.items():
|
|
121
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
122
|
+
merged[key] = _merge_dict(cast(dict[str, Any], merged[key]), value)
|
|
123
|
+
else:
|
|
124
|
+
merged[key] = value
|
|
125
|
+
return merged
|
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -22,13 +22,19 @@ from rich.console import Console
|
|
|
22
22
|
from glaip_sdk.branding import (
|
|
23
23
|
ACCENT_STYLE,
|
|
24
24
|
ERROR_STYLE,
|
|
25
|
+
INFO_STYLE,
|
|
25
26
|
SUCCESS_STYLE,
|
|
26
27
|
WARNING_STYLE,
|
|
27
28
|
)
|
|
28
|
-
from glaip_sdk.cli.commands.update import
|
|
29
|
+
from glaip_sdk.cli.commands.update import (
|
|
30
|
+
PACKAGE_NAME,
|
|
31
|
+
_build_command_parts,
|
|
32
|
+
_build_manual_upgrade_command,
|
|
33
|
+
_is_uv_managed_environment,
|
|
34
|
+
update_command,
|
|
35
|
+
)
|
|
29
36
|
from glaip_sdk.cli.constants import UPDATE_CHECK_ENABLED
|
|
30
|
-
from glaip_sdk.cli.hints import format_command_hint
|
|
31
|
-
from glaip_sdk.cli.utils import command_hint
|
|
37
|
+
from glaip_sdk.cli.hints import command_hint, format_command_hint
|
|
32
38
|
from glaip_sdk.rich_components import AIPPanel
|
|
33
39
|
|
|
34
40
|
FetchLatestVersion = Callable[[], str | None]
|
|
@@ -37,6 +43,7 @@ PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
|
|
|
37
43
|
DEFAULT_TIMEOUT = 1.5 # seconds
|
|
38
44
|
|
|
39
45
|
_LOGGER = logging.getLogger(__name__)
|
|
46
|
+
_UPDATE_VERSIONS_KEY = "_update_notifier_versions"
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
def _parse_version(value: str) -> Version | None:
|
|
@@ -146,6 +153,7 @@ def maybe_notify_update(
|
|
|
146
153
|
"- choose Update now or Skip to continue."
|
|
147
154
|
)
|
|
148
155
|
active_console.print(message)
|
|
156
|
+
_stash_update_versions(ctx, current_version, latest_version)
|
|
149
157
|
_handle_update_decision(active_console, ctx)
|
|
150
158
|
return
|
|
151
159
|
|
|
@@ -161,6 +169,7 @@ def maybe_notify_update(
|
|
|
161
169
|
)
|
|
162
170
|
active_console.print(panel)
|
|
163
171
|
if should_prompt:
|
|
172
|
+
_stash_update_versions(ctx, current_version, latest_version)
|
|
164
173
|
_handle_update_decision(active_console, ctx)
|
|
165
174
|
|
|
166
175
|
|
|
@@ -201,7 +210,13 @@ def _prompt_update_decision(console: Console) -> Literal["update", "skip"]:
|
|
|
201
210
|
|
|
202
211
|
while True:
|
|
203
212
|
try:
|
|
204
|
-
|
|
213
|
+
raw_response = console.input("Choice [1/2]: ")
|
|
214
|
+
# Strip whitespace and convert to lowercase
|
|
215
|
+
response = raw_response.strip().lower()
|
|
216
|
+
# Remove any non-printable control characters (but keep printable chars)
|
|
217
|
+
# This handles cases where ANSI escape sequences or other control chars leak into input
|
|
218
|
+
response = "".join(char for char in response if char.isprintable() or char.isspace())
|
|
219
|
+
response = response.strip() # Strip again after filtering
|
|
205
220
|
except (KeyboardInterrupt, EOFError):
|
|
206
221
|
console.print(f"\n[{WARNING_STYLE}]Update skipped.[/]")
|
|
207
222
|
return "skip"
|
|
@@ -214,19 +229,93 @@ def _prompt_update_decision(console: Console) -> Literal["update", "skip"]:
|
|
|
214
229
|
console.print(f"[{ERROR_STYLE}]Please enter 1 to update now or 2 to skip.[/]")
|
|
215
230
|
|
|
216
231
|
|
|
232
|
+
def _get_manual_upgrade_command(is_uv: bool) -> str:
|
|
233
|
+
"""Get the manual upgrade command for the given environment type.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
is_uv: True if running in uv tool environment, False for pip environment.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Manual upgrade command string.
|
|
240
|
+
"""
|
|
241
|
+
try:
|
|
242
|
+
return _build_manual_upgrade_command(include_prerelease=False, is_uv=is_uv)
|
|
243
|
+
except Exception:
|
|
244
|
+
# Fallback: rebuild from shared command parts to avoid hardcoded strings.
|
|
245
|
+
try:
|
|
246
|
+
command_parts, _ = _build_command_parts(
|
|
247
|
+
package_name=PACKAGE_NAME,
|
|
248
|
+
is_uv=is_uv,
|
|
249
|
+
force_reinstall=False,
|
|
250
|
+
include_prerelease=False,
|
|
251
|
+
)
|
|
252
|
+
except Exception:
|
|
253
|
+
command_parts = (
|
|
254
|
+
["uv", "tool", "install", "--upgrade", PACKAGE_NAME]
|
|
255
|
+
if is_uv
|
|
256
|
+
else ["pip", "install", "--upgrade", PACKAGE_NAME]
|
|
257
|
+
)
|
|
258
|
+
return " ".join(command_parts)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _show_proactive_uv_guidance(console: Console, is_uv: bool) -> None:
|
|
262
|
+
"""Show proactive guidance for uv environments before update attempt.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
console: Rich console for output.
|
|
266
|
+
is_uv: True if running in uv tool environment.
|
|
267
|
+
"""
|
|
268
|
+
if not is_uv:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
manual_cmd = _get_manual_upgrade_command(is_uv=True)
|
|
272
|
+
console.print(
|
|
273
|
+
f"[{INFO_STYLE}]💡 Detected uv tool environment.[/] "
|
|
274
|
+
f"If automatic update fails, run: [{ACCENT_STYLE}]{manual_cmd}[/]"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _show_error_guidance(console: Console, is_uv: bool) -> None:
|
|
279
|
+
"""Show error guidance with correct manual command based on environment.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
console: Rich console for output.
|
|
283
|
+
is_uv: True if running in uv tool environment.
|
|
284
|
+
"""
|
|
285
|
+
try:
|
|
286
|
+
manual_cmd = _get_manual_upgrade_command(is_uv=is_uv)
|
|
287
|
+
console.print(f"[{INFO_STYLE}]💡 Tip:[/] Run this command manually:\n [{ACCENT_STYLE}]{manual_cmd}[/]")
|
|
288
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
289
|
+
_LOGGER.debug("Failed to render update tip: %s", exc, exc_info=True)
|
|
290
|
+
|
|
291
|
+
|
|
217
292
|
def _run_update_command(console: Console, ctx: Any) -> None:
|
|
218
293
|
"""Invoke the built-in update command and surface any errors."""
|
|
294
|
+
# Detect uv environment proactively before attempting update
|
|
295
|
+
is_uv = _is_uv_managed_environment()
|
|
296
|
+
|
|
297
|
+
# Provide proactive guidance for uv environments
|
|
298
|
+
# This helps users on older versions (e.g., 0.6.19) that don't have uv detection
|
|
299
|
+
# in their update command
|
|
300
|
+
_show_proactive_uv_guidance(console, is_uv)
|
|
301
|
+
|
|
219
302
|
try:
|
|
220
303
|
ctx.invoke(update_command)
|
|
221
304
|
except click.ClickException as exc:
|
|
222
305
|
exc.show()
|
|
223
306
|
console.print(f"[{ERROR_STYLE}]Update command exited with an error.[/]")
|
|
307
|
+
_show_error_guidance(console, is_uv)
|
|
224
308
|
except click.Abort:
|
|
225
309
|
console.print(f"[{WARNING_STYLE}]Update aborted by user.[/]")
|
|
226
310
|
except Exception as exc: # pragma: no cover - defensive guard
|
|
227
311
|
console.print(f"[{ERROR_STYLE}]Unexpected error while running update: {exc}[/]")
|
|
312
|
+
# Also provide guidance for unexpected errors in uv environments
|
|
313
|
+
if is_uv:
|
|
314
|
+
manual_cmd = _get_manual_upgrade_command(is_uv=True)
|
|
315
|
+
console.print(f"[{INFO_STYLE}]💡 Tip:[/] Try running manually:\n [{ACCENT_STYLE}]{manual_cmd}[/]")
|
|
228
316
|
else:
|
|
229
|
-
_refresh_installed_version(console, ctx)
|
|
317
|
+
new_version = _refresh_installed_version(console, ctx)
|
|
318
|
+
_maybe_retry_update(console, ctx, new_version, is_uv)
|
|
230
319
|
|
|
231
320
|
|
|
232
321
|
@contextmanager
|
|
@@ -247,7 +336,7 @@ def _suppress_library_logging(
|
|
|
247
336
|
logger.setLevel(previous_level)
|
|
248
337
|
|
|
249
338
|
|
|
250
|
-
def _refresh_installed_version(console: Console, ctx: Any) -> None:
|
|
339
|
+
def _refresh_installed_version(console: Console, ctx: Any) -> str | None:
|
|
251
340
|
"""Reload runtime metadata after an in-process upgrade."""
|
|
252
341
|
new_version: str | None = None
|
|
253
342
|
branding_module: Any | None = None
|
|
@@ -271,12 +360,13 @@ def _refresh_installed_version(console: Console, ctx: Any) -> None:
|
|
|
271
360
|
try:
|
|
272
361
|
branding_cls = getattr(branding_module, "AIPBranding", None) if branding_module else None
|
|
273
362
|
session.refresh_branding(new_version, branding_cls=branding_cls)
|
|
274
|
-
return
|
|
363
|
+
return new_version
|
|
275
364
|
except Exception as exc: # pragma: no cover - defensive guard
|
|
276
365
|
_LOGGER.debug("Failed to refresh active slash session: %s", exc, exc_info=True)
|
|
277
366
|
|
|
278
367
|
if new_version:
|
|
279
368
|
console.print(f"[{SUCCESS_STYLE}]CLI now running glaip-sdk {new_version}.[/]")
|
|
369
|
+
return new_version
|
|
280
370
|
|
|
281
371
|
|
|
282
372
|
def _get_slash_session(ctx: Any) -> Any | None:
|
|
@@ -287,4 +377,122 @@ def _get_slash_session(ctx: Any) -> Any | None:
|
|
|
287
377
|
return None
|
|
288
378
|
|
|
289
379
|
|
|
380
|
+
def _stash_update_versions(ctx: Any | None, current_version: str, latest_version: str) -> None:
|
|
381
|
+
"""Persist update versions in the Click context for post-update checks."""
|
|
382
|
+
if ctx is None:
|
|
383
|
+
return
|
|
384
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
385
|
+
if isinstance(ctx_obj, dict):
|
|
386
|
+
ctx_obj[_UPDATE_VERSIONS_KEY] = {"current": current_version, "latest": latest_version}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _get_update_versions(ctx: Any | None) -> tuple[str | None, str | None]:
|
|
390
|
+
"""Return current/latest versions captured during the update prompt."""
|
|
391
|
+
if ctx is None:
|
|
392
|
+
return None, None
|
|
393
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
394
|
+
if not isinstance(ctx_obj, dict):
|
|
395
|
+
return None, None
|
|
396
|
+
payload = ctx_obj.get(_UPDATE_VERSIONS_KEY)
|
|
397
|
+
if not isinstance(payload, dict):
|
|
398
|
+
return None, None
|
|
399
|
+
current = payload.get("current")
|
|
400
|
+
latest = payload.get("latest")
|
|
401
|
+
return (
|
|
402
|
+
current if isinstance(current, str) else None,
|
|
403
|
+
latest if isinstance(latest, str) else None,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _should_retry_update(
|
|
408
|
+
ctx: Any,
|
|
409
|
+
console: Console,
|
|
410
|
+
new_version: str | None,
|
|
411
|
+
) -> tuple[str, str, Version, Version, Version] | None:
|
|
412
|
+
"""Check if update retry is needed and return parsed versions if so."""
|
|
413
|
+
if ctx is None or not hasattr(ctx, "invoke"):
|
|
414
|
+
return None
|
|
415
|
+
if not _should_prompt_for_action(console, ctx):
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
current_version, latest_version = _get_update_versions(ctx)
|
|
419
|
+
if not current_version or not latest_version or not new_version:
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
current = _parse_version(current_version)
|
|
423
|
+
latest = _parse_version(latest_version)
|
|
424
|
+
installed = _parse_version(new_version)
|
|
425
|
+
if current is None or latest is None or installed is None:
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
if installed >= latest:
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
# Note: installed > current case is handled in _maybe_retry_update()
|
|
432
|
+
# to allow warning message to be printed before returning
|
|
433
|
+
|
|
434
|
+
return current_version, latest_version, current, latest, installed
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _handle_reinstall_error(console: Console, exc: Exception, is_uv: bool) -> None:
|
|
438
|
+
"""Handle errors during reinstall attempt."""
|
|
439
|
+
if isinstance(exc, click.ClickException):
|
|
440
|
+
exc.show()
|
|
441
|
+
console.print(f"[{ERROR_STYLE}]Reinstall attempt failed.[/]")
|
|
442
|
+
_show_error_guidance(console, is_uv)
|
|
443
|
+
elif isinstance(exc, click.Abort):
|
|
444
|
+
console.print(f"[{WARNING_STYLE}]Reinstall skipped by user.[/]")
|
|
445
|
+
else:
|
|
446
|
+
console.print(f"[{ERROR_STYLE}]Unexpected error while reinstalling: {exc}[/]")
|
|
447
|
+
if is_uv:
|
|
448
|
+
manual_cmd = _get_manual_upgrade_command(is_uv=True)
|
|
449
|
+
console.print(f"[{INFO_STYLE}]💡 Tip:[/] Try running manually:\n [{ACCENT_STYLE}]{manual_cmd}[/]")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _check_final_version(
|
|
453
|
+
console: Console, new_version: str | None, latest_version: str, latest: Version, is_uv: bool
|
|
454
|
+
) -> None:
|
|
455
|
+
"""Check and report final version status after reinstall."""
|
|
456
|
+
installed = _parse_version(new_version) if isinstance(new_version, str) else None
|
|
457
|
+
if installed is None or installed < latest:
|
|
458
|
+
console.print(
|
|
459
|
+
f"[{WARNING_STYLE}]Still on {new_version}. Your package index may not have {latest_version} yet.[/]"
|
|
460
|
+
)
|
|
461
|
+
if is_uv:
|
|
462
|
+
console.print(
|
|
463
|
+
f"[{INFO_STYLE}]💡 Tip:[/] If you need PyPI immediately, set "
|
|
464
|
+
f"[{ACCENT_STYLE}]UV_INDEX_URL=https://pypi.org/simple[/]."
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _maybe_retry_update(
|
|
469
|
+
console: Console,
|
|
470
|
+
ctx: Any,
|
|
471
|
+
new_version: str | None,
|
|
472
|
+
is_uv: bool,
|
|
473
|
+
) -> None:
|
|
474
|
+
"""Retry once with reinstall when the update did not advance versions."""
|
|
475
|
+
versions = _should_retry_update(ctx, console, new_version)
|
|
476
|
+
if versions is None:
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
current_version, latest_version, current, latest, installed = versions
|
|
480
|
+
if installed > current:
|
|
481
|
+
console.print(f"[{WARNING_STYLE}]Update installed {new_version}, but {latest_version} is still available.[/]")
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
console.print(
|
|
485
|
+
f"[{WARNING_STYLE}]Update completed but version stayed at {new_version}. Retrying with reinstall...[/]"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
ctx.invoke(update_command, force_reinstall=True)
|
|
490
|
+
except Exception as exc:
|
|
491
|
+
_handle_reinstall_error(console, exc, is_uv)
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
new_version = _refresh_installed_version(console, ctx)
|
|
495
|
+
_check_final_version(console, new_version, latest_version, latest, is_uv)
|
|
496
|
+
|
|
497
|
+
|
|
290
498
|
__all__ = ["maybe_notify_update"]
|
glaip_sdk/cli/validators.py
CHANGED
|
@@ -13,7 +13,7 @@ from typing import Any
|
|
|
13
13
|
|
|
14
14
|
import click
|
|
15
15
|
|
|
16
|
-
from glaip_sdk.cli.
|
|
16
|
+
from glaip_sdk.cli.core.context import handle_best_effort_check
|
|
17
17
|
from glaip_sdk.utils.validation import (
|
|
18
18
|
coerce_timeout,
|
|
19
19
|
validate_agent_instruction,
|
glaip_sdk/client/__init__.py
CHANGED
|
@@ -7,5 +7,6 @@ Authors:
|
|
|
7
7
|
|
|
8
8
|
from glaip_sdk.client.agent_runs import AgentRunsClient
|
|
9
9
|
from glaip_sdk.client.main import Client
|
|
10
|
+
from glaip_sdk.client.schedules import AgentScheduleManager, ScheduleClient
|
|
10
11
|
|
|
11
|
-
__all__ = ["AgentRunsClient", "Client"]
|
|
12
|
+
__all__ = ["AgentRunsClient", "AgentScheduleManager", "Client", "ScheduleClient"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Schedule request payload builders for AIP SDK.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.models.schedule import ScheduleConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ScheduleListParams:
|
|
15
|
+
"""Parameters for listing schedules.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
limit: Maximum number of schedules to return (1-100, default 50)
|
|
19
|
+
page: Page number for pagination (default 1)
|
|
20
|
+
agent_id: Filter schedules by agent ID
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
limit: int | None = None
|
|
24
|
+
page: int | None = None
|
|
25
|
+
agent_id: str | None = None
|
|
26
|
+
|
|
27
|
+
def to_query_params(self) -> dict[str, Any]:
|
|
28
|
+
"""Convert to query parameters dictionary.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Dictionary of non-None parameters for the API request
|
|
32
|
+
"""
|
|
33
|
+
params: dict[str, Any] = {}
|
|
34
|
+
if self.limit is not None:
|
|
35
|
+
params["limit"] = self.limit
|
|
36
|
+
if self.page is not None:
|
|
37
|
+
params["page"] = self.page
|
|
38
|
+
if self.agent_id is not None:
|
|
39
|
+
params["agent_id"] = self.agent_id
|
|
40
|
+
return params
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_schedule(
|
|
44
|
+
schedule: ScheduleConfig | dict[str, str] | str | None,
|
|
45
|
+
) -> dict[str, str] | None:
|
|
46
|
+
"""Normalize schedule input to a dictionary for API requests.
|
|
47
|
+
|
|
48
|
+
Accepts multiple input formats for user convenience:
|
|
49
|
+
- ScheduleConfig: Pydantic model with cron fields
|
|
50
|
+
- dict: Dictionary with cron fields (minute, hour, etc.)
|
|
51
|
+
- str: Cron string like "0 9 * * 1-5"
|
|
52
|
+
- None: Returns None
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
schedule: Schedule in various formats
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dictionary suitable for API request or None
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If cron string format is invalid
|
|
62
|
+
TypeError: If schedule is an unsupported type
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
>>> normalize_schedule(ScheduleConfig(minute="0", hour="9"))
|
|
66
|
+
{'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '*'}
|
|
67
|
+
|
|
68
|
+
>>> normalize_schedule({"minute": "0", "hour": "9"})
|
|
69
|
+
{'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '*'}
|
|
70
|
+
|
|
71
|
+
>>> normalize_schedule("0 9 * * 1-5")
|
|
72
|
+
{'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '1-5'}
|
|
73
|
+
"""
|
|
74
|
+
if schedule is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
if isinstance(schedule, ScheduleConfig):
|
|
78
|
+
return schedule.model_dump()
|
|
79
|
+
|
|
80
|
+
if isinstance(schedule, dict):
|
|
81
|
+
# Validate and merge with defaults
|
|
82
|
+
return ScheduleConfig(**schedule).model_dump()
|
|
83
|
+
|
|
84
|
+
if isinstance(schedule, str):
|
|
85
|
+
# Parse cron string
|
|
86
|
+
config = ScheduleConfig.from_cron_string(schedule)
|
|
87
|
+
return config.model_dump()
|
|
88
|
+
|
|
89
|
+
raise TypeError(f"schedule must be ScheduleConfig, dict, or str, got {type(schedule).__name__}")
|
glaip_sdk/client/agents.py
CHANGED
|
@@ -13,11 +13,15 @@ from collections.abc import AsyncGenerator, Callable, Iterator, Mapping
|
|
|
13
13
|
from contextlib import asynccontextmanager
|
|
14
14
|
from os import PathLike
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import Any, BinaryIO
|
|
16
|
+
from typing import TYPE_CHECKING, Any, BinaryIO
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from glaip_sdk.client.schedules import ScheduleClient
|
|
20
|
+
from glaip_sdk.hitl.remote import RemoteHITLHandler
|
|
17
21
|
|
|
18
22
|
import httpx
|
|
19
23
|
from glaip_sdk.agents import Agent
|
|
20
|
-
from glaip_sdk.client.
|
|
24
|
+
from glaip_sdk.client.payloads.agent import (
|
|
21
25
|
AgentCreateRequest,
|
|
22
26
|
AgentListParams,
|
|
23
27
|
AgentListResult,
|
|
@@ -264,6 +268,7 @@ class AgentClient(BaseClient):
|
|
|
264
268
|
self._tool_client: ToolClient | None = None
|
|
265
269
|
self._mcp_client: MCPClient | None = None
|
|
266
270
|
self._runs_client: AgentRunsClient | None = None
|
|
271
|
+
self._schedule_client: ScheduleClient | None = None
|
|
267
272
|
|
|
268
273
|
def list_agents(
|
|
269
274
|
self,
|
|
@@ -411,6 +416,7 @@ class AgentClient(BaseClient):
|
|
|
411
416
|
timeout_seconds: float,
|
|
412
417
|
agent_name: str | None,
|
|
413
418
|
meta: dict[str, Any],
|
|
419
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
414
420
|
) -> tuple[str, dict[str, Any], float | None, float | None]:
|
|
415
421
|
"""Process stream events from an HTTP response.
|
|
416
422
|
|
|
@@ -420,6 +426,7 @@ class AgentClient(BaseClient):
|
|
|
420
426
|
timeout_seconds: Timeout in seconds.
|
|
421
427
|
agent_name: Optional agent name.
|
|
422
428
|
meta: Metadata dictionary.
|
|
429
|
+
hitl_handler: Optional HITL handler for approval callbacks.
|
|
423
430
|
|
|
424
431
|
Returns:
|
|
425
432
|
Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
|
|
@@ -431,6 +438,7 @@ class AgentClient(BaseClient):
|
|
|
431
438
|
timeout_seconds,
|
|
432
439
|
agent_name,
|
|
433
440
|
meta,
|
|
441
|
+
hitl_handler=hitl_handler,
|
|
434
442
|
)
|
|
435
443
|
|
|
436
444
|
def _finalize_renderer(
|
|
@@ -453,13 +461,11 @@ class AgentClient(BaseClient):
|
|
|
453
461
|
Returns:
|
|
454
462
|
Final text string.
|
|
455
463
|
"""
|
|
464
|
+
from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
|
|
465
|
+
|
|
456
466
|
manager = self._get_renderer_manager()
|
|
457
|
-
return
|
|
458
|
-
renderer,
|
|
459
|
-
final_text,
|
|
460
|
-
stats_usage,
|
|
461
|
-
started_monotonic,
|
|
462
|
-
finished_monotonic,
|
|
467
|
+
return finalize_render_manager(
|
|
468
|
+
manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
|
|
463
469
|
)
|
|
464
470
|
|
|
465
471
|
def _get_tool_client(self) -> ToolClient:
|
|
@@ -482,6 +488,20 @@ class AgentClient(BaseClient):
|
|
|
482
488
|
self._mcp_client = MCPClient(parent_client=self)
|
|
483
489
|
return self._mcp_client
|
|
484
490
|
|
|
491
|
+
@property
|
|
492
|
+
def schedules(self) -> "ScheduleClient":
|
|
493
|
+
"""Get or create the schedule client instance.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
ScheduleClient instance.
|
|
497
|
+
"""
|
|
498
|
+
if self._schedule_client is None:
|
|
499
|
+
# Import here to avoid circular import
|
|
500
|
+
from glaip_sdk.client.schedules import ScheduleClient # noqa: PLC0415
|
|
501
|
+
|
|
502
|
+
self._schedule_client = ScheduleClient(parent_client=self)
|
|
503
|
+
return self._schedule_client
|
|
504
|
+
|
|
485
505
|
def _normalise_reference_entry(
|
|
486
506
|
self,
|
|
487
507
|
entry: Any,
|
|
@@ -1096,6 +1116,7 @@ class AgentClient(BaseClient):
|
|
|
1096
1116
|
*,
|
|
1097
1117
|
renderer: RichStreamRenderer | str | None = "auto",
|
|
1098
1118
|
runtime_config: dict[str, Any] | None = None,
|
|
1119
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
1099
1120
|
**kwargs,
|
|
1100
1121
|
) -> str:
|
|
1101
1122
|
"""Run an agent with a message, streaming via a renderer.
|
|
@@ -1113,6 +1134,8 @@ class AgentClient(BaseClient):
|
|
|
1113
1134
|
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
1114
1135
|
"agent_config": {"planning": True},
|
|
1115
1136
|
}
|
|
1137
|
+
hitl_handler: Optional RemoteHITLHandler for approval callbacks.
|
|
1138
|
+
Set GLAIP_HITL_AUTO_APPROVE=true for auto-approval without handler.
|
|
1116
1139
|
**kwargs: Additional arguments to pass to the run API.
|
|
1117
1140
|
|
|
1118
1141
|
Returns:
|
|
@@ -1169,6 +1192,7 @@ class AgentClient(BaseClient):
|
|
|
1169
1192
|
timeout_seconds,
|
|
1170
1193
|
agent_name,
|
|
1171
1194
|
meta,
|
|
1195
|
+
hitl_handler=hitl_handler,
|
|
1172
1196
|
)
|
|
1173
1197
|
|
|
1174
1198
|
except KeyboardInterrupt:
|
|
@@ -1185,6 +1209,13 @@ class AgentClient(BaseClient):
|
|
|
1185
1209
|
if multipart_data:
|
|
1186
1210
|
multipart_data.close()
|
|
1187
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
|
+
|
|
1188
1219
|
return self._finalize_renderer(
|
|
1189
1220
|
r,
|
|
1190
1221
|
final_text,
|
|
@@ -1266,6 +1297,7 @@ class AgentClient(BaseClient):
|
|
|
1266
1297
|
*,
|
|
1267
1298
|
request_timeout: float | None = None,
|
|
1268
1299
|
runtime_config: dict[str, Any] | None = None,
|
|
1300
|
+
hitl_handler: "RemoteHITLHandler | None" = None,
|
|
1269
1301
|
**kwargs,
|
|
1270
1302
|
) -> AsyncGenerator[dict, None]:
|
|
1271
1303
|
"""Async run an agent with a message, yielding streaming JSON chunks.
|
|
@@ -1282,16 +1314,26 @@ class AgentClient(BaseClient):
|
|
|
1282
1314
|
"mcp_configs": {"mcp-id": {"setting": "on"}},
|
|
1283
1315
|
"agent_config": {"planning": True},
|
|
1284
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.
|
|
1285
1321
|
**kwargs: Additional arguments (chat_history, pii_mapping, etc.)
|
|
1286
1322
|
|
|
1287
1323
|
Yields:
|
|
1288
1324
|
Dictionary containing parsed JSON chunks from the streaming response
|
|
1289
1325
|
|
|
1290
1326
|
Raises:
|
|
1327
|
+
NotImplementedError: If hitl_handler is provided (async HITL not yet supported)
|
|
1291
1328
|
AgentTimeoutError: When agent execution times out
|
|
1292
1329
|
httpx.TimeoutException: When general timeout occurs
|
|
1293
1330
|
Exception: For other unexpected errors
|
|
1294
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
|
+
)
|
|
1295
1337
|
# Include runtime_config in kwargs only when caller hasn't already provided it
|
|
1296
1338
|
if runtime_config is not None and "runtime_config" not in kwargs:
|
|
1297
1339
|
kwargs["runtime_config"] = runtime_config
|