From 2b161a33d6789b5f0da637771e9afcc303281082 Mon Sep 17 00:00:00 2001 From: samecho Date: Wed, 28 Oct 2026 06:15:32 -0400 Subject: [PATCH] fix(tools): resolve nested models in legacy Vertex declarations Inline Pydantic schema references before validating parameters in the Vertex fallback. Preserve Vertex-specific fields and leave the default JSON-schema and Gemini paths unchanged. Fixes #6984 --- .../tools/_automatic_function_calling_util.py | 3 ++ .../tools/test_from_function_with_options.py | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+) 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."""