Skip to content

Commit 2fff882

Browse files
wuliang229copybara-github
authored andcommitted
feat(config): implement from_config() for BaseTool
PiperOrigin-RevId: 791520708
1 parent a3b31ca commit 2fff882

File tree

3 files changed

+134
-9
lines changed

3 files changed

+134
-9
lines changed

src/google/adk/agents/config_agent_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def _resolve_agent_class(agent_class: str) -> type[BaseAgent]:
6969
if "." not in agent_class_name:
7070
agent_class_name = f"google.adk.agents.{agent_class_name}"
7171

72-
agent_class = _resolve_fully_qualified_name(agent_class_name)
72+
agent_class = resolve_fully_qualified_name(agent_class_name)
7373
if inspect.isclass(agent_class) and issubclass(agent_class, BaseAgent):
7474
return agent_class
7575

@@ -103,8 +103,8 @@ def _load_config_from_path(config_path: str) -> AgentConfig:
103103
return AgentConfig.model_validate(config_data)
104104

105105

106-
@working_in_progress("_resolve_fully_qualified_name is not ready for use.")
107-
def _resolve_fully_qualified_name(name: str) -> Any:
106+
@working_in_progress("resolve_fully_qualified_name is not ready for use.")
107+
def resolve_fully_qualified_name(name: str) -> Any:
108108
try:
109109
module_path, obj_name = name.rsplit(".", 1)
110110
module = importlib.import_module(module_path)

src/google/adk/tools/base_tool.py

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@
1515
from __future__ import annotations
1616

1717
from abc import ABC
18+
import inspect
19+
import logging
1820
from typing import Any
21+
from typing import Callable
22+
from typing import get_args
23+
from typing import get_origin
24+
from typing import get_type_hints
1925
from typing import Optional
2026
from typing import Type
2127
from typing import TYPE_CHECKING
2228
from typing import TypeVar
29+
from typing import Union
2330

2431
from google.genai import types
2532
from pydantic import BaseModel
@@ -29,6 +36,8 @@
2936
from ..utils.variant_utils import GoogleLLMVariant
3037
from .tool_context import ToolContext
3138

39+
logger = logging.getLogger("google_adk." + __name__)
40+
3241
if TYPE_CHECKING:
3342
from ..models.llm_request import LlmRequest
3443

@@ -134,8 +143,9 @@ def from_config(
134143
) -> SelfTool:
135144
"""Creates a tool instance from a config.
136145
137-
Subclasses should override and implement this method to do custom
138-
initialization from a config.
146+
This default implementation uses inspect to automatically map config values
147+
to constructor arguments based on their type hints. Subclasses should
148+
override this method for custom initialization logic.
139149
140150
Args:
141151
config: The config for the tool.
@@ -145,7 +155,66 @@ def from_config(
145155
Returns:
146156
The tool instance.
147157
"""
148-
raise NotImplementedError(f"from_config for {cls} not implemented.")
158+
from ..agents import config_agent_utils
159+
160+
# Get the constructor signature and resolve type hints
161+
sig = inspect.signature(cls.__init__)
162+
type_hints = get_type_hints(cls.__init__)
163+
config_dict = config.model_dump()
164+
kwargs = {}
165+
166+
# Iterate through constructor parameters (skip "self")
167+
for param_name, _ in sig.parameters.items():
168+
if param_name == "self":
169+
continue
170+
param_type = type_hints.get(param_name)
171+
172+
if param_name in config_dict:
173+
value = config_dict[param_name]
174+
175+
# Get the actual type T of the parameter if it's Optional[T]
176+
if get_origin(param_type) is Union:
177+
# This is Optional[T] which is Union[T, None]
178+
args = get_args(param_type)
179+
if len(args) == 2 and type(None) in args:
180+
# Get the non-None type
181+
actual_type = args[0] if args[1] is type(None) else args[1]
182+
param_type = actual_type
183+
184+
if param_type in (int, str, bool, float):
185+
kwargs[param_name] = value
186+
elif (
187+
inspect.isclass(param_type)
188+
and issubclass(param_type, BaseModel)
189+
and value is not None
190+
):
191+
kwargs[param_name] = param_type.model_validate(value)
192+
elif param_type is Callable or get_origin(param_type) is Callable:
193+
kwargs[param_name] = config_agent_utils.resolve_fully_qualified_name(
194+
value
195+
)
196+
elif param_type in (list, set, dict):
197+
kwargs[param_name] = param_type(value)
198+
elif get_origin(param_type) is list:
199+
list_args = get_args(param_type)
200+
if issubclass(list_args[0], BaseModel):
201+
kwargs[param_name] = [
202+
list_args[0].model_validate(item) for item in value
203+
]
204+
elif list_args[0] in (int, str, bool, float):
205+
kwargs[param_name] = value
206+
elif list_args[0] is Callable or get_origin(list_args[0]) is Callable:
207+
kwargs[param_name] = [
208+
config_agent_utils.resolve_fully_qualified_name(item)
209+
for item in value
210+
]
211+
else:
212+
logger.warning(
213+
"Unsupported parsing for list argument: %s.", param_name
214+
)
215+
else:
216+
logger.warning("Unsupported parsing for argument: %s.", param_name)
217+
return cls(**kwargs)
149218

