solace-agent-mesh 1.4.3__py3-none-any.whl → 1.4.5__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 solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/adk/setup.py +545 -470
- solace_agent_mesh/agent/sac/app.py +36 -15
- solace_agent_mesh/agent/sac/component.py +7 -3
- solace_agent_mesh/agent/tools/builtin_artifact_tools.py +17 -0
- solace_agent_mesh/agent/tools/builtin_data_analysis_tools.py +4 -0
- solace_agent_mesh/agent/tools/tool_definition.py +8 -0
- solace_agent_mesh/agent/tools/web_tools.py +5 -0
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/images/sam-enterprise-credentials-b269f095349473118b2b33bdfcc40122.png +0 -0
- solace_agent_mesh/assets/docs/assets/js/ae0e903d.7c73bc4f.js +1 -0
- solace_agent_mesh/assets/docs/assets/js/{f284c35a.cad4dbf2.js → f284c35a.2b2f5048.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/{main.66eec320.js → main.2b4fe82a.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/{runtime~main.355446b2.js → runtime~main.c0805958.js} +1 -1
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +27 -11
- solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +3 -3
- solace_agent_mesh/assets/docs/img/sam-enterprise-credentials.png +0 -0
- solace_agent_mesh/assets/docs/lunr-index-1758644347760.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1758644347760.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/cli/commands/eval_cmd.py +3 -3
- solace_agent_mesh/client/webui/frontend/static/assets/main-B67MsY-v.js +339 -0
- solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
- solace_agent_mesh/common/utils/type_utils.py +28 -0
- solace_agent_mesh/gateway/http_sse/main.py +19 -0
- {solace_agent_mesh-1.4.3.dist-info → solace_agent_mesh-1.4.5.dist-info}/METADATA +3 -3
- {solace_agent_mesh-1.4.3.dist-info → solace_agent_mesh-1.4.5.dist-info}/RECORD +68 -65
- solace_agent_mesh/assets/docs/assets/js/ae0e903d.abca774a.js +0 -1
- solace_agent_mesh/assets/docs/lunr-index-1758138634356.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1758138634356.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-Cv2k8j3R.js +0 -339
- /solace_agent_mesh/assets/docs/assets/js/{main.66eec320.js.LICENSE.txt → main.2b4fe82a.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.4.3.dist-info → solace_agent_mesh-1.4.5.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.4.3.dist-info → solace_agent_mesh-1.4.5.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.4.3.dist-info → solace_agent_mesh-1.4.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,6 +7,7 @@ import functools
|
|
|
7
7
|
import inspect
|
|
8
8
|
from solace_ai_connector.common.log import log
|
|
9
9
|
from solace_ai_connector.common.utils import import_module
|
|
10
|
+
from ...common.utils.type_utils import is_subclass_by_name
|
|
10
11
|
|
|
11
12
|
from .app_llm_agent import AppLlmAgent
|
|
12
13
|
from .tool_wrapper import ADKToolWrapper
|
|
@@ -31,20 +32,30 @@ if TYPE_CHECKING:
|
|
|
31
32
|
from ..tools.registry import tool_registry
|
|
32
33
|
from ..tools.tool_definition import BuiltinTool
|
|
33
34
|
from ..tools.dynamic_tool import DynamicTool, DynamicToolProvider
|
|
34
|
-
from ..tools.tool_config_types import
|
|
35
|
+
from ..tools.tool_config_types import (
|
|
36
|
+
AnyToolConfig,
|
|
37
|
+
BuiltinToolConfig,
|
|
38
|
+
BuiltinGroupToolConfig,
|
|
39
|
+
McpToolConfig,
|
|
40
|
+
PythonToolConfig,
|
|
41
|
+
)
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
from ...agent.adk import callbacks as adk_callbacks
|
|
38
45
|
from ...agent.adk.models.lite_llm import LiteLlm
|
|
39
46
|
|
|
40
47
|
|
|
48
|
+
# Define a clear return type for all tool-loading helpers
|
|
49
|
+
ToolLoadingResult = Tuple[List[Union[BaseTool, Callable]], List[BuiltinTool], List[Callable]]
|
|
50
|
+
|
|
51
|
+
|
|
41
52
|
def _find_dynamic_tool_class(module) -> Optional[type]:
|
|
42
53
|
"""Finds a single non-abstract DynamicTool subclass in a module."""
|
|
43
54
|
found_classes = []
|
|
44
55
|
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
45
56
|
if (
|
|
46
|
-
|
|
47
|
-
and obj
|
|
57
|
+
is_subclass_by_name(obj, "DynamicTool")
|
|
58
|
+
and not is_subclass_by_name(obj, "DynamicToolProvider")
|
|
48
59
|
and not inspect.isabstract(obj)
|
|
49
60
|
):
|
|
50
61
|
found_classes.append(obj)
|
|
@@ -137,10 +148,8 @@ def _find_dynamic_tool_provider_class(module) -> Optional[type]:
|
|
|
137
148
|
"""Finds a single non-abstract DynamicToolProvider subclass in a module."""
|
|
138
149
|
found_classes = []
|
|
139
150
|
for name, obj in inspect.getmembers(module, inspect.isclass):
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
and obj is not DynamicToolProvider
|
|
143
|
-
and not inspect.isabstract(obj)
|
|
151
|
+
if is_subclass_by_name(obj, "DynamicToolProvider") and not inspect.isabstract(
|
|
152
|
+
obj
|
|
144
153
|
):
|
|
145
154
|
found_classes.append(obj)
|
|
146
155
|
if len(found_classes) > 1:
|
|
@@ -151,512 +160,445 @@ def _find_dynamic_tool_provider_class(module) -> Optional[type]:
|
|
|
151
160
|
return found_classes[0] if found_classes else None
|
|
152
161
|
|
|
153
162
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
def _check_and_register_tool_name(name: str, source: str, loaded_tool_names: Set[str]):
|
|
164
|
+
"""Checks for duplicate tool names and raises ValueError if found."""
|
|
165
|
+
if name in loaded_tool_names:
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"Configuration Error: Duplicate tool name '{name}' found from source '{source}'. "
|
|
168
|
+
"This name is already in use. Please resolve the conflict by renaming or "
|
|
169
|
+
"disabling one of the tools in your agent's configuration."
|
|
170
|
+
)
|
|
171
|
+
loaded_tool_names.add(name)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def _create_python_tool_lifecycle_hooks(
|
|
175
|
+
component: "SamAgentComponent",
|
|
176
|
+
tool_config_model: "PythonToolConfig",
|
|
177
|
+
loaded_python_tools: List[Union[BaseTool, Callable]],
|
|
178
|
+
) -> List[Callable]:
|
|
157
179
|
"""
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
180
|
+
Executes init hooks and collects cleanup hooks for a Python tool.
|
|
181
|
+
Handles both YAML-defined hooks and class-based init/cleanup methods.
|
|
182
|
+
Returns cleanup hooks in LIFO order.
|
|
183
|
+
"""
|
|
184
|
+
module_name = tool_config_model.component_module
|
|
185
|
+
base_path = tool_config_model.component_base_path
|
|
186
|
+
cleanup_hooks = []
|
|
187
|
+
|
|
188
|
+
# 1. YAML Init (runs first)
|
|
189
|
+
await _execute_lifecycle_hook(
|
|
190
|
+
component,
|
|
191
|
+
tool_config_model.init_function,
|
|
192
|
+
module_name,
|
|
193
|
+
base_path,
|
|
194
|
+
tool_config_model,
|
|
195
|
+
)
|
|
162
196
|
|
|
163
|
-
|
|
164
|
-
|
|
197
|
+
# 2. DynamicTool/Provider Init (runs second)
|
|
198
|
+
for tool_instance in loaded_python_tools:
|
|
199
|
+
if is_subclass_by_name(type(tool_instance), "DynamicTool"):
|
|
200
|
+
log.info(
|
|
201
|
+
"%s Executing .init() method for DynamicTool '%s'.",
|
|
202
|
+
component.log_identifier,
|
|
203
|
+
tool_instance.tool_name,
|
|
204
|
+
)
|
|
205
|
+
await tool_instance.init(component, tool_config_model)
|
|
206
|
+
|
|
207
|
+
# 3. Collect Cleanup Hooks (in reverse order of init)
|
|
208
|
+
# Class-based cleanup hook (will be executed first)
|
|
209
|
+
for tool_instance in loaded_python_tools:
|
|
210
|
+
if is_subclass_by_name(type(tool_instance), "DynamicTool"):
|
|
211
|
+
cleanup_hooks.append(
|
|
212
|
+
functools.partial(
|
|
213
|
+
tool_instance.cleanup, component, tool_config_model
|
|
214
|
+
)
|
|
215
|
+
)
|
|
165
216
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
217
|
+
# YAML-based cleanup hook (will be executed second)
|
|
218
|
+
yaml_cleanup_partial = _create_cleanup_partial(
|
|
219
|
+
component,
|
|
220
|
+
tool_config_model.cleanup_function,
|
|
221
|
+
module_name,
|
|
222
|
+
base_path,
|
|
223
|
+
tool_config_model,
|
|
224
|
+
)
|
|
225
|
+
if yaml_cleanup_partial:
|
|
226
|
+
cleanup_hooks.append(yaml_cleanup_partial)
|
|
171
227
|
|
|
172
|
-
|
|
173
|
-
|
|
228
|
+
# Return in LIFO order relative to init
|
|
229
|
+
return list(reversed(cleanup_hooks))
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _load_python_class_based_tool(
|
|
233
|
+
module: Any,
|
|
234
|
+
tool_config: Dict,
|
|
235
|
+
component: "SamAgentComponent",
|
|
236
|
+
) -> List[DynamicTool]:
|
|
174
237
|
"""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
tools_config = component.get_config("tools", [])
|
|
238
|
+
Loads a class-based tool, which can be a single DynamicTool or a
|
|
239
|
+
DynamicToolProvider that generates multiple tools.
|
|
240
|
+
"""
|
|
241
|
+
from pydantic import BaseModel, ValidationError
|
|
180
242
|
|
|
181
|
-
|
|
243
|
+
specific_tool_config = tool_config.get("tool_config")
|
|
244
|
+
dynamic_tools: List[DynamicTool] = []
|
|
245
|
+
module_name = module.__name__
|
|
182
246
|
|
|
183
|
-
|
|
247
|
+
# Determine the class to load
|
|
248
|
+
tool_class = None
|
|
249
|
+
class_name = tool_config.get("class_name")
|
|
250
|
+
if class_name:
|
|
251
|
+
tool_class = getattr(module, class_name)
|
|
252
|
+
else:
|
|
253
|
+
# Auto-discover: provider first, then single tool
|
|
254
|
+
tool_class = _find_dynamic_tool_provider_class(module)
|
|
255
|
+
if not tool_class:
|
|
256
|
+
tool_class = _find_dynamic_tool_class(module)
|
|
184
257
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"This name is already in use. Please resolve the conflict by renaming or "
|
|
191
|
-
"disabling one of the tools in your agent's configuration."
|
|
192
|
-
)
|
|
193
|
-
loaded_tool_names.add(name)
|
|
258
|
+
if not tool_class:
|
|
259
|
+
raise TypeError(
|
|
260
|
+
f"Module '{module_name}' does not contain a 'function_name' or 'class_name' to load, "
|
|
261
|
+
"and no DynamicTool or DynamicToolProvider subclass could be auto-discovered."
|
|
262
|
+
)
|
|
194
263
|
|
|
195
|
-
|
|
264
|
+
# Check for a Pydantic model declaration on the tool class
|
|
265
|
+
config_model: Optional[Type["BaseModel"]] = getattr(
|
|
266
|
+
tool_class, "config_model", None
|
|
267
|
+
)
|
|
268
|
+
validated_config: Union[dict, "BaseModel"] = specific_tool_config
|
|
269
|
+
|
|
270
|
+
if config_model:
|
|
271
|
+
log.debug(
|
|
272
|
+
"%s Found config_model '%s' for tool class '%s'. Validating...",
|
|
273
|
+
component.log_identifier,
|
|
274
|
+
config_model.__name__,
|
|
275
|
+
tool_class.__name__,
|
|
276
|
+
)
|
|
277
|
+
try:
|
|
278
|
+
# Validate the raw dict and get a Pydantic model instance
|
|
279
|
+
validated_config = config_model.model_validate(specific_tool_config or {})
|
|
280
|
+
log.debug(
|
|
281
|
+
"%s Successfully validated tool_config for '%s'.",
|
|
282
|
+
component.log_identifier,
|
|
283
|
+
tool_class.__name__,
|
|
284
|
+
)
|
|
285
|
+
except ValidationError as e:
|
|
286
|
+
# Provide a clear error message and raise
|
|
287
|
+
error_msg = (
|
|
288
|
+
f"Configuration error for tool '{tool_class.__name__}' from module '{module_name}'. "
|
|
289
|
+
f"The provided 'tool_config' in your YAML is invalid:\n{e}"
|
|
290
|
+
)
|
|
291
|
+
log.error("%s %s", component.log_identifier, error_msg)
|
|
292
|
+
raise ValueError(error_msg) from e
|
|
293
|
+
|
|
294
|
+
# Instantiate tools from the class
|
|
295
|
+
if is_subclass_by_name(tool_class, "DynamicToolProvider"):
|
|
296
|
+
provider_instance = tool_class()
|
|
297
|
+
dynamic_tools = provider_instance.get_all_tools_for_framework(
|
|
298
|
+
tool_config=validated_config
|
|
299
|
+
)
|
|
196
300
|
log.info(
|
|
197
|
-
"%s
|
|
301
|
+
"%s Loaded %d tools from DynamicToolProvider '%s' in %s",
|
|
302
|
+
component.log_identifier,
|
|
303
|
+
len(dynamic_tools),
|
|
304
|
+
tool_class.__name__,
|
|
305
|
+
module_name,
|
|
198
306
|
)
|
|
307
|
+
elif is_subclass_by_name(tool_class, "DynamicTool"):
|
|
308
|
+
tool_instance = tool_class(tool_config=validated_config)
|
|
309
|
+
dynamic_tools = [tool_instance]
|
|
199
310
|
else:
|
|
200
|
-
|
|
201
|
-
"
|
|
202
|
-
|
|
203
|
-
len(tools_config),
|
|
204
|
-
[tc.get("tool_type") for tc in tools_config],
|
|
311
|
+
raise TypeError(
|
|
312
|
+
f"Class '{tool_class.__name__}' in module '{module_name}' is not a valid "
|
|
313
|
+
"DynamicTool or DynamicToolProvider subclass."
|
|
205
314
|
)
|
|
315
|
+
|
|
316
|
+
# Post-process all generated tools
|
|
317
|
+
for tool in dynamic_tools:
|
|
318
|
+
tool.origin = "dynamic"
|
|
319
|
+
declaration = tool._get_declaration()
|
|
320
|
+
if not declaration:
|
|
321
|
+
log.warning(
|
|
322
|
+
"Dynamic tool '%s' from module '%s' did not generate a valid declaration. Skipping.",
|
|
323
|
+
tool.__class__.__name__,
|
|
324
|
+
module_name,
|
|
325
|
+
)
|
|
326
|
+
continue
|
|
206
327
|
log.info(
|
|
207
|
-
"%s
|
|
328
|
+
"%s Loaded dynamic tool: %s from %s",
|
|
208
329
|
component.log_identifier,
|
|
330
|
+
declaration.name,
|
|
331
|
+
module_name,
|
|
209
332
|
)
|
|
210
|
-
for tool_config in tools_config:
|
|
211
|
-
newly_loaded_tools = []
|
|
212
|
-
try:
|
|
213
|
-
tool_config_model = any_tool_adapter.validate_python(tool_config)
|
|
214
|
-
tool_type = tool_config_model.tool_type.lower()
|
|
215
333
|
|
|
216
|
-
|
|
217
|
-
module_name = tool_config.get("component_module")
|
|
218
|
-
base_path = tool_config.get("component_base_path")
|
|
219
|
-
if not module_name:
|
|
220
|
-
raise ValueError(
|
|
221
|
-
"'component_module' is required for python tools."
|
|
222
|
-
)
|
|
223
|
-
module = import_module(module_name, base_path=base_path)
|
|
224
|
-
|
|
225
|
-
# Case 1: Simple function-based tool
|
|
226
|
-
if "function_name" in tool_config:
|
|
227
|
-
function_name = tool_config.get("function_name")
|
|
228
|
-
tool_name = tool_config.get("tool_name")
|
|
229
|
-
tool_description = tool_config.get("tool_description")
|
|
230
|
-
|
|
231
|
-
func = getattr(module, function_name)
|
|
232
|
-
if not callable(func):
|
|
233
|
-
raise TypeError(
|
|
234
|
-
f"'{function_name}' in module '{module_name}' is not callable."
|
|
235
|
-
)
|
|
334
|
+
return dynamic_tools
|
|
236
335
|
|
|
237
|
-
specific_tool_config = tool_config.get("tool_config")
|
|
238
|
-
tool_callable = ADKToolWrapper(
|
|
239
|
-
func,
|
|
240
|
-
specific_tool_config,
|
|
241
|
-
function_name,
|
|
242
|
-
origin="python",
|
|
243
|
-
raw_string_args=tool_config.get("raw_string_args", []),
|
|
244
|
-
)
|
|
245
336
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
tool_callable.__name__ = tool_name
|
|
337
|
+
async def _load_python_tool(component: "SamAgentComponent", tool_config: Dict) -> ToolLoadingResult:
|
|
338
|
+
from pydantic import TypeAdapter
|
|
249
339
|
|
|
250
|
-
|
|
251
|
-
|
|
340
|
+
python_tool_adapter = TypeAdapter(PythonToolConfig)
|
|
341
|
+
tool_config_model = python_tool_adapter.validate_python(tool_config)
|
|
252
342
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
log.info(
|
|
259
|
-
"%s Loaded Python tool: %s from %s.",
|
|
260
|
-
component.log_identifier,
|
|
261
|
-
function_name,
|
|
262
|
-
module_name,
|
|
263
|
-
)
|
|
264
|
-
# Case 2: Advanced class-based dynamic tool or provider
|
|
265
|
-
else:
|
|
266
|
-
specific_tool_config = tool_config.get("tool_config")
|
|
267
|
-
dynamic_tools = []
|
|
268
|
-
|
|
269
|
-
# Determine the class to load
|
|
270
|
-
tool_class = None
|
|
271
|
-
class_name = tool_config.get("class_name")
|
|
272
|
-
if class_name:
|
|
273
|
-
tool_class = getattr(module, class_name)
|
|
274
|
-
else:
|
|
275
|
-
# Auto-discover: provider first, then single tool
|
|
276
|
-
tool_class = _find_dynamic_tool_provider_class(module)
|
|
277
|
-
if not tool_class:
|
|
278
|
-
tool_class = _find_dynamic_tool_class(module)
|
|
279
|
-
|
|
280
|
-
if not tool_class:
|
|
281
|
-
raise TypeError(
|
|
282
|
-
f"Module '{module_name}' does not contain a 'function_name' or 'class_name' to load, "
|
|
283
|
-
"and no DynamicTool or DynamicToolProvider subclass could be auto-discovered."
|
|
284
|
-
)
|
|
343
|
+
module_name = tool_config_model.component_module
|
|
344
|
+
base_path = tool_config_model.component_base_path
|
|
345
|
+
if not module_name:
|
|
346
|
+
raise ValueError("'component_module' is required for python tools.")
|
|
347
|
+
module = import_module(module_name, base_path=base_path)
|
|
285
348
|
|
|
286
|
-
|
|
287
|
-
config_model: Optional[Type["BaseModel"]] = getattr(
|
|
288
|
-
tool_class, "config_model", None
|
|
289
|
-
)
|
|
290
|
-
validated_config: Union[dict, "BaseModel"] = specific_tool_config
|
|
349
|
+
loaded_python_tools: List[Union[BaseTool, Callable]] = []
|
|
291
350
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
try:
|
|
300
|
-
# Validate the raw dict and get a Pydantic model instance
|
|
301
|
-
validated_config = config_model.model_validate(
|
|
302
|
-
specific_tool_config or {}
|
|
303
|
-
)
|
|
304
|
-
log.debug(
|
|
305
|
-
"%s Successfully validated tool_config for '%s'.",
|
|
306
|
-
component.log_identifier,
|
|
307
|
-
tool_class.__name__,
|
|
308
|
-
)
|
|
309
|
-
except ValidationError as e:
|
|
310
|
-
# Provide a clear error message and raise
|
|
311
|
-
error_msg = (
|
|
312
|
-
f"Configuration error for tool '{tool_class.__name__}' from module '{module_name}'. "
|
|
313
|
-
f"The provided 'tool_config' in your YAML is invalid:\n{e}"
|
|
314
|
-
)
|
|
315
|
-
log.error("%s %s", component.log_identifier, error_msg)
|
|
316
|
-
raise ValueError(error_msg) from e
|
|
317
|
-
|
|
318
|
-
# Instantiate tools from the class
|
|
319
|
-
if issubclass(tool_class, DynamicToolProvider):
|
|
320
|
-
provider_instance = tool_class()
|
|
321
|
-
dynamic_tools = (
|
|
322
|
-
provider_instance.get_all_tools_for_framework(
|
|
323
|
-
tool_config=validated_config
|
|
324
|
-
)
|
|
325
|
-
)
|
|
326
|
-
log.info(
|
|
327
|
-
"%s Loaded %d tools from DynamicToolProvider '%s' in %s",
|
|
328
|
-
component.log_identifier,
|
|
329
|
-
len(dynamic_tools),
|
|
330
|
-
tool_class.__name__,
|
|
331
|
-
module_name,
|
|
332
|
-
)
|
|
333
|
-
elif issubclass(tool_class, DynamicTool):
|
|
334
|
-
tool_instance = tool_class(tool_config=validated_config)
|
|
335
|
-
dynamic_tools = [tool_instance]
|
|
336
|
-
else:
|
|
337
|
-
raise TypeError(
|
|
338
|
-
f"Class '{tool_class.__name__}' in module '{module_name}' is not a valid "
|
|
339
|
-
"DynamicTool or DynamicToolProvider subclass."
|
|
340
|
-
)
|
|
351
|
+
# Case 1: Simple function-based tool
|
|
352
|
+
if tool_config_model.function_name:
|
|
353
|
+
func = getattr(module, tool_config_model.function_name)
|
|
354
|
+
if not callable(func):
|
|
355
|
+
raise TypeError(
|
|
356
|
+
f"'{tool_config_model.function_name}' in module '{module_name}' is not callable."
|
|
357
|
+
)
|
|
341
358
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
tool.__class__.__name__,
|
|
350
|
-
module_name,
|
|
351
|
-
)
|
|
352
|
-
continue
|
|
359
|
+
tool_callable = ADKToolWrapper(
|
|
360
|
+
func,
|
|
361
|
+
tool_config_model.tool_config,
|
|
362
|
+
tool_config_model.function_name,
|
|
363
|
+
origin="python",
|
|
364
|
+
raw_string_args=tool_config_model.raw_string_args,
|
|
365
|
+
)
|
|
353
366
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
newly_loaded_tools.append(tool)
|
|
359
|
-
log.info(
|
|
360
|
-
"%s Loaded dynamic tool: %s from %s",
|
|
361
|
-
component.log_identifier,
|
|
362
|
-
declaration.name,
|
|
363
|
-
module_name,
|
|
364
|
-
)
|
|
367
|
+
if tool_config_model.tool_name:
|
|
368
|
+
tool_callable.__name__ = tool_config_model.tool_name
|
|
369
|
+
if tool_config_model.tool_description:
|
|
370
|
+
tool_callable.__doc__ = tool_config_model.tool_description
|
|
365
371
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
)
|
|
372
|
+
loaded_python_tools.append(tool_callable)
|
|
373
|
+
log.info(
|
|
374
|
+
"%s Loaded Python tool: %s from %s.",
|
|
375
|
+
component.log_identifier,
|
|
376
|
+
tool_callable.__name__,
|
|
377
|
+
module_name,
|
|
378
|
+
)
|
|
379
|
+
# Case 2: Advanced class-based dynamic tool or provider
|
|
380
|
+
else:
|
|
381
|
+
dynamic_tools = _load_python_class_based_tool(module, tool_config, component)
|
|
382
|
+
loaded_python_tools.extend(dynamic_tools)
|
|
378
383
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
"%s Executing .init() method for DynamicTool '%s'.",
|
|
384
|
-
component.log_identifier,
|
|
385
|
-
tool_instance.tool_name,
|
|
386
|
-
)
|
|
387
|
-
await tool_instance.init(component, tool_config_model)
|
|
388
|
-
|
|
389
|
-
# 3. Collect Cleanup Hooks (LIFO order)
|
|
390
|
-
yaml_cleanup_partial = _create_cleanup_partial(
|
|
391
|
-
component,
|
|
392
|
-
tool_config_model.cleanup_function,
|
|
393
|
-
module_name,
|
|
394
|
-
base_path,
|
|
395
|
-
tool_config_model,
|
|
396
|
-
)
|
|
384
|
+
# --- Lifecycle Hook Execution for all Python Tools ---
|
|
385
|
+
cleanup_hooks = await _create_python_tool_lifecycle_hooks(
|
|
386
|
+
component, tool_config_model, loaded_python_tools
|
|
387
|
+
)
|
|
397
388
|
|
|
398
|
-
|
|
399
|
-
for tool_instance in newly_loaded_tools:
|
|
400
|
-
if isinstance(tool_instance, DynamicTool):
|
|
401
|
-
dynamic_cleanup_partials.append(
|
|
402
|
-
functools.partial(
|
|
403
|
-
tool_instance.cleanup, component, tool_config_model
|
|
404
|
-
)
|
|
405
|
-
)
|
|
389
|
+
return loaded_python_tools, [], cleanup_hooks
|
|
406
390
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
391
|
+
async def _load_builtin_tool(component: "SamAgentComponent", tool_config: Dict) -> ToolLoadingResult:
|
|
392
|
+
"""Loads a single built-in tool from the SAM or ADK tool registry."""
|
|
393
|
+
from pydantic import TypeAdapter
|
|
410
394
|
|
|
411
|
-
|
|
412
|
-
|
|
395
|
+
builtin_tool_adapter = TypeAdapter(BuiltinToolConfig)
|
|
396
|
+
tool_config_model = builtin_tool_adapter.validate_python(tool_config)
|
|
413
397
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
raise ValueError("'tool_name' required for builtin tool.")
|
|
418
|
-
|
|
419
|
-
_check_and_register_tool_name(tool_name, "builtin")
|
|
420
|
-
|
|
421
|
-
sam_tool_def = tool_registry.get_tool_by_name(tool_name)
|
|
422
|
-
if sam_tool_def:
|
|
423
|
-
specific_tool_config = tool_config.get("tool_config")
|
|
424
|
-
tool_callable = ADKToolWrapper(
|
|
425
|
-
sam_tool_def.implementation,
|
|
426
|
-
specific_tool_config,
|
|
427
|
-
sam_tool_def.name,
|
|
428
|
-
origin="builtin",
|
|
429
|
-
raw_string_args=sam_tool_def.raw_string_args,
|
|
430
|
-
)
|
|
431
|
-
loaded_tools.append(tool_callable)
|
|
432
|
-
newly_loaded_tools.append(tool_callable)
|
|
433
|
-
enabled_builtin_tools.append(sam_tool_def)
|
|
434
|
-
log.info(
|
|
435
|
-
"%s Loaded SAM built-in tool: %s",
|
|
436
|
-
component.log_identifier,
|
|
437
|
-
sam_tool_def.name,
|
|
438
|
-
)
|
|
439
|
-
continue
|
|
440
|
-
|
|
441
|
-
adk_tool = getattr(adk_tools_module, tool_name, None)
|
|
442
|
-
if adk_tool and isinstance(adk_tool, (BaseTool, Callable)):
|
|
443
|
-
adk_tool.origin = "adk_builtin"
|
|
444
|
-
loaded_tools.append(adk_tool)
|
|
445
|
-
log.info(
|
|
446
|
-
"%s Loaded ADK built-in tool: %s",
|
|
447
|
-
component.log_identifier,
|
|
448
|
-
tool_name,
|
|
449
|
-
)
|
|
450
|
-
continue
|
|
398
|
+
tool_name = tool_config_model.tool_name
|
|
399
|
+
if not tool_name:
|
|
400
|
+
raise ValueError("'tool_name' required for builtin tool.")
|
|
451
401
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
402
|
+
# Check SAM registry first
|
|
403
|
+
sam_tool_def = tool_registry.get_tool_by_name(tool_name)
|
|
404
|
+
if sam_tool_def:
|
|
405
|
+
tool_callable = ADKToolWrapper(
|
|
406
|
+
sam_tool_def.implementation,
|
|
407
|
+
tool_config_model.tool_config,
|
|
408
|
+
sam_tool_def.name,
|
|
409
|
+
origin="builtin",
|
|
410
|
+
raw_string_args=sam_tool_def.raw_string_args,
|
|
411
|
+
)
|
|
412
|
+
log.info(
|
|
413
|
+
"%s Loaded SAM built-in tool: %s",
|
|
414
|
+
component.log_identifier,
|
|
415
|
+
sam_tool_def.name,
|
|
416
|
+
)
|
|
417
|
+
return [tool_callable], [sam_tool_def], []
|
|
455
418
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
initializers_to_run: Dict[Callable, Dict] = {}
|
|
467
|
-
for tool_def in tools_in_group:
|
|
468
|
-
if (
|
|
469
|
-
tool_def.initializer
|
|
470
|
-
and tool_def.initializer not in initializers_to_run
|
|
471
|
-
):
|
|
472
|
-
initializers_to_run[tool_def.initializer] = tool_config.get(
|
|
473
|
-
"config", {}
|
|
474
|
-
)
|
|
419
|
+
# Fallback to ADK built-in tools module
|
|
420
|
+
adk_tool = getattr(adk_tools_module, tool_name, None)
|
|
421
|
+
if adk_tool and isinstance(adk_tool, (BaseTool, Callable)):
|
|
422
|
+
adk_tool.origin = "adk_builtin"
|
|
423
|
+
log.info(
|
|
424
|
+
"%s Loaded ADK built-in tool: %s",
|
|
425
|
+
component.log_identifier,
|
|
426
|
+
tool_name,
|
|
427
|
+
)
|
|
428
|
+
return [adk_tool], [], []
|
|
475
429
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
"%s Running initializer '%s' for tool group '%s'.",
|
|
480
|
-
component.log_identifier,
|
|
481
|
-
init_func.__name__,
|
|
482
|
-
group_name,
|
|
483
|
-
)
|
|
484
|
-
init_func(component, init_config)
|
|
485
|
-
log.info(
|
|
486
|
-
"%s Successfully executed initializer '%s' for tool group '%s'.",
|
|
487
|
-
component.log_identifier,
|
|
488
|
-
init_func.__name__,
|
|
489
|
-
group_name,
|
|
490
|
-
)
|
|
491
|
-
except Exception as e:
|
|
492
|
-
log.exception(
|
|
493
|
-
"%s Failed to run initializer '%s' for tool group '%s': %s",
|
|
494
|
-
component.log_identifier,
|
|
495
|
-
init_func.__name__,
|
|
496
|
-
group_name,
|
|
497
|
-
e,
|
|
498
|
-
)
|
|
499
|
-
raise e
|
|
430
|
+
raise ValueError(
|
|
431
|
+
f"Built-in tool '{tool_name}' not found in SAM or ADK registry."
|
|
432
|
+
)
|
|
500
433
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
tool_def.name, f"builtin-group:{group_name}"
|
|
505
|
-
)
|
|
506
|
-
specific_tool_config = tool_config.get("tool_configs", {}).get(
|
|
507
|
-
tool_def.name
|
|
508
|
-
)
|
|
509
|
-
tool_callable = ADKToolWrapper(
|
|
510
|
-
tool_def.implementation,
|
|
511
|
-
specific_tool_config,
|
|
512
|
-
tool_def.name,
|
|
513
|
-
origin="builtin",
|
|
514
|
-
raw_string_args=tool_def.raw_string_args,
|
|
515
|
-
)
|
|
516
|
-
loaded_tools.append(tool_callable)
|
|
517
|
-
newly_loaded_tools.append(tool_callable)
|
|
518
|
-
enabled_builtin_tools.append(tool_def)
|
|
519
|
-
group_tool_count += 1
|
|
520
|
-
log.info(
|
|
521
|
-
"Loaded %d tools from built-in group: %s",
|
|
522
|
-
group_tool_count,
|
|
523
|
-
group_name,
|
|
524
|
-
)
|
|
434
|
+
async def _load_builtin_group_tool(component: "SamAgentComponent", tool_config: Dict) -> ToolLoadingResult:
|
|
435
|
+
"""Loads a group of built-in tools by category from the SAM tool registry."""
|
|
436
|
+
from pydantic import TypeAdapter
|
|
525
437
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if not tool_name:
|
|
529
|
-
log.info(
|
|
530
|
-
"%s No specific 'tool_name' for MCP tool, will load all tools from server unless tool_filter is specified in MCPToolset itself.",
|
|
531
|
-
component.log_identifier,
|
|
532
|
-
)
|
|
438
|
+
group_tool_adapter = TypeAdapter(BuiltinGroupToolConfig)
|
|
439
|
+
tool_config_model = group_tool_adapter.validate_python(tool_config)
|
|
533
440
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
connection_type = connection_params_config.get("type", "").lower()
|
|
539
|
-
connection_args = {
|
|
540
|
-
k: v for k, v in connection_params_config.items() if k != "type"
|
|
541
|
-
}
|
|
542
|
-
connection_args["timeout"] = connection_args.get("timeout", 30)
|
|
543
|
-
|
|
544
|
-
environment_variables = tool_config.get("environment_variables")
|
|
545
|
-
env_param = {}
|
|
546
|
-
if connection_type == "stdio" and environment_variables:
|
|
547
|
-
if isinstance(environment_variables, dict):
|
|
548
|
-
env_param = environment_variables
|
|
549
|
-
log.debug(
|
|
550
|
-
"%s Found environment_variables for stdio MCP tool.",
|
|
551
|
-
component.log_identifier,
|
|
552
|
-
)
|
|
553
|
-
else:
|
|
554
|
-
log.warning(
|
|
555
|
-
"%s 'environment_variables' provided for stdio MCP tool but it is not a dictionary. Ignoring.",
|
|
556
|
-
component.log_identifier,
|
|
557
|
-
)
|
|
441
|
+
group_name = tool_config_model.group_name
|
|
442
|
+
if not group_name:
|
|
443
|
+
raise ValueError("'group_name' required for builtin-group.")
|
|
558
444
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
command_str = " ".join(cmd_arg)
|
|
564
|
-
elif isinstance(cmd_arg, str):
|
|
565
|
-
command_str = cmd_arg
|
|
566
|
-
else:
|
|
567
|
-
raise ValueError(
|
|
568
|
-
f"MCP tool 'command' parameter must be a string or a list of strings, got {type(cmd_arg)}"
|
|
569
|
-
)
|
|
570
|
-
if not isinstance(args_list, list):
|
|
571
|
-
raise ValueError(
|
|
572
|
-
f"MCP tool 'args' parameter must be a list, got {type(args_list)}"
|
|
573
|
-
)
|
|
574
|
-
final_connection_args = {
|
|
575
|
-
k: v
|
|
576
|
-
for k, v in connection_args.items()
|
|
577
|
-
if k not in ["command", "args", "timeout"]
|
|
578
|
-
}
|
|
579
|
-
connection_params = StdioConnectionParams(
|
|
580
|
-
server_params=StdioServerParameters(
|
|
581
|
-
command=command_str,
|
|
582
|
-
args=args_list,
|
|
583
|
-
**final_connection_args,
|
|
584
|
-
env=env_param if env_param else None,
|
|
585
|
-
),
|
|
586
|
-
timeout=connection_args.get("timeout"),
|
|
587
|
-
)
|
|
445
|
+
tools_in_group = tool_registry.get_tools_by_category(group_name)
|
|
446
|
+
if not tools_in_group:
|
|
447
|
+
log.warning("No tools found for built-in group: %s", group_name)
|
|
448
|
+
return [], [], []
|
|
588
449
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
450
|
+
# Run initializers for the group
|
|
451
|
+
initializers_to_run: Dict[Callable, Dict] = {}
|
|
452
|
+
for tool_def in tools_in_group:
|
|
453
|
+
if (
|
|
454
|
+
tool_def.initializer
|
|
455
|
+
and tool_def.initializer not in initializers_to_run
|
|
456
|
+
):
|
|
457
|
+
initializers_to_run[tool_def.initializer] = tool_config_model.tool_config
|
|
595
458
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
459
|
+
for init_func, init_config in initializers_to_run.items():
|
|
460
|
+
try:
|
|
461
|
+
log.info(
|
|
462
|
+
"%s Running initializer '%s' for tool group '%s'.",
|
|
463
|
+
component.log_identifier,
|
|
464
|
+
init_func.__name__,
|
|
465
|
+
group_name,
|
|
466
|
+
)
|
|
467
|
+
init_func(component, init_config)
|
|
468
|
+
log.info(
|
|
469
|
+
"%s Successfully executed initializer '%s' for tool group '%s'.",
|
|
470
|
+
component.log_identifier,
|
|
471
|
+
init_func.__name__,
|
|
472
|
+
group_name,
|
|
473
|
+
)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
log.exception(
|
|
476
|
+
"%s Failed to run initializer '%s' for tool group '%s': %s",
|
|
477
|
+
component.log_identifier,
|
|
478
|
+
init_func.__name__,
|
|
479
|
+
group_name,
|
|
480
|
+
e,
|
|
481
|
+
)
|
|
482
|
+
raise e
|
|
603
483
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
len(mcp_tools),
|
|
618
|
-
[tool.name for tool in mcp_tools],
|
|
619
|
-
)
|
|
620
|
-
for mcp_tool in mcp_tools:
|
|
621
|
-
log.debug(
|
|
622
|
-
"%s Registering MCP tool: %s",
|
|
623
|
-
component.log_identifier,
|
|
624
|
-
mcp_tool.name,
|
|
625
|
-
)
|
|
626
|
-
_check_and_register_tool_name(mcp_tool.name, "mcp")
|
|
627
|
-
except Exception as e:
|
|
628
|
-
log.error(
|
|
629
|
-
"%s Failed to discover tools from MCP server: %s",
|
|
630
|
-
component.log_identifier,
|
|
631
|
-
str(e),
|
|
632
|
-
)
|
|
633
|
-
raise
|
|
484
|
+
loaded_tools: List[Union[BaseTool, Callable]] = []
|
|
485
|
+
enabled_builtin_tools: List[BuiltinTool] = []
|
|
486
|
+
for tool_def in tools_in_group:
|
|
487
|
+
specific_tool_config = tool_config_model.tool_config.get(tool_def.name)
|
|
488
|
+
tool_callable = ADKToolWrapper(
|
|
489
|
+
tool_def.implementation,
|
|
490
|
+
specific_tool_config,
|
|
491
|
+
tool_def.name,
|
|
492
|
+
origin="builtin",
|
|
493
|
+
raw_string_args=tool_def.raw_string_args,
|
|
494
|
+
)
|
|
495
|
+
loaded_tools.append(tool_callable)
|
|
496
|
+
enabled_builtin_tools.append(tool_def)
|
|
634
497
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
498
|
+
log.info(
|
|
499
|
+
"Loaded %d tools from built-in group: %s",
|
|
500
|
+
len(loaded_tools),
|
|
501
|
+
group_name,
|
|
502
|
+
)
|
|
503
|
+
return loaded_tools, enabled_builtin_tools, []
|
|
504
|
+
|
|
505
|
+
async def _load_mcp_tool(component: "SamAgentComponent", tool_config: Dict) -> ToolLoadingResult:
|
|
506
|
+
"""Loads an MCP toolset based on connection parameters."""
|
|
507
|
+
from pydantic import TypeAdapter
|
|
508
|
+
|
|
509
|
+
mcp_tool_adapter = TypeAdapter(McpToolConfig)
|
|
510
|
+
tool_config_model = mcp_tool_adapter.validate_python(tool_config)
|
|
511
|
+
|
|
512
|
+
connection_params_config = tool_config_model.connection_params
|
|
513
|
+
if not connection_params_config:
|
|
514
|
+
raise ValueError("'connection_params' required for mcp tool.")
|
|
515
|
+
|
|
516
|
+
connection_type = connection_params_config.get("type", "").lower()
|
|
517
|
+
connection_args = {
|
|
518
|
+
k: v for k, v in connection_params_config.items() if k != "type"
|
|
519
|
+
}
|
|
520
|
+
connection_args["timeout"] = connection_args.get("timeout", 30)
|
|
521
|
+
|
|
522
|
+
environment_variables = tool_config_model.environment_variables
|
|
523
|
+
env_param = {}
|
|
524
|
+
if connection_type == "stdio" and environment_variables:
|
|
525
|
+
if isinstance(environment_variables, dict):
|
|
526
|
+
env_param = environment_variables
|
|
527
|
+
log.debug(
|
|
528
|
+
"%s Found environment_variables for stdio MCP tool.",
|
|
529
|
+
component.log_identifier,
|
|
530
|
+
)
|
|
531
|
+
else:
|
|
532
|
+
log.warning(
|
|
533
|
+
"%s 'environment_variables' provided for stdio MCP tool but it is not a dictionary. Ignoring.",
|
|
534
|
+
component.log_identifier,
|
|
535
|
+
)
|
|
643
536
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
537
|
+
if connection_type == "stdio":
|
|
538
|
+
cmd_arg = connection_args.get("command")
|
|
539
|
+
args_list = connection_args.get("args", [])
|
|
540
|
+
if isinstance(cmd_arg, list):
|
|
541
|
+
command_str = " ".join(cmd_arg)
|
|
542
|
+
elif isinstance(cmd_arg, str):
|
|
543
|
+
command_str = cmd_arg
|
|
544
|
+
else:
|
|
545
|
+
raise ValueError(
|
|
546
|
+
f"MCP tool 'command' parameter must be a string or a list of strings, got {type(cmd_arg)}"
|
|
547
|
+
)
|
|
548
|
+
if not isinstance(args_list, list):
|
|
549
|
+
raise ValueError(
|
|
550
|
+
f"MCP tool 'args' parameter must be a list, got {type(args_list)}"
|
|
551
|
+
)
|
|
552
|
+
final_connection_args = {
|
|
553
|
+
k: v
|
|
554
|
+
for k, v in connection_args.items()
|
|
555
|
+
if k not in ["command", "args", "timeout"]
|
|
556
|
+
}
|
|
557
|
+
connection_params = StdioConnectionParams(
|
|
558
|
+
server_params=StdioServerParameters(
|
|
559
|
+
command=command_str,
|
|
560
|
+
args=args_list,
|
|
561
|
+
**final_connection_args,
|
|
562
|
+
env=env_param if env_param else None,
|
|
563
|
+
),
|
|
564
|
+
timeout=connection_args.get("timeout"),
|
|
565
|
+
)
|
|
651
566
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
567
|
+
elif connection_type == "sse":
|
|
568
|
+
connection_params = SseServerParams(**connection_args)
|
|
569
|
+
else:
|
|
570
|
+
raise ValueError(f"Unsupported MCP connection type: {connection_type}")
|
|
571
|
+
|
|
572
|
+
tool_filter_list = (
|
|
573
|
+
[tool_config_model.tool_name] if tool_config_model.tool_name else None
|
|
574
|
+
)
|
|
575
|
+
if tool_filter_list:
|
|
576
|
+
log.info(
|
|
577
|
+
"%s MCP tool config specifies tool_name: '%s'. Applying as tool_filter.",
|
|
578
|
+
component.log_identifier,
|
|
579
|
+
tool_config_model.tool_name,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
mcp_toolset_instance = EmbedResolvingMCPToolset(
|
|
583
|
+
connection_params=connection_params,
|
|
584
|
+
tool_filter=tool_filter_list,
|
|
585
|
+
tool_config=tool_config,
|
|
586
|
+
)
|
|
587
|
+
mcp_toolset_instance.origin = "mcp"
|
|
588
|
+
|
|
589
|
+
log.info(
|
|
590
|
+
"%s Initialized MCPToolset (filter: %s) for server: %s",
|
|
591
|
+
component.log_identifier,
|
|
592
|
+
(tool_filter_list if tool_filter_list else "none (all tools)"),
|
|
593
|
+
connection_params,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
return [mcp_toolset_instance], [], []
|
|
597
|
+
|
|
598
|
+
def _load_internal_tools(component: "SamAgentComponent", loaded_tool_names: Set[str]) -> ToolLoadingResult:
|
|
599
|
+
"""Loads internal framework tools that are not explicitly configured by the user."""
|
|
600
|
+
loaded_tools: List[Union[BaseTool, Callable]] = []
|
|
601
|
+
enabled_builtin_tools: List[BuiltinTool] = []
|
|
660
602
|
|
|
661
603
|
internal_tool_names = ["_notify_artifact_save"]
|
|
662
604
|
if component.get_config("enable_auto_continuation", True):
|
|
@@ -664,7 +606,7 @@ async def load_adk_tools(
|
|
|
664
606
|
|
|
665
607
|
for tool_name in internal_tool_names:
|
|
666
608
|
try:
|
|
667
|
-
_check_and_register_tool_name(tool_name, "internal")
|
|
609
|
+
_check_and_register_tool_name(tool_name, "internal", loaded_tool_names)
|
|
668
610
|
except ValueError:
|
|
669
611
|
log.debug(
|
|
670
612
|
"%s Internal tool '%s' was already loaded explicitly. Skipping implicit load.",
|
|
@@ -699,6 +641,139 @@ async def load_adk_tools(
|
|
|
699
641
|
tool_name,
|
|
700
642
|
)
|
|
701
643
|
|
|
644
|
+
return loaded_tools, enabled_builtin_tools, []
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
async def load_adk_tools(
|
|
648
|
+
component,
|
|
649
|
+
) -> Tuple[List[Union[BaseTool, Callable]], List[BuiltinTool], List[Callable]]:
|
|
650
|
+
"""
|
|
651
|
+
Loads all configured tools for the agent.
|
|
652
|
+
- Explicitly configured tools (Python, MCP, ADK Built-ins) from YAML.
|
|
653
|
+
- SAM Built-in tools (Artifact, Data, etc.) from the tool registry,
|
|
654
|
+
filtered by agent configuration.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
component: The SamAgentComponent instance.
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
A tuple containing:
|
|
661
|
+
- A list of loaded tool callables/instances for the ADK agent.
|
|
662
|
+
- A list of enabled BuiltinTool definition objects for prompt generation.
|
|
663
|
+
- A list of awaitable cleanup functions for the tools.
|
|
664
|
+
|
|
665
|
+
Raises:
|
|
666
|
+
ImportError: If a configured tool or its dependencies cannot be loaded.
|
|
667
|
+
"""
|
|
668
|
+
loaded_tools: List[Union[BaseTool, Callable]] = []
|
|
669
|
+
enabled_builtin_tools: List[BuiltinTool] = []
|
|
670
|
+
loaded_tool_names: Set[str] = set()
|
|
671
|
+
cleanup_hooks: List[Callable] = []
|
|
672
|
+
tools_config = component.get_config("tools", [])
|
|
673
|
+
|
|
674
|
+
from pydantic import TypeAdapter, ValidationError
|
|
675
|
+
|
|
676
|
+
any_tool_adapter = TypeAdapter(AnyToolConfig)
|
|
677
|
+
|
|
678
|
+
if not tools_config:
|
|
679
|
+
log.info(
|
|
680
|
+
"%s No explicit tools configured in 'tools' list.", component.log_identifier
|
|
681
|
+
)
|
|
682
|
+
else:
|
|
683
|
+
log.info(
|
|
684
|
+
"%s Loading %d tool(s) from 'tools' list configuration...",
|
|
685
|
+
component.log_identifier,
|
|
686
|
+
len(tools_config),
|
|
687
|
+
)
|
|
688
|
+
for tool_config in tools_config:
|
|
689
|
+
try:
|
|
690
|
+
tool_config_model = any_tool_adapter.validate_python(tool_config)
|
|
691
|
+
tool_type = tool_config_model.tool_type.lower()
|
|
692
|
+
|
|
693
|
+
new_tools, new_builtins, new_cleanups = [], [], []
|
|
694
|
+
|
|
695
|
+
if tool_type == "python":
|
|
696
|
+
(
|
|
697
|
+
new_tools,
|
|
698
|
+
new_builtins,
|
|
699
|
+
new_cleanups,
|
|
700
|
+
) = await _load_python_tool(component, tool_config)
|
|
701
|
+
elif tool_type == "builtin":
|
|
702
|
+
(
|
|
703
|
+
new_tools,
|
|
704
|
+
new_builtins,
|
|
705
|
+
new_cleanups,
|
|
706
|
+
) = await _load_builtin_tool(component, tool_config)
|
|
707
|
+
elif tool_type == "builtin-group":
|
|
708
|
+
(
|
|
709
|
+
new_tools,
|
|
710
|
+
new_builtins,
|
|
711
|
+
new_cleanups,
|
|
712
|
+
) = await _load_builtin_group_tool(component, tool_config)
|
|
713
|
+
elif tool_type == "mcp":
|
|
714
|
+
(
|
|
715
|
+
new_tools,
|
|
716
|
+
new_builtins,
|
|
717
|
+
new_cleanups,
|
|
718
|
+
) = await _load_mcp_tool(component, tool_config)
|
|
719
|
+
else:
|
|
720
|
+
log.warning(
|
|
721
|
+
"%s Unknown tool type '%s' in config: %s",
|
|
722
|
+
component.log_identifier,
|
|
723
|
+
tool_type,
|
|
724
|
+
tool_config,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
# Centralized name checking and result aggregation
|
|
728
|
+
for tool in new_tools:
|
|
729
|
+
if isinstance(tool, EmbedResolvingMCPToolset):
|
|
730
|
+
# Special handling for MCPToolset which can load multiple tools
|
|
731
|
+
try:
|
|
732
|
+
mcp_tools = await tool.get_tools()
|
|
733
|
+
for mcp_tool in mcp_tools:
|
|
734
|
+
_check_and_register_tool_name(
|
|
735
|
+
mcp_tool.name, "mcp", loaded_tool_names
|
|
736
|
+
)
|
|
737
|
+
except Exception as e:
|
|
738
|
+
log.error(
|
|
739
|
+
"%s Failed to discover tools from MCP server for name registration: %s",
|
|
740
|
+
component.log_identifier,
|
|
741
|
+
str(e),
|
|
742
|
+
)
|
|
743
|
+
raise
|
|
744
|
+
else:
|
|
745
|
+
tool_name = getattr(
|
|
746
|
+
tool, "name", getattr(tool, "__name__", None)
|
|
747
|
+
)
|
|
748
|
+
if tool_name:
|
|
749
|
+
_check_and_register_tool_name(
|
|
750
|
+
tool_name, tool_type, loaded_tool_names
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
loaded_tools.extend(new_tools)
|
|
754
|
+
enabled_builtin_tools.extend(new_builtins)
|
|
755
|
+
# Prepend cleanup hooks to maintain LIFO execution order
|
|
756
|
+
cleanup_hooks = new_cleanups + cleanup_hooks
|
|
757
|
+
|
|
758
|
+
except Exception as e:
|
|
759
|
+
log.error(
|
|
760
|
+
"%s Failed to load tool config %s: %s",
|
|
761
|
+
component.log_identifier,
|
|
762
|
+
tool_config,
|
|
763
|
+
e,
|
|
764
|
+
)
|
|
765
|
+
raise e
|
|
766
|
+
|
|
767
|
+
# Load internal framework tools
|
|
768
|
+
(
|
|
769
|
+
internal_tools,
|
|
770
|
+
internal_builtins,
|
|
771
|
+
internal_cleanups,
|
|
772
|
+
) = _load_internal_tools(component, loaded_tool_names)
|
|
773
|
+
loaded_tools.extend(internal_tools)
|
|
774
|
+
enabled_builtin_tools.extend(internal_builtins)
|
|
775
|
+
cleanup_hooks.extend(internal_cleanups)
|
|
776
|
+
|
|
702
777
|
log.info(
|
|
703
778
|
"%s Finished loading tools. Total tools for ADK: %d. Total SAM built-ins for prompt: %d. Total cleanup hooks: %d. Peer tools added dynamically.",
|
|
704
779
|
component.log_identifier,
|