# Copyright 2026 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from collections.abc import Sequence from typing import Any from typing import AsyncGenerator from typing import Dict from typing import Generator from google.adk.tools import _automatic_function_calling_util from google.adk.utils.variant_utils import GoogleLLMVariant from google.genai import types import pydantic import pytest def test_from_function_with_options_no_return_annotation_gemini(): """Test from_function_with_options with no return annotation for GEMINI_API.""" def test_function(param: str): """A test function with no return annotation.""" return None declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.GEMINI_API ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # GEMINI_API should not have response schema assert declaration.response is None def test_from_function_with_options_no_return_annotation_vertex(): """Test from_function_with_options with no return annotation for VERTEX_AI.""" def test_function(param: str): """A test function with no return annotation.""" return None declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # VERTEX_AI should have response schema for functions with no return annotation # Changed: Now uses Any type instead of NULL for no return annotation assert declaration.response is not None assert declaration.response.type is None # Any type maps to None in schema def test_from_function_with_options_explicit_none_return_vertex(): """Test from_function_with_options with explicit None return for VERTEX_AI.""" def test_function(param: str) -> None: """A test function that explicitly returns None.""" pass declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # VERTEX_AI should have response schema for explicit None return assert declaration.response is not None assert declaration.response.type == types.Type.NULL def test_from_function_with_options_explicit_none_return_gemini(): """Test from_function_with_options with explicit None return for GEMINI_API.""" def test_function(param: str) -> None: """A test function that explicitly returns None.""" pass declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.GEMINI_API ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # GEMINI_API should not have response schema assert declaration.response is None def test_from_function_with_options_string_return_vertex(): """Test from_function_with_options with string return for VERTEX_AI.""" def test_function(param: str) -> str: """A test function that returns a string.""" return param declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # VERTEX_AI should have response schema for string return assert declaration.response is not None assert declaration.response.type == types.Type.STRING def test_from_function_with_options_dict_return_vertex(): """Test from_function_with_options with dict return for VERTEX_AI.""" def test_function(param: str) -> Dict[str, str]: """A test function that returns a dict.""" return {'result': param} declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # VERTEX_AI should have response schema for dict return assert declaration.response is not None assert declaration.response.type == types.Type.OBJECT def test_from_function_with_options_int_return_vertex(): """Test from_function_with_options with int return for VERTEX_AI.""" def test_function(param: str) -> int: """A test function that returns an int.""" return 42 declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # VERTEX_AI should have response schema for int return assert declaration.response is not None assert declaration.response.type == types.Type.INTEGER def test_from_function_with_options_any_annotation_vertex(): """Test from_function_with_options with Any type annotation for VERTEX_AI.""" def test_function(param: Any) -> Any: """A test function that uses Any type annotations.""" return param declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' # Any type should map to None in schema (TYPE_UNSPECIFIED behavior) assert declaration.parameters.properties['param'].type is None # VERTEX_AI should have response schema for Any return assert declaration.response is not None assert declaration.response.type is None # Any type maps to None in schema def test_from_function_with_options_no_params(): """Test from_function_with_options with no parameters.""" def test_function() -> None: """A test function with no parameters that returns None.""" pass declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' # No parameters should result in no parameters field or empty parameters assert ( declaration.parameters is None or len(declaration.parameters.properties) == 0 ) # VERTEX_AI should have response schema for None return assert declaration.response is not None assert declaration.response.type == types.Type.NULL def test_from_function_with_collections_type_parameter(): """Test from_function_with_options with collections type parameter.""" def test_function( artifact_key: str, input_edit_ids: Sequence[str], ) -> str: """Saves a sequence of edit IDs.""" return f'Saved {len(input_edit_ids)} edit IDs for artifact {artifact_key}' declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == types.Type.OBJECT assert ( declaration.parameters.properties['artifact_key'].type == types.Type.STRING ) assert ( declaration.parameters.properties['input_edit_ids'].type == types.Type.ARRAY ) assert ( declaration.parameters.properties['input_edit_ids'].items.type == types.Type.STRING ) assert declaration.response.type == types.Type.STRING def test_from_function_with_tuple_type_parameter(): """Test from_function_with_options with fixed-size homogeneous tuple.""" def test_function( coordinate: tuple[float, float], ) -> str: """Formats a coordinate pair.""" return f'{coordinate[0]}, {coordinate[1]}' declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == types.Type.OBJECT coordinate_schema = declaration.parameters.properties['coordinate'] assert coordinate_schema.type == types.Type.ARRAY assert coordinate_schema.items.type == types.Type.NUMBER # Fixed-size tuples pin the array length so the model emits exactly the # expected number of items. assert coordinate_schema.min_items == 2 assert coordinate_schema.max_items == 2 assert declaration.response.type == types.Type.STRING def test_from_function_with_variadic_tuple_type_parameter(): """Test from_function_with_options with variable-length homogeneous tuple.""" def test_function( tags: tuple[str, ...], ) -> str: """Joins tags.""" return ', '.join(tags) declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) tags_schema = declaration.parameters.properties['tags'] assert tags_schema.type == types.Type.ARRAY assert tags_schema.items.type == types.Type.STRING # Variadic tuples are unbounded, so no size constraints are set. assert tags_schema.min_items is None assert tags_schema.max_items is None def test_from_function_with_collections_return_type(): """Test from_function_with_options with collections return type.""" def test_function( names: list[str], ) -> Sequence[str]: """Returns a sequence of names.""" return names declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.response.type == types.Type.ARRAY assert declaration.response.items.type == types.Type.STRING def test_from_function_with_async_generator_return_vertex(): """Test from_function_with_options with AsyncGenerator return for VERTEX_AI.""" async def test_function(param: str) -> AsyncGenerator[str, None]: """A streaming function that yields strings.""" yield param declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # VERTEX_AI should extract yield type (str) from AsyncGenerator[str, None] assert declaration.response is not None assert declaration.response.type == types.Type.STRING def test_from_function_with_async_generator_return_gemini(): """Test from_function_with_options with AsyncGenerator return for GEMINI_API.""" async def test_function(param: str) -> AsyncGenerator[str, None]: """A streaming function that yields strings.""" yield param declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.GEMINI_API ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # GEMINI_API should not have response schema assert declaration.response is None def test_from_function_with_generator_return_vertex(): """Test from_function_with_options with Generator return for VERTEX_AI.""" def test_function(param: str) -> Generator[int, None, None]: """A streaming function that yields integers.""" yield 42 declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # VERTEX_AI should extract yield type (int) from Generator[int, None, None] assert declaration.response is not None assert declaration.response.type == types.Type.INTEGER def test_from_function_with_async_generator_complex_yield_type_vertex(): """Test from_function_with_options with AsyncGenerator yielding dict.""" async def test_function(param: str) -> AsyncGenerator[Dict[str, str], None]: """A streaming function that yields dicts.""" yield {'result': param} declaration = _automatic_function_calling_util.from_function_with_options( test_function, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'test_function' assert declaration.parameters.type == 'OBJECT' assert declaration.parameters.properties['param'].type == 'STRING' # VERTEX_AI should extract yield type (Dict[str, str]) from AsyncGenerator assert declaration.response is not None assert declaration.response.type == types.Type.OBJECT def test_required_fields_set_with_optional_tuple_parameter(): """Test that required fields are populated with optional tuple parameters.""" def complex_tool( query: str, mode: str = 'default', tags: tuple[str, ...] | None = None, ) -> str: """A tool where one param has a complex union type.""" return query declaration = _automatic_function_calling_util.from_function_with_options( complex_tool, GoogleLLMVariant.GEMINI_API ) assert declaration.name == 'complex_tool' assert declaration.parameters == types.Schema( type=types.Type.OBJECT, required=['query'], properties={ 'query': types.Schema(type=types.Type.STRING), 'mode': types.Schema(type=types.Type.STRING, default='default'), 'tags': types.Schema( items=types.Schema(type=types.Type.STRING), nullable=True, type=types.Type.ARRAY, ), }, ) def test_required_fields_set_in_json_schema_fallback(): """Required fields are populated when the json_schema fallback path is used. A parameter whose type `_parse_schema_from_parameter` cannot handle (here `Sequence[str]`) forces from_function_with_options onto the pydantic json_schema fallback branch. This verifies that branch still derives required fields correctly: parameters without defaults are required, parameters with defaults are not. """ def complex_tool( query: str, items: Sequence[str], mode: str = 'default', ) -> str: return query declaration = _automatic_function_calling_util.from_function_with_options( complex_tool, GoogleLLMVariant.VERTEX_AI ) assert declaration.name == 'complex_tool' assert declaration.parameters.type == types.Type.OBJECT # query and items have no defaults -> required; mode has a default -> not. assert set(declaration.parameters.required) == {'query', 'items'} assert declaration.parameters.properties['items'].type == types.Type.ARRAY assert declaration.parameters.properties['mode'].default == 'default' def test_schema_sanitization_for_complex_union_type(): """Test schema is sanitized for complex union type.""" def complex_tool( query: str, mode: str = 'default', tags: dict[str, str] | None = None, ) -> str: return query declaration = _automatic_function_calling_util.from_function_with_options( complex_tool, GoogleLLMVariant.GEMINI_API ) assert declaration.parameters.properties['tags'] == types.Schema( type=types.Type.OBJECT, nullable=True, ) def test_format_preservation_for_vertex_fallback(): """Test that format is preserved for VERTEX_AI variant in fallback path.""" class ComplexModel(pydantic.BaseModel): # Field with format that would be stripped by Gemini sanitization email: str = pydantic.Field(json_schema_extra={'format': 'email'}) # Complex field to trigger fallback (Sequence is not handled by # _parse_schema_from_parameter) complex_field: Sequence[str] def my_tool(param: ComplexModel) -> str: return f'ok {param}' # Run with VERTEX_AI, should preserve format declaration_vertex = ( _automatic_function_calling_util.from_function_with_options( my_tool, GoogleLLMVariant.VERTEX_AI ) ) # Check that format is preserved param_schema_vertex = declaration_vertex.parameters.properties['param'] assert param_schema_vertex.properties['email'].format == 'email' # Run with GEMINI_API, should strip format (current behavior) declaration_gemini = ( _automatic_function_calling_util.from_function_with_options( my_tool, GoogleLLMVariant.GEMINI_API ) ) param_schema_gemini = declaration_gemini.parameters.properties['param'] assert param_schema_gemini.properties['email'].format is None def test_tuple_types_work_in_json_schema_fallback() -> None: """Test that tuple schemas work in json schema fallback.""" def generate_image( prompt: str, input_bytes: list[tuple[bytes, str]] | None = None, ) -> dict[str, str]: """Generate an image from a prompt.""" del input_bytes return {'status': prompt} declaration = _automatic_function_calling_util.from_function_with_options( generate_image, GoogleLLMVariant.GEMINI_API ) assert declaration.parameters is not None assert declaration.parameters.required == ['prompt'] input_bytes_schema = declaration.parameters.properties['input_bytes'] assert input_bytes_schema.nullable assert input_bytes_schema.any_of is not None array_schema = next( schema for schema in input_bytes_schema.any_of if schema.type == types.Type.ARRAY ) assert array_schema.items is not None assert array_schema.items.type == types.Type.ARRAY assert array_schema.items.max_items == 2 assert array_schema.items.min_items == 2 assert array_schema.items.items is not None assert array_schema.items.items.any_of is not None assert len(array_schema.items.items.any_of) == 2 assert array_schema.items.items.any_of[0].type == types.Type.STRING assert array_schema.items.items.any_of[0].format is None assert array_schema.items.items.any_of[1].type == types.Type.STRING def test_from_function_with_options_any_type_with_default_value(): """Test that typing.Any with a default value works and doesn't crash.""" def my_tool(param: Any = 'default_string') -> str: return f'ok {param}' declaration = _automatic_function_calling_util.from_function_with_options( my_tool, GoogleLLMVariant.GEMINI_API ) assert declaration.parameters is not None assert declaration.parameters.properties['param'].default == 'default_string' # Any type maps to None (no type) in schema assert declaration.parameters.properties['param'].type is None