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,77 @@
|
|
|
1
|
+
"""Logging configuration for CLI to suppress noisy dependency warnings.
|
|
2
|
+
|
|
3
|
+
This module provides centralized logging suppression for optional dependencies
|
|
4
|
+
that emit noisy warnings during CLI usage. Warnings are suppressed by default
|
|
5
|
+
but can be shown using GLAIP_LOG_LEVEL=DEBUG.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import warnings
|
|
14
|
+
|
|
15
|
+
NOISY_LOGGERS = ["transformers", "gllm_privacy", "google.cloud.aiplatform"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NameFilter(logging.Filter):
|
|
19
|
+
"""Filter logs by logger name prefix."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, prefixes: list[str]) -> None:
|
|
22
|
+
"""Initialize filter with logger name prefixes to suppress.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
prefixes: List of logger name prefixes to filter out.
|
|
26
|
+
"""
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.prefixes = prefixes
|
|
29
|
+
|
|
30
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
31
|
+
"""Filter log records by name prefix.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
record: Log record to filter.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
False if record should be suppressed, True otherwise.
|
|
38
|
+
"""
|
|
39
|
+
return not any(record.name.startswith(p) for p in self.prefixes)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_cli_logging() -> None:
|
|
43
|
+
"""Suppress INFO from noisy third-party libraries.
|
|
44
|
+
|
|
45
|
+
Use GLAIP_LOG_LEVEL=DEBUG to see all warnings.
|
|
46
|
+
This function is idempotent - calling it multiple times is safe.
|
|
47
|
+
"""
|
|
48
|
+
# Check env level FIRST before any suppression
|
|
49
|
+
env_level = os.getenv("GLAIP_LOG_LEVEL", "").upper()
|
|
50
|
+
is_debug = env_level == "DEBUG"
|
|
51
|
+
|
|
52
|
+
if is_debug:
|
|
53
|
+
# Debug mode: show everything, no suppression
|
|
54
|
+
if env_level and hasattr(logging, env_level):
|
|
55
|
+
logging.basicConfig(level=getattr(logging, env_level))
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# Default mode: suppress noisy warnings
|
|
59
|
+
if env_level and hasattr(logging, env_level):
|
|
60
|
+
logging.basicConfig(level=getattr(logging, env_level))
|
|
61
|
+
|
|
62
|
+
# Add handler filter to suppress by name prefix (handles child loggers)
|
|
63
|
+
# Check if filter already exists to ensure idempotency
|
|
64
|
+
root_logger = logging.getLogger()
|
|
65
|
+
has_name_filter = any(isinstance(f, NameFilter) for h in root_logger.handlers for f in h.filters)
|
|
66
|
+
|
|
67
|
+
if not has_name_filter:
|
|
68
|
+
handler = logging.StreamHandler()
|
|
69
|
+
handler.addFilter(NameFilter(NOISY_LOGGERS))
|
|
70
|
+
root_logger.addHandler(handler)
|
|
71
|
+
|
|
72
|
+
# Suppress FutureWarning for GCS (idempotent - multiple calls are safe)
|
|
73
|
+
warnings.filterwarnings(
|
|
74
|
+
"ignore",
|
|
75
|
+
category=FutureWarning,
|
|
76
|
+
message=r".*google-cloud-storage.*",
|
|
77
|
+
)
|
|
@@ -9,10 +9,9 @@ Authors:
|
|
|
9
9
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from gllm_core.utils import LoggerManager
|
|
13
|
-
|
|
14
12
|
from glaip_sdk.runner.mcp_adapter.base_mcp_adapter import BaseMCPAdapter
|
|
15
13
|
from glaip_sdk.runner.mcp_adapter.mcp_config_builder import MCPConfigBuilder
|
|
14
|
+
from gllm_core.utils import LoggerManager
|
|
16
15
|
|
|
17
16
|
logger = LoggerManager().get_logger(__name__)
|
|
18
17
|
|
|
@@ -115,17 +114,117 @@ class LangChainMCPAdapter(BaseMCPAdapter):
|
|
|
115
114
|
if "server_url" in config and "url" not in config:
|
|
116
115
|
config["url"] = config.pop("server_url")
|
|
117
116
|
|
|
117
|
+
self._validate_converted_config(
|
|
118
|
+
mcp_name=mcp.name,
|
|
119
|
+
transport=mcp.transport,
|
|
120
|
+
config=config,
|
|
121
|
+
)
|
|
122
|
+
|
|
118
123
|
# Convert authentication to headers using MCPConfigBuilder
|
|
124
|
+
# Merge with existing headers (auth headers take precedence for conflicts)
|
|
119
125
|
if hasattr(mcp, "authentication") and mcp.authentication:
|
|
120
|
-
|
|
121
|
-
if
|
|
122
|
-
config
|
|
126
|
+
auth_headers = MCPConfigBuilder.build_headers_from_auth(mcp.authentication)
|
|
127
|
+
if auth_headers:
|
|
128
|
+
existing_headers = config.get("headers", {})
|
|
129
|
+
config["headers"] = {**existing_headers, **auth_headers}
|
|
123
130
|
else:
|
|
124
131
|
logger.warning("Failed to build headers from authentication for MCP '%s'", mcp.name)
|
|
125
132
|
|
|
126
133
|
logger.debug("Converted MCP '%s' with transport '%s'", mcp.name, mcp.transport)
|
|
127
134
|
return config
|
|
128
135
|
|
|
136
|
+
def _validate_converted_config(self, mcp_name: str, transport: str, config: dict[str, Any]) -> None:
|
|
137
|
+
"""Validate converted MCP config matches aip-agents schema expectations.
|
|
138
|
+
|
|
139
|
+
This method performs transport-specific validation after the glaip-sdk MCP
|
|
140
|
+
has been converted into the `aip-agents` `mcp_config` dictionary.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
mcp_name: The MCP server name.
|
|
144
|
+
transport: The MCP transport type.
|
|
145
|
+
config: The converted MCP configuration dictionary.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ValueError: If the configuration is invalid for the chosen transport.
|
|
149
|
+
"""
|
|
150
|
+
self._validate_transport_config(mcp_name, transport)
|
|
151
|
+
if transport in ("http", "sse"):
|
|
152
|
+
self._validate_http_sse_config(
|
|
153
|
+
mcp_name=mcp_name,
|
|
154
|
+
transport=transport,
|
|
155
|
+
config=config,
|
|
156
|
+
)
|
|
157
|
+
return
|
|
158
|
+
if transport == "stdio":
|
|
159
|
+
self._validate_stdio_config(
|
|
160
|
+
mcp_name=mcp_name,
|
|
161
|
+
config=config,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _validate_transport_config(self, mcp_name: str, transport: str) -> None:
|
|
165
|
+
"""Validate that the MCP transport is supported by local mode.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
mcp_name: The MCP server name.
|
|
169
|
+
transport: The MCP transport type.
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
ValueError: If the transport is not one of 'http', 'sse', or 'stdio'.
|
|
173
|
+
"""
|
|
174
|
+
if transport not in ("http", "sse", "stdio"):
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"Invalid MCP config for '{mcp_name}': transport must be one of "
|
|
177
|
+
f"'http', 'sse', or 'stdio'. Got: {transport!r}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _validate_http_sse_config(self, mcp_name: str, transport: str, config: dict[str, Any]) -> None:
|
|
181
|
+
"""Validate http/sse config has a usable URL.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
mcp_name: The MCP server name.
|
|
185
|
+
transport: The MCP transport type ('http' or 'sse').
|
|
186
|
+
config: The converted MCP configuration dictionary.
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ValueError: If url is missing/empty or does not use http(s) scheme.
|
|
190
|
+
"""
|
|
191
|
+
url = config.get("url")
|
|
192
|
+
if not isinstance(url, str) or not url:
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"Invalid MCP config for '{mcp_name}': transport='{transport}' "
|
|
195
|
+
"requires config['url'] as a non-empty string."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if not (url.startswith("http://") or url.startswith("https://")):
|
|
199
|
+
raise ValueError(
|
|
200
|
+
f"Invalid MCP config for '{mcp_name}': config['url'] must start with "
|
|
201
|
+
f"'http://' or 'https://'. Got: {url!r}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def _validate_stdio_config(self, mcp_name: str, config: dict[str, Any]) -> None:
|
|
205
|
+
"""Validate stdio config has a usable command and optional args list.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
mcp_name: The MCP server name.
|
|
209
|
+
config: The converted MCP configuration dictionary.
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
ValueError: If command is missing/empty or args is not a list of strings.
|
|
213
|
+
"""
|
|
214
|
+
command = config.get("command")
|
|
215
|
+
if not isinstance(command, str) or not command:
|
|
216
|
+
raise ValueError(
|
|
217
|
+
f"Invalid MCP config for '{mcp_name}': transport='stdio' "
|
|
218
|
+
"requires config['command'] as a non-empty string."
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
args = config.get("args")
|
|
222
|
+
if args is not None and (not isinstance(args, list) or any(not isinstance(x, str) for x in args)):
|
|
223
|
+
raise ValueError(
|
|
224
|
+
f"Invalid MCP config for '{mcp_name}': transport='stdio' expects "
|
|
225
|
+
"config['args'] to be a list[str] if provided."
|
|
226
|
+
)
|
|
227
|
+
|
|
129
228
|
def _is_platform_mcp(self, ref: Any) -> bool:
|
|
130
229
|
"""Check if ref is platform-specific (not supported locally)."""
|
|
131
230
|
# MCP.from_native() or MCP.from_id() instances
|
|
@@ -15,6 +15,9 @@ from glaip_sdk.runner.tool_adapter.base_tool_adapter import BaseToolAdapter
|
|
|
15
15
|
|
|
16
16
|
logger = LoggerManager().get_logger(__name__)
|
|
17
17
|
|
|
18
|
+
# Constant for unknown tool name placeholder
|
|
19
|
+
_UNKNOWN_TOOL_NAME = "<unknown>"
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
class LangChainToolAdapter(BaseToolAdapter):
|
|
20
23
|
"""Adapts glaip-sdk tools to LangChain BaseTool format for aip-agents.
|
|
@@ -71,8 +74,30 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
71
74
|
if self._is_langchain_tool(tool_ref):
|
|
72
75
|
return self._instantiate_langchain_tool(tool_ref)
|
|
73
76
|
|
|
74
|
-
# 3.
|
|
77
|
+
# 3. Native tools with discovered class
|
|
75
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
|
|
76
101
|
raise ValueError(self._get_platform_tool_error(tool_ref))
|
|
77
102
|
|
|
78
103
|
# 4. Unknown type
|
|
@@ -81,6 +106,15 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
81
106
|
"Local mode only supports LangChain BaseTool classes/instances."
|
|
82
107
|
)
|
|
83
108
|
|
|
109
|
+
def _has_explicit_attr(self, ref: Any, attr: str) -> bool:
|
|
110
|
+
"""Check if attribute is explicitly set on the object.
|
|
111
|
+
|
|
112
|
+
This avoids false positives from objects like MagicMock, where hasattr()
|
|
113
|
+
can return True even if the attribute was never set.
|
|
114
|
+
"""
|
|
115
|
+
ref_dict = getattr(ref, "__dict__", None)
|
|
116
|
+
return isinstance(ref_dict, dict) and attr in ref_dict
|
|
117
|
+
|
|
84
118
|
def _is_tool_wrapper(self, ref: Any) -> bool:
|
|
85
119
|
"""Check if ref is a Tool.from_langchain() wrapper.
|
|
86
120
|
|
|
@@ -90,7 +124,13 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
90
124
|
Returns:
|
|
91
125
|
True if ref is a Tool.from_langchain() wrapper.
|
|
92
126
|
"""
|
|
93
|
-
|
|
127
|
+
if self._has_explicit_attr(ref, "langchain_tool") and hasattr(ref, "id") and hasattr(ref, "name"):
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
if self._has_explicit_attr(ref, "tool_class"):
|
|
131
|
+
return getattr(ref, "tool_class", None) is not None
|
|
132
|
+
|
|
133
|
+
return False
|
|
94
134
|
|
|
95
135
|
def _extract_from_wrapper(self, wrapper: Any) -> Any:
|
|
96
136
|
"""Extract underlying LangChain tool from Tool.from_langchain().
|
|
@@ -100,8 +140,29 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
100
140
|
|
|
101
141
|
Returns:
|
|
102
142
|
LangChain BaseTool instance.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If the wrapper's underlying tool is not a valid LangChain tool.
|
|
103
146
|
"""
|
|
104
|
-
langchain_tool = wrapper
|
|
147
|
+
langchain_tool = getattr(wrapper, "langchain_tool", None)
|
|
148
|
+
if langchain_tool is None:
|
|
149
|
+
langchain_tool = getattr(wrapper, "tool_class", None)
|
|
150
|
+
|
|
151
|
+
# Validate the extracted object is a valid LangChain tool
|
|
152
|
+
if langchain_tool is None:
|
|
153
|
+
wrapper_name = getattr(wrapper, "name", _UNKNOWN_TOOL_NAME)
|
|
154
|
+
raise ValueError(
|
|
155
|
+
f"Tool wrapper '{wrapper_name}' does not contain a valid LangChain tool. "
|
|
156
|
+
"Ensure Tool.from_langchain() was called with a LangChain BaseTool class or instance."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Validate it's actually a LangChain tool (class or instance)
|
|
160
|
+
if not self._is_langchain_tool(langchain_tool):
|
|
161
|
+
wrapper_name = getattr(wrapper, "name", _UNKNOWN_TOOL_NAME)
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"Tool wrapper '{wrapper_name}' contains an invalid tool type: {type(langchain_tool)}. "
|
|
164
|
+
"Expected a LangChain BaseTool class or instance."
|
|
165
|
+
)
|
|
105
166
|
|
|
106
167
|
# If it's a class, instantiate it
|
|
107
168
|
if isinstance(langchain_tool, type):
|
|
@@ -109,7 +170,7 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
109
170
|
|
|
110
171
|
logger.debug(
|
|
111
172
|
"Extracted LangChain tool from wrapper: %s",
|
|
112
|
-
getattr(langchain_tool, "name",
|
|
173
|
+
getattr(langchain_tool, "name", _UNKNOWN_TOOL_NAME),
|
|
113
174
|
)
|
|
114
175
|
return langchain_tool
|
|
115
176
|
|
|
@@ -155,8 +216,10 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
155
216
|
return True
|
|
156
217
|
|
|
157
218
|
# Tool.from_native() instances
|
|
158
|
-
if hasattr(ref, "id") and hasattr(ref, "name") and not
|
|
159
|
-
|
|
219
|
+
if hasattr(ref, "id") and hasattr(ref, "name") and not self._has_explicit_attr(ref, "langchain_tool"):
|
|
220
|
+
tool_class = getattr(ref, "tool_class", None) if self._has_explicit_attr(ref, "tool_class") else None
|
|
221
|
+
if tool_class is None:
|
|
222
|
+
return True
|
|
160
223
|
|
|
161
224
|
return False
|
|
162
225
|
|
|
@@ -173,5 +236,7 @@ class LangChainToolAdapter(BaseToolAdapter):
|
|
|
173
236
|
get_local_mode_not_supported_for_tool_message,
|
|
174
237
|
)
|
|
175
238
|
|
|
176
|
-
tool_name = ref if isinstance(ref, str) else getattr(ref, "name",
|
|
239
|
+
tool_name = ref if isinstance(ref, str) else getattr(ref, "name", None)
|
|
240
|
+
if tool_name is None:
|
|
241
|
+
tool_name = getattr(getattr(ref, "tool_class", None), "__name__", _UNKNOWN_TOOL_NAME)
|
|
177
242
|
return get_local_mode_not_supported_for_tool_message(tool_name)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Schedules runtime package.
|
|
2
|
+
|
|
3
|
+
This package contains runtime schedule resource objects (class-based) that
|
|
4
|
+
encapsulate behavior and API interactions via attached clients.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.schedules.base import (
|
|
11
|
+
Schedule,
|
|
12
|
+
ScheduleListResult,
|
|
13
|
+
ScheduleRun,
|
|
14
|
+
ScheduleRunListResult,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Schedule",
|
|
19
|
+
"ScheduleListResult",
|
|
20
|
+
"ScheduleRun",
|
|
21
|
+
"ScheduleRunListResult",
|
|
22
|
+
]
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Schedule runtime resources.
|
|
2
|
+
|
|
3
|
+
This module contains class-based runtime resources for schedules.
|
|
4
|
+
|
|
5
|
+
The runtime resources:
|
|
6
|
+
- Are not Pydantic models.
|
|
7
|
+
- Are returned from public client APIs.
|
|
8
|
+
- Delegate API operations to a bound ScheduleClient.
|
|
9
|
+
|
|
10
|
+
Authors:
|
|
11
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from glaip_sdk.models.agent_runs import RunStatus
|
|
21
|
+
from glaip_sdk.models.schedule import (
|
|
22
|
+
ScheduleConfig,
|
|
23
|
+
ScheduleMetadata,
|
|
24
|
+
ScheduleResponse,
|
|
25
|
+
ScheduleRunResponse,
|
|
26
|
+
ScheduleRunResult,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
30
|
+
from glaip_sdk.client.schedules import ScheduleClient
|
|
31
|
+
|
|
32
|
+
_SCHEDULE_CLIENT_REQUIRED_MSG = "No client available. Use client.schedules.get() to get a client-connected schedule."
|
|
33
|
+
_SCHEDULE_RUN_CLIENT_REQUIRED_MSG = (
|
|
34
|
+
"No client available. Use client.schedules.list_runs() to get a client-connected schedule run."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Schedule:
|
|
39
|
+
"""Runtime schedule resource.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
id (str): The schedule ID.
|
|
43
|
+
next_run_time (str | None): Next run time as returned by the API.
|
|
44
|
+
time_until_next_run (str | None): Human readable duration until next run.
|
|
45
|
+
metadata (ScheduleMetadata | None): Schedule metadata.
|
|
46
|
+
created_at (datetime | None): Creation timestamp.
|
|
47
|
+
updated_at (datetime | None): Update timestamp.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
id: str,
|
|
54
|
+
next_run_time: str | None = None,
|
|
55
|
+
time_until_next_run: str | None = None,
|
|
56
|
+
metadata: ScheduleMetadata | None = None,
|
|
57
|
+
created_at: datetime | None = None,
|
|
58
|
+
updated_at: datetime | None = None,
|
|
59
|
+
_client: ScheduleClient | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Initialize a runtime Schedule."""
|
|
62
|
+
self.id = id
|
|
63
|
+
self.next_run_time = next_run_time
|
|
64
|
+
self.time_until_next_run = time_until_next_run
|
|
65
|
+
self.metadata = metadata
|
|
66
|
+
self.created_at = created_at
|
|
67
|
+
self.updated_at = updated_at
|
|
68
|
+
self._client = _client
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def from_response(cls, response: ScheduleResponse, *, client: ScheduleClient) -> Schedule:
|
|
72
|
+
"""Build a runtime Schedule from a DTO response.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
response: Parsed schedule response DTO.
|
|
76
|
+
client: ScheduleClient to bind.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Runtime Schedule.
|
|
80
|
+
"""
|
|
81
|
+
return cls(
|
|
82
|
+
id=response.id,
|
|
83
|
+
next_run_time=response.next_run_time,
|
|
84
|
+
time_until_next_run=response.time_until_next_run,
|
|
85
|
+
metadata=response.metadata,
|
|
86
|
+
created_at=response.created_at,
|
|
87
|
+
updated_at=response.updated_at,
|
|
88
|
+
_client=client,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def agent_id(self) -> str | None:
|
|
93
|
+
"""Agent ID derived from metadata."""
|
|
94
|
+
return self.metadata.agent_id if self.metadata else None
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def input(self) -> str | None:
|
|
98
|
+
"""Input text derived from metadata."""
|
|
99
|
+
return self.metadata.input if self.metadata else None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def schedule_config(self) -> ScheduleConfig | None:
|
|
103
|
+
"""Schedule configuration derived from metadata."""
|
|
104
|
+
return self.metadata.schedule if self.metadata else None
|
|
105
|
+
|
|
106
|
+
def update(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
input: str | None = None,
|
|
110
|
+
schedule: ScheduleConfig | dict[str, str] | str | None = None,
|
|
111
|
+
) -> Schedule:
|
|
112
|
+
"""Update this schedule."""
|
|
113
|
+
if self._client is None:
|
|
114
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
115
|
+
return self._client.update(self.id, input=input, schedule=schedule)
|
|
116
|
+
|
|
117
|
+
def delete(self) -> None:
|
|
118
|
+
"""Delete this schedule."""
|
|
119
|
+
if self._client is None:
|
|
120
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
121
|
+
self._client.delete(self.id)
|
|
122
|
+
|
|
123
|
+
def list_runs(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
status: RunStatus | None = None,
|
|
127
|
+
limit: int | None = None,
|
|
128
|
+
page: int | None = None,
|
|
129
|
+
) -> ScheduleRunListResult:
|
|
130
|
+
"""List runs for this schedule."""
|
|
131
|
+
if self._client is None:
|
|
132
|
+
raise RuntimeError(_SCHEDULE_CLIENT_REQUIRED_MSG)
|
|
133
|
+
if self.agent_id is None:
|
|
134
|
+
raise ValueError("Schedule has no agent_id")
|
|
135
|
+
return self._client.list_runs(
|
|
136
|
+
self.agent_id,
|
|
137
|
+
schedule_id=self.id,
|
|
138
|
+
status=status,
|
|
139
|
+
limit=limit,
|
|
140
|
+
page=page,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def __repr__(self) -> str:
|
|
144
|
+
"""Return a developer-friendly representation."""
|
|
145
|
+
parts: list[str] = [f"id={self.id!r}"]
|
|
146
|
+
if self.agent_id is not None:
|
|
147
|
+
parts.append(f"agent_id={self.agent_id!r}")
|
|
148
|
+
if self.next_run_time is not None:
|
|
149
|
+
parts.append(f"next_run_time={self.next_run_time!r}")
|
|
150
|
+
if self.time_until_next_run is not None:
|
|
151
|
+
parts.append(f"time_until_next_run={self.time_until_next_run!r}")
|
|
152
|
+
if self.created_at is not None:
|
|
153
|
+
parts.append(f"created_at={self.created_at!r}")
|
|
154
|
+
return f"Schedule({', '.join(parts)})"
|
|
155
|
+
|
|
156
|
+
def __str__(self) -> str:
|
|
157
|
+
"""Return a readable string representation."""
|
|
158
|
+
return self.__repr__()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class ScheduleRun:
|
|
162
|
+
"""Runtime schedule run resource."""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
*,
|
|
167
|
+
id: str,
|
|
168
|
+
agent_id: str,
|
|
169
|
+
schedule_id: str | None = None,
|
|
170
|
+
status: RunStatus,
|
|
171
|
+
run_type: str | None = None,
|
|
172
|
+
started_at: datetime | None = None,
|
|
173
|
+
completed_at: datetime | None = None,
|
|
174
|
+
input: str | None = None,
|
|
175
|
+
config: ScheduleConfig | dict[str, str] | None = None,
|
|
176
|
+
created_at: datetime | None = None,
|
|
177
|
+
updated_at: datetime | None = None,
|
|
178
|
+
_client: ScheduleClient | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Initialize a runtime ScheduleRun."""
|
|
181
|
+
self.id = id
|
|
182
|
+
self.agent_id = agent_id
|
|
183
|
+
self.schedule_id = schedule_id
|
|
184
|
+
self.status = status
|
|
185
|
+
self.run_type = run_type
|
|
186
|
+
self.started_at = started_at
|
|
187
|
+
self.completed_at = completed_at
|
|
188
|
+
self.input = input
|
|
189
|
+
self.config = config
|
|
190
|
+
self.created_at = created_at
|
|
191
|
+
self.updated_at = updated_at
|
|
192
|
+
self._client = _client
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def from_response(cls, response: ScheduleRunResponse, *, client: ScheduleClient) -> ScheduleRun:
|
|
196
|
+
"""Build a runtime ScheduleRun from a DTO response."""
|
|
197
|
+
return cls(
|
|
198
|
+
id=response.id,
|
|
199
|
+
agent_id=response.agent_id,
|
|
200
|
+
schedule_id=response.schedule_id,
|
|
201
|
+
status=response.status,
|
|
202
|
+
run_type=response.run_type,
|
|
203
|
+
started_at=response.started_at,
|
|
204
|
+
completed_at=response.completed_at,
|
|
205
|
+
input=response.input,
|
|
206
|
+
config=response.config,
|
|
207
|
+
created_at=response.created_at,
|
|
208
|
+
updated_at=response.updated_at,
|
|
209
|
+
_client=client,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def get_result(self) -> ScheduleRunResult:
|
|
213
|
+
"""Retrieve the full output payload for this run."""
|
|
214
|
+
if self._client is None:
|
|
215
|
+
raise RuntimeError(_SCHEDULE_RUN_CLIENT_REQUIRED_MSG)
|
|
216
|
+
if self.agent_id is None:
|
|
217
|
+
raise ValueError("Schedule run has no agent_id")
|
|
218
|
+
return self._client.get_run_result(self.agent_id, self.id)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def duration(self) -> str | None:
|
|
222
|
+
"""Formatted duration (HH:MM:SS) when both timestamps are available."""
|
|
223
|
+
if not self.started_at or not self.completed_at:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
total_seconds = int((self.completed_at - self.started_at).total_seconds())
|
|
227
|
+
minutes, seconds = divmod(total_seconds, 60)
|
|
228
|
+
hours, minutes = divmod(minutes, 60)
|
|
229
|
+
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
|
230
|
+
|
|
231
|
+
def __repr__(self) -> str:
|
|
232
|
+
"""Return a developer-friendly representation."""
|
|
233
|
+
parts: list[str] = [f"id={self.id!r}", f"status={self.status!r}"]
|
|
234
|
+
if self.started_at is not None:
|
|
235
|
+
parts.append(f"started_at={self.started_at.isoformat()!r}")
|
|
236
|
+
duration = self.duration
|
|
237
|
+
if duration is not None:
|
|
238
|
+
parts.append(f"duration={duration!r}")
|
|
239
|
+
return f"ScheduleRun({', '.join(parts)})"
|
|
240
|
+
|
|
241
|
+
def __str__(self) -> str:
|
|
242
|
+
"""Return a readable string representation."""
|
|
243
|
+
return self.__repr__()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclass
|
|
247
|
+
class ScheduleListResult:
|
|
248
|
+
"""Paginated list wrapper for runtime schedules."""
|
|
249
|
+
|
|
250
|
+
items: list[Schedule]
|
|
251
|
+
total: int | None = field(default=None)
|
|
252
|
+
page: int | None = field(default=None)
|
|
253
|
+
limit: int | None = field(default=None)
|
|
254
|
+
has_next: bool | None = field(default=None)
|
|
255
|
+
has_prev: bool | None = field(default=None)
|
|
256
|
+
|
|
257
|
+
def __iter__(self):
|
|
258
|
+
"""Iterate over schedules."""
|
|
259
|
+
yield from self.items
|
|
260
|
+
|
|
261
|
+
def __len__(self) -> int:
|
|
262
|
+
"""Return the number of schedules in this page."""
|
|
263
|
+
return self.items.__len__()
|
|
264
|
+
|
|
265
|
+
def __getitem__(self, index: int) -> Schedule:
|
|
266
|
+
"""Return the schedule at the given index."""
|
|
267
|
+
return self.items[index]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@dataclass
|
|
271
|
+
class ScheduleRunListResult:
|
|
272
|
+
"""Paginated list wrapper for runtime schedule runs."""
|
|
273
|
+
|
|
274
|
+
items: list[ScheduleRun]
|
|
275
|
+
total: int | None = field(default=None)
|
|
276
|
+
page: int | None = field(default=None)
|
|
277
|
+
limit: int | None = field(default=None)
|
|
278
|
+
has_next: bool | None = field(default=None)
|
|
279
|
+
has_prev: bool | None = field(default=None)
|
|
280
|
+
|
|
281
|
+
def __iter__(self):
|
|
282
|
+
"""Iterate over schedule runs."""
|
|
283
|
+
yield from self.items
|
|
284
|
+
|
|
285
|
+
def __len__(self) -> int:
|
|
286
|
+
"""Return the number of runs in this page."""
|
|
287
|
+
return self.items.__len__()
|
|
288
|
+
|
|
289
|
+
def __getitem__(self, index: int) -> ScheduleRun:
|
|
290
|
+
"""Return the run at the given index."""
|
|
291
|
+
return self.items[index]
|