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.

Files changed (115) hide show
  1. agentrun_operation_sdk/cli/__init__.py +1 -0
  2. agentrun_operation_sdk/cli/cli.py +19 -0
  3. agentrun_operation_sdk/cli/common.py +21 -0
  4. agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
  5. agentrun_operation_sdk/cli/runtime/commands.py +203 -0
  6. agentrun_operation_sdk/client/client.py +75 -0
  7. agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
  8. agentrun_operation_sdk/operations/runtime/configure.py +101 -0
  9. agentrun_operation_sdk/operations/runtime/launch.py +82 -0
  10. agentrun_operation_sdk/operations/runtime/models.py +31 -0
  11. agentrun_operation_sdk/services/runtime.py +152 -0
  12. agentrun_operation_sdk/utils/logging_config.py +72 -0
  13. agentrun_operation_sdk/utils/runtime/config.py +94 -0
  14. agentrun_operation_sdk/utils/runtime/container.py +280 -0
  15. agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
  16. agentrun_operation_sdk/utils/runtime/schema.py +56 -0
  17. agentrun_sdk/__init__.py +7 -0
  18. agentrun_sdk/agent/__init__.py +25 -0
  19. agentrun_sdk/agent/agent.py +696 -0
  20. agentrun_sdk/agent/agent_result.py +46 -0
  21. agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
  22. agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
  23. agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
  24. agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
  25. agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
  26. agentrun_sdk/agent/state.py +97 -0
  27. agentrun_sdk/event_loop/__init__.py +9 -0
  28. agentrun_sdk/event_loop/event_loop.py +499 -0
  29. agentrun_sdk/event_loop/streaming.py +319 -0
  30. agentrun_sdk/experimental/__init__.py +4 -0
  31. agentrun_sdk/experimental/hooks/__init__.py +15 -0
  32. agentrun_sdk/experimental/hooks/events.py +123 -0
  33. agentrun_sdk/handlers/__init__.py +10 -0
  34. agentrun_sdk/handlers/callback_handler.py +70 -0
  35. agentrun_sdk/hooks/__init__.py +49 -0
  36. agentrun_sdk/hooks/events.py +80 -0
  37. agentrun_sdk/hooks/registry.py +247 -0
  38. agentrun_sdk/models/__init__.py +10 -0
  39. agentrun_sdk/models/anthropic.py +432 -0
  40. agentrun_sdk/models/bedrock.py +649 -0
  41. agentrun_sdk/models/litellm.py +225 -0
  42. agentrun_sdk/models/llamaapi.py +438 -0
  43. agentrun_sdk/models/mistral.py +539 -0
  44. agentrun_sdk/models/model.py +95 -0
  45. agentrun_sdk/models/ollama.py +357 -0
  46. agentrun_sdk/models/openai.py +436 -0
  47. agentrun_sdk/models/sagemaker.py +598 -0
  48. agentrun_sdk/models/writer.py +449 -0
  49. agentrun_sdk/multiagent/__init__.py +22 -0
  50. agentrun_sdk/multiagent/a2a/__init__.py +15 -0
  51. agentrun_sdk/multiagent/a2a/executor.py +148 -0
  52. agentrun_sdk/multiagent/a2a/server.py +252 -0
  53. agentrun_sdk/multiagent/base.py +92 -0
  54. agentrun_sdk/multiagent/graph.py +555 -0
  55. agentrun_sdk/multiagent/swarm.py +656 -0
  56. agentrun_sdk/py.typed +1 -0
  57. agentrun_sdk/session/__init__.py +18 -0
  58. agentrun_sdk/session/file_session_manager.py +216 -0
  59. agentrun_sdk/session/repository_session_manager.py +152 -0
  60. agentrun_sdk/session/s3_session_manager.py +272 -0
  61. agentrun_sdk/session/session_manager.py +73 -0
  62. agentrun_sdk/session/session_repository.py +51 -0
  63. agentrun_sdk/telemetry/__init__.py +21 -0
  64. agentrun_sdk/telemetry/config.py +194 -0
  65. agentrun_sdk/telemetry/metrics.py +476 -0
  66. agentrun_sdk/telemetry/metrics_constants.py +15 -0
  67. agentrun_sdk/telemetry/tracer.py +563 -0
  68. agentrun_sdk/tools/__init__.py +17 -0
  69. agentrun_sdk/tools/decorator.py +569 -0
  70. agentrun_sdk/tools/executor.py +137 -0
  71. agentrun_sdk/tools/loader.py +152 -0
  72. agentrun_sdk/tools/mcp/__init__.py +13 -0
  73. agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
  74. agentrun_sdk/tools/mcp/mcp_client.py +423 -0
  75. agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
  76. agentrun_sdk/tools/mcp/mcp_types.py +63 -0
  77. agentrun_sdk/tools/registry.py +607 -0
  78. agentrun_sdk/tools/structured_output.py +421 -0
  79. agentrun_sdk/tools/tools.py +217 -0
  80. agentrun_sdk/tools/watcher.py +136 -0
  81. agentrun_sdk/types/__init__.py +5 -0
  82. agentrun_sdk/types/collections.py +23 -0
  83. agentrun_sdk/types/content.py +188 -0
  84. agentrun_sdk/types/event_loop.py +48 -0
  85. agentrun_sdk/types/exceptions.py +81 -0
  86. agentrun_sdk/types/guardrails.py +254 -0
  87. agentrun_sdk/types/media.py +89 -0
  88. agentrun_sdk/types/session.py +152 -0
  89. agentrun_sdk/types/streaming.py +201 -0
  90. agentrun_sdk/types/tools.py +258 -0
  91. agentrun_sdk/types/traces.py +5 -0
  92. agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
  93. agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
  94. agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
  95. agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
  96. agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
  97. agentrun_wrapper/__init__.py +11 -0
  98. agentrun_wrapper/_utils/__init__.py +6 -0
  99. agentrun_wrapper/_utils/endpoints.py +16 -0
  100. agentrun_wrapper/identity/__init__.py +5 -0
  101. agentrun_wrapper/identity/auth.py +211 -0
  102. agentrun_wrapper/memory/__init__.py +6 -0
  103. agentrun_wrapper/memory/client.py +1697 -0
  104. agentrun_wrapper/memory/constants.py +103 -0
  105. agentrun_wrapper/memory/controlplane.py +626 -0
  106. agentrun_wrapper/py.typed +1 -0
  107. agentrun_wrapper/runtime/__init__.py +13 -0
  108. agentrun_wrapper/runtime/app.py +473 -0
  109. agentrun_wrapper/runtime/context.py +34 -0
  110. agentrun_wrapper/runtime/models.py +25 -0
  111. agentrun_wrapper/services/__init__.py +1 -0
  112. agentrun_wrapper/services/identity.py +192 -0
  113. agentrun_wrapper/tools/__init__.py +6 -0
  114. agentrun_wrapper/tools/browser_client.py +325 -0
  115. 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