glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.7__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 +156 -32
- 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/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 +1 -1
- glaip_sdk/cli/slash/session.py +58 -13
- glaip_sdk/cli/slash/tui/__init__.py +26 -1
- glaip_sdk/cli/slash/tui/accounts.tcss +7 -5
- glaip_sdk/cli/slash/tui/accounts_app.py +70 -9
- glaip_sdk/cli/slash/tui/clipboard.py +147 -0
- glaip_sdk/cli/slash/tui/context.py +59 -0
- glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
- glaip_sdk/cli/slash/tui/terminal.py +402 -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 +86 -0
- glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
- glaip_sdk/cli/slash/tui/toast.py +123 -0
- glaip_sdk/cli/transcript/history.py +1 -1
- glaip_sdk/cli/transcript/viewer.py +5 -3
- 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 +367 -3
- glaip_sdk/client/schedules.py +439 -0
- glaip_sdk/client/tools.py +57 -26
- 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/registry/tool.py +273 -59
- glaip_sdk/runner/__init__.py +20 -3
- glaip_sdk/runner/deps.py +5 -8
- glaip_sdk/runner/langgraph.py +317 -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 +44 -11
- 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.7.dist-info}/METADATA +47 -37
- glaip_sdk-0.7.7.dist-info/RECORD +213 -0
- {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
- glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
- glaip_sdk-0.7.7.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
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:
|
|
@@ -256,6 +266,9 @@ class Tool:
|
|
|
256
266
|
Returns:
|
|
257
267
|
A Tool reference that will be uploaded during Agent.deploy().
|
|
258
268
|
|
|
269
|
+
Raises:
|
|
270
|
+
ValueError: If the tool class has no valid string 'name' attribute or field.
|
|
271
|
+
|
|
259
272
|
Example:
|
|
260
273
|
>>> from langchain_core.tools import BaseTool
|
|
261
274
|
>>>
|
|
@@ -267,7 +280,34 @@ class Tool:
|
|
|
267
280
|
>>>
|
|
268
281
|
>>> greeting_tool = Tool.from_langchain(GreetingTool)
|
|
269
282
|
"""
|
|
270
|
-
|
|
283
|
+
# Extract name from tool_class to populate the name attribute
|
|
284
|
+
tool_name = cls._extract_tool_name(tool_class)
|
|
285
|
+
return cls(name=tool_name, tool_class=tool_class, type=ToolType.CUSTOM)
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def _extract_tool_name(tool_class: type) -> str:
|
|
289
|
+
"""Extract tool name from a LangChain tool class.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
tool_class: A LangChain BaseTool subclass.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
The extracted tool name.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
ValueError: If name cannot be extracted or is not a valid string.
|
|
299
|
+
"""
|
|
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
|
|
305
|
+
|
|
306
|
+
# If we can't extract the name, raise an error
|
|
307
|
+
raise ValueError(
|
|
308
|
+
f"Cannot extract name from tool class {tool_class.__name__}. "
|
|
309
|
+
f"Ensure the tool class has a 'name' attribute or field with a valid string value."
|
|
310
|
+
)
|
|
271
311
|
|
|
272
312
|
def get_import_path(self) -> str | None:
|
|
273
313
|
"""Get the import path for custom tools.
|
|
@@ -292,15 +332,8 @@ class Tool:
|
|
|
292
332
|
return self.name
|
|
293
333
|
|
|
294
334
|
if self.tool_class is not None:
|
|
295
|
-
#
|
|
296
|
-
|
|
297
|
-
name_field = self.tool_class.model_fields.get("name")
|
|
298
|
-
if name_field and name_field.default:
|
|
299
|
-
return name_field.default
|
|
300
|
-
|
|
301
|
-
# Direct name attribute
|
|
302
|
-
if hasattr(self.tool_class, "name"):
|
|
303
|
-
return self.tool_class.name
|
|
335
|
+
# Reuse extraction logic for consistency
|
|
336
|
+
return self._extract_tool_name(self.tool_class)
|
|
304
337
|
|
|
305
338
|
raise ValueError(f"Cannot determine name for tool: {self}")
|
|
306
339
|
|
glaip_sdk/utils/__init__.py
CHANGED
glaip_sdk/utils/bundler.py
CHANGED
|
@@ -14,6 +14,7 @@ import inspect
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
16
|
from glaip_sdk.utils.import_resolver import ImportResolver
|
|
17
|
+
from glaip_sdk.utils.tool_detection import is_tool_plugin_decorator
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class ToolBundler:
|
|
@@ -50,9 +51,14 @@ class ToolBundler:
|
|
|
50
51
|
self.tool_dir = self.tool_file.parent
|
|
51
52
|
self._import_resolver = ImportResolver(self.tool_dir)
|
|
52
53
|
|
|
53
|
-
def bundle(self) -> str:
|
|
54
|
+
def bundle(self, add_tool_plugin_decorator: bool = True) -> str:
|
|
54
55
|
"""Bundle tool source code with inlined local imports.
|
|
55
56
|
|
|
57
|
+
Args:
|
|
58
|
+
add_tool_plugin_decorator: If True, add @tool_plugin decorator to BaseTool classes.
|
|
59
|
+
Set to False for newer servers (0.1.85+) where decorator is optional.
|
|
60
|
+
Defaults to True for backward compatibility with older servers.
|
|
61
|
+
|
|
56
62
|
Returns:
|
|
57
63
|
Bundled source code with all local dependencies inlined.
|
|
58
64
|
"""
|
|
@@ -62,6 +68,16 @@ class ToolBundler:
|
|
|
62
68
|
tree = ast.parse(full_source)
|
|
63
69
|
local_imports, external_imports = self._import_resolver.categorize_imports(tree)
|
|
64
70
|
|
|
71
|
+
# NOTE: The @tool_plugin decorator is REQUIRED by older servers (< 0.1.85) for remote execution.
|
|
72
|
+
# Newer servers (0.1.85+) make the decorator optional.
|
|
73
|
+
# The server validates uploaded tool code and will reject tools without the decorator
|
|
74
|
+
# with error: "No classes found with @tool_plugin decorator".
|
|
75
|
+
# See: docs/resources/reference/schemas/tools.md - "Plugin Requirements"
|
|
76
|
+
# TESTED: Commenting out this decorator addition causes HTTP 400 ValidationError from older servers.
|
|
77
|
+
# We try without decorator first (for new servers), then retry with decorator if validation fails.
|
|
78
|
+
if add_tool_plugin_decorator:
|
|
79
|
+
self._add_tool_plugin_decorator(tree)
|
|
80
|
+
|
|
65
81
|
# Extract main code nodes (excluding imports, docstrings, glaip_sdk.Tool subclasses)
|
|
66
82
|
main_code_nodes = self._extract_main_code_nodes(tree)
|
|
67
83
|
|
|
@@ -71,6 +87,13 @@ class ToolBundler:
|
|
|
71
87
|
# Merge all external imports
|
|
72
88
|
all_external_imports = external_imports + inlined_external_imports
|
|
73
89
|
|
|
90
|
+
# NOTE: The gllm_plugin.tools import is REQUIRED when decorator is added.
|
|
91
|
+
# Without this import, the decorator will cause a NameError when the server executes the code.
|
|
92
|
+
# TESTED: Commenting out this import causes NameError when server tries to use the decorator.
|
|
93
|
+
# This import is added automatically during bundling so source files can remain clean.
|
|
94
|
+
if add_tool_plugin_decorator:
|
|
95
|
+
self._ensure_tool_plugin_import(all_external_imports)
|
|
96
|
+
|
|
74
97
|
# Build bundled code
|
|
75
98
|
bundled_code = ["# Bundled tool with inlined local imports\n"]
|
|
76
99
|
bundled_code.extend(self._import_resolver.format_external_imports(all_external_imports))
|
|
@@ -109,6 +132,103 @@ class ToolBundler:
|
|
|
109
132
|
main_code_nodes.append(ast.unparse(node))
|
|
110
133
|
return main_code_nodes
|
|
111
134
|
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _add_tool_plugin_decorator(tree: ast.AST) -> None:
|
|
137
|
+
"""Add @tool_plugin decorator to BaseTool classes that don't have it.
|
|
138
|
+
|
|
139
|
+
This allows tools to be clean (without decorator) for local use,
|
|
140
|
+
while the decorator is automatically added during bundling for remote execution.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
tree: AST tree to modify in-place.
|
|
144
|
+
"""
|
|
145
|
+
for node in ast.walk(tree):
|
|
146
|
+
if not isinstance(node, ast.ClassDef):
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if not ToolBundler._inherits_from_base_tool(node):
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
if ToolBundler._has_tool_plugin_decorator(node):
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
decorator_call = ToolBundler._create_tool_plugin_decorator()
|
|
156
|
+
node.decorator_list.insert(0, decorator_call)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _inherits_from_base_tool(class_node: ast.ClassDef) -> bool:
|
|
160
|
+
"""Check if a class inherits from BaseTool.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
class_node: AST ClassDef node to check.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True if class inherits from BaseTool.
|
|
167
|
+
"""
|
|
168
|
+
for base in class_node.bases:
|
|
169
|
+
if isinstance(base, ast.Name) and base.id == "BaseTool":
|
|
170
|
+
return True
|
|
171
|
+
if isinstance(base, ast.Attribute) and base.attr == "BaseTool":
|
|
172
|
+
# Handle nested attributes like langchain_core.tools.BaseTool
|
|
173
|
+
# Check if the value chain leads to langchain_core
|
|
174
|
+
value = base.value
|
|
175
|
+
while isinstance(value, ast.Attribute):
|
|
176
|
+
value = value.value
|
|
177
|
+
if isinstance(value, ast.Name) and value.id == "langchain_core":
|
|
178
|
+
return True
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _has_tool_plugin_decorator(class_node: ast.ClassDef) -> bool:
|
|
183
|
+
"""Check if a class already has the @tool_plugin decorator.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
class_node: AST ClassDef node to check.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if decorator already exists.
|
|
190
|
+
"""
|
|
191
|
+
for decorator in class_node.decorator_list:
|
|
192
|
+
if is_tool_plugin_decorator(decorator):
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def _create_tool_plugin_decorator() -> ast.Call:
|
|
198
|
+
"""Create a @tool_plugin decorator AST node.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
AST Call node representing @tool_plugin(version="1.0.0").
|
|
202
|
+
"""
|
|
203
|
+
return ast.Call(
|
|
204
|
+
func=ast.Name(id="tool_plugin", ctx=ast.Load()),
|
|
205
|
+
args=[],
|
|
206
|
+
keywords=[ast.keyword(arg="version", value=ast.Constant(value="1.0.0"))],
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _ensure_tool_plugin_import(external_imports: list) -> None:
|
|
211
|
+
"""Ensure gllm_plugin.tools import is present in external imports.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
external_imports: List of external import nodes (modified in-place).
|
|
215
|
+
"""
|
|
216
|
+
# Check if import already exists
|
|
217
|
+
for import_node in external_imports:
|
|
218
|
+
if isinstance(import_node, ast.ImportFrom) and import_node.module == "gllm_plugin.tools":
|
|
219
|
+
# Check if tool_plugin is in the names
|
|
220
|
+
for alias in import_node.names:
|
|
221
|
+
if alias.name == "tool_plugin":
|
|
222
|
+
return # Import already present
|
|
223
|
+
|
|
224
|
+
# Add the import
|
|
225
|
+
import_node = ast.ImportFrom(
|
|
226
|
+
module="gllm_plugin.tools",
|
|
227
|
+
names=[ast.alias(name="tool_plugin")],
|
|
228
|
+
level=0,
|
|
229
|
+
)
|
|
230
|
+
external_imports.append(import_node)
|
|
231
|
+
|
|
112
232
|
@staticmethod
|
|
113
233
|
def _is_sdk_tool_subclass(node: ast.ClassDef) -> bool:
|
|
114
234
|
"""Check if AST class definition inherits from Tool.
|
|
@@ -135,7 +255,7 @@ class ToolBundler:
|
|
|
135
255
|
return False
|
|
136
256
|
|
|
137
257
|
@classmethod
|
|
138
|
-
def bundle_from_source(cls, file_path: Path) -> tuple[str, str, str]:
|
|
258
|
+
def bundle_from_source(cls, file_path: Path, add_tool_plugin_decorator: bool = True) -> tuple[str, str, str]:
|
|
139
259
|
"""Extract tool info directly from source file without importing.
|
|
140
260
|
|
|
141
261
|
This is used as a fallback when the tool class cannot be imported
|
|
@@ -143,6 +263,9 @@ class ToolBundler:
|
|
|
143
263
|
|
|
144
264
|
Args:
|
|
145
265
|
file_path: Path to the tool source file.
|
|
266
|
+
add_tool_plugin_decorator: If True, add @tool_plugin decorator to BaseTool classes.
|
|
267
|
+
Set to False for newer servers (0.1.85+) where decorator is optional.
|
|
268
|
+
Defaults to True for backward compatibility with older servers.
|
|
146
269
|
|
|
147
270
|
Returns:
|
|
148
271
|
Tuple of (name, description, bundled_source_code).
|
|
@@ -160,6 +283,12 @@ class ToolBundler:
|
|
|
160
283
|
tool_dir = file_path.parent
|
|
161
284
|
import_resolver = ImportResolver(tool_dir)
|
|
162
285
|
|
|
286
|
+
# NOTE: The @tool_plugin decorator is REQUIRED by older servers (< 0.1.85) for remote execution.
|
|
287
|
+
# Newer servers (0.1.85+) make the decorator optional.
|
|
288
|
+
# See bundle() method for detailed explanation.
|
|
289
|
+
if add_tool_plugin_decorator:
|
|
290
|
+
cls._add_tool_plugin_decorator(tree)
|
|
291
|
+
|
|
163
292
|
# Find tool name and description from class definitions
|
|
164
293
|
tool_name, tool_description = cls._extract_tool_metadata(tree, file_path.stem)
|
|
165
294
|
|
|
@@ -180,6 +309,13 @@ class ToolBundler:
|
|
|
180
309
|
|
|
181
310
|
# Build bundled code
|
|
182
311
|
all_external_imports = external_imports + inlined_external_imports
|
|
312
|
+
|
|
313
|
+
# NOTE: The gllm_plugin.tools import is REQUIRED when decorator is added.
|
|
314
|
+
# See bundle() method for detailed explanation.
|
|
315
|
+
# TESTED: Commenting out this import causes NameError when server tries to use the decorator.
|
|
316
|
+
if add_tool_plugin_decorator:
|
|
317
|
+
cls._ensure_tool_plugin_import(all_external_imports)
|
|
318
|
+
|
|
183
319
|
bundled_code = ["# Bundled tool with inlined local imports\n"]
|
|
184
320
|
bundled_code.extend(import_resolver.format_external_imports(all_external_imports))
|
|
185
321
|
|
|
@@ -13,6 +13,8 @@ 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
|
+
|
|
16
18
|
|
|
17
19
|
class ImportResolver:
|
|
18
20
|
"""Resolves and categorizes Python imports for tool bundling.
|
|
@@ -215,11 +217,49 @@ class ImportResolver:
|
|
|
215
217
|
True if import should be skipped.
|
|
216
218
|
"""
|
|
217
219
|
if isinstance(node, ast.ImportFrom):
|
|
218
|
-
return
|
|
220
|
+
return self._should_skip_import_from(node)
|
|
219
221
|
if isinstance(node, ast.Import):
|
|
220
|
-
return
|
|
222
|
+
return self._should_skip_regular_import(node)
|
|
221
223
|
return False
|
|
222
224
|
|
|
225
|
+
def _should_skip_import_from(self, node: ast.ImportFrom) -> bool:
|
|
226
|
+
"""Check if ImportFrom node should be skipped.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
node: ImportFrom node to check.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if import should be skipped.
|
|
233
|
+
"""
|
|
234
|
+
if not node.module:
|
|
235
|
+
return False
|
|
236
|
+
return self._is_module_excluded(node.module)
|
|
237
|
+
|
|
238
|
+
def _should_skip_regular_import(self, node: ast.Import) -> bool:
|
|
239
|
+
"""Check if Import node should be skipped.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
node: Import node to check.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
True if any alias should be skipped.
|
|
246
|
+
"""
|
|
247
|
+
return any(self._is_module_excluded(alias.name) for alias in node.names)
|
|
248
|
+
|
|
249
|
+
def _is_module_excluded(self, module_name: str) -> bool:
|
|
250
|
+
"""Check if a module name should be excluded.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
module_name: Module name to check.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
True if module is excluded.
|
|
257
|
+
"""
|
|
258
|
+
# Exact match for glaip_sdk or match excluded submodules with boundary
|
|
259
|
+
if module_name == "glaip_sdk":
|
|
260
|
+
return True
|
|
261
|
+
return any(module_name == m or module_name.startswith(m + ".") for m in self.EXCLUDED_MODULES)
|
|
262
|
+
|
|
223
263
|
@staticmethod
|
|
224
264
|
def _build_import_strings(future_imports: list, regular_imports: list) -> list[str]:
|
|
225
265
|
"""Build formatted import strings from import nodes.
|
|
@@ -444,15 +484,7 @@ class ImportResolver:
|
|
|
444
484
|
Returns:
|
|
445
485
|
True if decorator is @tool_plugin.
|
|
446
486
|
"""
|
|
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
|
|
487
|
+
return is_tool_plugin_decorator(decorator)
|
|
456
488
|
|
|
457
489
|
@staticmethod
|
|
458
490
|
def _filter_bases(bases: list) -> list:
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
+
import sys
|
|
11
12
|
from datetime import datetime, timezone
|
|
12
13
|
from time import monotonic
|
|
13
14
|
from typing import Any
|
|
@@ -349,6 +350,9 @@ class RichStreamRenderer(TranscriptModeMixin):
|
|
|
349
350
|
self._handle_status_event(ev)
|
|
350
351
|
elif kind == "content":
|
|
351
352
|
self._handle_content_event(content)
|
|
353
|
+
elif kind == "token":
|
|
354
|
+
# Token events should stream content incrementally with immediate console output
|
|
355
|
+
self._handle_token_event(content)
|
|
352
356
|
elif kind == "final_response":
|
|
353
357
|
self._handle_final_response_event(content, metadata)
|
|
354
358
|
elif kind in {"agent_step", "agent_thinking_step"}:
|
|
@@ -368,6 +372,31 @@ class RichStreamRenderer(TranscriptModeMixin):
|
|
|
368
372
|
self.state.append_transcript_text(content)
|
|
369
373
|
self._ensure_live()
|
|
370
374
|
|
|
375
|
+
def _handle_token_event(self, content: str) -> None:
|
|
376
|
+
"""Handle token streaming events - print immediately for real-time streaming."""
|
|
377
|
+
if content:
|
|
378
|
+
self.state.append_transcript_text(content)
|
|
379
|
+
# Print token content directly to stdout for immediate visibility when not verbose
|
|
380
|
+
# This bypasses Rich's Live display which has refresh rate limitations
|
|
381
|
+
if not self.verbose:
|
|
382
|
+
try:
|
|
383
|
+
# Mark that we're streaming tokens directly to prevent Live display from starting
|
|
384
|
+
self._streaming_tokens_directly = True
|
|
385
|
+
# Stop Live display if active to prevent it from intercepting stdout
|
|
386
|
+
# and causing each token to appear on a new line
|
|
387
|
+
if self.live is not None:
|
|
388
|
+
self._stop_live_display()
|
|
389
|
+
# Write directly to stdout - tokens will stream on the same line
|
|
390
|
+
# since we're bypassing Rich's console which adds newlines
|
|
391
|
+
sys.stdout.write(content)
|
|
392
|
+
sys.stdout.flush()
|
|
393
|
+
except Exception:
|
|
394
|
+
# Fallback to live display if direct write fails
|
|
395
|
+
self._ensure_live()
|
|
396
|
+
else:
|
|
397
|
+
# In verbose mode, use normal live display (debug panels handle the output)
|
|
398
|
+
self._ensure_live()
|
|
399
|
+
|
|
371
400
|
def _handle_final_response_event(self, content: str, metadata: dict[str, Any]) -> None:
|
|
372
401
|
"""Handle final response events."""
|
|
373
402
|
if content:
|
|
@@ -521,6 +550,18 @@ class RichStreamRenderer(TranscriptModeMixin):
|
|
|
521
550
|
if getattr(self, "_transcript_mode_enabled", False):
|
|
522
551
|
return
|
|
523
552
|
|
|
553
|
+
# When verbose=False and tokens were streamed directly, skip final panel
|
|
554
|
+
# The user's script will print the final result, avoiding duplication
|
|
555
|
+
if not self.verbose and getattr(self, "_streaming_tokens_directly", False):
|
|
556
|
+
# Add a newline after streaming tokens for clean separation
|
|
557
|
+
try:
|
|
558
|
+
sys.stdout.write("\n")
|
|
559
|
+
sys.stdout.flush()
|
|
560
|
+
except Exception:
|
|
561
|
+
pass
|
|
562
|
+
self.state.printed_final_output = True
|
|
563
|
+
return
|
|
564
|
+
|
|
524
565
|
if self.verbose:
|
|
525
566
|
panel = build_final_panel(
|
|
526
567
|
self.state,
|
|
@@ -597,6 +638,19 @@ class RichStreamRenderer(TranscriptModeMixin):
|
|
|
597
638
|
|
|
598
639
|
def _finalize_display(self) -> None:
|
|
599
640
|
"""Finalize live display and render final output."""
|
|
641
|
+
# When verbose=False and tokens were streamed directly, skip live display updates
|
|
642
|
+
# to avoid showing duplicate final result
|
|
643
|
+
if not self.verbose and getattr(self, "_streaming_tokens_directly", False):
|
|
644
|
+
# Just add a newline after streaming tokens for clean separation
|
|
645
|
+
try:
|
|
646
|
+
sys.stdout.write("\n")
|
|
647
|
+
sys.stdout.flush()
|
|
648
|
+
except Exception:
|
|
649
|
+
pass
|
|
650
|
+
self._stop_live_display()
|
|
651
|
+
self.state.printed_final_output = True
|
|
652
|
+
return
|
|
653
|
+
|
|
600
654
|
# Final refresh
|
|
601
655
|
self._ensure_live()
|
|
602
656
|
|
|
@@ -629,6 +683,10 @@ class RichStreamRenderer(TranscriptModeMixin):
|
|
|
629
683
|
"""Ensure live display is updated."""
|
|
630
684
|
if getattr(self, "_transcript_mode_enabled", False):
|
|
631
685
|
return
|
|
686
|
+
# When verbose=False, don't start Live display if we're streaming tokens directly
|
|
687
|
+
# This prevents Live from intercepting stdout and causing tokens to appear on separate lines
|
|
688
|
+
if not self.verbose and getattr(self, "_streaming_tokens_directly", False):
|
|
689
|
+
return
|
|
632
690
|
if not self._ensure_live_stack():
|
|
633
691
|
return
|
|
634
692
|
|
|
@@ -315,7 +315,7 @@ def normalize_runtime_config_keys(
|
|
|
315
315
|
|
|
316
316
|
|
|
317
317
|
def _get_name_from_class(cls: type) -> str:
|
|
318
|
-
"""Extract name from a class, handling Pydantic models.
|
|
318
|
+
"""Extract name from a class, handling Pydantic models and @property descriptors.
|
|
319
319
|
|
|
320
320
|
Args:
|
|
321
321
|
cls: The class to extract name from.
|
|
@@ -323,9 +323,10 @@ def _get_name_from_class(cls: type) -> str:
|
|
|
323
323
|
Returns:
|
|
324
324
|
The resolved name string.
|
|
325
325
|
"""
|
|
326
|
-
# Try class-level name attribute first
|
|
326
|
+
# Try class-level name attribute first, but guard against @property descriptors
|
|
327
|
+
# When a class has @property name, getattr returns the property object, not a string
|
|
327
328
|
class_name = getattr(cls, "name", None)
|
|
328
|
-
if class_name:
|
|
329
|
+
if isinstance(class_name, str) and class_name:
|
|
329
330
|
return class_name
|
|
330
331
|
|
|
331
332
|
# For Pydantic models, check model_fields for default value
|
|
@@ -355,24 +356,26 @@ def get_name_from_key(key: object) -> str | None:
|
|
|
355
356
|
Raises:
|
|
356
357
|
ValueError: If the key cannot be resolved to a valid name.
|
|
357
358
|
"""
|
|
358
|
-
#
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if name:
|
|
362
|
-
return name
|
|
363
|
-
raise ValueError(f"Unable to resolve config key: {key!r}")
|
|
364
|
-
|
|
365
|
-
# Class type (not instance)
|
|
359
|
+
# Class type (not instance) - must check BEFORE hasattr("name")
|
|
360
|
+
# because classes with @property name will have hasattr return True
|
|
361
|
+
# but getattr returns the property descriptor, not a string
|
|
366
362
|
if isinstance(key, type):
|
|
367
363
|
return _get_name_from_class(key)
|
|
368
364
|
|
|
369
|
-
# String key
|
|
365
|
+
# String key - check early to avoid attribute access
|
|
370
366
|
if isinstance(key, str):
|
|
371
367
|
if is_uuid(key):
|
|
372
368
|
logger.warning("UUID '%s' not supported in local mode, skipping", key)
|
|
373
369
|
return None
|
|
374
370
|
return key
|
|
375
371
|
|
|
372
|
+
# Instance with name attribute
|
|
373
|
+
if hasattr(key, "name"):
|
|
374
|
+
name = getattr(key, "name", None)
|
|
375
|
+
# Guard against @property that returns non-string (e.g., descriptor)
|
|
376
|
+
if isinstance(name, str) and name:
|
|
377
|
+
return name
|
|
378
|
+
|
|
376
379
|
raise ValueError(f"Unable to resolve config key: {key!r}")
|
|
377
380
|
|
|
378
381
|
|
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:
|