diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 3d18bbee..13fc6f71 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -17,6 +17,7 @@ from __future__ import annotations import functools import inspect import logging +from types import UnionType from typing import Any from typing import Callable from typing import cast @@ -168,9 +169,10 @@ class FunctionTool(BaseTool): target_type = type_hints.get(param_name, param.annotation) if target_type != inspect.Parameter.empty: - # Handle Optional[PydanticModel] types - if get_origin(param.annotation) is Union: - union_args = get_args(param.annotation) + # Handle Optional/Union types (e.g. Optional[PydanticModel], PydanticModel | None) + origin = get_origin(target_type) + if origin is Union or origin is UnionType: + union_args = get_args(target_type) # Find the non-None type in Optional[T] (which is Union[T, None]) non_none_types = [ arg for arg in union_args if arg is not type(None) @@ -187,12 +189,12 @@ class FunctionTool(BaseTool): continue try: converted_args[param_name] = pydantic.TypeAdapter( - param.annotation + target_type ).validate_python(args[param_name]) except Exception as e: logger.warning( f"Failed to convert argument '{param_name}' to" - f' {param.annotation}: {e}' + f' {target_type}: {e}' ) continue diff --git a/tests/unittests/tools/test_function_tool_with_import_annotations.py b/tests/unittests/tools/test_function_tool_with_import_annotations.py index 0d171628..c917bc2c 100644 --- a/tests/unittests/tools/test_function_tool_with_import_annotations.py +++ b/tests/unittests/tools/test_function_tool_with_import_annotations.py @@ -16,6 +16,7 @@ from __future__ import annotations from typing import Any from typing import Dict +from typing import Optional from google.adk.tools import _automatic_function_calling_util from google.adk.tools.function_tool import FunctionTool @@ -229,3 +230,57 @@ def test_preprocess_args_with_list_of_pydantic_models_and_annotations(): assert processed_args['items'][0].name == 'Burger' assert processed_args['items'][0].quantity == 10 assert processed_args['items'][1].quantity == 5 + + +def test_preprocess_args_with_optional_pydantic_model_and_annotations(): + """Test _preprocess_args converts dict to Optional[Pydantic] model with string annotations.""" + + def function_with_optional(item: Optional[ItemModel] = None) -> int: + return item.quantity if item else 0 + + tool = FunctionTool(function_with_optional) + input_args = {'item': {'name': 'Burger', 'quantity': 10}} + processed_args = tool._preprocess_args(input_args) + + assert isinstance(processed_args['item'], ItemModel) + assert processed_args['item'].name == 'Burger' + assert processed_args['item'].quantity == 10 + + +def test_preprocess_args_with_pipe_union_pydantic_model_and_annotations(): + """Test _preprocess_args converts dict to BaseModel | None with string annotations.""" + + def function_with_pipe_union(item: ItemModel | None = None) -> int: + return item.quantity if item else 0 + + tool = FunctionTool(function_with_pipe_union) + input_args = {'item': {'name': 'Pizza', 'quantity': 5}} + processed_args = tool._preprocess_args(input_args) + + assert isinstance(processed_args['item'], ItemModel) + assert processed_args['item'].name == 'Pizza' + assert processed_args['item'].quantity == 5 + + +def test_preprocess_args_with_optional_list_of_pydantic_models_and_annotations(): + """Test _preprocess_args converts dicts in Optional[list[BaseModel]] with string annotations.""" + + def function_with_optional_list( + items: Optional[list[ItemModel]] = None, + ) -> int: + return sum(item.quantity for item in items) if items else 0 + + tool = FunctionTool(function_with_optional_list) + input_args = { + 'items': [ + {'name': 'Burger', 'quantity': 10}, + {'name': 'Pizza', 'quantity': 5}, + ] + } + processed_args = tool._preprocess_args(input_args) + + assert isinstance(processed_args['items'], list) + assert len(processed_args['items']) == 2 + assert all(isinstance(item, ItemModel) for item in processed_args['items']) + assert processed_args['items'][0].quantity == 10 + assert processed_args['items'][1].quantity == 5