diff --git a/src/google/adk/tools/_automatic_function_calling_util.py b/src/google/adk/tools/_automatic_function_calling_util.py index dde0c8ced80..a08e532d93b 100644 --- a/src/google/adk/tools/_automatic_function_calling_util.py +++ b/src/google/adk/tools/_automatic_function_calling_util.py @@ -40,6 +40,7 @@ from ..utils._callable_utils import get_callable_doc from ..utils._callable_utils import unwrap_callable from ..utils.variant_utils import GoogleLLMVariant +from ._gemini_schema_util import _dereference_schema from ._gemini_schema_util import _sanitize_schema_formats_for_gemini logger = logging.getLogger('google_adk.' + __name__) @@ -420,6 +421,8 @@ def from_function_with_options( sanitized_schema = _sanitize_schema_formats_for_gemini( json_schema_dict ) + else: + sanitized_schema = _dereference_schema(json_schema_dict) parameters_json_schema[name] = types.Schema.model_validate( sanitized_schema ) diff --git a/tests/unittests/tools/test_from_function_with_options.py b/tests/unittests/tools/test_from_function_with_options.py index 869db8655cf..d778b4c8a77 100644 --- a/tests/unittests/tools/test_from_function_with_options.py +++ b/tests/unittests/tools/test_from_function_with_options.py @@ -13,6 +13,7 @@ # limitations under the License. from collections.abc import Sequence +import datetime from typing import Any from typing import AsyncGenerator from typing import Dict @@ -460,6 +461,55 @@ def complex_tool( ) +@pytest.mark.parametrize('shape', ['model', 'list', 'mapping', 'optional']) +def test_vertex_fallback_resolves_nested_model_references(shape): + """Nested models keep their fields when another parameter needs fallback.""" + + class Address(pydantic.BaseModel): + city: str + email: str = pydantic.Field(json_schema_extra={'format': 'email'}) + label: str = 'home' + + class Person(pydantic.BaseModel): + address: Address + + parameter_type = { + 'model': Person, + 'list': list[Person], + 'mapping': dict[str, Person], + 'optional': Person | None, + }[shape] + + def schedule(person: parameter_type, when: datetime.datetime, notify=True): + return 'ok' + + declaration = _automatic_function_calling_util.from_function_with_options( + schedule, GoogleLLMVariant.VERTEX_AI + ) + + person_schema = declaration.parameters.properties['person'] + if shape == 'list': + assert person_schema.type == types.Type.ARRAY + person_schema = person_schema.items + elif shape == 'mapping': + assert person_schema.type == types.Type.OBJECT + person_schema = types.Schema.model_validate( + person_schema.additional_properties + ) + elif shape == 'optional': + assert person_schema.any_of[1].type == types.Type.NULL + person_schema = person_schema.any_of[0] + assert person_schema.type == types.Type.OBJECT + address_schema = person_schema.properties['address'] + assert address_schema.properties['city'].type == types.Type.STRING + assert address_schema.properties['email'].format == 'email' + assert address_schema.properties['label'].default == 'home' + assert set(address_schema.required) == {'city', 'email'} + assert declaration.parameters.properties['when'].format == 'date-time' + assert declaration.parameters.properties['notify'].default is True + assert set(declaration.parameters.required) == {'person', 'when'} + + def test_format_preservation_for_vertex_fallback(): """Test that format is preserved for VERTEX_AI variant in fallback path."""