agentrun-sdk 0.1.2__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.
Potentially problematic release.
This version of agentrun-sdk might be problematic. Click here for more details.
- agentrun_operation_sdk/cli/__init__.py +1 -0
- agentrun_operation_sdk/cli/cli.py +19 -0
- agentrun_operation_sdk/cli/common.py +21 -0
- agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
- agentrun_operation_sdk/cli/runtime/commands.py +203 -0
- agentrun_operation_sdk/client/client.py +75 -0
- agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
- agentrun_operation_sdk/operations/runtime/configure.py +101 -0
- agentrun_operation_sdk/operations/runtime/launch.py +82 -0
- agentrun_operation_sdk/operations/runtime/models.py +31 -0
- agentrun_operation_sdk/services/runtime.py +152 -0
- agentrun_operation_sdk/utils/logging_config.py +72 -0
- agentrun_operation_sdk/utils/runtime/config.py +94 -0
- agentrun_operation_sdk/utils/runtime/container.py +280 -0
- agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
- agentrun_operation_sdk/utils/runtime/schema.py +56 -0
- agentrun_sdk/__init__.py +7 -0
- agentrun_sdk/agent/__init__.py +25 -0
- agentrun_sdk/agent/agent.py +696 -0
- agentrun_sdk/agent/agent_result.py +46 -0
- agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
- agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
- agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
- agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
- agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
- agentrun_sdk/agent/state.py +97 -0
- agentrun_sdk/event_loop/__init__.py +9 -0
- agentrun_sdk/event_loop/event_loop.py +499 -0
- agentrun_sdk/event_loop/streaming.py +319 -0
- agentrun_sdk/experimental/__init__.py +4 -0
- agentrun_sdk/experimental/hooks/__init__.py +15 -0
- agentrun_sdk/experimental/hooks/events.py +123 -0
- agentrun_sdk/handlers/__init__.py +10 -0
- agentrun_sdk/handlers/callback_handler.py +70 -0
- agentrun_sdk/hooks/__init__.py +49 -0
- agentrun_sdk/hooks/events.py +80 -0
- agentrun_sdk/hooks/registry.py +247 -0
- agentrun_sdk/models/__init__.py +10 -0
- agentrun_sdk/models/anthropic.py +432 -0
- agentrun_sdk/models/bedrock.py +649 -0
- agentrun_sdk/models/litellm.py +225 -0
- agentrun_sdk/models/llamaapi.py +438 -0
- agentrun_sdk/models/mistral.py +539 -0
- agentrun_sdk/models/model.py +95 -0
- agentrun_sdk/models/ollama.py +357 -0
- agentrun_sdk/models/openai.py +436 -0
- agentrun_sdk/models/sagemaker.py +598 -0
- agentrun_sdk/models/writer.py +449 -0
- agentrun_sdk/multiagent/__init__.py +22 -0
- agentrun_sdk/multiagent/a2a/__init__.py +15 -0
- agentrun_sdk/multiagent/a2a/executor.py +148 -0
- agentrun_sdk/multiagent/a2a/server.py +252 -0
- agentrun_sdk/multiagent/base.py +92 -0
- agentrun_sdk/multiagent/graph.py +555 -0
- agentrun_sdk/multiagent/swarm.py +656 -0
- agentrun_sdk/py.typed +1 -0
- agentrun_sdk/session/__init__.py +18 -0
- agentrun_sdk/session/file_session_manager.py +216 -0
- agentrun_sdk/session/repository_session_manager.py +152 -0
- agentrun_sdk/session/s3_session_manager.py +272 -0
- agentrun_sdk/session/session_manager.py +73 -0
- agentrun_sdk/session/session_repository.py +51 -0
- agentrun_sdk/telemetry/__init__.py +21 -0
- agentrun_sdk/telemetry/config.py +194 -0
- agentrun_sdk/telemetry/metrics.py +476 -0
- agentrun_sdk/telemetry/metrics_constants.py +15 -0
- agentrun_sdk/telemetry/tracer.py +563 -0
- agentrun_sdk/tools/__init__.py +17 -0
- agentrun_sdk/tools/decorator.py +569 -0
- agentrun_sdk/tools/executor.py +137 -0
- agentrun_sdk/tools/loader.py +152 -0
- agentrun_sdk/tools/mcp/__init__.py +13 -0
- agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
- agentrun_sdk/tools/mcp/mcp_client.py +423 -0
- agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
- agentrun_sdk/tools/mcp/mcp_types.py +63 -0
- agentrun_sdk/tools/registry.py +607 -0
- agentrun_sdk/tools/structured_output.py +421 -0
- agentrun_sdk/tools/tools.py +217 -0
- agentrun_sdk/tools/watcher.py +136 -0
- agentrun_sdk/types/__init__.py +5 -0
- agentrun_sdk/types/collections.py +23 -0
- agentrun_sdk/types/content.py +188 -0
- agentrun_sdk/types/event_loop.py +48 -0
- agentrun_sdk/types/exceptions.py +81 -0
- agentrun_sdk/types/guardrails.py +254 -0
- agentrun_sdk/types/media.py +89 -0
- agentrun_sdk/types/session.py +152 -0
- agentrun_sdk/types/streaming.py +201 -0
- agentrun_sdk/types/tools.py +258 -0
- agentrun_sdk/types/traces.py +5 -0
- agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
- agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
- agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
- agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
- agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
- agentrun_wrapper/__init__.py +11 -0
- agentrun_wrapper/_utils/__init__.py +6 -0
- agentrun_wrapper/_utils/endpoints.py +16 -0
- agentrun_wrapper/identity/__init__.py +5 -0
- agentrun_wrapper/identity/auth.py +211 -0
- agentrun_wrapper/memory/__init__.py +6 -0
- agentrun_wrapper/memory/client.py +1697 -0
- agentrun_wrapper/memory/constants.py +103 -0
- agentrun_wrapper/memory/controlplane.py +626 -0
- agentrun_wrapper/py.typed +1 -0
- agentrun_wrapper/runtime/__init__.py +13 -0
- agentrun_wrapper/runtime/app.py +473 -0
- agentrun_wrapper/runtime/context.py +34 -0
- agentrun_wrapper/runtime/models.py +25 -0
- agentrun_wrapper/services/__init__.py +1 -0
- agentrun_wrapper/services/identity.py +192 -0
- agentrun_wrapper/tools/__init__.py +6 -0
- agentrun_wrapper/tools/browser_client.py +325 -0
- agentrun_wrapper/tools/code_interpreter_client.py +186 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""Tool registry.
|
|
2
|
+
|
|
3
|
+
This module provides the central registry for all tools available to the agent, including discovery, validation, and
|
|
4
|
+
invocation capabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import inspect
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from importlib import import_module, util
|
|
12
|
+
from os.path import expanduser
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
15
|
+
|
|
16
|
+
from typing_extensions import TypedDict, cast
|
|
17
|
+
|
|
18
|
+
from .decorator import DecoratedFunctionTool
|
|
19
|
+
|
|
20
|
+
from ..types.tools import AgentTool, ToolSpec
|
|
21
|
+
from .tools import PythonAgentTool, normalize_schema, normalize_tool_spec
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ToolRegistry:
|
|
27
|
+
"""Central registry for all tools available to the agent.
|
|
28
|
+
|
|
29
|
+
This class manages tool registration, validation, discovery, and invocation.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
"""Initialize the tool registry."""
|
|
34
|
+
self.registry: Dict[str, AgentTool] = {}
|
|
35
|
+
self.dynamic_tools: Dict[str, AgentTool] = {}
|
|
36
|
+
self.tool_config: Optional[Dict[str, Any]] = None
|
|
37
|
+
|
|
38
|
+
def process_tools(self, tools: List[Any]) -> List[str]:
|
|
39
|
+
"""Process tools list that can contain tool names, paths, imported modules, or functions.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
tools: List of tool specifications.
|
|
43
|
+
Can be:
|
|
44
|
+
|
|
45
|
+
- String tool names (e.g., "calculator")
|
|
46
|
+
- File paths (e.g., "/path/to/tool.py")
|
|
47
|
+
- Imported Python modules (e.g., a module object)
|
|
48
|
+
- Functions decorated with @tool
|
|
49
|
+
- Dictionaries with name/path keys
|
|
50
|
+
- Instance of an AgentTool
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of tool names that were processed.
|
|
54
|
+
"""
|
|
55
|
+
tool_names = []
|
|
56
|
+
|
|
57
|
+
def add_tool(tool: Any) -> None:
|
|
58
|
+
# Case 1: String file path
|
|
59
|
+
if isinstance(tool, str):
|
|
60
|
+
# Extract tool name from path
|
|
61
|
+
tool_name = os.path.basename(tool).split(".")[0]
|
|
62
|
+
self.load_tool_from_filepath(tool_name=tool_name, tool_path=tool)
|
|
63
|
+
tool_names.append(tool_name)
|
|
64
|
+
|
|
65
|
+
# Case 2: Dictionary with name and path
|
|
66
|
+
elif isinstance(tool, dict) and "name" in tool and "path" in tool:
|
|
67
|
+
self.load_tool_from_filepath(tool_name=tool["name"], tool_path=tool["path"])
|
|
68
|
+
tool_names.append(tool["name"])
|
|
69
|
+
|
|
70
|
+
# Case 3: Dictionary with path only
|
|
71
|
+
elif isinstance(tool, dict) and "path" in tool:
|
|
72
|
+
tool_name = os.path.basename(tool["path"]).split(".")[0]
|
|
73
|
+
self.load_tool_from_filepath(tool_name=tool_name, tool_path=tool["path"])
|
|
74
|
+
tool_names.append(tool_name)
|
|
75
|
+
|
|
76
|
+
# Case 4: Imported Python module
|
|
77
|
+
elif hasattr(tool, "__file__") and inspect.ismodule(tool):
|
|
78
|
+
# Get the module file path
|
|
79
|
+
module_path = tool.__file__
|
|
80
|
+
# Extract the tool name from the module name
|
|
81
|
+
tool_name = tool.__name__.split(".")[-1]
|
|
82
|
+
|
|
83
|
+
# Check for TOOL_SPEC in module to validate it's a Strands tool
|
|
84
|
+
if hasattr(tool, "TOOL_SPEC") and hasattr(tool, tool_name) and module_path:
|
|
85
|
+
self.load_tool_from_filepath(tool_name=tool_name, tool_path=module_path)
|
|
86
|
+
tool_names.append(tool_name)
|
|
87
|
+
else:
|
|
88
|
+
function_tools = self._scan_module_for_tools(tool)
|
|
89
|
+
for function_tool in function_tools:
|
|
90
|
+
self.register_tool(function_tool)
|
|
91
|
+
tool_names.append(function_tool.tool_name)
|
|
92
|
+
|
|
93
|
+
if not function_tools:
|
|
94
|
+
logger.warning("tool_name=<%s>, module_path=<%s> | invalid agent tool", tool_name, module_path)
|
|
95
|
+
|
|
96
|
+
# Case 5: AgentTools (which also covers @tool)
|
|
97
|
+
elif isinstance(tool, AgentTool):
|
|
98
|
+
self.register_tool(tool)
|
|
99
|
+
tool_names.append(tool.tool_name)
|
|
100
|
+
# Case 6: Nested iterable (list, tuple, etc.) - add each sub-tool
|
|
101
|
+
elif isinstance(tool, Iterable) and not isinstance(tool, (str, bytes, bytearray)):
|
|
102
|
+
for t in tool:
|
|
103
|
+
add_tool(t)
|
|
104
|
+
else:
|
|
105
|
+
logger.warning("tool=<%s> | unrecognized tool specification", tool)
|
|
106
|
+
|
|
107
|
+
for a_tool in tools:
|
|
108
|
+
add_tool(a_tool)
|
|
109
|
+
|
|
110
|
+
return tool_names
|
|
111
|
+
|
|
112
|
+
def load_tool_from_filepath(self, tool_name: str, tool_path: str) -> None:
|
|
113
|
+
"""Load a tool from a file path.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
tool_name: Name of the tool.
|
|
117
|
+
tool_path: Path to the tool file.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
FileNotFoundError: If the tool file is not found.
|
|
121
|
+
ValueError: If the tool cannot be loaded.
|
|
122
|
+
"""
|
|
123
|
+
from .loader import ToolLoader
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
tool_path = expanduser(tool_path)
|
|
127
|
+
if not os.path.exists(tool_path):
|
|
128
|
+
raise FileNotFoundError(f"Tool file not found: {tool_path}")
|
|
129
|
+
|
|
130
|
+
loaded_tool = ToolLoader.load_tool(tool_path, tool_name)
|
|
131
|
+
loaded_tool.mark_dynamic()
|
|
132
|
+
|
|
133
|
+
# Because we're explicitly registering the tool we don't need an allowlist
|
|
134
|
+
self.register_tool(loaded_tool)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
exception_str = str(e)
|
|
137
|
+
logger.exception("tool_name=<%s> | failed to load tool", tool_name)
|
|
138
|
+
raise ValueError(f"Failed to load tool {tool_name}: {exception_str}") from e
|
|
139
|
+
|
|
140
|
+
def get_all_tools_config(self) -> Dict[str, Any]:
|
|
141
|
+
"""Dynamically generate tool configuration by combining built-in and dynamic tools.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Dictionary containing all tool configurations.
|
|
145
|
+
"""
|
|
146
|
+
tool_config = {}
|
|
147
|
+
logger.debug("getting tool configurations")
|
|
148
|
+
|
|
149
|
+
# Add all registered tools
|
|
150
|
+
for tool_name, tool in self.registry.items():
|
|
151
|
+
# Make a deep copy to avoid modifying the original
|
|
152
|
+
spec = tool.tool_spec.copy()
|
|
153
|
+
try:
|
|
154
|
+
# Normalize the schema before validation
|
|
155
|
+
spec = normalize_tool_spec(spec)
|
|
156
|
+
self.validate_tool_spec(spec)
|
|
157
|
+
tool_config[tool_name] = spec
|
|
158
|
+
logger.debug("tool_name=<%s> | loaded tool config", tool_name)
|
|
159
|
+
except ValueError as e:
|
|
160
|
+
logger.warning("tool_name=<%s> | spec validation failed | %s", tool_name, e)
|
|
161
|
+
|
|
162
|
+
# Add any dynamic tools
|
|
163
|
+
for tool_name, tool in self.dynamic_tools.items():
|
|
164
|
+
if tool_name not in tool_config:
|
|
165
|
+
# Make a deep copy to avoid modifying the original
|
|
166
|
+
spec = tool.tool_spec.copy()
|
|
167
|
+
try:
|
|
168
|
+
# Normalize the schema before validation
|
|
169
|
+
spec = normalize_tool_spec(spec)
|
|
170
|
+
self.validate_tool_spec(spec)
|
|
171
|
+
tool_config[tool_name] = spec
|
|
172
|
+
logger.debug("tool_name=<%s> | loaded dynamic tool config", tool_name)
|
|
173
|
+
except ValueError as e:
|
|
174
|
+
logger.warning("tool_name=<%s> | dynamic tool spec validation failed | %s", tool_name, e)
|
|
175
|
+
|
|
176
|
+
logger.debug("tool_count=<%s> | tools configured", len(tool_config))
|
|
177
|
+
return tool_config
|
|
178
|
+
|
|
179
|
+
# mypy has problems converting between DecoratedFunctionTool <-> AgentTool
|
|
180
|
+
def register_tool(self, tool: AgentTool) -> None:
|
|
181
|
+
"""Register a tool function with the given name.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
tool: The tool to register.
|
|
185
|
+
"""
|
|
186
|
+
logger.debug(
|
|
187
|
+
"tool_name=<%s>, tool_type=<%s>, is_dynamic=<%s> | registering tool",
|
|
188
|
+
tool.tool_name,
|
|
189
|
+
tool.tool_type,
|
|
190
|
+
tool.is_dynamic,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if self.registry.get(tool.tool_name) is None:
|
|
194
|
+
normalized_name = tool.tool_name.replace("-", "_")
|
|
195
|
+
|
|
196
|
+
matching_tools = [
|
|
197
|
+
tool_name
|
|
198
|
+
for (tool_name, tool) in self.registry.items()
|
|
199
|
+
if tool_name.replace("-", "_") == normalized_name
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
if matching_tools:
|
|
203
|
+
raise ValueError(
|
|
204
|
+
f"Tool name '{tool.tool_name}' already exists as '{matching_tools[0]}'."
|
|
205
|
+
" Cannot add a duplicate tool which differs by a '-' or '_'"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Register in main registry
|
|
209
|
+
self.registry[tool.tool_name] = tool
|
|
210
|
+
|
|
211
|
+
# Register in dynamic tools if applicable
|
|
212
|
+
if tool.is_dynamic:
|
|
213
|
+
self.dynamic_tools[tool.tool_name] = tool
|
|
214
|
+
|
|
215
|
+
if not tool.supports_hot_reload:
|
|
216
|
+
logger.debug("tool_name=<%s>, tool_type=<%s> | skipping hot reloading", tool.tool_name, tool.tool_type)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
logger.debug(
|
|
220
|
+
"tool_name=<%s>, tool_registry=<%s>, dynamic_tools=<%s> | tool registered",
|
|
221
|
+
tool.tool_name,
|
|
222
|
+
list(self.registry.keys()),
|
|
223
|
+
list(self.dynamic_tools.keys()),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def get_tools_dirs(self) -> List[Path]:
|
|
227
|
+
"""Get all tool directory paths.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
A list of Path objects for current working directory's "./tools/".
|
|
231
|
+
"""
|
|
232
|
+
# Current working directory's tools directory
|
|
233
|
+
cwd_tools_dir = Path.cwd() / "tools"
|
|
234
|
+
|
|
235
|
+
# Return all directories that exist
|
|
236
|
+
tool_dirs = []
|
|
237
|
+
for directory in [cwd_tools_dir]:
|
|
238
|
+
if directory.exists() and directory.is_dir():
|
|
239
|
+
tool_dirs.append(directory)
|
|
240
|
+
logger.debug("tools_dir=<%s> | found tools directory", directory)
|
|
241
|
+
else:
|
|
242
|
+
logger.debug("tools_dir=<%s> | tools directory not found", directory)
|
|
243
|
+
|
|
244
|
+
return tool_dirs
|
|
245
|
+
|
|
246
|
+
def discover_tool_modules(self) -> Dict[str, Path]:
|
|
247
|
+
"""Discover available tool modules in all tools directories.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Dictionary mapping tool names to their full paths.
|
|
251
|
+
"""
|
|
252
|
+
tool_modules = {}
|
|
253
|
+
tools_dirs = self.get_tools_dirs()
|
|
254
|
+
|
|
255
|
+
for tools_dir in tools_dirs:
|
|
256
|
+
logger.debug("tools_dir=<%s> | scanning", tools_dir)
|
|
257
|
+
|
|
258
|
+
# Find Python tools
|
|
259
|
+
for extension in ["*.py"]:
|
|
260
|
+
for item in tools_dir.glob(extension):
|
|
261
|
+
if item.is_file() and not item.name.startswith("__"):
|
|
262
|
+
module_name = item.stem
|
|
263
|
+
# If tool already exists, newer paths take precedence
|
|
264
|
+
if module_name in tool_modules:
|
|
265
|
+
logger.debug("tools_dir=<%s>, module_name=<%s> | tool overridden", tools_dir, module_name)
|
|
266
|
+
tool_modules[module_name] = item
|
|
267
|
+
|
|
268
|
+
logger.debug("tool_modules=<%s> | discovered", list(tool_modules.keys()))
|
|
269
|
+
return tool_modules
|
|
270
|
+
|
|
271
|
+
def reload_tool(self, tool_name: str) -> None:
|
|
272
|
+
"""Reload a specific tool module.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
tool_name: Name of the tool to reload.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
FileNotFoundError: If the tool file cannot be found.
|
|
279
|
+
ImportError: If there are issues importing the tool module.
|
|
280
|
+
ValueError: If the tool specification is invalid or required components are missing.
|
|
281
|
+
Exception: For other errors during tool reloading.
|
|
282
|
+
"""
|
|
283
|
+
try:
|
|
284
|
+
# Check for tool file
|
|
285
|
+
logger.debug("tool_name=<%s> | searching directories for tool", tool_name)
|
|
286
|
+
tools_dirs = self.get_tools_dirs()
|
|
287
|
+
tool_path = None
|
|
288
|
+
|
|
289
|
+
# Search for the tool file in all tool directories
|
|
290
|
+
for tools_dir in tools_dirs:
|
|
291
|
+
temp_path = tools_dir / f"{tool_name}.py"
|
|
292
|
+
if temp_path.exists():
|
|
293
|
+
tool_path = temp_path
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
if not tool_path:
|
|
297
|
+
raise FileNotFoundError(f"No tool file found for: {tool_name}")
|
|
298
|
+
|
|
299
|
+
logger.debug("tool_name=<%s> | reloading tool", tool_name)
|
|
300
|
+
|
|
301
|
+
# Add tool directory to path temporarily
|
|
302
|
+
tool_dir = str(tool_path.parent)
|
|
303
|
+
sys.path.insert(0, tool_dir)
|
|
304
|
+
try:
|
|
305
|
+
# Load the module directly using spec
|
|
306
|
+
spec = util.spec_from_file_location(tool_name, str(tool_path))
|
|
307
|
+
if spec is None:
|
|
308
|
+
raise ImportError(f"Could not load spec for {tool_name}")
|
|
309
|
+
|
|
310
|
+
module = util.module_from_spec(spec)
|
|
311
|
+
sys.modules[tool_name] = module
|
|
312
|
+
|
|
313
|
+
if spec.loader is None:
|
|
314
|
+
raise ImportError(f"Could not load {tool_name}")
|
|
315
|
+
|
|
316
|
+
spec.loader.exec_module(module)
|
|
317
|
+
|
|
318
|
+
finally:
|
|
319
|
+
# Remove the temporary path
|
|
320
|
+
sys.path.remove(tool_dir)
|
|
321
|
+
|
|
322
|
+
# Look for function-based tools first
|
|
323
|
+
try:
|
|
324
|
+
function_tools = self._scan_module_for_tools(module)
|
|
325
|
+
|
|
326
|
+
if function_tools:
|
|
327
|
+
for function_tool in function_tools:
|
|
328
|
+
# Register the function-based tool
|
|
329
|
+
self.register_tool(function_tool)
|
|
330
|
+
|
|
331
|
+
# Update tool configuration if available
|
|
332
|
+
if self.tool_config is not None:
|
|
333
|
+
self._update_tool_config(self.tool_config, {"spec": function_tool.tool_spec})
|
|
334
|
+
|
|
335
|
+
logger.debug("tool_name=<%s> | successfully reloaded function-based tool from module", tool_name)
|
|
336
|
+
return
|
|
337
|
+
except ImportError:
|
|
338
|
+
logger.debug("function tool loader not available | falling back to traditional tools")
|
|
339
|
+
|
|
340
|
+
# Fall back to traditional module-level tools
|
|
341
|
+
if not hasattr(module, "TOOL_SPEC"):
|
|
342
|
+
raise ValueError(
|
|
343
|
+
f"Tool {tool_name} is missing TOOL_SPEC (neither at module level nor as a decorated function)"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
expected_func_name = tool_name
|
|
347
|
+
if not hasattr(module, expected_func_name):
|
|
348
|
+
raise ValueError(f"Tool {tool_name} is missing {expected_func_name} function")
|
|
349
|
+
|
|
350
|
+
tool_function = getattr(module, expected_func_name)
|
|
351
|
+
if not callable(tool_function):
|
|
352
|
+
raise ValueError(f"Tool {tool_name} function is not callable")
|
|
353
|
+
|
|
354
|
+
# Validate tool spec
|
|
355
|
+
self.validate_tool_spec(module.TOOL_SPEC)
|
|
356
|
+
|
|
357
|
+
new_tool = PythonAgentTool(tool_name, module.TOOL_SPEC, tool_function)
|
|
358
|
+
|
|
359
|
+
# Register the tool
|
|
360
|
+
self.register_tool(new_tool)
|
|
361
|
+
|
|
362
|
+
# Update tool configuration if available
|
|
363
|
+
if self.tool_config is not None:
|
|
364
|
+
self._update_tool_config(self.tool_config, {"spec": module.TOOL_SPEC})
|
|
365
|
+
logger.debug("tool_name=<%s> | successfully reloaded tool", tool_name)
|
|
366
|
+
|
|
367
|
+
except Exception:
|
|
368
|
+
logger.exception("tool_name=<%s> | failed to reload tool", tool_name)
|
|
369
|
+
raise
|
|
370
|
+
|
|
371
|
+
def initialize_tools(self, load_tools_from_directory: bool = False) -> None:
|
|
372
|
+
"""Initialize all tools by discovering and loading them dynamically from all tool directories.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
load_tools_from_directory: Whether to reload tools if changes are made at runtime.
|
|
376
|
+
"""
|
|
377
|
+
self.tool_config = None
|
|
378
|
+
|
|
379
|
+
# Then discover and load other tools
|
|
380
|
+
tool_modules = self.discover_tool_modules()
|
|
381
|
+
successful_loads = 0
|
|
382
|
+
total_tools = len(tool_modules)
|
|
383
|
+
tool_import_errors = {}
|
|
384
|
+
|
|
385
|
+
# Process Python tools
|
|
386
|
+
for tool_name, tool_path in tool_modules.items():
|
|
387
|
+
if tool_name in ["__init__"]:
|
|
388
|
+
continue
|
|
389
|
+
|
|
390
|
+
if not load_tools_from_directory:
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
# Add directory to path temporarily
|
|
395
|
+
tool_dir = str(tool_path.parent)
|
|
396
|
+
sys.path.insert(0, tool_dir)
|
|
397
|
+
try:
|
|
398
|
+
module = import_module(tool_name)
|
|
399
|
+
finally:
|
|
400
|
+
if tool_dir in sys.path:
|
|
401
|
+
sys.path.remove(tool_dir)
|
|
402
|
+
|
|
403
|
+
# Process Python tool
|
|
404
|
+
if tool_path.suffix == ".py":
|
|
405
|
+
# Check for decorated function tools first
|
|
406
|
+
try:
|
|
407
|
+
function_tools = self._scan_module_for_tools(module)
|
|
408
|
+
|
|
409
|
+
if function_tools:
|
|
410
|
+
for function_tool in function_tools:
|
|
411
|
+
self.register_tool(function_tool)
|
|
412
|
+
successful_loads += 1
|
|
413
|
+
else:
|
|
414
|
+
# Fall back to traditional tools
|
|
415
|
+
# Check for expected tool function
|
|
416
|
+
expected_func_name = tool_name
|
|
417
|
+
if hasattr(module, expected_func_name):
|
|
418
|
+
tool_function = getattr(module, expected_func_name)
|
|
419
|
+
if not callable(tool_function):
|
|
420
|
+
logger.warning(
|
|
421
|
+
"tool_name=<%s> | tool function exists but is not callable", tool_name
|
|
422
|
+
)
|
|
423
|
+
continue
|
|
424
|
+
|
|
425
|
+
# Validate tool spec before registering
|
|
426
|
+
if not hasattr(module, "TOOL_SPEC"):
|
|
427
|
+
logger.warning("tool_name=<%s> | tool is missing TOOL_SPEC | skipping", tool_name)
|
|
428
|
+
continue
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
self.validate_tool_spec(module.TOOL_SPEC)
|
|
432
|
+
except ValueError as e:
|
|
433
|
+
logger.warning("tool_name=<%s> | tool spec validation failed | %s", tool_name, e)
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
tool_spec = module.TOOL_SPEC
|
|
437
|
+
tool = PythonAgentTool(tool_name, tool_spec, tool_function)
|
|
438
|
+
self.register_tool(tool)
|
|
439
|
+
successful_loads += 1
|
|
440
|
+
|
|
441
|
+
else:
|
|
442
|
+
logger.warning("tool_name=<%s> | tool function missing", tool_name)
|
|
443
|
+
except ImportError:
|
|
444
|
+
# Function tool loader not available, fall back to traditional tools
|
|
445
|
+
# Check for expected tool function
|
|
446
|
+
expected_func_name = tool_name
|
|
447
|
+
if hasattr(module, expected_func_name):
|
|
448
|
+
tool_function = getattr(module, expected_func_name)
|
|
449
|
+
if not callable(tool_function):
|
|
450
|
+
logger.warning("tool_name=<%s> | tool function exists but is not callable", tool_name)
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
# Validate tool spec before registering
|
|
454
|
+
if not hasattr(module, "TOOL_SPEC"):
|
|
455
|
+
logger.warning("tool_name=<%s> | tool is missing TOOL_SPEC | skipping", tool_name)
|
|
456
|
+
continue
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
self.validate_tool_spec(module.TOOL_SPEC)
|
|
460
|
+
except ValueError as e:
|
|
461
|
+
logger.warning("tool_name=<%s> | tool spec validation failed | %s", tool_name, e)
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
tool_spec = module.TOOL_SPEC
|
|
465
|
+
tool = PythonAgentTool(tool_name, tool_spec, tool_function)
|
|
466
|
+
self.register_tool(tool)
|
|
467
|
+
successful_loads += 1
|
|
468
|
+
|
|
469
|
+
else:
|
|
470
|
+
logger.warning("tool_name=<%s> | tool function missing", tool_name)
|
|
471
|
+
|
|
472
|
+
except Exception as e:
|
|
473
|
+
logger.warning("tool_name=<%s> | failed to load tool | %s", tool_name, e)
|
|
474
|
+
tool_import_errors[tool_name] = str(e)
|
|
475
|
+
|
|
476
|
+
# Log summary
|
|
477
|
+
logger.debug("tool_count=<%d>, success_count=<%d> | finished loading tools", total_tools, successful_loads)
|
|
478
|
+
if tool_import_errors:
|
|
479
|
+
for tool_name, error in tool_import_errors.items():
|
|
480
|
+
logger.debug("tool_name=<%s> | import error | %s", tool_name, error)
|
|
481
|
+
|
|
482
|
+
def get_all_tool_specs(self) -> list[ToolSpec]:
|
|
483
|
+
"""Get all the tool specs for all tools in this registry..
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
A list of ToolSpecs.
|
|
487
|
+
"""
|
|
488
|
+
all_tools = self.get_all_tools_config()
|
|
489
|
+
tools: List[ToolSpec] = [tool_spec for tool_spec in all_tools.values()]
|
|
490
|
+
return tools
|
|
491
|
+
|
|
492
|
+
def validate_tool_spec(self, tool_spec: ToolSpec) -> None:
|
|
493
|
+
"""Validate tool specification against required schema.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
tool_spec: Tool specification to validate.
|
|
497
|
+
|
|
498
|
+
Raises:
|
|
499
|
+
ValueError: If the specification is invalid.
|
|
500
|
+
"""
|
|
501
|
+
required_fields = ["name", "description"]
|
|
502
|
+
missing_fields = [field for field in required_fields if field not in tool_spec]
|
|
503
|
+
if missing_fields:
|
|
504
|
+
raise ValueError(f"Missing required fields in tool spec: {', '.join(missing_fields)}")
|
|
505
|
+
|
|
506
|
+
if "json" not in tool_spec["inputSchema"]:
|
|
507
|
+
# Convert direct schema to proper format
|
|
508
|
+
json_schema = normalize_schema(tool_spec["inputSchema"])
|
|
509
|
+
tool_spec["inputSchema"] = {"json": json_schema}
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
# Validate json schema fields
|
|
513
|
+
json_schema = tool_spec["inputSchema"]["json"]
|
|
514
|
+
|
|
515
|
+
# Ensure schema has required fields
|
|
516
|
+
if "type" not in json_schema:
|
|
517
|
+
json_schema["type"] = "object"
|
|
518
|
+
if "properties" not in json_schema:
|
|
519
|
+
json_schema["properties"] = {}
|
|
520
|
+
if "required" not in json_schema:
|
|
521
|
+
json_schema["required"] = []
|
|
522
|
+
|
|
523
|
+
# Validate property definitions
|
|
524
|
+
for prop_name, prop_def in json_schema.get("properties", {}).items():
|
|
525
|
+
if not isinstance(prop_def, dict):
|
|
526
|
+
json_schema["properties"][prop_name] = {
|
|
527
|
+
"type": "string",
|
|
528
|
+
"description": f"Property {prop_name}",
|
|
529
|
+
}
|
|
530
|
+
continue
|
|
531
|
+
|
|
532
|
+
# It is expected that type and description are already included in referenced $def.
|
|
533
|
+
if "$ref" in prop_def:
|
|
534
|
+
continue
|
|
535
|
+
|
|
536
|
+
if "type" not in prop_def:
|
|
537
|
+
prop_def["type"] = "string"
|
|
538
|
+
if "description" not in prop_def:
|
|
539
|
+
prop_def["description"] = f"Property {prop_name}"
|
|
540
|
+
|
|
541
|
+
class NewToolDict(TypedDict):
|
|
542
|
+
"""Dictionary type for adding or updating a tool in the configuration.
|
|
543
|
+
|
|
544
|
+
Attributes:
|
|
545
|
+
spec: The tool specification that defines the tool's interface and behavior.
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
spec: ToolSpec
|
|
549
|
+
|
|
550
|
+
def _update_tool_config(self, tool_config: Dict[str, Any], new_tool: NewToolDict) -> None:
|
|
551
|
+
"""Update tool configuration with a new tool.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
tool_config: The current tool configuration dictionary.
|
|
555
|
+
new_tool: The new tool to add/update.
|
|
556
|
+
|
|
557
|
+
Raises:
|
|
558
|
+
ValueError: If the new tool spec is invalid.
|
|
559
|
+
"""
|
|
560
|
+
if not new_tool.get("spec"):
|
|
561
|
+
raise ValueError("Invalid tool format - missing spec")
|
|
562
|
+
|
|
563
|
+
# Validate tool spec before updating
|
|
564
|
+
try:
|
|
565
|
+
self.validate_tool_spec(new_tool["spec"])
|
|
566
|
+
except ValueError as e:
|
|
567
|
+
raise ValueError(f"Tool specification validation failed: {str(e)}") from e
|
|
568
|
+
|
|
569
|
+
new_tool_name = new_tool["spec"]["name"]
|
|
570
|
+
existing_tool_idx = None
|
|
571
|
+
|
|
572
|
+
# Find if tool already exists
|
|
573
|
+
for idx, tool_entry in enumerate(tool_config["tools"]):
|
|
574
|
+
if tool_entry["toolSpec"]["name"] == new_tool_name:
|
|
575
|
+
existing_tool_idx = idx
|
|
576
|
+
break
|
|
577
|
+
|
|
578
|
+
# Update existing tool or add new one
|
|
579
|
+
new_tool_entry = {"toolSpec": new_tool["spec"]}
|
|
580
|
+
if existing_tool_idx is not None:
|
|
581
|
+
tool_config["tools"][existing_tool_idx] = new_tool_entry
|
|
582
|
+
logger.debug("tool_name=<%s> | updated existing tool", new_tool_name)
|
|
583
|
+
else:
|
|
584
|
+
tool_config["tools"].append(new_tool_entry)
|
|
585
|
+
logger.debug("tool_name=<%s> | added new tool", new_tool_name)
|
|
586
|
+
|
|
587
|
+
def _scan_module_for_tools(self, module: Any) -> List[AgentTool]:
|
|
588
|
+
"""Scan a module for function-based tools.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
module: The module to scan.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
List of FunctionTool instances found in the module.
|
|
595
|
+
"""
|
|
596
|
+
tools: List[AgentTool] = []
|
|
597
|
+
|
|
598
|
+
for name, obj in inspect.getmembers(module):
|
|
599
|
+
if isinstance(obj, DecoratedFunctionTool):
|
|
600
|
+
# Create a function tool with correct name
|
|
601
|
+
try:
|
|
602
|
+
# Cast as AgentTool for mypy
|
|
603
|
+
tools.append(cast(AgentTool, obj))
|
|
604
|
+
except Exception as e:
|
|
605
|
+
logger.warning("tool_name=<%s> | failed to create function tool | %s", name, e)
|
|
606
|
+
|
|
607
|
+
return tools
|