150219

151220
def _find_tool_with_function_declarations(
@@ -218,9 +287,9 @@ class ToolConfig(BaseModel):
218287
my_tool_arg2: value2
219288
```
220289
221-
4. For user-defined functions that generate tool instances, the `name` is the
222-
fully qualified path to the function and `config` is passed to the function
223-
as arguments.
290+
4. For user-defined functions that generate tool instances, the `name` is
291+
the fully qualified path to the function and `config` is passed to the
292+
function as arguments.
224293
225294
```
226295
tools:
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.adk.tools import VertexAiSearchTool
16+
from google.adk.tools.base_tool import ToolConfig
17+
from google.genai import types
18+
import yaml
19+
20+
21+
def test_vertex_ai_search_tool_config():
22+
yaml_content = """\
23+
name: VertexAiSearchTool
24+
args:
25+
data_store_specs:
26+
- data_store: projects/my-project/locations/us-central1/collections/my-collection/dataStores/my-datastore1
27+
filter: filter1
28+
- data_store: projects/my-project/locations/us-central1/collections/my-collection/dataStores/my-dataStore2
29+
filter: filter2
30+
filter: filter
31+
max_results: 10
32+
search_engine_id: projects/my-project/locations/us-central1/collections/my-collection/engines/my-engine
33+
"""
34+
config_data = yaml.safe_load(yaml_content)
35+
config = ToolConfig.model_validate(config_data)
36+
37+
tool = VertexAiSearchTool.from_config(config.args, "")
38+
assert isinstance(tool, VertexAiSearchTool)
39+
assert isinstance(tool.data_store_specs[0], types.VertexAISearchDataStoreSpec)
40+
assert (
41+
tool.data_store_specs[0].data_store
42+
== "projects/my-project/locations/us-central1/collections/my-collection/dataStores/my-datastore1"
43+
)
44+
assert tool.data_store_specs[0].filter == "filter1"
45+
assert isinstance(tool.data_store_specs[0], types.VertexAISearchDataStoreSpec)
46+
assert (
47+
tool.data_store_specs[1].data_store
48+
== "projects/my-project/locations/us-central1/collections/my-collection/dataStores/my-dataStore2"
49+
)
50+
assert tool.data_store_specs[1].filter == "filter2"
51+
assert tool.filter == "filter"
52+
assert tool.max_results == 10
53+
assert (
54+
tool.search_engine_id
55+
== "projects/my-project/locations/us-central1/collections/my-collection/engines/my-engine"
56+
)

0 commit comments

Comments
 (0)