Skip to content

Commit 268815d

Browse files
GWealecopybara-github
authored andcommitted
fix: stop automatic function calling from failing on Vertex AI return types
Vertex AI raised an error whenever it could not derive a response schema for a tool's return type, so a tool that worked fine on the Gemini API crashed on Vertex AI. The Gemini API path simply skips the return-type schema, so the two backends diverged. Broaden the return-type schema derivation to degrade the same way: on failure, log a warning and omit the response schema, deferring validation to the model API instead of rejecting an otherwise valid tool. Close #3543 Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 952421539
1 parent 6e9895c commit 268815d

2 files changed

Lines changed: 131 additions & 3 deletions

File tree

src/google/adk/tools/_automatic_function_calling_util.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import collections.abc
1818
import inspect
19+
import logging
1920
from types import FunctionType
2021
import typing
2122
from typing import Any
@@ -39,6 +40,8 @@
3940
from ..utils.variant_utils import GoogleLLMVariant
4041
from ._gemini_schema_util import _sanitize_schema_formats_for_gemini
4142

43+
logger = logging.getLogger('google_adk.' + __name__)
44+
4245
_py_type_2_schema_type = {
4346
'str': types.Type.STRING,
4447
'int': types.Type.INTEGER,
@@ -486,7 +489,9 @@ def from_function_with_options(
486489
func.__name__,
487490
)
488491
)
489-
except ValueError:
492+
# Intentionally broad: schema derivation can raise non-ValueError types
493+
# (newer Python/pydantic); any failure degrades with a warning below.
494+
except Exception as primary_error:
490495
try:
491496
response_json_schema = (
492497
_function_parameter_parse_util._generate_json_schema_for_parameter(
@@ -495,8 +500,14 @@ def from_function_with_options(
495500
)
496501
response_json_schema = types.Schema.model_validate(response_json_schema)
497502
except Exception as e:
498-
_function_parameter_parse_util._raise_for_unsupported_param(
499-
return_value, func.__name__, e
503+
# Degrade like GEMINI_API instead of rejecting a valid return type: omit
504+
# the response schema and defer validation to the model API.
505+
logger.warning(
506+
'Could not generate a response schema for the return type of %s;'
507+
' omitting it. Fallback error: %s. Original error: %s',
508+
func.__name__,
509+
e,
510+
primary_error,
500511
)
501512
if response_schema:
502513
declaration.response = response_schema

tests/unittests/tools/test_from_function_with_options.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,120 @@ def my_tool(param: Any = 'default_string') -> str:
535535
assert declaration.parameters.properties['param'].default == 'default_string'
536536
# Any type maps to None (no type) in schema
537537
assert declaration.parameters.properties['param'].type is None
538+
539+
540+
class _UnserializableReturn:
541+
"""A plain class that has no genai/JSON schema representation."""
542+
543+
544+
def test_from_function_with_options_unserializable_return_vertex_degrades_gracefully():
545+
"""VERTEX_AI omits the response schema instead of raising when it can't be derived."""
546+
547+
def test_function(param: str) -> _UnserializableReturn:
548+
"""A function whose return type cannot be turned into a schema."""
549+
return _UnserializableReturn()
550+
551+
declaration = _automatic_function_calling_util.from_function_with_options(
552+
test_function, GoogleLLMVariant.VERTEX_AI
553+
)
554+
555+
assert declaration.name == 'test_function'
556+
# Parameters are still populated; only the return schema is dropped.
557+
assert declaration.parameters.type == 'OBJECT'
558+
assert declaration.parameters.properties['param'].type == 'STRING'
559+
assert declaration.response is None
560+
561+
562+
def test_from_function_with_options_logs_warning_on_return_schema_failure(
563+
caplog,
564+
):
565+
"""A warning naming the function is emitted when the return schema is dropped."""
566+
567+
def test_function(param: str) -> _UnserializableReturn:
568+
"""A function whose return type cannot be turned into a schema."""
569+
return _UnserializableReturn()
570+
571+
with caplog.at_level(
572+
'WARNING',
573+
logger='google_adk.google.adk.tools._automatic_function_calling_util',
574+
):
575+
_automatic_function_calling_util.from_function_with_options(
576+
test_function, GoogleLLMVariant.VERTEX_AI
577+
)
578+
579+
warnings = [r for r in caplog.records if r.levelname == 'WARNING']
580+
assert len(warnings) == 1
581+
assert 'test_function' in warnings[0].getMessage()
582+
583+
584+
def test_from_function_with_options_valid_pydantic_return_still_gets_schema_vertex():
585+
"""A serializable pydantic return type keeps producing a response schema."""
586+
587+
class MyModel(pydantic.BaseModel):
588+
result: str
589+
590+
def test_function(param: str) -> MyModel:
591+
"""A function that returns a valid pydantic model."""
592+
return MyModel(result=param)
593+
594+
declaration = _automatic_function_calling_util.from_function_with_options(
595+
test_function, GoogleLLMVariant.VERTEX_AI
596+
)
597+
598+
assert declaration.name == 'test_function'
599+
assert declaration.response is not None
600+
assert declaration.response.type == types.Type.OBJECT
601+
602+
603+
def test_from_function_with_options_non_value_error_return_degrades_gracefully(
604+
monkeypatch,
605+
):
606+
"""A non-ValueError from schema parsing is caught (not propagated) and degrades."""
607+
608+
parse_util = _automatic_function_calling_util._function_parameter_parse_util
609+
original_parse = parse_util._parse_schema_from_parameter
610+
611+
def _raise_type_error_for_return(variant, param, func_name):
612+
# Only the return-schema parse should raise; leave parameter parsing intact.
613+
if param.name == 'return_value':
614+
raise TypeError('simulated non-ValueError from schema parsing')
615+
return original_parse(variant, param, func_name)
616+
617+
monkeypatch.setattr(
618+
parse_util,
619+
'_parse_schema_from_parameter',
620+
_raise_type_error_for_return,
621+
)
622+
623+
def test_function(param: str) -> _UnserializableReturn:
624+
"""A function whose return schema parsing raises a non-ValueError."""
625+
return _UnserializableReturn()
626+
627+
declaration = _automatic_function_calling_util.from_function_with_options(
628+
test_function, GoogleLLMVariant.VERTEX_AI
629+
)
630+
631+
assert declaration.name == 'test_function'
632+
assert declaration.response is None
633+
634+
635+
def test_from_function_with_options_warning_includes_original_error(caplog):
636+
"""The warning names both the fallback and the original parsing error."""
637+
638+
def test_function(param: str) -> _UnserializableReturn:
639+
"""A function whose return type cannot be turned into a schema."""
640+
return _UnserializableReturn()
641+
642+
with caplog.at_level(
643+
'WARNING',
644+
logger='google_adk.google.adk.tools._automatic_function_calling_util',
645+
):
646+
_automatic_function_calling_util.from_function_with_options(
647+
test_function, GoogleLLMVariant.VERTEX_AI
648+
)
649+
650+
warnings = [r for r in caplog.records if r.levelname == 'WARNING']
651+
assert len(warnings) == 1
652+
message = warnings[0].getMessage()
653+
assert 'Fallback error:' in message
654+
assert 'Original error:' in message

0 commit comments

Comments
 (0)