glaip-sdk 0.7.6__py3-none-any.whl → 0.7.8__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/account_store.py +15 -0
- glaip_sdk/cli/config.py +1 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +155 -10
- glaip_sdk/cli/slash/tui/context.py +37 -12
- glaip_sdk/cli/slash/tui/theme/manager.py +28 -2
- glaip_sdk/cli/tui_settings.py +125 -0
- glaip_sdk/registry/tool.py +93 -41
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
- glaip_sdk/tools/base.py +39 -17
- glaip_sdk/utils/__init__.py +1 -0
- glaip_sdk/utils/tool_detection.py +254 -7
- {glaip_sdk-0.7.6.dist-info → glaip_sdk-0.7.8.dist-info}/METADATA +1 -1
- {glaip_sdk-0.7.6.dist-info → glaip_sdk-0.7.8.dist-info}/RECORD +16 -15
- {glaip_sdk-0.7.6.dist-info → glaip_sdk-0.7.8.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.7.6.dist-info → glaip_sdk-0.7.8.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.7.6.dist-info → glaip_sdk-0.7.8.dist-info}/top_level.txt +0 -0
glaip_sdk/cli/account_store.py
CHANGED
|
@@ -523,6 +523,21 @@ class AccountStore:
|
|
|
523
523
|
|
|
524
524
|
self._save_config(config)
|
|
525
525
|
|
|
526
|
+
def save_config_updates(self, config: dict[str, Any]) -> None:
|
|
527
|
+
"""Save config updates, preserving all existing keys.
|
|
528
|
+
|
|
529
|
+
This method allows external code to update arbitrary config keys
|
|
530
|
+
(e.g., TUI preferences) while preserving the full config structure.
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
config: Complete configuration dictionary to save. This should
|
|
534
|
+
include all keys that should be preserved, not just updates.
|
|
535
|
+
|
|
536
|
+
Raises:
|
|
537
|
+
AccountStoreError: If config file cannot be written.
|
|
538
|
+
"""
|
|
539
|
+
self._save_config(config)
|
|
540
|
+
|
|
526
541
|
|
|
527
542
|
# Global instance for convenience
|
|
528
543
|
_account_store = AccountStore()
|
glaip_sdk/cli/config.py
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
Provides a minimal interactive list with the same columns/order as the Rich
|
|
4
4
|
fallback (name, API URL, masked key, status) and keyboard navigation.
|
|
5
5
|
|
|
6
|
+
Integrates with TUI foundation services:
|
|
7
|
+
- KeybindRegistry: Centralized keybind registration with scoped actions
|
|
8
|
+
- ClipboardAdapter: Cross-platform clipboard operations with OSC 52 support
|
|
9
|
+
- ToastBus: Non-blocking toast notifications for user feedback
|
|
10
|
+
|
|
6
11
|
Authors:
|
|
7
12
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
8
13
|
"""
|
|
@@ -23,10 +28,13 @@ from glaip_sdk.cli.slash.accounts_shared import (
|
|
|
23
28
|
env_credentials_present,
|
|
24
29
|
)
|
|
25
30
|
from glaip_sdk.cli.slash.tui.background_tasks import BackgroundTaskMixin
|
|
26
|
-
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
|
|
31
|
+
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter, ClipboardResult
|
|
27
32
|
from glaip_sdk.cli.slash.tui.context import TUIContext
|
|
33
|
+
from glaip_sdk.cli.slash.tui.keybind_registry import KeybindRegistry
|
|
28
34
|
from glaip_sdk.cli.slash.tui.loading import hide_loading_indicator, show_loading_indicator
|
|
35
|
+
from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
|
|
29
36
|
from glaip_sdk.cli.slash.tui.theme.catalog import _BUILTIN_THEMES
|
|
37
|
+
from glaip_sdk.cli.slash.tui.toast import ToastBus
|
|
30
38
|
from glaip_sdk.cli.validators import validate_api_key
|
|
31
39
|
from glaip_sdk.utils.validation import validate_url
|
|
32
40
|
|
|
@@ -84,6 +92,30 @@ FORM_KEY_ID = "#form-key"
|
|
|
84
92
|
# CSS file name
|
|
85
93
|
CSS_FILE_NAME = "accounts.tcss"
|
|
86
94
|
|
|
95
|
+
KEYBIND_SCOPE = "accounts"
|
|
96
|
+
KEYBIND_CATEGORY = "Accounts"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class KeybindDef:
|
|
101
|
+
"""Keybind definition with action, key, and description."""
|
|
102
|
+
|
|
103
|
+
action: str
|
|
104
|
+
key: str
|
|
105
|
+
description: str
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
KEYBIND_DEFINITIONS: tuple[KeybindDef, ...] = (
|
|
109
|
+
KeybindDef("switch_row", "enter", "Switch"),
|
|
110
|
+
KeybindDef("focus_filter", "/", "Filter"),
|
|
111
|
+
KeybindDef("add_account", "a", "Add"),
|
|
112
|
+
KeybindDef("edit_account", "e", "Edit"),
|
|
113
|
+
KeybindDef("delete_account", "d", "Delete"),
|
|
114
|
+
KeybindDef("copy_account", "c", "Copy"),
|
|
115
|
+
KeybindDef("clear_or_exit", "escape", "Close"),
|
|
116
|
+
KeybindDef("app_exit", "q", "Close"),
|
|
117
|
+
)
|
|
118
|
+
|
|
87
119
|
|
|
88
120
|
@dataclass
|
|
89
121
|
class AccountsTUICallbacks:
|
|
@@ -250,6 +282,27 @@ class AccountFormModal(_AccountFormBase): # pragma: no cover - interactive
|
|
|
250
282
|
self._connection_tester = connection_tester
|
|
251
283
|
self._validate_name = validate_name
|
|
252
284
|
|
|
285
|
+
def _get_api_url_suggestions(self, _value: str) -> list[str]:
|
|
286
|
+
"""Get API URL suggestions from existing accounts.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
_value: Current input value (unused, but required by Textual's suggestor API).
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
List of unique API URLs from existing accounts.
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
store = get_account_store()
|
|
296
|
+
accounts = store.list_accounts()
|
|
297
|
+
# Extract unique API URLs, excluding the current account's URL in edit mode
|
|
298
|
+
existing_url = self._existing.get("api_url", "")
|
|
299
|
+
urls = {account.get("api_url", "") for account in accounts.values() if account.get("api_url")}
|
|
300
|
+
if existing_url in urls:
|
|
301
|
+
urls.remove(existing_url)
|
|
302
|
+
return sorted(urls)
|
|
303
|
+
except Exception: # pragma: no cover - defensive
|
|
304
|
+
return []
|
|
305
|
+
|
|
253
306
|
def compose(self) -> ComposeResult:
|
|
254
307
|
"""Render the form controls."""
|
|
255
308
|
title = "Add account" if self._mode == "add" else "Edit account"
|
|
@@ -259,7 +312,12 @@ class AccountFormModal(_AccountFormBase): # pragma: no cover - interactive
|
|
|
259
312
|
id="form-name",
|
|
260
313
|
disabled=self._mode == "edit",
|
|
261
314
|
)
|
|
262
|
-
url_input = Input(
|
|
315
|
+
url_input = Input(
|
|
316
|
+
value=self._existing.get("api_url", ""),
|
|
317
|
+
placeholder="https://api.example.com",
|
|
318
|
+
id="form-url",
|
|
319
|
+
suggestions=self._get_api_url_suggestions,
|
|
320
|
+
)
|
|
263
321
|
key_input = Input(value="", placeholder="sk-...", password=True, id="form-key")
|
|
264
322
|
test_checkbox = Checkbox(
|
|
265
323
|
"Test connection before save",
|
|
@@ -427,8 +485,13 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
427
485
|
self._env_lock = env_lock
|
|
428
486
|
self._callbacks = callbacks
|
|
429
487
|
self._ctx = ctx
|
|
488
|
+
self._keybinds: KeybindRegistry | None = None
|
|
489
|
+
self._toast_bus: ToastBus | None = None
|
|
490
|
+
self._toast_ready = False
|
|
491
|
+
self._clipboard: ClipboardAdapter | None = None
|
|
430
492
|
self._filter_text: str = ""
|
|
431
493
|
self._is_switching = False
|
|
494
|
+
self._initialize_context_services()
|
|
432
495
|
|
|
433
496
|
def compose(self) -> ComposeResult:
|
|
434
497
|
"""Build the Textual layout."""
|
|
@@ -481,6 +544,52 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
481
544
|
main = self.query_one(Vertical)
|
|
482
545
|
main.styles.gap = 0
|
|
483
546
|
self._update_filter_button_visibility()
|
|
547
|
+
self._prepare_toasts()
|
|
548
|
+
self._register_keybinds()
|
|
549
|
+
|
|
550
|
+
def _initialize_context_services(self) -> None:
|
|
551
|
+
if self._ctx:
|
|
552
|
+
if self._ctx.keybinds is None:
|
|
553
|
+
self._ctx.keybinds = KeybindRegistry()
|
|
554
|
+
if self._ctx.toasts is None:
|
|
555
|
+
self._ctx.toasts = ToastBus()
|
|
556
|
+
if self._ctx.clipboard is None:
|
|
557
|
+
self._ctx.clipboard = ClipboardAdapter(terminal=self._ctx.terminal)
|
|
558
|
+
self._keybinds = self._ctx.keybinds
|
|
559
|
+
self._toast_bus = self._ctx.toasts
|
|
560
|
+
self._clipboard = self._ctx.clipboard
|
|
561
|
+
else:
|
|
562
|
+
# Fallback: create services independently when ctx is None
|
|
563
|
+
terminal = TerminalCapabilities(
|
|
564
|
+
tty=True, ansi=True, osc52=False, osc11_bg=None, mouse=False, truecolor=False
|
|
565
|
+
)
|
|
566
|
+
self._clipboard = ClipboardAdapter(terminal=terminal)
|
|
567
|
+
|
|
568
|
+
def _prepare_toasts(self) -> None:
|
|
569
|
+
"""Prepare toast system by marking ready and clearing any existing toasts."""
|
|
570
|
+
self._toast_ready = True
|
|
571
|
+
if self._toast_bus:
|
|
572
|
+
self._toast_bus.clear()
|
|
573
|
+
|
|
574
|
+
def _register_keybinds(self) -> None:
|
|
575
|
+
if not self._keybinds:
|
|
576
|
+
return
|
|
577
|
+
for keybind_def in KEYBIND_DEFINITIONS:
|
|
578
|
+
scoped_action = f"{KEYBIND_SCOPE}.{keybind_def.action}"
|
|
579
|
+
if self._keybinds.get(scoped_action):
|
|
580
|
+
continue
|
|
581
|
+
try:
|
|
582
|
+
self._keybinds.register(
|
|
583
|
+
action=scoped_action,
|
|
584
|
+
key=keybind_def.key,
|
|
585
|
+
description=keybind_def.description,
|
|
586
|
+
category=KEYBIND_CATEGORY,
|
|
587
|
+
)
|
|
588
|
+
except ValueError as e:
|
|
589
|
+
# Expected: duplicate registration (already registered by another component)
|
|
590
|
+
# Silently skip to allow multiple apps to register same keybinds
|
|
591
|
+
logging.debug(f"Skipping duplicate keybind registration: {scoped_action}", exc_info=e)
|
|
592
|
+
continue
|
|
484
593
|
|
|
485
594
|
def _header_text(self) -> str:
|
|
486
595
|
"""Build header text with active account and host."""
|
|
@@ -643,6 +752,24 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
643
752
|
"""Hide the loading indicator."""
|
|
644
753
|
hide_loading_indicator(self, ACCOUNTS_LOADING_ID)
|
|
645
754
|
|
|
755
|
+
def _handle_switch_scheduling_error(self, exc: Exception) -> None:
|
|
756
|
+
"""Handle errors when scheduling the switch task fails.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
exc: The exception that occurred during task scheduling.
|
|
760
|
+
"""
|
|
761
|
+
self._hide_loading()
|
|
762
|
+
self._is_switching = False
|
|
763
|
+
error_msg = f"Switch failed to start: {exc}"
|
|
764
|
+
if self._toast_ready and self._toast_bus:
|
|
765
|
+
self._toast_bus.show(message=error_msg, variant="error")
|
|
766
|
+
try:
|
|
767
|
+
self._set_status(error_msg, "red")
|
|
768
|
+
except Exception:
|
|
769
|
+
# App not mounted yet, status update not possible
|
|
770
|
+
logging.error(error_msg, exc_info=exc)
|
|
771
|
+
logging.getLogger(__name__).debug("Failed to schedule switch task", exc_info=exc)
|
|
772
|
+
|
|
646
773
|
def _clear_filter(self) -> None:
|
|
647
774
|
"""Clear the filter input and reset filter state."""
|
|
648
775
|
filter_input = self.query_one(FILTER_INPUT_ID, Input)
|
|
@@ -665,7 +792,9 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
665
792
|
|
|
666
793
|
if switched:
|
|
667
794
|
self._active_account = name
|
|
668
|
-
|
|
795
|
+
status_msg = message or f"Switched to '{name}'."
|
|
796
|
+
if self._toast_ready and self._toast_bus:
|
|
797
|
+
self._toast_bus.show(message=status_msg, variant="success")
|
|
669
798
|
self._update_header()
|
|
670
799
|
self._reload_rows()
|
|
671
800
|
else:
|
|
@@ -674,11 +803,7 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
674
803
|
try:
|
|
675
804
|
self.track_task(perform(), logger=logging.getLogger(__name__))
|
|
676
805
|
except Exception as exc:
|
|
677
|
-
|
|
678
|
-
self._hide_loading()
|
|
679
|
-
self._is_switching = False
|
|
680
|
-
self._set_status(f"Switch failed to start: {exc}", "red")
|
|
681
|
-
logging.getLogger(__name__).debug("Failed to schedule switch task", exc_info=exc)
|
|
806
|
+
self._handle_switch_scheduling_error(exc)
|
|
682
807
|
|
|
683
808
|
def _update_header(self) -> None:
|
|
684
809
|
"""Refresh header text to reflect active/lock state."""
|
|
@@ -781,12 +906,32 @@ class AccountsTextualApp(BackgroundTaskMixin, _AppBase): # pragma: no cover - i
|
|
|
781
906
|
return
|
|
782
907
|
|
|
783
908
|
text = f"Account: {name}\nURL: {account.get('api_url', '')}"
|
|
784
|
-
adapter = ClipboardAdapter()
|
|
785
|
-
|
|
909
|
+
adapter = self._clipboard or ClipboardAdapter(terminal=self._ctx.terminal if self._ctx else None)
|
|
910
|
+
# OSC 52 works by writing to stdout, no custom writer needed
|
|
911
|
+
try:
|
|
912
|
+
asyncio.get_running_loop()
|
|
913
|
+
except RuntimeError:
|
|
914
|
+
result = adapter.copy(text)
|
|
915
|
+
self._handle_copy_result(name, result)
|
|
916
|
+
return
|
|
917
|
+
|
|
918
|
+
async def perform() -> None:
|
|
919
|
+
result = await asyncio.to_thread(adapter.copy, text)
|
|
920
|
+
self._handle_copy_result(name, result)
|
|
921
|
+
|
|
922
|
+
self.track_task(perform(), logger=logging.getLogger(__name__))
|
|
786
923
|
|
|
924
|
+
def _handle_copy_result(self, name: str, result: ClipboardResult) -> None:
|
|
925
|
+
"""Update UI state after a copy attempt."""
|
|
787
926
|
if result.success:
|
|
927
|
+
if self._toast_ready and self._toast_bus:
|
|
928
|
+
self._toast_bus.copy_success(label=name)
|
|
929
|
+
# Status fallback until toast widget is implemented (see specs/workflow/tui-toast-system/spec.md Phase 2)
|
|
788
930
|
self._set_status(f"Copied '{name}' to clipboard.", "green")
|
|
789
931
|
else:
|
|
932
|
+
if self._toast_ready and self._toast_bus:
|
|
933
|
+
self._toast_bus.show(message=f"Copy failed: {result.message}", variant="warning")
|
|
934
|
+
# Status fallback until toast widget is implemented (see specs/workflow/tui-toast-system/spec.md Phase 2)
|
|
790
935
|
self._set_status(f"Copy failed: {result.message}", "red")
|
|
791
936
|
|
|
792
937
|
def _check_env_lock_hotkey(self) -> bool:
|
|
@@ -12,13 +12,14 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import os
|
|
14
14
|
from dataclasses import dataclass
|
|
15
|
-
from typing import TYPE_CHECKING
|
|
16
15
|
|
|
16
|
+
from glaip_sdk.cli.account_store import get_account_store
|
|
17
|
+
from glaip_sdk.cli.slash.tui.clipboard import ClipboardAdapter
|
|
18
|
+
from glaip_sdk.cli.slash.tui.keybind_registry import KeybindRegistry
|
|
17
19
|
from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
|
|
18
20
|
from glaip_sdk.cli.slash.tui.theme import ThemeManager
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
from glaip_sdk.cli.slash.tui.toast import ToastBus
|
|
21
|
+
from glaip_sdk.cli.slash.tui.toast import ToastBus
|
|
22
|
+
from glaip_sdk.cli.tui_settings import load_tui_settings
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
@dataclass
|
|
@@ -37,23 +38,47 @@ class TUIContext:
|
|
|
37
38
|
"""
|
|
38
39
|
|
|
39
40
|
terminal: TerminalCapabilities
|
|
40
|
-
keybinds:
|
|
41
|
+
keybinds: KeybindRegistry | None = None
|
|
41
42
|
theme: ThemeManager | None = None
|
|
42
43
|
toasts: ToastBus | None = None
|
|
43
|
-
clipboard:
|
|
44
|
+
clipboard: ClipboardAdapter | None = None
|
|
44
45
|
|
|
45
46
|
@classmethod
|
|
46
47
|
async def create(cls) -> TUIContext:
|
|
47
48
|
"""Create a TUIContext instance with detected terminal capabilities.
|
|
48
49
|
|
|
49
50
|
This factory method detects terminal capabilities asynchronously and
|
|
50
|
-
returns a populated TUIContext instance
|
|
51
|
-
theme, toasts, clipboard)
|
|
51
|
+
returns a populated TUIContext instance with all services initialized
|
|
52
|
+
(keybinds, theme, toasts, clipboard).
|
|
52
53
|
|
|
53
54
|
Returns:
|
|
54
|
-
TUIContext instance with
|
|
55
|
+
TUIContext instance with all services initialized.
|
|
55
56
|
"""
|
|
56
57
|
terminal = await TerminalCapabilities.detect()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
store = get_account_store()
|
|
59
|
+
settings = load_tui_settings(store=store)
|
|
60
|
+
|
|
61
|
+
# Handle env var override: normalize empty strings and "default" to None
|
|
62
|
+
# Empty string from os.getenv() is falsy, so strip() result becomes None in the or expression
|
|
63
|
+
env_theme = os.getenv("AIP_TUI_THEME")
|
|
64
|
+
env_theme = env_theme.strip() if env_theme else None
|
|
65
|
+
if env_theme and env_theme.lower() == "default":
|
|
66
|
+
env_theme = None
|
|
67
|
+
|
|
68
|
+
theme_name = env_theme or settings.theme_name
|
|
69
|
+
theme = ThemeManager(
|
|
70
|
+
terminal,
|
|
71
|
+
mode=settings.theme_mode,
|
|
72
|
+
theme=theme_name,
|
|
73
|
+
settings_store=store,
|
|
74
|
+
)
|
|
75
|
+
keybinds = KeybindRegistry()
|
|
76
|
+
toasts = ToastBus()
|
|
77
|
+
clipboard = ClipboardAdapter(terminal=terminal)
|
|
78
|
+
return cls(
|
|
79
|
+
terminal=terminal,
|
|
80
|
+
keybinds=keybinds,
|
|
81
|
+
theme=theme,
|
|
82
|
+
toasts=toasts,
|
|
83
|
+
clipboard=clipboard,
|
|
84
|
+
)
|
|
@@ -6,9 +6,11 @@ import logging
|
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Literal
|
|
8
8
|
|
|
9
|
+
from glaip_sdk.cli.account_store import AccountStore, AccountStoreError
|
|
9
10
|
from glaip_sdk.cli.slash.tui.terminal import TerminalCapabilities
|
|
10
11
|
from glaip_sdk.cli.slash.tui.theme.catalog import default_theme_name_for_mode, get_builtin_theme
|
|
11
12
|
from glaip_sdk.cli.slash.tui.theme.tokens import ThemeTokens
|
|
13
|
+
from glaip_sdk.cli.tui_settings import persist_tui_theme
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
@@ -30,11 +32,13 @@ class ThemeManager:
|
|
|
30
32
|
*,
|
|
31
33
|
mode: ThemeMode | str = ThemeMode.AUTO,
|
|
32
34
|
theme: str | None = None,
|
|
35
|
+
settings_store: AccountStore | None = None,
|
|
33
36
|
) -> None:
|
|
34
37
|
"""Initialize the theme manager."""
|
|
35
38
|
self._terminal = terminal
|
|
36
39
|
self._mode = self._coerce_mode(mode)
|
|
37
|
-
self._theme = theme
|
|
40
|
+
self._theme = self._normalize_theme_name(theme)
|
|
41
|
+
self._settings_store = settings_store
|
|
38
42
|
|
|
39
43
|
@property
|
|
40
44
|
def mode(self) -> ThemeMode:
|
|
@@ -70,10 +74,12 @@ class ThemeManager:
|
|
|
70
74
|
def set_mode(self, mode: ThemeMode | str) -> None:
|
|
71
75
|
"""Set auto/light/dark mode."""
|
|
72
76
|
self._mode = self._coerce_mode(mode)
|
|
77
|
+
self._persist_preferences()
|
|
73
78
|
|
|
74
79
|
def set_theme(self, theme: str | None) -> None:
|
|
75
80
|
"""Set explicit theme name (or None to use the default)."""
|
|
76
|
-
self._theme = theme
|
|
81
|
+
self._theme = self._normalize_theme_name(theme)
|
|
82
|
+
self._persist_preferences()
|
|
77
83
|
|
|
78
84
|
def _coerce_mode(self, mode: ThemeMode | str) -> ThemeMode:
|
|
79
85
|
"""Coerce a mode value to ThemeMode enum, defaulting to AUTO on invalid input."""
|
|
@@ -84,3 +90,23 @@ class ThemeManager:
|
|
|
84
90
|
except ValueError:
|
|
85
91
|
logger.warning(f"Invalid theme mode '{mode}', defaulting to AUTO")
|
|
86
92
|
return ThemeMode.AUTO
|
|
93
|
+
|
|
94
|
+
def _persist_preferences(self) -> None:
|
|
95
|
+
if self._settings_store is None:
|
|
96
|
+
return
|
|
97
|
+
try:
|
|
98
|
+
persist_tui_theme(mode=self._mode.value, name=self._theme, store=self._settings_store)
|
|
99
|
+
except (OSError, AccountStoreError) as exc:
|
|
100
|
+
# Log recoverable errors (permissions, I/O) as warnings
|
|
101
|
+
logger.warning(f"Failed to persist TUI theme preferences: {exc}")
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
# Log unexpected errors at error level for debugging
|
|
104
|
+
logger.error(f"Unexpected error persisting TUI theme preferences: {exc}", exc_info=True)
|
|
105
|
+
|
|
106
|
+
def _normalize_theme_name(self, theme: str | None) -> str | None:
|
|
107
|
+
if not isinstance(theme, str):
|
|
108
|
+
return None
|
|
109
|
+
cleaned = theme.strip()
|
|
110
|
+
if not cleaned or cleaned.lower() == "default":
|
|
111
|
+
return None
|
|
112
|
+
return cleaned
|
|
@@ -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/registry/tool.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tool registry for glaip_sdk.
|
|
2
2
|
|
|
3
|
-
This module provides
|
|
3
|
+
This module provides a ToolRegistry that caches deployed tools
|
|
4
4
|
to avoid redundant API calls when deploying agents with tools.
|
|
5
5
|
|
|
6
6
|
Authors:
|
|
@@ -92,7 +92,7 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
92
92
|
Raises:
|
|
93
93
|
ValueError: If name cannot be extracted from the reference.
|
|
94
94
|
"""
|
|
95
|
-
# Lazy import to avoid circular dependency
|
|
95
|
+
# Lazy import to avoid circular dependency: Tool -> ToolRegistry -> Tool
|
|
96
96
|
from glaip_sdk.tools.base import Tool # noqa: PLC0415
|
|
97
97
|
|
|
98
98
|
if isinstance(ref, str):
|
|
@@ -129,6 +129,34 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
129
129
|
if hasattr(tool, "id") and tool.id:
|
|
130
130
|
self._cache[tool.id] = tool
|
|
131
131
|
|
|
132
|
+
def _resolve_native_platform_tool(self, name: str, tool_class: type | None = None) -> Tool:
|
|
133
|
+
"""Find a native tool on the platform and cache it.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
name: The tool name to look up.
|
|
137
|
+
tool_class: Optional local implementation to preserve.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The resolved Tool object.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If the tool is not found on the platform.
|
|
144
|
+
"""
|
|
145
|
+
from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
|
|
146
|
+
|
|
147
|
+
logger.info("Looking up native tool: %s", name)
|
|
148
|
+
tool = find_tool(name)
|
|
149
|
+
if tool:
|
|
150
|
+
# Preserve local implementation if provided
|
|
151
|
+
if tool_class:
|
|
152
|
+
tool.tool_class = tool_class
|
|
153
|
+
self._cache_tool(tool, name)
|
|
154
|
+
return tool
|
|
155
|
+
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"Native tool '{name}' not found on platform. Ensure the tool is deployed or check for name mismatches."
|
|
158
|
+
)
|
|
159
|
+
|
|
132
160
|
def _resolve_tool_instance(self, ref: Any, name: str) -> Tool | None:
|
|
133
161
|
"""Resolve a ToolClass instance.
|
|
134
162
|
|
|
@@ -139,11 +167,9 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
139
167
|
Returns:
|
|
140
168
|
The resolved tool, or None if not a ToolClass instance.
|
|
141
169
|
"""
|
|
142
|
-
# Lazy imports to avoid circular dependency
|
|
170
|
+
# Lazy imports to avoid circular dependency: Tool -> ToolRegistry -> Tool
|
|
143
171
|
from glaip_sdk.tools.base import Tool as ToolClass # noqa: PLC0415
|
|
144
172
|
from glaip_sdk.tools.base import ToolType # noqa: PLC0415
|
|
145
|
-
from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
|
|
146
|
-
from glaip_sdk.utils.sync import update_or_create_tool # noqa: PLC0415
|
|
147
173
|
|
|
148
174
|
# Use try/except to handle mocked Tool class in tests
|
|
149
175
|
try:
|
|
@@ -162,19 +188,11 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
162
188
|
|
|
163
189
|
# Tool.from_native() - look up on platform
|
|
164
190
|
if ref.tool_type == ToolType.NATIVE:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
self._cache[name] = tool
|
|
169
|
-
return tool
|
|
170
|
-
raise ValueError(f"Native tool not found on platform: {name}")
|
|
171
|
-
|
|
172
|
-
# Tool.from_langchain() - upload the tool_class
|
|
191
|
+
return self._resolve_native_platform_tool(name, tool_class=getattr(ref, "tool_class", None))
|
|
192
|
+
|
|
193
|
+
# Tool.from_langchain() - resolve the inner tool_class (promoted or uploaded)
|
|
173
194
|
if ref.tool_class is not None:
|
|
174
|
-
|
|
175
|
-
tool = update_or_create_tool(ref.tool_class)
|
|
176
|
-
self._cache_tool(tool, name)
|
|
177
|
-
return tool
|
|
195
|
+
return self._resolve_custom_tool(ref.tool_class, name)
|
|
178
196
|
|
|
179
197
|
# Unresolvable Tool instance - neither native nor has tool_class
|
|
180
198
|
raise ValueError(
|
|
@@ -193,8 +211,6 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
193
211
|
Returns:
|
|
194
212
|
The resolved tool, or None if not a deployed tool.
|
|
195
213
|
"""
|
|
196
|
-
from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
|
|
197
|
-
|
|
198
214
|
# Already deployed tool (not a ToolClass, but has id/name)
|
|
199
215
|
# This handles API response objects and backward compatibility
|
|
200
216
|
if not (hasattr(ref, "id") and hasattr(ref, "name") and not isinstance(ref, type)):
|
|
@@ -207,32 +223,50 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
207
223
|
return ref
|
|
208
224
|
|
|
209
225
|
# Tool without ID (backward compatibility) - look up on platform
|
|
210
|
-
|
|
211
|
-
tool = find_tool(name)
|
|
212
|
-
if tool:
|
|
213
|
-
# Use _cache_tool to cache by both name and ID if available
|
|
214
|
-
self._cache_tool(tool, name)
|
|
215
|
-
return tool
|
|
216
|
-
raise ValueError(f"Native tool not found on platform: {name}")
|
|
226
|
+
return self._resolve_native_platform_tool(name)
|
|
217
227
|
|
|
218
228
|
def _resolve_custom_tool(self, ref: Any, name: str) -> Tool | None:
|
|
219
|
-
"""Resolve a custom tool class.
|
|
229
|
+
"""Resolve a custom tool class, promoting aip_agents.tools classes to NATIVE.
|
|
230
|
+
|
|
231
|
+
This method handles two main paths:
|
|
232
|
+
1. **Promotion**: If the tool class is from `aip_agents.tools`, it is automatically
|
|
233
|
+
promoted to a `NATIVE` tool type. It then performs a platform lookup to link it
|
|
234
|
+
with the deployed native tool while preserving the local `tool_class` for local execution.
|
|
235
|
+
2. **Upload**: If it is a standard LangChain tool, it is uploaded to the platform
|
|
236
|
+
as a custom tool.
|
|
220
237
|
|
|
221
238
|
Args:
|
|
222
|
-
ref: The tool reference to resolve.
|
|
239
|
+
ref: The tool reference (usually a class) to resolve.
|
|
223
240
|
name: The extracted tool name.
|
|
224
241
|
|
|
225
242
|
Returns:
|
|
226
243
|
The resolved tool, or None if not a custom tool.
|
|
227
244
|
"""
|
|
228
|
-
|
|
245
|
+
# aip_agents tools are automatically promoted to NATIVE
|
|
246
|
+
if self._is_aip_agents_tool(ref):
|
|
247
|
+
from glaip_sdk.utils.tool_detection import get_tool_name # noqa: PLC0415
|
|
248
|
+
|
|
249
|
+
# Get name from class attribute or field
|
|
250
|
+
tool_name = get_tool_name(ref)
|
|
251
|
+
if tool_name is None:
|
|
252
|
+
raise ValueError(f"Tool class {ref.__name__} has no 'name' attribute")
|
|
253
|
+
|
|
254
|
+
return self._resolve_native_platform_tool(tool_name, tool_class=ref)
|
|
229
255
|
|
|
230
256
|
if not self._is_custom_tool(ref):
|
|
231
257
|
return None
|
|
232
258
|
|
|
259
|
+
# Regular custom tools - upload to platform
|
|
260
|
+
from glaip_sdk.utils.sync import update_or_create_tool # noqa: PLC0415
|
|
261
|
+
|
|
233
262
|
logger.info("Uploading custom tool: %s", name)
|
|
234
263
|
tool = update_or_create_tool(ref)
|
|
264
|
+
|
|
265
|
+
# Cache the resolved tool
|
|
235
266
|
self._cache_tool(tool, name)
|
|
267
|
+
if hasattr(tool, "id") and tool.id:
|
|
268
|
+
self._cache[tool.id] = tool
|
|
269
|
+
|
|
236
270
|
return tool
|
|
237
271
|
|
|
238
272
|
def _resolve_dict_tool(self, ref: Any, name: str) -> Tool | None:
|
|
@@ -245,6 +279,7 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
245
279
|
Returns:
|
|
246
280
|
The resolved tool, or None if not a dict.
|
|
247
281
|
"""
|
|
282
|
+
# Lazy imports to avoid circular dependency
|
|
248
283
|
from glaip_sdk.tools.base import Tool as ToolClass # noqa: PLC0415
|
|
249
284
|
|
|
250
285
|
if not isinstance(ref, dict):
|
|
@@ -268,18 +303,10 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
268
303
|
Returns:
|
|
269
304
|
The resolved tool, or None if not a string.
|
|
270
305
|
"""
|
|
271
|
-
from glaip_sdk.utils.discovery import find_tool # noqa: PLC0415
|
|
272
|
-
|
|
273
306
|
if not isinstance(ref, str):
|
|
274
307
|
return None
|
|
275
308
|
|
|
276
|
-
|
|
277
|
-
tool = find_tool(name)
|
|
278
|
-
if tool:
|
|
279
|
-
# Use _cache_tool to cache by both name and ID for consistency
|
|
280
|
-
self._cache_tool(tool, name)
|
|
281
|
-
return tool
|
|
282
|
-
raise ValueError(f"Tool not found on platform: {name}")
|
|
309
|
+
return self._resolve_native_platform_tool(name)
|
|
283
310
|
|
|
284
311
|
def _resolve_and_cache(self, ref: Any, name: str) -> Tool:
|
|
285
312
|
"""Resolve tool reference - upload if class, find if string/native.
|
|
@@ -292,7 +319,7 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
292
319
|
The resolved glaip_sdk.models.Tool object.
|
|
293
320
|
|
|
294
321
|
Raises:
|
|
295
|
-
ValueError: If
|
|
322
|
+
ValueError: If tool cannot be resolved.
|
|
296
323
|
"""
|
|
297
324
|
# Try each resolution strategy in order
|
|
298
325
|
resolvers = [
|
|
@@ -310,6 +337,24 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
310
337
|
|
|
311
338
|
raise ValueError(f"Could not resolve tool reference: {ref}")
|
|
312
339
|
|
|
340
|
+
def _is_aip_agents_tool(self, ref: Any) -> bool:
|
|
341
|
+
"""Check if reference is an aip-agents tool.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
ref: The reference to check.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
True if ref is from aip_agents.tools package.
|
|
348
|
+
"""
|
|
349
|
+
try:
|
|
350
|
+
from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
|
|
351
|
+
is_aip_agents_tool,
|
|
352
|
+
)
|
|
353
|
+
except ImportError:
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
return is_aip_agents_tool(ref)
|
|
357
|
+
|
|
313
358
|
def _is_custom_tool(self, ref: Any) -> bool:
|
|
314
359
|
"""Check if reference is a custom tool class/instance.
|
|
315
360
|
|
|
@@ -323,10 +368,18 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
323
368
|
from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
|
|
324
369
|
is_langchain_tool,
|
|
325
370
|
)
|
|
371
|
+
|
|
372
|
+
is_tool = is_langchain_tool(ref)
|
|
326
373
|
except ImportError:
|
|
374
|
+
is_tool = hasattr(ref, "args_schema") or hasattr(ref, "_run")
|
|
375
|
+
if is_tool:
|
|
376
|
+
logger.warning("tool_detection module missing; identifying tool via fallback attributes.")
|
|
377
|
+
|
|
378
|
+
# aip_agents tools are NOT custom - they're native
|
|
379
|
+
if is_tool and self._is_aip_agents_tool(ref):
|
|
327
380
|
return False
|
|
328
381
|
|
|
329
|
-
return
|
|
382
|
+
return is_tool
|
|
330
383
|
|
|
331
384
|
def resolve(self, ref: Any) -> Tool:
|
|
332
385
|
"""Resolve a tool reference to a platform Tool object.
|
|
@@ -348,7 +401,6 @@ class ToolRegistry(BaseRegistry["Tool"]):
|
|
|
348
401
|
# Use _cache_tool to cache by both name and ID for consistency
|
|
349
402
|
self._cache_tool(ref, name)
|
|
350
403
|
return ref
|
|
351
|
-
|
|
352
404
|
# Tool without ID (e.g., from Tool.from_native()) - needs platform lookup
|
|
353
405
|
# Fall through to normal resolution
|
|
354
406
|
|
|
@@ -9,9 +9,10 @@ Authors:
|
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from glaip_sdk.runner.tool_adapter.base_tool_adapter import BaseToolAdapter
|
|
13
12
|
from gllm_core.utils import LoggerManager
|
|
14
13
|
|
|
14
|
+
from glaip_sdk.runner.tool_adapter.base_tool_adapter import BaseToolAdapter
|
|
15
|
+
|
|
15
16
|
logger = LoggerManager().get_logger(__name__)
|
|
16
17
|
|
|
17
18
|
# Constant for unknown tool name placeholder
|
|
@@ -73,8 +74,30 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
73
74
|
if self._is_langchain_tool(tool_ref):
|
|
74
75
|
return self._instantiate_langchain_tool(tool_ref)
|
|
75
76
|
|
|
76
|
-
# 3.
|
|
77
|
+
# 3. Native tools with discovered class
|
|
77
78
|
if self._is_platform_tool(tool_ref):
|
|
79
|
+
# Try to discover local implementation for native tool
|
|
80
|
+
from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
|
|
81
|
+
find_aip_agents_tool_class,
|
|
82
|
+
get_tool_name,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Get tool name from reference
|
|
86
|
+
tool_name = get_tool_name(tool_ref) if not isinstance(tool_ref, str) else tool_ref
|
|
87
|
+
|
|
88
|
+
if tool_name:
|
|
89
|
+
discovered_class = find_aip_agents_tool_class(tool_name)
|
|
90
|
+
if discovered_class:
|
|
91
|
+
logger.info("Instantiating native tool locally: %s", tool_name)
|
|
92
|
+
try:
|
|
93
|
+
return discovered_class()
|
|
94
|
+
except TypeError as exc:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Could not instantiate native tool '{tool_name}'. "
|
|
97
|
+
"Ensure it has a zero-argument constructor or adjust the instantiation logic."
|
|
98
|
+
) from exc
|
|
99
|
+
|
|
100
|
+
# If no local class found, raise platform tool error
|
|
78
101
|
raise ValueError(self._get_platform_tool_error(tool_ref))
|
|
79
102
|
|
|
80
103
|
# 4. Unknown type
|
glaip_sdk/tools/base.py
CHANGED
|
@@ -231,6 +231,9 @@ class Tool:
|
|
|
231
231
|
Native tools are pre-existing tools on the GL AIP platform
|
|
232
232
|
that don't require uploading (e.g., "time_tool", "web_search").
|
|
233
233
|
|
|
234
|
+
For local execution, automatically discovers the corresponding aip_agents.tools
|
|
235
|
+
class if available. If not found, tool can only be used after deployment.
|
|
236
|
+
|
|
234
237
|
Args:
|
|
235
238
|
name: The name of the native tool on the platform.
|
|
236
239
|
|
|
@@ -241,7 +244,14 @@ class Tool:
|
|
|
241
244
|
>>> time_tool = Tool.from_native("time_tool")
|
|
242
245
|
>>> web_search = Tool.from_native("web_search")
|
|
243
246
|
"""
|
|
244
|
-
|
|
247
|
+
# Try to discover local implementation for native execution
|
|
248
|
+
from glaip_sdk.utils.tool_detection import ( # noqa: PLC0415
|
|
249
|
+
find_aip_agents_tool_class,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
tool_class = find_aip_agents_tool_class(name)
|
|
253
|
+
|
|
254
|
+
return cls(name=name, type=ToolType.NATIVE, tool_class=tool_class)
|
|
245
255
|
|
|
246
256
|
@classmethod
|
|
247
257
|
def from_langchain(cls, tool_class: type) -> Tool:
|
|
@@ -287,19 +297,11 @@ class Tool:
|
|
|
287
297
|
Raises:
|
|
288
298
|
ValueError: If name cannot be extracted or is not a valid string.
|
|
289
299
|
"""
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if isinstance(name_field.default, str):
|
|
296
|
-
return name_field.default
|
|
297
|
-
|
|
298
|
-
# Try direct name attribute
|
|
299
|
-
if hasattr(tool_class, "name"):
|
|
300
|
-
name_attr = getattr(tool_class, "name")
|
|
301
|
-
if isinstance(name_attr, str):
|
|
302
|
-
return name_attr
|
|
300
|
+
from glaip_sdk.utils.tool_detection import get_tool_name # noqa: PLC0415
|
|
301
|
+
|
|
302
|
+
name = get_tool_name(tool_class)
|
|
303
|
+
if name:
|
|
304
|
+
return name
|
|
303
305
|
|
|
304
306
|
# If we can't extract the name, raise an error
|
|
305
307
|
raise ValueError(
|
|
@@ -385,12 +387,22 @@ class Tool:
|
|
|
385
387
|
if not self._client:
|
|
386
388
|
raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
|
|
387
389
|
|
|
390
|
+
# Handle both Client (has .tools) and ToolClient (direct methods)
|
|
391
|
+
# Priority: Check if client has a 'tools' attribute (Client instance)
|
|
392
|
+
# Otherwise, use client directly (ToolClient instance)
|
|
393
|
+
if hasattr(self._client, "tools") and self._client.tools is not None:
|
|
394
|
+
# Main Client instance - use the tools sub-client
|
|
395
|
+
tools_client = self._client.tools
|
|
396
|
+
else:
|
|
397
|
+
# ToolClient instance - use directly
|
|
398
|
+
tools_client = self._client
|
|
399
|
+
|
|
388
400
|
# Check if file upload is requested
|
|
389
401
|
if "file" in kwargs:
|
|
390
402
|
file_path = kwargs.pop("file")
|
|
391
|
-
response =
|
|
403
|
+
response = tools_client.update_tool_via_file(self._id, file_path, **kwargs)
|
|
392
404
|
else:
|
|
393
|
-
response =
|
|
405
|
+
response = tools_client.update_tool(tool_id=self._id, **kwargs)
|
|
394
406
|
|
|
395
407
|
# Update local properties from response
|
|
396
408
|
if hasattr(response, "name") and response.name:
|
|
@@ -414,7 +426,17 @@ class Tool:
|
|
|
414
426
|
if not self._client:
|
|
415
427
|
raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
|
|
416
428
|
|
|
417
|
-
|
|
429
|
+
# Handle both Client (has .tools) and ToolClient (direct methods)
|
|
430
|
+
# Priority: Check if client has a 'tools' attribute (Client instance)
|
|
431
|
+
# Otherwise, use client directly (ToolClient instance)
|
|
432
|
+
if hasattr(self._client, "tools") and self._client.tools is not None:
|
|
433
|
+
# Main Client instance - use the tools sub-client
|
|
434
|
+
tools_client = self._client.tools
|
|
435
|
+
else:
|
|
436
|
+
# ToolClient instance - use directly
|
|
437
|
+
tools_client = self._client
|
|
438
|
+
|
|
439
|
+
tools_client.delete_tool(self._id)
|
|
418
440
|
self._id = None
|
|
419
441
|
self._client = None
|
|
420
442
|
|
glaip_sdk/utils/__init__.py
CHANGED
|
@@ -4,9 +4,140 @@ Authors:
|
|
|
4
4
|
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
7
9
|
import ast
|
|
10
|
+
import importlib
|
|
11
|
+
import inspect
|
|
12
|
+
import pkgutil
|
|
13
|
+
from functools import lru_cache
|
|
8
14
|
from typing import Any
|
|
9
15
|
|
|
16
|
+
# Constants for frequently used strings to avoid duplication (S1192)
|
|
17
|
+
_NAME = "name"
|
|
18
|
+
_AIP_AGENTS_TOOLS = "aip_agents.tools"
|
|
19
|
+
_BASE_TOOL = "BaseTool"
|
|
20
|
+
|
|
21
|
+
# Internal map to store all discovered tools in the session
|
|
22
|
+
_DISCOVERED_TOOLS: dict[str, type] | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _should_skip_module(module_name: str) -> bool:
|
|
26
|
+
"""Check if module should be skipped during tool discovery."""
|
|
27
|
+
short_name = module_name.rsplit(".", 1)[-1]
|
|
28
|
+
return short_name.startswith("_") or "test" in short_name
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_pydantic_field_default(cls: type, attr_name: str, field_name: str) -> str | None:
|
|
32
|
+
"""Extract default value from a Pydantic field."""
|
|
33
|
+
try:
|
|
34
|
+
fields = getattr(cls, attr_name, {})
|
|
35
|
+
field = fields.get(field_name)
|
|
36
|
+
# Broad exception handling needed because:
|
|
37
|
+
# - model_fields/__fields__ might be a descriptor that raises AttributeError
|
|
38
|
+
# - field.default might raise during access
|
|
39
|
+
# - Various Pydantic internals can raise unexpected exceptions
|
|
40
|
+
if field and hasattr(field, "default") and isinstance(field.default, str):
|
|
41
|
+
return field.default
|
|
42
|
+
except Exception: # pylint: disable=broad-except
|
|
43
|
+
pass
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_name_from_pydantic_v2(cls: type) -> str | None:
|
|
48
|
+
"""Extract name from Pydantic v2 model_fields."""
|
|
49
|
+
return _get_pydantic_field_default(cls, "model_fields", _NAME)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _get_name_from_pydantic_v1(cls: type) -> str | None:
|
|
53
|
+
"""Extract name from Pydantic v1 __fields__."""
|
|
54
|
+
return _get_pydantic_field_default(cls, "__fields__", _NAME)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_tool_name(ref: Any) -> str | None:
|
|
58
|
+
"""Extract tool name from a tool class or instance.
|
|
59
|
+
|
|
60
|
+
Handles LangChain BaseTool (Pydantic v1/v2) and standard classes.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
ref: Tool class or instance.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The extracted tool name, or None if not found.
|
|
67
|
+
"""
|
|
68
|
+
if ref is None:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
# 1. Try instance 'name' attribute
|
|
72
|
+
if not isinstance(ref, type):
|
|
73
|
+
try:
|
|
74
|
+
name = getattr(ref, _NAME, None)
|
|
75
|
+
if isinstance(name, str):
|
|
76
|
+
return name
|
|
77
|
+
except Exception: # pylint: disable=broad-except
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
cls = ref if isinstance(ref, type) else type(ref)
|
|
81
|
+
|
|
82
|
+
# 2. Try class 'model_fields' (Pydantic v2)
|
|
83
|
+
# Check Pydantic v2 first for forward compatibility
|
|
84
|
+
name = _get_name_from_pydantic_v2(cls)
|
|
85
|
+
if name:
|
|
86
|
+
return name
|
|
87
|
+
|
|
88
|
+
# 3. Try class '__fields__' (Pydantic v1)
|
|
89
|
+
name = _get_name_from_pydantic_v1(cls)
|
|
90
|
+
if name:
|
|
91
|
+
return name
|
|
92
|
+
|
|
93
|
+
# 4. Try direct class attribute
|
|
94
|
+
if hasattr(cls, _NAME):
|
|
95
|
+
try:
|
|
96
|
+
name_attr = getattr(cls, _NAME)
|
|
97
|
+
if isinstance(name_attr, str):
|
|
98
|
+
return name_attr
|
|
99
|
+
except Exception: # pylint: disable=broad-except
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _check_langchain_standard(ref: Any) -> bool:
|
|
106
|
+
"""Perform standard isinstance/issubclass check for LangChain tool."""
|
|
107
|
+
try:
|
|
108
|
+
from langchain_core.tools import BaseTool # noqa: PLC0415
|
|
109
|
+
|
|
110
|
+
# Check if BaseTool is actually a type to avoid TypeError in issubclass/isinstance
|
|
111
|
+
if isinstance(BaseTool, type):
|
|
112
|
+
if isinstance(ref, type) and issubclass(ref, BaseTool):
|
|
113
|
+
return True
|
|
114
|
+
if isinstance(ref, BaseTool):
|
|
115
|
+
return True
|
|
116
|
+
except (ImportError, TypeError):
|
|
117
|
+
pass
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _check_langchain_fallback(ref: Any) -> bool:
|
|
122
|
+
"""Perform name-based fallback check for LangChain tool (robust for mocks).
|
|
123
|
+
|
|
124
|
+
This fallback handles cases where:
|
|
125
|
+
- BaseTool is mocked in tests
|
|
126
|
+
- BaseTool is re-imported through internal modules (e.g., runner)
|
|
127
|
+
- isinstance/issubclass checks fail due to module reloading
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
cls = ref if isinstance(ref, type) else getattr(ref, "__class__", None)
|
|
131
|
+
if cls and hasattr(cls, "__mro__"):
|
|
132
|
+
for c in cls.__mro__:
|
|
133
|
+
c_name = getattr(c, "__name__", None)
|
|
134
|
+
c_module = getattr(c, "__module__", "")
|
|
135
|
+
if c_name == _BASE_TOOL and ("langchain" in c_module or "runner" in c_module):
|
|
136
|
+
return True
|
|
137
|
+
except (AttributeError, TypeError):
|
|
138
|
+
pass
|
|
139
|
+
return False
|
|
140
|
+
|
|
10
141
|
|
|
11
142
|
def is_langchain_tool(ref: Any) -> bool:
|
|
12
143
|
"""Check if ref is a LangChain BaseTool class or instance.
|
|
@@ -21,17 +152,133 @@ def is_langchain_tool(ref: Any) -> bool:
|
|
|
21
152
|
Returns:
|
|
22
153
|
True if ref is a LangChain BaseTool class or instance.
|
|
23
154
|
"""
|
|
155
|
+
if ref is None:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# 1. Standard check (preferred)
|
|
159
|
+
if _check_langchain_standard(ref):
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
# 2. Name-based check (robust fallback for mocks and re-imports)
|
|
163
|
+
return _check_langchain_fallback(ref)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def is_aip_agents_tool(ref: Any) -> bool:
|
|
167
|
+
"""Check if ref is an aip-agents tool class or instance.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
ref: Object to check.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
True if ref is from aip_agents.tools package.
|
|
174
|
+
"""
|
|
24
175
|
try:
|
|
25
|
-
|
|
176
|
+
# Check class module
|
|
177
|
+
if isinstance(ref, type):
|
|
178
|
+
return ref.__module__.startswith(_AIP_AGENTS_TOOLS)
|
|
179
|
+
|
|
180
|
+
# Check instance class
|
|
181
|
+
if hasattr(ref, "__class__"):
|
|
182
|
+
return ref.__class__.__module__.startswith(_AIP_AGENTS_TOOLS)
|
|
183
|
+
|
|
184
|
+
return False
|
|
185
|
+
except (AttributeError, TypeError):
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _get_discovered_classes_from_module(module: Any) -> list[type]:
|
|
190
|
+
"""Extract BaseTool subclasses from a module."""
|
|
191
|
+
discovered_classes = []
|
|
192
|
+
for attr_name in dir(module):
|
|
193
|
+
if attr_name.startswith("_"):
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
attr = getattr(module, attr_name)
|
|
198
|
+
if inspect.isclass(attr) and is_langchain_tool(attr):
|
|
199
|
+
# Ensure it's not the BaseTool class itself
|
|
200
|
+
if getattr(attr, "__name__", None) != _BASE_TOOL:
|
|
201
|
+
discovered_classes.append(attr)
|
|
202
|
+
except Exception: # pylint: disable=broad-except
|
|
203
|
+
continue
|
|
204
|
+
return discovered_classes
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _import_and_map_module(module_name: str, tools_map: dict[str, type]) -> None:
|
|
208
|
+
"""Import a single module and extract its tools."""
|
|
209
|
+
try:
|
|
210
|
+
module = importlib.import_module(module_name)
|
|
211
|
+
classes = _get_discovered_classes_from_module(module)
|
|
212
|
+
for tool_class in classes:
|
|
213
|
+
name = get_tool_name(tool_class)
|
|
214
|
+
if name:
|
|
215
|
+
tools_map[name] = tool_class
|
|
216
|
+
except Exception: # pylint: disable=broad-except
|
|
217
|
+
# Broad catch to skip broken modules during discovery
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _walk_and_map_package(package: Any, tools_map: dict[str, type]) -> None:
|
|
222
|
+
"""Walk through a package and map all tools found."""
|
|
223
|
+
try:
|
|
224
|
+
# Walk packages using the package's path and name
|
|
225
|
+
for _, module_name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."):
|
|
226
|
+
if _should_skip_module(module_name):
|
|
227
|
+
continue # pragma: no cover
|
|
26
228
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return True
|
|
31
|
-
except ImportError:
|
|
229
|
+
_import_and_map_module(module_name, tools_map)
|
|
230
|
+
except Exception: # pylint: disable=broad-except
|
|
231
|
+
# Broad catch for walk_packages failure
|
|
32
232
|
pass
|
|
33
233
|
|
|
34
|
-
|
|
234
|
+
|
|
235
|
+
def _get_all_aip_agents_tools() -> dict[str, type]:
|
|
236
|
+
"""Discover and map all tools in aip_agents.tools (once per session)."""
|
|
237
|
+
global _DISCOVERED_TOOLS # pylint: disable=global-statement
|
|
238
|
+
if _DISCOVERED_TOOLS is None:
|
|
239
|
+
_DISCOVERED_TOOLS = {}
|
|
240
|
+
try:
|
|
241
|
+
package = importlib.import_module(_AIP_AGENTS_TOOLS)
|
|
242
|
+
if hasattr(package, "__path__"):
|
|
243
|
+
_walk_and_map_package(package, _DISCOVERED_TOOLS)
|
|
244
|
+
except (ImportError, AttributeError):
|
|
245
|
+
pass
|
|
246
|
+
return _DISCOVERED_TOOLS
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@lru_cache(maxsize=128)
|
|
250
|
+
def find_aip_agents_tool_class(name: str) -> type | None:
|
|
251
|
+
"""Find and return a native tool class by tool name.
|
|
252
|
+
|
|
253
|
+
Searches aip_agents.tools submodules for BaseTool subclasses
|
|
254
|
+
with matching 'name' attribute. Uses caching to improve performance.
|
|
255
|
+
|
|
256
|
+
Note:
|
|
257
|
+
Results are discovered once per session and cached. If tools are
|
|
258
|
+
dynamically added to the path after the first call, they may not
|
|
259
|
+
be discovered until the session restarts.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
name (str): The tool name to search for (e.g., "google_serper").
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
type|None: The discovered tool class, or None if not found.
|
|
266
|
+
|
|
267
|
+
Examples:
|
|
268
|
+
>>> find_aip_agents_tool_class("google_serper")
|
|
269
|
+
<class 'aip_agents.tools.web_search.serper_tool.GoogleSerperTool'>
|
|
270
|
+
|
|
271
|
+
>>> find_aip_agents_tool_class("nonexistent")
|
|
272
|
+
None
|
|
273
|
+
"""
|
|
274
|
+
return _get_all_aip_agents_tools().get(name)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def clear_discovery_cache() -> None:
|
|
278
|
+
"""Clear the tool discovery cache (internal use for testing)."""
|
|
279
|
+
global _DISCOVERED_TOOLS # pylint: disable=global-statement
|
|
280
|
+
_DISCOVERED_TOOLS = None
|
|
281
|
+
find_aip_agents_tool_class.cache_clear()
|
|
35
282
|
|
|
36
283
|
|
|
37
284
|
def is_tool_plugin_decorator(decorator: ast.expr) -> bool:
|
|
@@ -7,10 +7,10 @@ glaip_sdk/rich_components.py,sha256=44Z0V1ZQleVh9gUDGwRR5mriiYFnVGOhm7fFxZYbP8c,
|
|
|
7
7
|
glaip_sdk/agents/__init__.py,sha256=VfYov56edbWuySXFEbWJ_jLXgwnFzPk1KB-9-mfsUCc,776
|
|
8
8
|
glaip_sdk/agents/base.py,sha256=GQnzCw2cqlrbxwdoWFfhBcBlEDgubY4tlD6gr1b3zps,44539
|
|
9
9
|
glaip_sdk/cli/__init__.py,sha256=xCCfuF1Yc7mpCDcfhHZTX0vizvtrDSLeT8MJ3V7m5A0,156
|
|
10
|
-
glaip_sdk/cli/account_store.py,sha256=
|
|
10
|
+
glaip_sdk/cli/account_store.py,sha256=u_memecwEQssustZs2wYBrHbEmKUlDfmmL-zO1F3n3A,19034
|
|
11
11
|
glaip_sdk/cli/agent_config.py,sha256=YAbFKrTNTRqNA6b0i0Q3pH-01rhHDRi5v8dxSFwGSwM,2401
|
|
12
12
|
glaip_sdk/cli/auth.py,sha256=bqOHMGIOCg3KXssme3uJBBjEbK0rCEppQ6oq-gJ-hzA,24276
|
|
13
|
-
glaip_sdk/cli/config.py,sha256=
|
|
13
|
+
glaip_sdk/cli/config.py,sha256=hFKQSdqELQZLKBuFnhEirNLDyPJwEmpQuD8nHuWxokg,3051
|
|
14
14
|
glaip_sdk/cli/constants.py,sha256=zqcVtzfj6huW97gbCmhkFqntge1H-c1vnkGqTazADgU,895
|
|
15
15
|
glaip_sdk/cli/context.py,sha256=--Y5vc6lgoAV7cRoUAr9UxSQaLmkMg29FolA7EwoRqM,3803
|
|
16
16
|
glaip_sdk/cli/display.py,sha256=ojgWdGeD5KUnGOmWNqqK4JP-1EaWHWX--DWze3BmIz0,12137
|
|
@@ -23,6 +23,7 @@ glaip_sdk/cli/mcp_validators.py,sha256=cwbz7p_p7_9xVuuF96OBQOdmEgo5UObU6iWWQ2X03
|
|
|
23
23
|
glaip_sdk/cli/pager.py,sha256=TmiMDNpUMuZju7QJ6A_ITqIoEf8Dhv8U6mTXx2Fga1k,7935
|
|
24
24
|
glaip_sdk/cli/resolution.py,sha256=AGvv7kllLcuvk_jdaArJqH3lId4IDEXpHceRZwy14xY,2448
|
|
25
25
|
glaip_sdk/cli/rich_helpers.py,sha256=kO47N8e506rxrN6Oc9mbAWN3Qb536oQPWZy1s9A616g,819
|
|
26
|
+
glaip_sdk/cli/tui_settings.py,sha256=ey9bSlolEj3_SMmjVVV_PY0FkfH1psAFPOl03NlykvI,3867
|
|
26
27
|
glaip_sdk/cli/update_notifier.py,sha256=0zpWxr4nSyz0tiLWyC7EEO2deAnVmsRcVlMV79G2QMI,18049
|
|
27
28
|
glaip_sdk/cli/validators.py,sha256=k4J2ACYJPF6UmWJfENt9OHWdp4RNArVxR3hoeqauO88,5629
|
|
28
29
|
glaip_sdk/cli/commands/__init__.py,sha256=6Z3ASXDut0lAbUX_umBFtxPzzFyqoiZfVeTahThFu1A,219
|
|
@@ -80,10 +81,10 @@ glaip_sdk/cli/slash/remote_runs_controller.py,sha256=a5X5rYgb9l6dHhvTewRUCj-hAo7
|
|
|
80
81
|
glaip_sdk/cli/slash/session.py,sha256=Zn2hXND_Tfameh_PI8g4VIMd7GPWxwhtPNMN9p6cF7M,65081
|
|
81
82
|
glaip_sdk/cli/slash/tui/__init__.py,sha256=oBUzeoslYwPKVlhqhgg4I7480b77vQNc9ec0NgdTC1s,977
|
|
82
83
|
glaip_sdk/cli/slash/tui/accounts.tcss,sha256=BCjIuTetmVjydv6DCliY38Cze2LUEu7IY44sL5nIuLU,1194
|
|
83
|
-
glaip_sdk/cli/slash/tui/accounts_app.py,sha256=
|
|
84
|
+
glaip_sdk/cli/slash/tui/accounts_app.py,sha256=baxAN1kUs3xyOvk6VCrdwcdEL89D37FkLlFP8mXrKHE,42049
|
|
84
85
|
glaip_sdk/cli/slash/tui/background_tasks.py,sha256=SAe1mV2vXB3mJcSGhelU950vf8Lifjhws9iomyIVFKw,2422
|
|
85
86
|
glaip_sdk/cli/slash/tui/clipboard.py,sha256=HL_RWIdONyRmDtTYuDzxJTS_mRcLxuR37Ac9Ug5nh40,4730
|
|
86
|
-
glaip_sdk/cli/slash/tui/context.py,sha256=
|
|
87
|
+
glaip_sdk/cli/slash/tui/context.py,sha256=XgHsXPl8LDDwIueP_jhP6xqpx_zHOk0FF19rWob_z5Q,3128
|
|
87
88
|
glaip_sdk/cli/slash/tui/keybind_registry.py,sha256=_rK05BxTxNudYc4iJ9gDxpgeUkjDAq8rarIT-9A-jyM,6739
|
|
88
89
|
glaip_sdk/cli/slash/tui/loading.py,sha256=nW5pv_Tnl9FUOPR3Qf2O5gt1AGHSo3b5-Uofg34F6AE,1909
|
|
89
90
|
glaip_sdk/cli/slash/tui/remote_runs_app.py,sha256=RCrI-c5ilKV6Iy1lz2Aok9xo2Ou02vqcXACMXTdodnE,24716
|
|
@@ -91,7 +92,7 @@ glaip_sdk/cli/slash/tui/terminal.py,sha256=iC31XChTL34gXY6vXdSIX3HmD36tuA9EYTPZ2
|
|
|
91
92
|
glaip_sdk/cli/slash/tui/toast.py,sha256=LP_myZwgnrdowrRxGK24lMlx7iZt7iOwFhrbc4NW0DY,3493
|
|
92
93
|
glaip_sdk/cli/slash/tui/theme/__init__.py,sha256=rtM2ik83YNCRcI1qh_Sf3rnxco2OvCNNT3NbHY6cLvw,432
|
|
93
94
|
glaip_sdk/cli/slash/tui/theme/catalog.py,sha256=G52eU3h8YI9D8XUALVg1KVZ4Lq65VnZdgPS3F_P7XLE,2544
|
|
94
|
-
glaip_sdk/cli/slash/tui/theme/manager.py,sha256=
|
|
95
|
+
glaip_sdk/cli/slash/tui/theme/manager.py,sha256=LBnxEMIwz-8cAlZGYk5tIoAJbOJyGYsmDlyuGJ-LlX4,3945
|
|
95
96
|
glaip_sdk/cli/slash/tui/theme/tokens.py,sha256=ympMRny_d-gHtmnPR-lmNZ-C9SGBy2q-MH81l0L1h-Y,1423
|
|
96
97
|
glaip_sdk/cli/transcript/__init__.py,sha256=yiYHyNtebMCu3BXu56Xm5RBC2tDc865q8UGPnoe6QRs,920
|
|
97
98
|
glaip_sdk/cli/transcript/cache.py,sha256=Wi1uln6HP1U6F-MRTrfnxi9bn6XJTxwWXhREIRPoMqQ,17439
|
|
@@ -137,7 +138,7 @@ glaip_sdk/registry/__init__.py,sha256=mjvElYE-wwmbriGe-c6qy4on0ccEuWxW_EWWrSbptC
|
|
|
137
138
|
glaip_sdk/registry/agent.py,sha256=F0axW4BIUODqnttIOzxnoS5AqQkLZ1i48FTeZNnYkhA,5203
|
|
138
139
|
glaip_sdk/registry/base.py,sha256=0x2ZBhiERGUcf9mQeWlksSYs5TxDG6FxBYQToYZa5D4,4143
|
|
139
140
|
glaip_sdk/registry/mcp.py,sha256=kNJmiijIbZL9Btx5o2tFtbaT-WG6O4Xf_nl3wz356Ow,7978
|
|
140
|
-
glaip_sdk/registry/tool.py,sha256=
|
|
141
|
+
glaip_sdk/registry/tool.py,sha256=c0Ja4rFYMOKs_1yjDLDZxCId4IjQzprwXzX0iIL8Fio,14979
|
|
141
142
|
glaip_sdk/runner/__init__.py,sha256=orJ3nLR9P-n1qMaAMWZ_xRS4368YnDpdltg-bX5BlUk,2210
|
|
142
143
|
glaip_sdk/runner/base.py,sha256=KIjcSAyDCP9_mn2H4rXR5gu1FZlwD9pe0gkTBmr6Yi4,2663
|
|
143
144
|
glaip_sdk/runner/deps.py,sha256=Du3hr2R5RHOYCRAv7RVmx661x-ayVXIeZ8JD7ODirTA,3884
|
|
@@ -149,12 +150,12 @@ glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py,sha256=b58GuadPz7q7aXoJyTY
|
|
|
149
150
|
glaip_sdk/runner/mcp_adapter/mcp_config_builder.py,sha256=fQcRaueDuyUzXUSVn9N8QxfaYNIteEO_R_uibx_0Icw,3440
|
|
150
151
|
glaip_sdk/runner/tool_adapter/__init__.py,sha256=scv8sSPxSWjlSNEace03R230YbmWgphLgqINKvDjWmM,480
|
|
151
152
|
glaip_sdk/runner/tool_adapter/base_tool_adapter.py,sha256=nL--eicV0St5_0PZZSEhRurHDZHNwhGN2cKOUh0C5IY,1400
|
|
152
|
-
glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py,sha256=
|
|
153
|
+
glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py,sha256=SgfQM5NgKyYBs34juxv3TCEicJbKgFIVPPZa22tA9AU,8484
|
|
153
154
|
glaip_sdk/schedules/__init__.py,sha256=Ty__lE8ta3a6O7EiEsSXliVOwA3EBLKxKRsjAJt2WUg,482
|
|
154
155
|
glaip_sdk/schedules/base.py,sha256=ZRKWknoxQOYMhX8mjQ7S7oqpy6Wr0xdbzcgIrycsEQ8,9727
|
|
155
156
|
glaip_sdk/tools/__init__.py,sha256=rhGzEqQFCzeMrxmikBuNrMz4PyYczwic28boDKVmoHs,585
|
|
156
|
-
glaip_sdk/tools/base.py,sha256=
|
|
157
|
-
glaip_sdk/utils/__init__.py,sha256=
|
|
157
|
+
glaip_sdk/tools/base.py,sha256=KRaWWX5cKAvEKtBr4iSOaKQlQ973A4pNOW2KVvA1aYs,17353
|
|
158
|
+
glaip_sdk/utils/__init__.py,sha256=5a1kNLtUriwd1qAT6RU083GOyABS7LMZQacDP4yS9S4,2830
|
|
158
159
|
glaip_sdk/utils/agent_config.py,sha256=RhcHsSOVwOaSC2ggnPuHn36Aa0keGJhs8KGb2InvzRk,7262
|
|
159
160
|
glaip_sdk/utils/bundler.py,sha256=fLumFj1MqqqGA1Mwn05v_cEKPALv3rIPEMvaURpxZ80,15171
|
|
160
161
|
glaip_sdk/utils/client.py,sha256=otPUOIDvLCCsvFBNR8YMZFtRrORggmvvlFjl3YeeTqQ,3121
|
|
@@ -172,7 +173,7 @@ glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDa
|
|
|
172
173
|
glaip_sdk/utils/runtime_config.py,sha256=Gl9-CQ4lYZ39vRSgtdfcSU3CXshVDDuTOdSzjvsCgG0,14070
|
|
173
174
|
glaip_sdk/utils/serialization.py,sha256=z-qpvWLSBrGK3wbUclcA1UIKLXJedTnMSwPdq-FF4lo,13308
|
|
174
175
|
glaip_sdk/utils/sync.py,sha256=71egWp5qm_8tYpWZyGazvnP4NnyW16rcmzjGVicmQEE,6043
|
|
175
|
-
glaip_sdk/utils/tool_detection.py,sha256=
|
|
176
|
+
glaip_sdk/utils/tool_detection.py,sha256=B7xze014TZyqWI4JqLhkZrbtT5h32CjQEXRswtdcljI,9808
|
|
176
177
|
glaip_sdk/utils/tool_storage_provider.py,sha256=lampwUeWu4Uy8nBG7C4ZT-M6AHoWZS0m67HdLx21VDg,5396
|
|
177
178
|
glaip_sdk/utils/validation.py,sha256=hB_k3lvHdIFUiSwHStrC0Eqnhx0OG2UvwqASeem0HuQ,6859
|
|
178
179
|
glaip_sdk/utils/a2a/__init__.py,sha256=_X8AvDOsHeppo5n7rP5TeisVxlAdkZDTFReBk_9lmxo,876
|
|
@@ -206,8 +207,8 @@ glaip_sdk/utils/rendering/steps/format.py,sha256=Chnq7OBaj8XMeBntSBxrX5zSmrYeGcO
|
|
|
206
207
|
glaip_sdk/utils/rendering/steps/manager.py,sha256=BiBmTeQMQhjRMykgICXsXNYh1hGsss-fH9BIGVMWFi0,13194
|
|
207
208
|
glaip_sdk/utils/rendering/viewer/__init__.py,sha256=XrxmE2cMAozqrzo1jtDFm8HqNtvDcYi2mAhXLXn5CjI,457
|
|
208
209
|
glaip_sdk/utils/rendering/viewer/presenter.py,sha256=mlLMTjnyeyPVtsyrAbz1BJu9lFGQSlS-voZ-_Cuugv0,5725
|
|
209
|
-
glaip_sdk-0.7.
|
|
210
|
-
glaip_sdk-0.7.
|
|
211
|
-
glaip_sdk-0.7.
|
|
212
|
-
glaip_sdk-0.7.
|
|
213
|
-
glaip_sdk-0.7.
|
|
210
|
+
glaip_sdk-0.7.8.dist-info/METADATA,sha256=srMagYZpJce1Yhs0KJo0636l_He6YAFCS1HVtEY8k9Q,8365
|
|
211
|
+
glaip_sdk-0.7.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
212
|
+
glaip_sdk-0.7.8.dist-info/entry_points.txt,sha256=NkhO6FfgX9Zrjn63GuKphf-dLw7KNJvucAcXc7P3aMk,54
|
|
213
|
+
glaip_sdk-0.7.8.dist-info/top_level.txt,sha256=td7yXttiYX2s94-4wFhv-5KdT0rSZ-pnJRSire341hw,10
|
|
214
|
+
glaip_sdk-0.7.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|