solace-agent-mesh 1.3.0__py3-none-any.whl → 1.3.1__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.

Files changed (88) hide show
  1. solace_agent_mesh/agent/adk/setup.py +141 -34
  2. solace_agent_mesh/agent/sac/app.py +1 -2
  3. solace_agent_mesh/agent/tools/__init__.py +1 -0
  4. solace_agent_mesh/agent/tools/dynamic_tool.py +362 -0
  5. solace_agent_mesh/assets/docs/404.html +3 -3
  6. solace_agent_mesh/assets/docs/assets/js/42b3f8d8.d97b8e94.js +1 -0
  7. solace_agent_mesh/assets/docs/assets/js/483cef9a.4e972867.js +1 -0
  8. solace_agent_mesh/assets/docs/assets/js/55f47984.cf3781c4.js +1 -0
  9. solace_agent_mesh/assets/docs/assets/js/664b740a.1b744a32.js +1 -0
  10. solace_agent_mesh/assets/docs/assets/js/75384d09.c193a8f0.js +1 -0
  11. solace_agent_mesh/assets/docs/assets/js/9a09e75d.d6607c56.js +1 -0
  12. solace_agent_mesh/assets/docs/assets/js/aba87c2f.071e2d94.js +1 -0
  13. solace_agent_mesh/assets/docs/assets/js/ae0e903d.4d8dda10.js +1 -0
  14. solace_agent_mesh/assets/docs/assets/js/c835a94d.146e3186.js +1 -0
  15. solace_agent_mesh/assets/docs/assets/js/f284c35a.7334119c.js +1 -0
  16. solace_agent_mesh/assets/docs/assets/js/main.1c79039d.js +2 -0
  17. solace_agent_mesh/assets/docs/assets/js/runtime~main.858117b7.js +1 -0
  18. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/installation/index.html +29 -0
  19. solace_agent_mesh/assets/docs/docs/documentation/Enterprise/single-sign-on/index.html +25 -0
  20. solace_agent_mesh/assets/docs/docs/documentation/{migration-guides/a2a-upgrade-to-0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html → Migrations/A2A Upgrade To 0.3.0/a2a-gateway-upgrade-to-0.3.0/index.html } +6 -6
  21. solace_agent_mesh/assets/docs/docs/documentation/{migration-guides/a2a-upgrade-to-0.3.0/a2a-technical-migration-map/index.html → Migrations/A2A Upgrade To 0.3.0/a2a-technical-migration-map/index.html } +6 -6
  22. solace_agent_mesh/assets/docs/docs/documentation/concepts/agents/index.html +19 -27
  23. solace_agent_mesh/assets/docs/docs/documentation/concepts/architecture/index.html +4 -4
  24. solace_agent_mesh/assets/docs/docs/documentation/concepts/cli/index.html +4 -4
  25. solace_agent_mesh/assets/docs/docs/documentation/concepts/gateways/index.html +4 -4
  26. solace_agent_mesh/assets/docs/docs/documentation/concepts/orchestrator/index.html +4 -4
  27. solace_agent_mesh/assets/docs/docs/documentation/concepts/plugins/index.html +4 -4
  28. solace_agent_mesh/assets/docs/docs/documentation/deployment/debugging/index.html +4 -4
  29. solace_agent_mesh/assets/docs/docs/documentation/deployment/deploy/index.html +4 -4
  30. solace_agent_mesh/assets/docs/docs/documentation/deployment/observability/index.html +4 -4
  31. solace_agent_mesh/assets/docs/docs/documentation/getting-started/component-overview/index.html +4 -4
  32. solace_agent_mesh/assets/docs/docs/documentation/getting-started/configurations/index.html +4 -4
  33. solace_agent_mesh/assets/docs/docs/documentation/getting-started/installation/index.html +4 -4
  34. solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +4 -4
  35. solace_agent_mesh/assets/docs/docs/documentation/getting-started/quick-start/index.html +4 -4
  36. solace_agent_mesh/assets/docs/docs/documentation/tutorials/bedrock-agents/index.html +4 -4
  37. solace_agent_mesh/assets/docs/docs/documentation/tutorials/custom-agent/index.html +4 -4
  38. solace_agent_mesh/assets/docs/docs/documentation/tutorials/event-mesh-gateway/index.html +4 -4
  39. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mcp-integration/index.html +4 -4
  40. solace_agent_mesh/assets/docs/docs/documentation/tutorials/mongodb-integration/index.html +4 -4
  41. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rag-integration/index.html +4 -4
  42. solace_agent_mesh/assets/docs/docs/documentation/tutorials/rest-gateway/index.html +4 -4
  43. solace_agent_mesh/assets/docs/docs/documentation/tutorials/slack-integration/index.html +4 -4
  44. solace_agent_mesh/assets/docs/docs/documentation/tutorials/sql-database/index.html +4 -4
  45. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/artifact-management/index.html +4 -4
  46. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/audio-tools/index.html +4 -4
  47. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/data-analysis-tools/index.html +4 -4
  48. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/embeds/index.html +4 -4
  49. solace_agent_mesh/assets/docs/docs/documentation/user-guide/builtin-tools/index.html +4 -4
  50. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-agents/index.html +5 -4
  51. solace_agent_mesh/assets/docs/docs/documentation/user-guide/create-gateways/index.html +4 -4
  52. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-python-tools/index.html +63 -0
  53. solace_agent_mesh/assets/docs/docs/documentation/user-guide/creating-service-providers/index.html +4 -4
  54. solace_agent_mesh/assets/docs/docs/documentation/user-guide/solace-ai-connector/index.html +4 -4
  55. solace_agent_mesh/assets/docs/docs/documentation/user-guide/structure/index.html +4 -4
  56. solace_agent_mesh/assets/docs/lunr-index-1757531604543.json +1 -0
  57. solace_agent_mesh/assets/docs/lunr-index.json +1 -1
  58. solace_agent_mesh/assets/docs/search-doc-1757531604543.json +1 -0
  59. solace_agent_mesh/assets/docs/search-doc.json +1 -1
  60. solace_agent_mesh/assets/docs/sitemap.xml +1 -1
  61. solace_agent_mesh/cli/__init__.py +1 -1
  62. solace_agent_mesh/client/webui/frontend/static/assets/{authCallback-vY5eu2lI.js → authCallback-CAX9u8a7.js} +1 -1
  63. solace_agent_mesh/client/webui/frontend/static/assets/{client-BeBkzgWW.js → client-DXU9SPI5.js} +1 -1
  64. solace_agent_mesh/client/webui/frontend/static/assets/{main-Bjys1KQs.js → main-C1k9E0aC.js} +26 -26
  65. solace_agent_mesh/client/webui/frontend/static/assets/{vendor-CE0AeXyK.js → vendor-B0BEKoAR.js} +69 -74
  66. solace_agent_mesh/client/webui/frontend/static/auth-callback.html +3 -3
  67. solace_agent_mesh/client/webui/frontend/static/index.html +3 -3
  68. solace_agent_mesh/gateway/http_sse/dependencies.py +6 -6
  69. solace_agent_mesh/gateway/http_sse/main.py +2 -2
  70. solace_agent_mesh/gateway/http_sse/routers/{agents.py → agent_cards.py} +7 -7
  71. solace_agent_mesh/gateway/http_sse/services/{agent_service.py → agent_card_service.py} +19 -19
  72. {solace_agent_mesh-1.3.0.dist-info → solace_agent_mesh-1.3.1.dist-info}/METADATA +1 -1
  73. {solace_agent_mesh-1.3.0.dist-info → solace_agent_mesh-1.3.1.dist-info}/RECORD +77 -70
  74. solace_agent_mesh/assets/docs/assets/js/42b3f8d8.8ccb9901.js +0 -1
  75. solace_agent_mesh/assets/docs/assets/js/55f47984.c484bf96.js +0 -1
  76. solace_agent_mesh/assets/docs/assets/js/6e0db977.39a79ca9.js +0 -1
  77. solace_agent_mesh/assets/docs/assets/js/75384d09.bf78fbdb.js +0 -1
  78. solace_agent_mesh/assets/docs/assets/js/90dd9cf6.88f385ea.js +0 -1
  79. solace_agent_mesh/assets/docs/assets/js/aba87c2f.76376d7c.js +0 -1
  80. solace_agent_mesh/assets/docs/assets/js/f284c35a.fb68323a.js +0 -1
  81. solace_agent_mesh/assets/docs/assets/js/main.08d30374.js +0 -2
  82. solace_agent_mesh/assets/docs/assets/js/runtime~main.458efb1d.js +0 -1
  83. solace_agent_mesh/assets/docs/lunr-index-1757433031159.json +0 -1
  84. solace_agent_mesh/assets/docs/search-doc-1757433031159.json +0 -1
  85. /solace_agent_mesh/assets/docs/assets/js/{main.08d30374.js.LICENSE.txt → main.1c79039d.js.LICENSE.txt} +0 -0
  86. {solace_agent_mesh-1.3.0.dist-info → solace_agent_mesh-1.3.1.dist-info}/WHEEL +0 -0
  87. {solace_agent_mesh-1.3.0.dist-info → solace_agent_mesh-1.3.1.dist-info}/entry_points.txt +0 -0
  88. {solace_agent_mesh-1.3.0.dist-info → solace_agent_mesh-1.3.1.dist-info}/licenses/LICENSE +0 -0
