2222from google .adk .utils .variant_utils import GoogleLLMVariant
2323from google .genai import types
2424import pydantic
25+ import pytest
2526
2627
2728def test_from_function_with_options_no_return_annotation_gemini ():
@@ -229,6 +230,72 @@ def test_function(
229230 assert declaration .response .type == types .Type .STRING
230231
231232
233+ def test_from_function_with_tuple_type_parameter ():
234+ """Test from_function_with_options with fixed-size homogeneous tuple."""
235+
236+ def test_function (
237+ coordinate : tuple [float , float ],
238+ ) -> str :
239+ """Formats a coordinate pair."""
240+ return f'{ coordinate [0 ]} , { coordinate [1 ]} '
241+
242+ declaration = _automatic_function_calling_util .from_function_with_options (
243+ test_function , GoogleLLMVariant .VERTEX_AI
244+ )
245+
246+ assert declaration .name == 'test_function'
247+ assert declaration .parameters .type == types .Type .OBJECT
248+ coordinate_schema = declaration .parameters .properties ['coordinate' ]
249+ assert coordinate_schema .type == types .Type .ARRAY
250+ assert coordinate_schema .items .type == types .Type .NUMBER
251+ # Fixed-size tuples pin the array length so the model emits exactly the
252+ # expected number of items.
253+ assert coordinate_schema .min_items == 2
254+ assert coordinate_schema .max_items == 2
255+ assert declaration .response .type == types .Type .STRING
256+
257+
258+ def test_from_function_with_variadic_tuple_type_parameter ():
259+ """Test from_function_with_options with variable-length homogeneous tuple."""
260+
261+ def test_function (
262+ tags : tuple [str , ...],
263+ ) -> str :
264+ """Joins tags."""
265+ return ', ' .join (tags )
266+
267+ declaration = _automatic_function_calling_util .from_function_with_options (
268+ test_function , GoogleLLMVariant .VERTEX_AI
269+ )
270+
271+ tags_schema = declaration .parameters .properties ['tags' ]
272+ assert tags_schema .type == types .Type .ARRAY
273+ assert tags_schema .items .type == types .Type .STRING
274+ # Variadic tuples are unbounded, so no size constraints are set.
275+ assert tags_schema .min_items is None
276+ assert tags_schema .max_items is None
277+
278+
279+ def test_from_function_with_heterogeneous_tuple_raises ():
280+ """Heterogeneous tuples can't map to a single-`items` array schema.
281+
282+ `google.genai.types.Schema` arrays carry a single `items` type (no
283+ positional `prefixItems`), so a tuple like `tuple[str, int]` cannot be
284+ represented. It must surface a clear error rather than be silently coerced
285+ into an incorrect homogeneous array.
286+ """
287+
288+ def test_function (
289+ pair : tuple [str , int ],
290+ ) -> str :
291+ return f'{ pair [0 ]} : { pair [1 ]} '
292+
293+ with pytest .raises (ValueError ):
294+ _automatic_function_calling_util .from_function_with_options (
295+ test_function , GoogleLLMVariant .VERTEX_AI
296+ )
297+
298+
232299def test_from_function_with_collections_return_type ():
233300 """Test from_function_with_options with collections return type."""
234301
@@ -322,14 +389,8 @@ async def test_function(param: str) -> AsyncGenerator[Dict[str, str], None]:
322389 assert declaration .response .type == types .Type .OBJECT
323390
324391
325- def test_required_fields_set_in_json_schema_fallback ():
326- """Test that required fields are populated when the json_schema fallback path is used.
327-
328- When a parameter has a complex type (e.g. tuple[str, ...] | None) that
329- _parse_schema_from_parameter can't handle, from_function_with_options falls
330- back to the parameters_json_schema branch. This test verifies that the
331- required fields are correctly populated in that fallback branch.
332- """
392+ def test_required_fields_set_with_optional_tuple_parameter ():
393+ """Test that required fields are populated with optional tuple parameters."""
333394
334395 def complex_tool (
335396 query : str ,
@@ -351,19 +412,43 @@ def complex_tool(
351412 'query' : types .Schema (type = types .Type .STRING ),
352413 'mode' : types .Schema (type = types .Type .STRING , default = 'default' ),
353414 'tags' : types .Schema (
354- any_of = [
355- types .Schema (
356- items = types .Schema (type = types .Type .STRING ),
357- type = types .Type .ARRAY ,
358- ),
359- types .Schema (type = types .Type .NULL ),
360- ],
415+ items = types .Schema (type = types .Type .STRING ),
361416 nullable = True ,
417+ type = types .Type .ARRAY ,
362418 ),
363419 },
364420 )
365421
366422
423+ def test_required_fields_set_in_json_schema_fallback ():
424+ """Required fields are populated when the json_schema fallback path is used.
425+
426+ A parameter whose type `_parse_schema_from_parameter` cannot handle (here
427+ `Sequence[str]`) forces from_function_with_options onto the pydantic
428+ json_schema fallback branch. This verifies that branch still derives required
429+ fields correctly: parameters without defaults are required, parameters with
430+ defaults are not.
431+ """
432+
433+ def complex_tool (
434+ query : str ,
435+ items : Sequence [str ],
436+ mode : str = 'default' ,
437+ ) -> str :
438+ return query
439+
440+ declaration = _automatic_function_calling_util .from_function_with_options (
441+ complex_tool , GoogleLLMVariant .VERTEX_AI
442+ )
443+
444+ assert declaration .name == 'complex_tool'
445+ assert declaration .parameters .type == types .Type .OBJECT
446+ # query and items have no defaults -> required; mode has a default -> not.
447+ assert set (declaration .parameters .required ) == {'query' , 'items' }
448+ assert declaration .parameters .properties ['items' ].type == types .Type .ARRAY
449+ assert declaration .parameters .properties ['mode' ].default == 'default'
450+
451+
367452def test_schema_sanitization_for_complex_union_type ():
368453 """Test schema is sanitized for complex union type."""
369454
@@ -390,8 +475,9 @@ def test_format_preservation_for_vertex_fallback():
390475 class ComplexModel (pydantic .BaseModel ):
391476 # Field with format that would be stripped by Gemini sanitization
392477 email : str = pydantic .Field (json_schema_extra = {'format' : 'email' })
393- # Complex field to trigger fallback (tuple is not handled by _parse_schema_from_parameter)
394- complex_field : tuple [str , ...]
478+ # Complex field to trigger fallback (Sequence is not handled by
479+ # _parse_schema_from_parameter)
480+ complex_field : Sequence [str ]
395481
396482 def my_tool (param : ComplexModel ) -> str :
397483 return f'ok { param } '
0 commit comments