glaip-sdk 0.6.19__py3-none-any.whl → 0.7.27__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/agents/base.py +283 -30
- glaip_sdk/agents/component.py +233 -0
- 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 +116 -0
- glaip_sdk/cli/commands/agents/_common.py +562 -0
- glaip_sdk/cli/commands/agents/create.py +155 -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 +1 -1
- glaip_sdk/cli/commands/configure.py +1 -2
- 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/entrypoint.py +20 -0
- glaip_sdk/cli/main.py +112 -35
- glaip_sdk/cli/pager.py +3 -3
- glaip_sdk/cli/resolution.py +2 -1
- glaip_sdk/cli/slash/accounts_controller.py +3 -1
- glaip_sdk/cli/slash/agent_session.py +1 -1
- glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
- glaip_sdk/cli/slash/session.py +343 -20
- glaip_sdk/cli/slash/tui/__init__.py +29 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
- glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
- glaip_sdk/cli/slash/tui/clipboard.py +316 -0
- glaip_sdk/cli/slash/tui/context.py +92 -0
- glaip_sdk/cli/slash/tui/indicators.py +341 -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 +184 -0
- glaip_sdk/cli/slash/tui/loading.py +43 -21
- glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
- 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 +388 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +1 -1
- 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 +293 -17
- glaip_sdk/client/base.py +25 -0
- glaip_sdk/client/hitl.py +136 -0
- glaip_sdk/client/main.py +7 -5
- 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} +28 -48
- glaip_sdk/client/payloads/agent/responses.py +43 -0
- glaip_sdk/client/run_rendering.py +109 -30
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +52 -23
- glaip_sdk/config/constants.py +22 -2
- glaip_sdk/guardrails/__init__.py +80 -0
- glaip_sdk/guardrails/serializer.py +91 -0
- glaip_sdk/hitl/__init__.py +35 -2
- glaip_sdk/hitl/base.py +64 -0
- glaip_sdk/hitl/callback.py +43 -0
- glaip_sdk/hitl/local.py +1 -31
- glaip_sdk/hitl/remote.py +523 -0
- glaip_sdk/models/__init__.py +47 -1
- glaip_sdk/models/_provider_mappings.py +101 -0
- glaip_sdk/models/_validation.py +97 -0
- glaip_sdk/models/agent.py +2 -1
- glaip_sdk/models/agent_runs.py +2 -1
- glaip_sdk/models/constants.py +141 -0
- glaip_sdk/models/model.py +170 -0
- 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/ptc.py +145 -0
- glaip_sdk/registry/tool.py +270 -57
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +4 -1
- glaip_sdk/runner/langgraph.py +251 -27
- glaip_sdk/runner/logging_config.py +77 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
- glaip_sdk/runner/ptc_adapter.py +98 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
- 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/agent_config.py +8 -2
- glaip_sdk/utils/bundler.py +138 -2
- glaip_sdk/utils/import_resolver.py +427 -49
- glaip_sdk/utils/runtime_config.py +3 -2
- glaip_sdk/utils/sync.py +31 -11
- glaip_sdk/utils/tool_detection.py +274 -6
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +22 -8
- glaip_sdk-0.7.27.dist-info/RECORD +227 -0
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +1 -1
- glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -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.19.dist-info/RECORD +0 -163
- glaip_sdk-0.6.19.dist-info/entry_points.txt +0 -2
- {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/top_level.txt +0 -0
|
@@ -13,6 +13,10 @@ import ast
|
|
|
13
13
|
import importlib
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
+
from glaip_sdk.utils.tool_detection import is_tool_plugin_decorator
|
|
17
|
+
|
|
18
|
+
INIT_FILE = "__init__.py"
|
|
19
|
+
|
|
16
20
|
|
|
17
21
|
class ImportResolver:
|
|
18
22
|
"""Resolves and categorizes Python imports for tool bundling.
|
|
@@ -44,6 +48,36 @@ class ImportResolver:
|
|
|
44
48
|
"""
|
|
45
49
|
self.tool_dir = tool_dir
|
|
46
50
|
self._processed_modules: set[str] = set()
|
|
51
|
+
self._package_root: Path = self._find_package_root(tool_dir)
|
|
52
|
+
|
|
53
|
+
def _find_package_root(self, start_path: Path) -> Path:
|
|
54
|
+
"""Find package root using standard project markers.
|
|
55
|
+
|
|
56
|
+
Searches upward from start_path for common Python project markers:
|
|
57
|
+
- pyproject.toml (modern Python standard)
|
|
58
|
+
- setup.py (legacy setuptools)
|
|
59
|
+
- .git (version control root)
|
|
60
|
+
|
|
61
|
+
Falls back to start_path.parent if no markers found.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
start_path: Starting directory to search from.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Path to the identified package root.
|
|
68
|
+
"""
|
|
69
|
+
# Check current directory and all parents
|
|
70
|
+
for parent in [start_path, *start_path.parents]:
|
|
71
|
+
if (parent / "pyproject.toml").exists():
|
|
72
|
+
return parent
|
|
73
|
+
if (parent / "setup.py").exists():
|
|
74
|
+
return parent
|
|
75
|
+
if (parent / ".git").exists():
|
|
76
|
+
return parent
|
|
77
|
+
|
|
78
|
+
# Fallback: return parent of start_path
|
|
79
|
+
# This maintains backward compatibility with original behavior
|
|
80
|
+
return start_path.parent
|
|
47
81
|
|
|
48
82
|
def categorize_imports(self, tree: ast.AST) -> tuple[list, list]:
|
|
49
83
|
"""Categorize imports into local and external.
|
|
@@ -61,7 +95,7 @@ class ImportResolver:
|
|
|
61
95
|
for node in ast.walk(tree):
|
|
62
96
|
if isinstance(node, ast.ImportFrom):
|
|
63
97
|
if self.is_local_import(node):
|
|
64
|
-
module_file = self.
|
|
98
|
+
module_file = self._resolve_import_path(node)
|
|
65
99
|
local_imports.append((node.module, module_file, node))
|
|
66
100
|
else:
|
|
67
101
|
external_imports.append(node)
|
|
@@ -70,6 +104,25 @@ class ImportResolver:
|
|
|
70
104
|
|
|
71
105
|
return local_imports, external_imports
|
|
72
106
|
|
|
107
|
+
def _resolve_import_path(self, node: ast.ImportFrom) -> Path:
|
|
108
|
+
"""Resolve import node to file path.
|
|
109
|
+
|
|
110
|
+
Handles both absolute and relative imports.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
node: ImportFrom AST node.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Path to the module file.
|
|
117
|
+
"""
|
|
118
|
+
if node.level > 0:
|
|
119
|
+
return self.resolve_relative_import_path(node)
|
|
120
|
+
|
|
121
|
+
if not node.module:
|
|
122
|
+
return self.tool_dir / INIT_FILE
|
|
123
|
+
|
|
124
|
+
return self.resolve_module_path(node.module)
|
|
125
|
+
|
|
73
126
|
def is_local_import(self, node: ast.ImportFrom) -> bool:
|
|
74
127
|
"""Check if import is local to the tool directory.
|
|
75
128
|
|
|
@@ -79,16 +132,83 @@ class ImportResolver:
|
|
|
79
132
|
Returns:
|
|
80
133
|
True if import is local.
|
|
81
134
|
"""
|
|
135
|
+
# Handle relative imports (level > 0)
|
|
136
|
+
if node.level > 0:
|
|
137
|
+
return self._is_local_relative_import(node)
|
|
138
|
+
|
|
139
|
+
# Handle absolute imports with no module name
|
|
82
140
|
if not node.module:
|
|
83
141
|
return False
|
|
84
142
|
|
|
85
|
-
# Handle package imports
|
|
143
|
+
# Handle dotted package imports
|
|
86
144
|
if "." in node.module:
|
|
87
145
|
return self._is_local_package_import(node.module)
|
|
88
146
|
|
|
147
|
+
# Handle simple module imports
|
|
89
148
|
potential_file = self.tool_dir / f"{node.module}.py"
|
|
90
149
|
return potential_file.exists()
|
|
91
150
|
|
|
151
|
+
def _is_local_relative_import(self, node: ast.ImportFrom) -> bool:
|
|
152
|
+
"""Check if a relative import is local.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
node: ImportFrom node with node.level > 0.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if the relative import resolves to a local file.
|
|
159
|
+
"""
|
|
160
|
+
if node.level == 0:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
base_dir = self._calculate_relative_base_dir(node.level)
|
|
164
|
+
if not base_dir:
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
if node.module is None:
|
|
168
|
+
return self._is_init_file_present(base_dir)
|
|
169
|
+
|
|
170
|
+
return self._check_module_exists(base_dir, node.module)
|
|
171
|
+
|
|
172
|
+
def _calculate_relative_base_dir(self, level: int) -> Path | None:
|
|
173
|
+
"""Calculate base directory for relative import.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
level: Relative import level (1=current, 2=parent, etc.).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Base directory path or None if exceeds filesystem root.
|
|
180
|
+
"""
|
|
181
|
+
base_dir = self.tool_dir
|
|
182
|
+
for _ in range(level - 1):
|
|
183
|
+
if base_dir == base_dir.parent:
|
|
184
|
+
return None
|
|
185
|
+
base_dir = base_dir.parent
|
|
186
|
+
return base_dir
|
|
187
|
+
|
|
188
|
+
def _is_init_file_present(self, base_dir: Path) -> bool:
|
|
189
|
+
"""Check if __init__.py exists in directory.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
base_dir: Directory to check.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if __init__.py exists.
|
|
196
|
+
"""
|
|
197
|
+
return (base_dir / INIT_FILE).exists()
|
|
198
|
+
|
|
199
|
+
def _check_module_exists(self, base_dir: Path, module: str) -> bool:
|
|
200
|
+
"""Check if module exists in base directory.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
base_dir: Base directory to search from.
|
|
204
|
+
module: Module name (dotted or simple).
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if module file or package exists.
|
|
208
|
+
"""
|
|
209
|
+
parts = module.split(".")
|
|
210
|
+
return self._module_parts_exist(base_dir, parts)
|
|
211
|
+
|
|
92
212
|
def _is_local_package_import(self, module: str) -> bool:
|
|
93
213
|
"""Check if a dotted module path is local.
|
|
94
214
|
|
|
@@ -100,29 +220,141 @@ class ImportResolver:
|
|
|
100
220
|
"""
|
|
101
221
|
parts = module.split(".")
|
|
102
222
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
223
|
+
if self._check_module_in_tool_dir(parts):
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
if self._check_module_in_subdirectory(parts):
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
if self._check_module_in_ancestor_packages(parts):
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
def _check_module_in_tool_dir(self, parts: list[str]) -> bool:
|
|
235
|
+
"""Check if module is in the current tool directory (Case 1).
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
parts: Module path split into components.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
True if module is found in tool_dir.
|
|
242
|
+
"""
|
|
243
|
+
if parts[0] != self.tool_dir.name:
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
remaining_parts = parts[1:]
|
|
247
|
+
if not remaining_parts:
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
return self._module_parts_exist(self.tool_dir, remaining_parts)
|
|
251
|
+
|
|
252
|
+
def _check_module_in_subdirectory(self, parts: list[str]) -> bool:
|
|
253
|
+
"""Check if module is in a subdirectory of tool_dir (Case 2).
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
parts: Module path split into components.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True if module is found in a subdirectory.
|
|
260
|
+
"""
|
|
116
261
|
package_dir = self.tool_dir / parts[0]
|
|
117
|
-
if package_dir.is_dir():
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
262
|
+
if not package_dir.is_dir():
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
return self._module_parts_exist(self.tool_dir, parts)
|
|
266
|
+
|
|
267
|
+
def _check_module_in_ancestor_packages(self, parts: list[str]) -> bool:
|
|
268
|
+
"""Check if module is in the package root.
|
|
269
|
+
|
|
270
|
+
Uses the discovered package root (via project markers) to resolve
|
|
271
|
+
imports from the package root. Also falls back to directory name
|
|
272
|
+
matching for backward compatibility.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
parts: Module path split into components.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
True if module is found in the package root.
|
|
279
|
+
"""
|
|
280
|
+
# Strategy 1: Check if package root contains the module
|
|
281
|
+
if self._check_module_in_package_root(parts):
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
# Strategy 2: Fallback to directory name matching (backward compatibility)
|
|
285
|
+
return self._check_module_by_directory_name(parts)
|
|
286
|
+
|
|
287
|
+
def _check_module_in_package_root(self, parts: list[str]) -> bool:
|
|
288
|
+
"""Check if module exists in the discovered package root.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
parts: Module path split into components.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
True if module exists in package root.
|
|
295
|
+
"""
|
|
296
|
+
# Resolve module from package root
|
|
297
|
+
module_path = self._resolve_from_package_root_path(self._package_root, parts)
|
|
298
|
+
return module_path.exists()
|
|
299
|
+
|
|
300
|
+
def _check_module_by_directory_name(self, parts: list[str]) -> bool:
|
|
301
|
+
"""Check module by matching directory names (legacy fallback).
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
parts: Module path split into components.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
True if module found by directory name matching.
|
|
308
|
+
"""
|
|
309
|
+
current = self.tool_dir
|
|
310
|
+
|
|
311
|
+
while current != current.parent:
|
|
312
|
+
if parts[0] == current.name:
|
|
313
|
+
return self._resolve_from_package_root(current, parts)
|
|
314
|
+
current = current.parent
|
|
123
315
|
|
|
124
316
|
return False
|
|
125
317
|
|
|
318
|
+
def _resolve_from_package_root(self, package_root: Path, parts: list[str]) -> bool:
|
|
319
|
+
"""Resolve module from a found package root directory.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
package_root: The identified package root directory.
|
|
323
|
+
parts: Module path split into components.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
True if module exists in the package root.
|
|
327
|
+
"""
|
|
328
|
+
remaining_parts = parts[1:]
|
|
329
|
+
|
|
330
|
+
if not remaining_parts:
|
|
331
|
+
return (package_root / INIT_FILE).exists()
|
|
332
|
+
|
|
333
|
+
return self._module_parts_exist(package_root, remaining_parts)
|
|
334
|
+
|
|
335
|
+
def _module_parts_exist(self, base_dir: Path, parts: list[str]) -> bool:
|
|
336
|
+
"""Check if module parts exist as a file or package.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
base_dir: Base directory to search from.
|
|
340
|
+
parts: Remaining module path components.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
True if module file or package exists.
|
|
344
|
+
"""
|
|
345
|
+
if len(parts) == 1:
|
|
346
|
+
module_file = base_dir / f"{parts[0]}.py"
|
|
347
|
+
if module_file.exists():
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
if len(parts) > 1:
|
|
351
|
+
module_file = base_dir / "/".join(parts[:-1]) / f"{parts[-1]}.py"
|
|
352
|
+
if module_file.exists():
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
init_file = base_dir / "/".join(parts) / INIT_FILE
|
|
356
|
+
return init_file.exists()
|
|
357
|
+
|
|
126
358
|
def resolve_module_path(self, module_name: str) -> Path:
|
|
127
359
|
"""Resolve module name to file path.
|
|
128
360
|
|
|
@@ -136,6 +368,40 @@ class ImportResolver:
|
|
|
136
368
|
return self._resolve_dotted_module_path(module_name)
|
|
137
369
|
return self.tool_dir / f"{module_name}.py"
|
|
138
370
|
|
|
371
|
+
def resolve_relative_import_path(self, node: ast.ImportFrom) -> Path:
|
|
372
|
+
"""Resolve relative import to file path.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
node: ImportFrom node with node.level > 0.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Path to the module file.
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
ValueError: If relative import level exceeds directory depth.
|
|
382
|
+
"""
|
|
383
|
+
base_dir = self._calculate_relative_base_dir(node.level)
|
|
384
|
+
if base_dir is None:
|
|
385
|
+
raise ValueError(f"Invalid relative import: level {node.level} exceeds directory depth")
|
|
386
|
+
|
|
387
|
+
if node.module is None:
|
|
388
|
+
return base_dir / INIT_FILE
|
|
389
|
+
|
|
390
|
+
return self._resolve_dotted_module_path_from_base(base_dir, node.module)
|
|
391
|
+
|
|
392
|
+
def _resolve_dotted_module_path_from_base(self, base_dir: Path, module_name: str) -> Path:
|
|
393
|
+
"""Resolve dotted module path from a specific base directory.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
base_dir: Base directory to resolve from.
|
|
397
|
+
module_name: Dotted module path (e.g., 'package.module').
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Path to the module file.
|
|
401
|
+
"""
|
|
402
|
+
parts = module_name.split(".")
|
|
403
|
+
return self._build_module_path(base_dir, parts)
|
|
404
|
+
|
|
139
405
|
def _resolve_dotted_module_path(self, module_name: str) -> Path:
|
|
140
406
|
"""Resolve a dotted module path to a file path.
|
|
141
407
|
|
|
@@ -147,25 +413,107 @@ class ImportResolver:
|
|
|
147
413
|
"""
|
|
148
414
|
parts = module_name.split(".")
|
|
149
415
|
|
|
150
|
-
|
|
151
|
-
if
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if module_path.exists():
|
|
156
|
-
return module_path
|
|
157
|
-
elif len(remaining_parts) > 1:
|
|
158
|
-
module_path = self.tool_dir / "/".join(remaining_parts[:-1]) / f"{remaining_parts[-1]}.py"
|
|
159
|
-
if module_path.exists():
|
|
160
|
-
return module_path
|
|
161
|
-
|
|
162
|
-
# Case 2: Standard package/module.py
|
|
163
|
-
module_path = self.tool_dir / "/".join(parts[:-1]) / f"{parts[-1]}.py"
|
|
416
|
+
module_path = self._resolve_from_tool_dir(parts)
|
|
417
|
+
if module_path and module_path.exists():
|
|
418
|
+
return module_path
|
|
419
|
+
|
|
420
|
+
module_path = self._resolve_from_subdir(parts)
|
|
164
421
|
if module_path.exists():
|
|
165
422
|
return module_path
|
|
166
423
|
|
|
167
|
-
|
|
168
|
-
|
|
424
|
+
module_path = self._resolve_from_ancestor_packages(parts)
|
|
425
|
+
if module_path and module_path.exists():
|
|
426
|
+
return module_path
|
|
427
|
+
|
|
428
|
+
return self.tool_dir / "/".join(parts) / INIT_FILE
|
|
429
|
+
|
|
430
|
+
def _resolve_from_tool_dir(self, parts: list[str]) -> Path | None:
|
|
431
|
+
"""Resolve module from current tool directory.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
parts: Module path split into components.
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Path to potential module file, or None if not applicable.
|
|
438
|
+
"""
|
|
439
|
+
if parts[0] != self.tool_dir.name:
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
remaining_parts = parts[1:]
|
|
443
|
+
if not remaining_parts:
|
|
444
|
+
return self.tool_dir / INIT_FILE
|
|
445
|
+
|
|
446
|
+
return self._build_module_path(self.tool_dir, remaining_parts)
|
|
447
|
+
|
|
448
|
+
def _resolve_from_subdir(self, parts: list[str]) -> Path:
|
|
449
|
+
"""Resolve module from a subdirectory.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
parts: Module path split into components.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Path to potential module file.
|
|
456
|
+
"""
|
|
457
|
+
return self._build_module_path(self.tool_dir, parts)
|
|
458
|
+
|
|
459
|
+
def _resolve_from_ancestor_packages(self, parts: list[str]) -> Path | None:
|
|
460
|
+
"""Resolve module from an ancestor directory (Package Root Traversal).
|
|
461
|
+
|
|
462
|
+
Traverses up the directory tree looking for a matching package root.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
parts: Module path split into components.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Path to module if found in ancestor, None otherwise.
|
|
469
|
+
"""
|
|
470
|
+
current = self.tool_dir
|
|
471
|
+
|
|
472
|
+
while current != current.parent:
|
|
473
|
+
if parts[0] == current.name:
|
|
474
|
+
return self._resolve_from_package_root_path(current, parts)
|
|
475
|
+
current = current.parent
|
|
476
|
+
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
def _resolve_from_package_root_path(self, package_root: Path, parts: list[str]) -> Path:
|
|
480
|
+
"""Build module path from a found package root.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
package_root: The identified package root directory.
|
|
484
|
+
parts: Module path split into components.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
Path to potential module file.
|
|
488
|
+
"""
|
|
489
|
+
remaining_parts = parts[1:]
|
|
490
|
+
|
|
491
|
+
if not remaining_parts:
|
|
492
|
+
return package_root / INIT_FILE
|
|
493
|
+
|
|
494
|
+
return self._build_module_path(package_root, remaining_parts)
|
|
495
|
+
|
|
496
|
+
def _build_module_path(self, base_dir: Path, parts: list[str]) -> Path:
|
|
497
|
+
"""Build potential module file path from parts.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
base_dir: Base directory to search from.
|
|
501
|
+
parts: Module path components.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Path to potential module file.
|
|
505
|
+
"""
|
|
506
|
+
if len(parts) == 1:
|
|
507
|
+
module_path = base_dir / f"{parts[0]}.py"
|
|
508
|
+
if module_path.exists():
|
|
509
|
+
return module_path
|
|
510
|
+
|
|
511
|
+
if len(parts) > 1:
|
|
512
|
+
module_path = base_dir / "/".join(parts[:-1]) / f"{parts[-1]}.py"
|
|
513
|
+
if module_path.exists():
|
|
514
|
+
return module_path
|
|
515
|
+
|
|
516
|
+
return base_dir / "/".join(parts) / INIT_FILE
|
|
169
517
|
|
|
170
518
|
def format_external_imports(self, external_imports: list) -> list[str]:
|
|
171
519
|
"""Format external imports as code strings.
|
|
@@ -215,11 +563,49 @@ class ImportResolver:
|
|
|
215
563
|
True if import should be skipped.
|
|
216
564
|
"""
|
|
217
565
|
if isinstance(node, ast.ImportFrom):
|
|
218
|
-
return
|
|
566
|
+
return self._should_skip_import_from(node)
|
|
219
567
|
if isinstance(node, ast.Import):
|
|
220
|
-
return
|
|
568
|
+
return self._should_skip_regular_import(node)
|
|
221
569
|
return False
|
|
222
570
|
|
|
571
|
+
def _should_skip_import_from(self, node: ast.ImportFrom) -> bool:
|
|
572
|
+
"""Check if ImportFrom node should be skipped.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
node: ImportFrom node to check.
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
True if import should be skipped.
|
|
579
|
+
"""
|
|
580
|
+
if not node.module:
|
|
581
|
+
return False
|
|
582
|
+
return self._is_module_excluded(node.module)
|
|
583
|
+
|
|
584
|
+
def _should_skip_regular_import(self, node: ast.Import) -> bool:
|
|
585
|
+
"""Check if Import node should be skipped.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
node: Import node to check.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
True if any alias should be skipped.
|
|
592
|
+
"""
|
|
593
|
+
return any(self._is_module_excluded(alias.name) for alias in node.names)
|
|
594
|
+
|
|
595
|
+
def _is_module_excluded(self, module_name: str) -> bool:
|
|
596
|
+
"""Check if a module name should be excluded.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
module_name: Module name to check.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
True if module is excluded.
|
|
603
|
+
"""
|
|
604
|
+
# Exact match for glaip_sdk or match excluded submodules with boundary
|
|
605
|
+
if module_name == "glaip_sdk":
|
|
606
|
+
return True
|
|
607
|
+
return any(module_name == m or module_name.startswith(m + ".") for m in self.EXCLUDED_MODULES)
|
|
608
|
+
|
|
223
609
|
@staticmethod
|
|
224
610
|
def _build_import_strings(future_imports: list, regular_imports: list) -> list[str]:
|
|
225
611
|
"""Build formatted import strings from import nodes.
|
|
@@ -381,7 +767,7 @@ class ImportResolver:
|
|
|
381
767
|
The node if external, None if local.
|
|
382
768
|
"""
|
|
383
769
|
if isinstance(node, ast.ImportFrom):
|
|
384
|
-
if node.
|
|
770
|
+
if node.level > 0:
|
|
385
771
|
return None
|
|
386
772
|
temp_resolver = ImportResolver(tool_dir)
|
|
387
773
|
if temp_resolver.is_local_import(node):
|
|
@@ -444,15 +830,7 @@ class ImportResolver:
|
|
|
444
830
|
Returns:
|
|
445
831
|
True if decorator is @tool_plugin.
|
|
446
832
|
"""
|
|
447
|
-
|
|
448
|
-
return True
|
|
449
|
-
if (
|
|
450
|
-
isinstance(decorator, ast.Call)
|
|
451
|
-
and isinstance(decorator.func, ast.Name)
|
|
452
|
-
and decorator.func.id == "tool_plugin"
|
|
453
|
-
):
|
|
454
|
-
return True
|
|
455
|
-
return False
|
|
833
|
+
return is_tool_plugin_decorator(decorator)
|
|
456
834
|
|
|
457
835
|
@staticmethod
|
|
458
836
|
def _filter_bases(bases: list) -> list:
|
|
@@ -10,6 +10,7 @@ Authors:
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from typing import TYPE_CHECKING
|
|
13
|
+
from collections.abc import Mapping
|
|
13
14
|
|
|
14
15
|
from glaip_sdk.utils.resource_refs import is_uuid
|
|
15
16
|
from gllm_core.utils import LoggerManager
|
|
@@ -379,14 +380,14 @@ def get_name_from_key(key: object) -> str | None:
|
|
|
379
380
|
raise ValueError(f"Unable to resolve config key: {key!r}")
|
|
380
381
|
|
|
381
382
|
|
|
382
|
-
def normalize_local_config_keys(config:
|
|
383
|
+
def normalize_local_config_keys(config: Mapping[object, object]) -> dict[str, object]:
|
|
383
384
|
"""Normalize all keys in a config dict to names for local mode.
|
|
384
385
|
|
|
385
386
|
Converts instance/class/string keys to string names without using
|
|
386
387
|
registry. UUID keys are skipped with a warning.
|
|
387
388
|
|
|
388
389
|
Args:
|
|
389
|
-
config: Dict with instance/class/string keys and any values.
|
|
390
|
+
config: Dict/Mapping with instance/class/string keys and any values.
|
|
390
391
|
|
|
391
392
|
Returns:
|
|
392
393
|
Dict with string name keys only. UUID keys are omitted.
|
glaip_sdk/utils/sync.py
CHANGED
|
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
from typing import TYPE_CHECKING, Any
|
|
17
17
|
|
|
18
|
+
from glaip_sdk.exceptions import ValidationError
|
|
18
19
|
from glaip_sdk.utils.bundler import ToolBundler
|
|
19
20
|
from glaip_sdk.utils.import_resolver import load_class
|
|
20
21
|
from gllm_core.utils import LoggerManager
|
|
@@ -94,19 +95,38 @@ def update_or_create_tool(tool_ref: Any) -> Tool:
|
|
|
94
95
|
tool_name = _extract_tool_name(tool_class)
|
|
95
96
|
tool_description = _extract_tool_description(tool_class)
|
|
96
97
|
|
|
97
|
-
# Bundle source code
|
|
98
|
+
# Bundle source code - try without decorator first (for newer servers 0.1.85+)
|
|
99
|
+
# If validation fails, retry with decorator for older servers (< 0.1.85)
|
|
98
100
|
bundler = ToolBundler(tool_class)
|
|
99
|
-
bundled_source = bundler.bundle()
|
|
100
101
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
try:
|
|
103
|
+
# Try without decorator first (for newer servers where it's optional)
|
|
104
|
+
bundled_source = bundler.bundle(add_tool_plugin_decorator=False)
|
|
105
|
+
logger.info("Tool info: name='%s', description='%s...'", tool_name, tool_description[:50])
|
|
106
|
+
logger.info("Bundled source code (without decorator): %d characters", len(bundled_source))
|
|
107
|
+
|
|
108
|
+
# Attempt upload without decorator
|
|
109
|
+
return client.tools.upsert_tool(
|
|
110
|
+
tool_name,
|
|
111
|
+
code=bundled_source,
|
|
112
|
+
description=tool_description,
|
|
113
|
+
)
|
|
114
|
+
except ValidationError as e:
|
|
115
|
+
# Check if error is about missing @tool_plugin decorator
|
|
116
|
+
error_message = str(e).lower()
|
|
117
|
+
if "@tool_plugin decorator" in error_message or "no classes found" in error_message:
|
|
118
|
+
# Retry with decorator for older servers (< 0.1.85)
|
|
119
|
+
logger.info("Server requires @tool_plugin decorator, retrying with decorator added")
|
|
120
|
+
bundled_source = bundler.bundle(add_tool_plugin_decorator=True)
|
|
121
|
+
logger.info("Bundled source code (with decorator): %d characters", len(bundled_source))
|
|
122
|
+
|
|
123
|
+
return client.tools.upsert_tool(
|
|
124
|
+
tool_name,
|
|
125
|
+
code=bundled_source,
|
|
126
|
+
description=tool_description,
|
|
127
|
+
)
|
|
128
|
+
# Re-raise if it's a different validation error
|
|
129
|
+
raise
|
|
110
130
|
|
|
111
131
|
|
|
112
132
|
def update_or_create_agent(agent_config: dict[str, Any]) -> Agent:
|