Skip to content

Commit e2ba826

Browse files
committed
fix: Support pipe operator (X | Y) union syntax in function parameter parser
The parser only recognized `typing.Union` but not `types.UnionType` (created by Python 3.10+ pipe syntax like `list[str] | None`), causing a ValueError fallback. This adds `types.UnionType` checks in three conditions within `_parse_schema_from_parameter()`. Fixes #3591
1 parent a2e43aa commit e2ba826

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

src/google/adk/tools/_function_parameter_parse_util.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ def _parse_schema_from_parameter(
247247
_raise_if_schema_unsupported(variant, schema)
248248
return schema
249249
if (
250-
get_origin(param.annotation) is Union
250+
get_origin(param.annotation) in (Union, typing_types.UnionType)
251251
# only parse simple UnionType, example int | str | float | bool
252-
# complex types.UnionType will be invoked in raise branch
252+
# complex UnionType will be handled in GenericAlias block below
253253
and all(
254254
(_is_builtin_primitive_or_compound(arg) or arg is type(None))
255255
for arg in get_args(param.annotation)
@@ -287,8 +287,10 @@ def _parse_schema_from_parameter(
287287
schema.default = param.default
288288
_raise_if_schema_unsupported(variant, schema)
289289
return schema
290-
if isinstance(param.annotation, _GenericAlias) or isinstance(
291-
param.annotation, typing_types.GenericAlias
290+
if (
291+
isinstance(param.annotation, _GenericAlias)
292+
or isinstance(param.annotation, typing_types.GenericAlias)
293+
or isinstance(param.annotation, typing_types.UnionType)
292294
):
293295
origin = get_origin(param.annotation)
294296
args = get_args(param.annotation)
@@ -330,7 +332,7 @@ def _parse_schema_from_parameter(
330332
schema.default = param.default
331333
_raise_if_schema_unsupported(variant, schema)
332334
return schema
333-
if origin is Union:
335+
if origin in (Union, typing_types.UnionType):
334336
schema.any_of = []
335337
schema.type = types.Type.OBJECT
336338
unique_types = set()

tests/unittests/tools/test_build_function_declaration.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,3 +661,120 @@ def greet(name: str = 'World') -> str:
661661
schema = decl.parameters_json_schema
662662
assert schema['properties']['name']['default'] == 'World'
663663
assert 'name' not in schema.get('required', [])
664+
665+
666+
# ── Pipe-union (X | Y) tests ──────────────────────────────────────────
667+
668+
669+
def test_pipe_union_optional_list():
670+
"""list[str] | None should parse as ARRAY with nullable=True."""
671+
672+
def func(a: list[str] | None):
673+
pass
674+
675+
decl = _automatic_function_calling_util.build_function_declaration(
676+
func=func, variant=GoogleLLMVariant.VERTEX_AI
677+
)
678+
prop = decl.parameters.properties['a']
679+
assert prop.type == types.Type.ARRAY
680+
assert prop.nullable is True
681+
682+
683+
def test_pipe_union_optional_dict():
684+
"""dict[str, int] | None should parse as OBJECT with nullable=True."""
685+
686+
def func(a: dict[str, int] | None):
687+
pass
688+
689+
decl = _automatic_function_calling_util.build_function_declaration(
690+
func=func, variant=GoogleLLMVariant.VERTEX_AI
691+
)
692+
prop = decl.parameters.properties['a']
693+
assert prop.type == types.Type.OBJECT
694+
assert prop.nullable is True
695+
696+
697+
def test_pipe_union_optional_list_with_default():
698+
"""list[str] | None = None should parse as ARRAY, nullable, no default."""
699+
700+
def func(a: list[str] | None = None):
701+
pass
702+
703+
decl = _automatic_function_calling_util.build_function_declaration(
704+
func=func, variant=GoogleLLMVariant.VERTEX_AI
705+
)
706+
prop = decl.parameters.properties['a']
707+
assert prop.type == types.Type.ARRAY
708+
assert prop.nullable is True
709+
710+
711+
def test_pipe_union_simple_primitives():
712+
"""int | str should produce any_of with two types."""
713+
714+
def func(a: int | str):
715+
pass
716+
717+
decl = _automatic_function_calling_util.build_function_declaration(
718+
func=func, variant=GoogleLLMVariant.VERTEX_AI
719+
)
720+
prop = decl.parameters.properties['a']
721+
assert prop.any_of is not None
722+
assert len(prop.any_of) == 2
723+
724+
725+
def test_pipe_union_simple_primitives_with_none():
726+
"""int | str | None should produce any_of + nullable."""
727+
728+
def func(a: int | str | None):
729+
pass
730+
731+
decl = _automatic_function_calling_util.build_function_declaration(
732+
func=func, variant=GoogleLLMVariant.VERTEX_AI
733+
)
734+
prop = decl.parameters.properties['a']
735+
assert prop.any_of is not None
736+
assert len(prop.any_of) == 2
737+
assert prop.nullable is True
738+
739+
740+
def test_pipe_union_complex_multi_type():
741+
"""list[str] | dict[str, int] should produce any_of (VERTEX_AI)."""
742+
743+
def func(a: list[str] | dict[str, int]):
744+
pass
745+
746+
decl = _automatic_function_calling_util.build_function_declaration(
747+
func=func, variant=GoogleLLMVariant.VERTEX_AI
748+
)
749+
prop = decl.parameters.properties['a']
750+
assert prop.any_of is not None
751+
assert len(prop.any_of) == 2
752+
753+
754+
def test_pipe_union_complex_falls_back_for_gemini_api():
755+
"""Complex pipe union for GEMINI_API falls back to pydantic schema."""
756+
757+
def func(a: list[str] | dict[str, int]):
758+
pass
759+
760+
decl = _automatic_function_calling_util.build_function_declaration(
761+
func=func, variant=GoogleLLMVariant.GEMINI_API
762+
)
763+
# GEMINI_API does not support any_of, so the parser falls back to
764+
# pydantic-based json schema generation.
765+
assert decl.name == 'func'
766+
767+
768+
def test_typing_union_optional_list_still_works():
769+
"""Regression: typing.Union[list[str], None] must still work."""
770+
import typing
771+
772+
def func(a: typing.Union[list[str], None]):
773+
pass
774+
775+
decl = _automatic_function_calling_util.build_function_declaration(
776+
func=func, variant=GoogleLLMVariant.VERTEX_AI
777+
)
778+
prop = decl.parameters.properties['a']
779+
assert prop.type == types.Type.ARRAY
780+
assert prop.nullable is True

0 commit comments

Comments
 (0)