fix: handle optional and union pydantic models with string annotations

Merge https://github.com/google/adk-python/pull/6471

Support Optional, Union and | annotations for Pydantic models when string annotations are enabled.

PiperOrigin-RevId: 957289257
This commit is contained in:
Henry Su
2026-07-31 12:52:34 -07:00
committed by Copybara-Service
parent 6396ce60f9
commit 94d08cd69f
2 changed files with 62 additions and 5 deletions
+7 -5
View File
@@ -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
@@ -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