@@ -18,7 +18,6 @@ from google.adk import tools as adk_tools_module
18
18
  from google.adk.agents.callback_context import CallbackContext
19
19
  from google.adk.models.llm_request import LlmRequest
20
20
  from google.adk.models.llm_response import LlmResponse
21
- from google.adk.tools.mcp_tool import MCPToolset
22
21
  from google.adk.tools.mcp_tool.mcp_session_manager import (
23
22
  SseServerParams,
24
23
  StdioConnectionParams,
@@ -28,12 +27,49 @@ from mcp import StdioServerParameters
28
27
 
29
28
  from ..tools.registry import tool_registry
30
29
  from ..tools.tool_definition import BuiltinTool
30
+ from ..tools.dynamic_tool import DynamicTool, DynamicToolProvider
31
31
 
32
32
 
33
33
  from ...agent.adk import callbacks as adk_callbacks
34
34
  from ...agent.adk.models.lite_llm import LiteLlm
35
35
 
36
36
 
37
+ def _find_dynamic_tool_class(module) -> Optional[type]:
38
+ """Finds a single non-abstract DynamicTool subclass in a module."""
39
+ found_classes = []
40
+ for name, obj in inspect.getmembers(module, inspect.isclass):
41
+ if (
42
+ issubclass(obj, DynamicTool)
43
+ and obj is not DynamicTool
44
+ and not inspect.isabstract(obj)
45
+ ):
46
+ found_classes.append(obj)
47
+ if len(found_classes) > 1:
48
+ raise TypeError(
49
+ f"Module '{module.__name__}' contains multiple DynamicTool subclasses. "
50
+ "Please specify which one to use with 'class_name' in the config."
51
+ )
52
+ return found_classes[0] if found_classes else None
53
+
54
+
55
+ def _find_dynamic_tool_provider_class(module) -> Optional[type]:
56
+ """Finds a single non-abstract DynamicToolProvider subclass in a module."""
57
+ found_classes = []
58
+ for name, obj in inspect.getmembers(module, inspect.isclass):
59
+ if (
60
+ issubclass(obj, DynamicToolProvider)
61
+ and obj is not DynamicToolProvider
62
+ and not inspect.isabstract(obj)
63
+ ):
64
+ found_classes.append(obj)
65
+ if len(found_classes) > 1:
66
+ raise TypeError(
67
+ f"Module '{module.__name__}' contains multiple DynamicToolProvider subclasses. "
68
+ "Only one is permitted per module."
69
+ )
70
+ return found_classes[0] if found_classes else None
71
+
72
+
37
73
  async def load_adk_tools(
38
74
  component,
39
75
  ) -> Tuple[List[Union[BaseTool, Callable]], List[BuiltinTool]]:
@@ -90,48 +126,119 @@ async def load_adk_tools(
90
126
  try:
91
127
  if tool_type == "python":
92
128
  module_name = tool_config.get("component_module")
93
- function_name = tool_config.get("function_name")
94
- tool_name = tool_config.get("tool_name")
95
- tool_description = tool_config.get("tool_description")
96
129
  base_path = tool_config.get("component_base_path")
97
- if not module_name or not function_name:
130
+ if not module_name:
98
131
  raise ValueError(
99
- "'component_module' and 'function_name' required for python tool."
132
+ "'component_module' is required for python tools."
100
133
  )
101
-
102
134
  module = import_module(module_name, base_path=base_path)
103
- func = getattr(module, function_name)
104
- if not callable(func):
105
- raise TypeError(
106
- f"'{function_name}' in module '{module_name}' is not callable."
135
+
136
+ # Case 1: Simple function-based tool
137
+ if "function_name" in tool_config:
138
+ function_name = tool_config.get("function_name")
139
+ tool_name = tool_config.get("tool_name")
140
+ tool_description = tool_config.get("tool_description")
141
+
142
+ func = getattr(module, function_name)
143
+ if not callable(func):
144
+ raise TypeError(
145
+ f"'{function_name}' in module '{module_name}' is not callable."
146
+ )
147
+
148
+ specific_tool_config = tool_config.get("tool_config")
149
+ tool_callable = ADKToolWrapper(
150
+ func,
151
+ specific_tool_config,
152
+ function_name,
153
+ origin="python",
154
+ raw_string_args=tool_config.get("raw_string_args", []),
107
155
  )
108
156
 
109
- specific_tool_config = tool_config.get("tool_config")
110
- tool_callable = ADKToolWrapper(
111
- func,
112
- specific_tool_config,
113
- function_name,
114
- origin="python",
115
- raw_string_args=tool_config.get("raw_string_args", []),
116
- )
157
+ if tool_name:
158
+ function_name = tool_name
159
+ tool_callable.__name__ = tool_name
160
+
161
+ if tool_description:
162
+ tool_callable.__doc__ = tool_description
163
+
164
+ _check_and_register_tool_name(
165
+ function_name, f"python:{module_name}"
166
+ )
167
+ loaded_tools.append(tool_callable)
168
+ log.info(
169
+ "%s Loaded Python tool: %s from %s.",
170
+ component.log_identifier,
171
+ function_name,
172
+ module_name,
173
+ )
174
+ # Case 2: Advanced class-based dynamic tool or provider
175
+ else:
176
+ specific_tool_config = tool_config.get("tool_config")
177
+ dynamic_tools = []
117
178
 
118
- if tool_name:
119
- function_name = tool_name
120
- tool_callable.__name__ = tool_name
179
+ # Determine the class to load
180
+ tool_class = None
181
+ class_name = tool_config.get("class_name")
182
+ if class_name:
183
+ tool_class = getattr(module, class_name)
184
+ else:
185
+ # Auto-discover: provider first, then single tool
186
+ tool_class = _find_dynamic_tool_provider_class(module)
187
+ if not tool_class:
188
+ tool_class = _find_dynamic_tool_class(module)
189
+
190
+ if not tool_class:
191
+ raise TypeError(
192
+ f"Module '{module_name}' does not contain a 'function_name' or 'class_name' to load, "
193
+ "and no DynamicTool or DynamicToolProvider subclass could be auto-discovered."
194
+ )
121
195
 
122
- if tool_description:
123
- tool_callable.__doc__ = tool_description
196
+ # Instantiate tools from the class
197
+ if issubclass(tool_class, DynamicToolProvider):
198
+ provider_instance = tool_class()
199
+ dynamic_tools = (
200
+ provider_instance.get_all_tools_for_framework(
201
+ tool_config=specific_tool_config
202
+ )
203
+ )
204
+ log.info(
205
+ "%s Loaded %d tools from DynamicToolProvider '%s' in %s",
206
+ component.log_identifier,
207
+ len(dynamic_tools),
208
+ tool_class.__name__,
209
+ module_name,
210
+ )
211
+ elif issubclass(tool_class, DynamicTool):
212
+ tool_instance = tool_class(tool_config=specific_tool_config)
213
+ dynamic_tools = [tool_instance]
214
+ else:
215
+ raise TypeError(
216
+ f"Class '{tool_class.__name__}' in module '{module_name}' is not a valid "
217
+ "DynamicTool or DynamicToolProvider subclass."
218
+ )
124
219
 
125
- _check_and_register_tool_name(
126
- function_name, f"python:{module_name}"
127
- )
128
- loaded_tools.append(tool_callable)
129
- log.info(
130
- "%s Loaded Python tool: %s from %s.",
131
- component.log_identifier,
132
- function_name,
133
- module_name,
134
- )
220
+ # Process all generated tools
221
+ for tool in dynamic_tools:
222
+ tool.origin = "dynamic"
223
+ declaration = tool._get_declaration()
224
+ if not declaration:
225
+ log.warning(
226
+ "Dynamic tool '%s' from module '%s' did not generate a valid declaration. Skipping.",
227
+ tool.__class__.__name__,
228
+ module_name,
229
+ )
230
+ continue
231
+
232
+ _check_and_register_tool_name(
233
+ declaration.name, f"dynamic:{module_name}"
234
+ )
235
+ loaded_tools.append(tool)
236
+ log.info(
237
+ "%s Loaded dynamic tool: %s from %s",
238
+ component.log_identifier,
239
+ declaration.name,
240
+ module_name,
241
+ )
135
242
 
136
243
  elif tool_type == "builtin":
137
244
  tool_name = tool_config.get("tool_name")
@@ -14,7 +14,6 @@ patch_adk()
14
14
 
15
15
  from typing import Any, Dict
16
16
  from solace_ai_connector.flow.app import App
17
- from solace_ai_connector.common.log import log
18
17
 
19
18
  from ...common.a2a import (
20
19
  get_agent_request_topic,
@@ -714,4 +713,4 @@ class SamAgentApp(App):
714
713
  log.debug("Set broker_config.temporary_queue = True")
715
714
 
716
715
  super().__init__(app_info, **kwargs)
717
- log.debug("A2A_ADK_App initialization complete.")
716
+ log.debug("A2A_ADK_App initialization complete.")
@@ -12,3 +12,4 @@ from . import audio_tools
12
12
  from . import image_tools
13
13
  from . import web_tools
14
14
  from . import test_tools
15
+ from . import dynamic_tool
@@ -0,0 +1,362 @@
1
+ """
2
+ Defines the base classes and helpers for "dynamic" tools.
3
+ Dynamic tools allow for programmatic definition of tool names, descriptions,
4
+ and parameter schemas, offering more flexibility than standard Python tools.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import (
9
+ Optional,
10
+ List,
11
+ Callable,
12
+ Dict,
13
+ Any,
14
+ get_origin,
15
+ get_args,
16
+ Union,
17
+ Literal,
18
+ )
19
+ import inspect
20
+
21
+ from google.adk.tools import BaseTool, ToolContext
22
+ from google.genai import types as adk_types
23
+ from solace_ai_connector.common.log import log
24
+
25
+ from solace_agent_mesh.agent.utils.context_helpers import get_original_session_id
26
+
27
+ from ...common.utils.embeds import (
28
+ resolve_embeds_in_string,
29
+ evaluate_embed,
30
+ EARLY_EMBED_TYPES,
31
+ LATE_EMBED_TYPES,
32
+ EMBED_DELIMITER_OPEN,
33
+ )
34
+
35
+
36
+ # --- Base Class for Programmatic Tools ---
37
+
38
+
39
+ class DynamicTool(BaseTool, ABC):
40
+ """
41
+ Base class for dynamic tools that can define their own function names,
42
+ descriptions, and parameter schemas programmatically.
43
+ """
44
+
45
+ def __init__(self, tool_config: Optional[dict] = None):
46
+ # Initialize with placeholder values, will be overridden by properties
47
+ super().__init__(
48
+ name="dynamic_tool_placeholder", description="dynamic_tool_placeholder"
49
+ )
50
+ self.tool_config = tool_config or {}
51
+
52
+ @property
53
+ @abstractmethod
54
+ def tool_name(self) -> str:
55
+ """Return the function name that the LLM will call."""
56
+ pass
57
+
58
+ @property
59
+ @abstractmethod
60
+ def tool_description(self) -> str:
61
+ """Return the description of what this tool does."""
62
+ pass
63
+
64
+ @property
65
+ @abstractmethod
66
+ def parameters_schema(self) -> adk_types.Schema:
67
+ """Return the ADK Schema defining the tool's parameters."""
68
+ pass
69
+
70
+ @property
71
+ def raw_string_args(self) -> List[str]:
72
+ """
73
+ Return a list of argument names that should not have embeds resolved.
74
+ Subclasses can override this property.
75
+ """
76
+ return []
77
+
78
+ @property
79
+ def resolution_type(self) -> Literal["early", "all"]:
80
+ """
81
+ Determines which embeds to resolve. 'early' resolves simple embeds like
82
+ math and uuid. 'all' also resolves 'artifact_content'.
83
+ Defaults to 'early'.
84
+ """
85
+ return "early"
86
+
87
+ def _get_declaration(self) -> Optional[Any]:
88
+ """
89
+ Generate the FunctionDeclaration for this dynamic tool.
90
+ This follows the same pattern as PeerAgentTool and MCP tools.
91
+ """
92
+ # Update the tool name to match what the module defines
93
+ self.name = self.tool_name
94
+
95
+ return adk_types.FunctionDeclaration(
96
+ name=self.tool_name,
97
+ description=self.tool_description,
98
+ parameters=self.parameters_schema,
99
+ )
100
+
101
+ async def run_async(
102
+ self, *, args: Dict[str, Any], tool_context: ToolContext
103
+ ) -> Dict[str, Any]:
104
+ """
105
+ Asynchronously runs the tool with the given arguments.
106
+ This method resolves embeds in arguments and then delegates the call
107
+ to the abstract _run_async_impl.
108
+ """
109
+ log_identifier = f"[DynamicTool:{self.tool_name}]"
110
+ resolved_kwargs = args.copy()
111
+
112
+ types_to_resolve = EARLY_EMBED_TYPES
113
+ if self.resolution_type == "all":
114
+ types_to_resolve = EARLY_EMBED_TYPES.union(LATE_EMBED_TYPES)
115
+
116
+ # Unlike ADKToolWrapper, DynamicTools receive all args in a single dict.
117
+ # We iterate through this dict to resolve embeds.
118
+ for key, value in args.items():
119
+ if key in self.raw_string_args and isinstance(value, str):
120
+ log.debug(
121
+ "%s Skipping embed resolution for raw string kwarg '%s'",
122
+ log_identifier,
123
+ key,
124
+ )
125
+ elif isinstance(value, str) and EMBED_DELIMITER_OPEN in value:
126
+ log.debug("%s Resolving embeds for kwarg '%s'", log_identifier, key)
127
+ # Create the resolution context
128
+ if hasattr(tool_context, "_invocation_context"):
129
+ # Use the invocation context if available
130
+ invocation_context = tool_context._invocation_context
131
+ else:
132
+ # Error if no invocation context is found
133
+ raise RuntimeError(
134
+ f"{log_identifier} No invocation context found in ToolContext. Cannot resolve embeds."
135
+ )
136
+ session_context = invocation_context.session
137
+ if not session_context:
138
+ raise RuntimeError(
139
+ f"{log_identifier} No session context found in invocation context. Cannot resolve embeds."
140
+ )
141
+ resolution_context = {
142
+ "artifact_service": invocation_context.artifact_service,
143
+ "session_context": {
144
+ "session_id": get_original_session_id(invocation_context),
145
+ "user_id": session_context.user_id,
146
+ "app_name": session_context.app_name,
147
+ },
148
+ }
149
+ resolved_value, _, _ = await resolve_embeds_in_string(
150
+ text=value,
151
+ context=resolution_context,
152
+ resolver_func=evaluate_embed,
153
+ types_to_resolve=types_to_resolve,
154
+ log_identifier=log_identifier,
155
+ config=self.tool_config,
156
+ )
157
+ resolved_kwargs[key] = resolved_value
158
+
159
+ return await self._run_async_impl(
160
+ args=resolved_kwargs, tool_context=tool_context, credential=None
161
+ )
162
+
163
+ @abstractmethod
164
+ async def _run_async_impl(
165
+ self, args: dict, tool_context: ToolContext, credential: Optional[str] = None
166
+ ) -> dict:
167
+ """
168
+ Implement the actual tool logic.
169
+ Must return a dictionary response.
170
+ """
171
+ pass
172
+
173
+
174
+ # --- Internal Adapter for Function-Based Tools ---
175
+
176
+
177
+ def _get_schema_from_signature(func: Callable) -> adk_types.Schema:
178
+ """
179
+ Introspects a function's signature and generates an ADK Schema for its parameters.
180
+ """
181
+ sig = inspect.signature(func)
182
+ properties = {}
183
+ required = []
184
+
185
+ type_map = {
186
+ str: adk_types.Type.STRING,
187
+ int: adk_types.Type.INTEGER,
188
+ float: adk_types.Type.NUMBER,
189
+ bool: adk_types.Type.BOOLEAN,
190
+ list: adk_types.Type.ARRAY,
191
+ dict: adk_types.Type.OBJECT,
192
+ }
193
+
194
+ for param in sig.parameters.values():
195
+ if param.name in ("tool_context", "tool_config", "kwargs", "self", "cls"):
196
+ continue
197
+
198
+ param_type = param.annotation
199
+ is_optional = False
200
+
201
+ # Handle Optional[T] which is Union[T, None]
202
+ origin = get_origin(param_type)
203
+ args = get_args(param_type)
204
+ if origin is Union and type(None) in args:
205
+ is_optional = True
206
+ # Get the actual type from Union[T, None]
207
+ param_type = next((t for t in args if t is not type(None)), Any)
208
+
209
+ adk_type = type_map.get(param_type)
210
+ if not adk_type:
211
+ # Default to string if type is not supported or specified (e.g., Any)
212
+ adk_type = adk_types.Type.STRING
213
+
214
+ properties[param.name] = adk_types.Schema(type=adk_type, nullable=is_optional)
215
+
216
+ if param.default is inspect.Parameter.empty and not is_optional:
217
+ required.append(param.name)
218
+
219
+ return adk_types.Schema(
220
+ type=adk_types.Type.OBJECT,
221
+ properties=properties,
222
+ required=required,
223
+ )
224
+
225
+
226
+ class _FunctionAsDynamicTool(DynamicTool):
227
+ """
228
+ Internal adapter to wrap a standard Python function as a DynamicTool.
229
+ """
230
+
231
+ def __init__(
232
+ self,
233
+ func: Callable,
234
+ tool_config: Optional[dict] = None,
235
+ provider_instance: Optional[Any] = None,
236
+ ):
237
+ super().__init__(tool_config=tool_config)
238
+ self._func = func
239
+ self._provider_instance = provider_instance
240
+ self._schema = _get_schema_from_signature(func)
241
+
242
+ # Check if the function is an instance method that needs `self`
243
+ self._is_instance_method = False
244
+ sig = inspect.signature(self._func)
245
+ if sig.parameters:
246
+ first_param = next(iter(sig.parameters.values()))
247
+ if first_param.name == "self":
248
+ self._is_instance_method = True
249
+
250
+ @property
251
+ def tool_name(self) -> str:
252
+ return self._func.__name__
253
+
254
+ @property
255
+ def tool_description(self) -> str:
256
+ return inspect.getdoc(self._func) or ""
257
+
258
+ @property
259
+ def parameters_schema(self) -> adk_types.Schema:
260
+ return self._schema
261
+
262
+ async def _run_async_impl(
263
+ self,
264
+ args: dict,
265
+ tool_context: ToolContext,
266
+ credential: Optional[str] = None,
267
+ ) -> dict:
268
+ # Inject tool_context and tool_config if the function expects them
269
+ sig = inspect.signature(self._func)
270
+ if "tool_context" in sig.parameters:
271
+ args["tool_context"] = tool_context
272
+ if "tool_config" in sig.parameters:
273
+ args["tool_config"] = self.tool_config
274
+
275
+ if self._provider_instance and self._is_instance_method:
276
+ # It's an instance method, call it on the provider instance
277
+ return await self._func(self._provider_instance, **args)
278
+ else:
279
+ # It's a static method or a standalone function
280
+ return await self._func(**args)
281
+
282
+
283
+ # --- Base Class for Tool Providers ---
284
+
285
+
286
+ class DynamicToolProvider(ABC):
287
+ """
288
+ Base class for dynamic tool providers that can generate a list of tools
289
+ programmatically from a single configuration block.
290
+ """
291
+
292
+ _decorated_tools: List[Callable] = []
293
+
294
+ @classmethod
295
+ def register_tool(cls, func: Callable) -> Callable:
296
+ """
297
+ A decorator to register a standard async function as a tool.
298
+ The decorated function's signature and docstring will be used to
299
+ create the tool definition.
300
+ """
301
+ # This check is crucial. It runs for each decorated method.
302
+ # If the current class `cls` is using the list from the base class
303
+ # `DynamicToolProvider`, it creates a new, empty list just for `cls`.
304
+ # On subsequent decorator calls for the same `cls`, this condition will
305
+ # be false, and it will append to the existing list.
306
+ if (
307
+ not hasattr(cls, "_decorated_tools")
308
+ or cls._decorated_tools is DynamicToolProvider._decorated_tools
309
+ ):
310
+ cls._decorated_tools = []
311
+
312
+ cls._decorated_tools.append(func)
313
+ return func
314
+
315
+ def _create_tools_from_decorators(
316
+ self, tool_config: Optional[dict] = None
317
+ ) -> List[DynamicTool]:
318
+ """
319
+ Internal helper to convert decorated functions into DynamicTool instances.
320
+ """
321
+ tools = []
322
+ for func in self._decorated_tools:
323
+ adapter = _FunctionAsDynamicTool(func, tool_config, provider_instance=self)
324
+ tools.append(adapter)
325
+ return tools
326
+
327
+ def get_all_tools_for_framework(
328
+ self, tool_config: Optional[dict] = None
329
+ ) -> List[DynamicTool]:
330
+ """
331
+ Framework-internal method that automatically combines decorated tools with custom tools.
332
+ This is called by the ADK setup code, not by users.
333
+
334
+ Args:
335
+ tool_config: The configuration dictionary from the agent's YAML file.
336
+
337
+ Returns:
338
+ A list of all DynamicTool objects (decorated + custom).
339
+ """
340
+ # Get tools from decorators automatically
341
+ decorated_tools = self._create_tools_from_decorators(tool_config)
342
+
343
+ # Get custom tools from the user's implementation
344
+ custom_tools = self.create_tools(tool_config)
345
+
346
+ return decorated_tools + custom_tools
347
+
348
+ @abstractmethod
349
+ def create_tools(self, tool_config: Optional[dict] = None) -> List[DynamicTool]:
350
+ """
351
+ Generate and return a list of custom DynamicTool instances.
352
+
353
+ Note: Tools registered with the @register_tool decorator are automatically
354
+ included by the framework - you don't need to handle them here.
355
+
356
+ Args:
357
+ tool_config: The configuration dictionary from the agent's YAML file.
358
+
359
+ Returns:
360
+ A list of custom DynamicTool objects (decorated tools are added automatically).
361
+ """
362
+ pass