fix: Ignore AsyncGenerator return types in function declarations

For Vertex model backend, we send response back. This doesn't work for streaming tools that the return type is AsyncGenerator. So the fix here is to ignore the return type when it's AsyncGenerator.

We can't distinguish streaming vs non-streaming tool with AsyncGenerator though as LiveRequestQueue is optional in streaming tool.

Adds an `ignore_response` option to `build_function_declaration` to skip including the return type in the function declaration. This is enabled for tools that return `AsyncGenerator`, as the model does not yet support understanding these return types, while streaming tools can still handle them. Also, removes redundant return statements in `_get_mandatory_params`.

PiperOrigin-RevId: 794392846
This commit is contained in:
Hangfei Lin
2025-08-12 21:45:52 -07:00
committed by Copybara-Service
parent 8c65967cdc
commit e2518dc371
22 changed files with 134 additions and 54 deletions
@@ -15,6 +15,8 @@
"""
This agent aims to test the Langchain tool with Langchain's StructuredTool
"""
from __future__ import annotations
from google.adk.agents.llm_agent import Agent
from google.adk.tools.langchain_tool import LangchainTool
from langchain.tools import tool
@@ -23,11 +25,13 @@ from pydantic import BaseModel
async def add(x, y) -> int:
"""Adds two numbers."""
return x + y
@tool
def minus(x, y) -> int:
"""Minus two numbers."""
return x - y
+2 -1
View File
@@ -109,7 +109,8 @@ ToolUnion: TypeAlias = Union[Callable, BaseTool, BaseToolset]
async def _convert_tool_union_to_tools(
tool_union: ToolUnion, ctx: ReadonlyContext
tool_union: ToolUnion,
ctx: ReadonlyContext,
) -> list[BaseTool]:
if isinstance(tool_union, BaseTool):
return [tool_union]
@@ -73,7 +73,7 @@ class BaseLlmFlow(ABC):
invocation_context: InvocationContext,
) -> AsyncGenerator[Event, None]:
"""Runs the flow using live api."""
llm_request = LlmRequest()
llm_request = LlmRequest(live_connect_config=types.LiveConnectConfig())
event_id = Event.new_id()
# Preprocess before calling the LLM.
@@ -373,7 +373,9 @@ class BaseLlmFlow(ABC):
yield event
async def _preprocess_async(
self, invocation_context: InvocationContext, llm_request: LlmRequest
self,
invocation_context: InvocationContext,
llm_request: LlmRequest,
) -> AsyncGenerator[Event, None]:
from ...agents.llm_agent import LlmAgent
+25 -24
View File
@@ -57,30 +57,31 @@ class _BasicLlmRequestProcessor(BaseLlmRequestProcessor):
if agent.output_schema and not agent.tools:
llm_request.set_output_schema(agent.output_schema)
llm_request.live_connect_config.response_modalities = (
invocation_context.run_config.response_modalities
)
llm_request.live_connect_config.speech_config = (
invocation_context.run_config.speech_config
)
llm_request.live_connect_config.output_audio_transcription = (
invocation_context.run_config.output_audio_transcription
)
llm_request.live_connect_config.input_audio_transcription = (
invocation_context.run_config.input_audio_transcription
)
llm_request.live_connect_config.realtime_input_config = (
invocation_context.run_config.realtime_input_config
)
llm_request.live_connect_config.enable_affective_dialog = (
invocation_context.run_config.enable_affective_dialog
)
llm_request.live_connect_config.proactivity = (
invocation_context.run_config.proactivity
)
llm_request.live_connect_config.session_resumption = (
invocation_context.run_config.session_resumption
)
if llm_request.live_connect_config:
llm_request.live_connect_config.response_modalities = (
invocation_context.run_config.response_modalities
)
llm_request.live_connect_config.speech_config = (
invocation_context.run_config.speech_config
)
llm_request.live_connect_config.output_audio_transcription = (
invocation_context.run_config.output_audio_transcription
)
llm_request.live_connect_config.input_audio_transcription = (
invocation_context.run_config.input_audio_transcription
)
llm_request.live_connect_config.realtime_input_config = (
invocation_context.run_config.realtime_input_config
)
llm_request.live_connect_config.enable_affective_dialog = (
invocation_context.run_config.enable_affective_dialog
)
llm_request.live_connect_config.proactivity = (
invocation_context.run_config.proactivity
)
llm_request.live_connect_config.session_resumption = (
invocation_context.run_config.session_resumption
)
# TODO: handle tool append here, instead of in BaseTool.process_llm_request.
+24 -4
View File
@@ -14,6 +14,9 @@
from __future__ import annotations
from collections.abc import AsyncGenerator as ABCAsyncGenerator
import inspect
from typing import get_origin
from typing import Optional
from google.genai import types
@@ -22,6 +25,7 @@ from pydantic import ConfigDict
from pydantic import Field
from ..tools.base_tool import BaseTool
from ..tools.function_tool import FunctionTool
def _find_tool_with_function_declarations(
@@ -66,13 +70,13 @@ class LlmRequest(BaseModel):
config: types.GenerateContentConfig = Field(
default_factory=types.GenerateContentConfig
)
live_connect_config: types.LiveConnectConfig = Field(
default_factory=types.LiveConnectConfig
)
"""Additional config for the generate content request.
tools in generate_content_config should not be set.
"""
live_connect_config: Optional[types.LiveConnectConfig] = None
"""Live connection config.
"""
tools_dict: dict[str, BaseTool] = Field(default_factory=dict, exclude=True)
"""The tools dictionary."""
@@ -99,7 +103,23 @@ class LlmRequest(BaseModel):
return
declarations = []
for tool in tools:
declaration = tool._get_declaration()
if self.live_connect_config is not None:
# ignore response for tools that returns AsyncGenerator that the model
# can't understand yet even though the model can't handle it, streaming
# tools can handle it.
# to check type, use typing.collections.abc.AsyncGenerator and not
# typing.AsyncGenerator
is_async_generator_return = False
if isinstance(tool, FunctionTool):
signature = inspect.signature(tool.func)
is_async_generator_return = (
get_origin(signature.return_annotation) is ABCAsyncGenerator
)
declaration = tool._get_declaration(
ignore_return_declaration=is_async_generator_return
)
else:
declaration = tool._get_declaration()
if declaration:
declarations.append(declaration)
self.tools_dict[tool.name] = tool
@@ -195,6 +195,7 @@ def build_function_declaration(
func: Union[Callable, BaseModel],
ignore_params: Optional[list[str]] = None,
variant: GoogleLLMVariant = GoogleLLMVariant.GEMINI_API,
ignore_return_declaration: bool = False,
) -> types.FunctionDeclaration:
signature = inspect.signature(func)
should_update_signature = False
@@ -232,9 +233,11 @@ def build_function_declaration(
new_func.__annotations__ = func.__annotations__
return (
from_function_with_options(func, variant)
from_function_with_options(func, variant, ignore_return_declaration)
if not should_update_signature
else from_function_with_options(new_func, variant)
else from_function_with_options(
new_func, variant, ignore_return_declaration
)
)
@@ -293,6 +296,7 @@ def build_function_declaration_util(
def from_function_with_options(
func: Callable,
variant: GoogleLLMVariant = GoogleLLMVariant.GEMINI_API,
ignore_return_declaration: bool = False,
) -> 'types.FunctionDeclaration':
parameters_properties = {}
@@ -324,7 +328,8 @@ def from_function_with_options(
declaration.parameters
)
)
if variant == GoogleLLMVariant.GEMINI_API:
if variant == GoogleLLMVariant.GEMINI_API or ignore_return_declaration:
return declaration
return_annotation = inspect.signature(func).return_annotation
+4 -1
View File
@@ -15,6 +15,7 @@
from __future__ import annotations
from typing import Any
from typing import Optional
from typing import TYPE_CHECKING
from google.genai import types
@@ -61,7 +62,9 @@ class AgentTool(BaseTool):
return data
@override
def _get_declaration(self) -> types.FunctionDeclaration:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
from ..agents.llm_agent import LlmAgent
from ..utils.variant_utils import GoogleLLMVariant
@@ -20,7 +20,7 @@ from typing import Dict
from typing import Optional
from typing import Union
from google.genai.types import FunctionDeclaration
from google.genai import types
from typing_extensions import override
from ...auth.auth_credential import AuthCredential
@@ -115,7 +115,9 @@ class IntegrationConnectorTool(BaseTool):
self._auth_credential = auth_credential
@override
def _get_declaration(self) -> FunctionDeclaration:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
"""Returns the function declaration in the Gemini Schema format."""
schema_dict = self._rest_api_tool._operation_parser.get_json_schema()
for field in self.EXCLUDE_FIELDS:
@@ -126,7 +128,7 @@ class IntegrationConnectorTool(BaseTool):
schema_dict['required'].remove(field)
parameters = _to_gemini_schema(schema_dict)
function_decl = FunctionDeclaration(
function_decl = types.FunctionDeclaration(
name=self.name, description=self.description, parameters=parameters
)
return function_decl
+3 -1
View File
@@ -78,7 +78,9 @@ class BaseTool(ABC):
self.is_long_running = is_long_running
self.custom_metadata = custom_metadata
def _get_declaration(self) -> Optional[types.FunctionDeclaration]:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
"""Gets the OpenAPI specification of this tool in the form of a FunctionDeclaration.
NOTE:
+5 -1
View File
@@ -14,6 +14,8 @@
from __future__ import annotations
from typing import Optional
from google.genai import types
from typing_extensions import override
@@ -62,7 +64,9 @@ class CrewaiTool(FunctionTool):
self.description = tool.description
@override
def _get_declaration(self) -> types.FunctionDeclaration:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
"""Build the function declaration for the tool."""
function_declaration = _automatic_function_calling_util.build_function_declaration_for_params_for_crewai(
False,
+4 -1
View File
@@ -62,7 +62,9 @@ class FunctionTool(BaseTool):
self._ignore_params = ['tool_context', 'input_stream']
@override
def _get_declaration(self) -> Optional[types.FunctionDeclaration]:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
function_decl = types.FunctionDeclaration.model_validate(
build_function_declaration(
func=self.func,
@@ -70,6 +72,7 @@ class FunctionTool(BaseTool):
# input_stream is for streaming tool
ignore_params=self._ignore_params,
variant=self._api_variant,
ignore_return_declaration=ignore_return_declaration,
)
)
@@ -11,14 +11,13 @@
# 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 __future__ import annotations
from typing import Any
from typing import Dict
from typing import Optional
from google.genai.types import FunctionDeclaration
from google.genai import types
from typing_extensions import override
from ...auth.auth_credential import AuthCredential
@@ -52,7 +51,9 @@ class GoogleApiTool(BaseTool):
self.configure_auth(client_id, client_secret)
@override
def _get_declaration(self) -> FunctionDeclaration:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
return self._rest_api_tool._get_declaration()
@override
+3 -1
View File
@@ -101,7 +101,9 @@ class LangchainTool(FunctionTool):
# else: keep default from FunctionTool
@override
def _get_declaration(self) -> types.FunctionDeclaration:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
"""Build the function declaration for the tool.
Returns:
+5 -1
View File
@@ -16,6 +16,7 @@ from __future__ import annotations
import json
from typing import Any
from typing import Optional
from typing import TYPE_CHECKING
from google.genai import types
@@ -37,7 +38,10 @@ class LoadArtifactsTool(BaseTool):
description='Loads the artifacts and adds them to the session.',
)
def _get_declaration(self) -> types.FunctionDeclaration | None:
@override
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
return types.FunctionDeclaration(
name=self.name,
description=self.description,
+4 -1
View File
@@ -14,6 +14,7 @@
from __future__ import annotations
from typing import Optional
from typing import TYPE_CHECKING
from google.genai import types
@@ -58,7 +59,9 @@ class LoadMemoryTool(FunctionTool):
super().__init__(load_memory)
@override
def _get_declaration(self) -> types.FunctionDeclaration | None:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
return types.FunctionDeclaration(
name=self.name,
description=self.description,
+3 -1
View File
@@ -45,7 +45,9 @@ class LongRunningFunctionTool(FunctionTool):
self.is_long_running = True
@override
def _get_declaration(self) -> Optional[types.FunctionDeclaration]:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
declaration = super()._get_declaration()
if declaration:
instruction = (
+4 -1
View File
@@ -19,6 +19,7 @@ import logging
from typing import Optional
from fastapi.openapi.models import APIKeyIn
from google.genai import types
from google.genai.types import FunctionDeclaration
from typing_extensions import override
@@ -97,7 +98,9 @@ class MCPTool(BaseAuthenticatedTool):
self._mcp_session_manager = mcp_session_manager
@override
def _get_declaration(self) -> FunctionDeclaration:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
"""Gets the function declaration for the tool.
Returns:
@@ -23,6 +23,7 @@ from typing import Tuple
from typing import Union
from fastapi.openapi.models import Operation
from google.genai import types
from google.genai.types import FunctionDeclaration
import requests
from typing_extensions import override
@@ -181,7 +182,9 @@ class RestApiTool(BaseTool):
return RestApiTool.from_parsed_operation(operation)
@override
def _get_declaration(self) -> FunctionDeclaration:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
"""Returns the function declaration in the Gemini Schema format."""
schema_dict = self._operation_parser.get_json_schema()
parameters = _to_gemini_schema(schema_dict)
@@ -11,6 +11,9 @@
# 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 __future__ import annotations
from typing import Optional
from google.genai import types
from typing_extensions import override
@@ -21,7 +24,9 @@ from ..base_tool import BaseTool
class BaseRetrievalTool(BaseTool):
@override
def _get_declaration(self) -> types.FunctionDeclaration:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
return types.FunctionDeclaration(
name=self.name,
description=self.description,
@@ -81,7 +81,9 @@ class SetModelResponseTool(BaseTool):
)
@override
def _get_declaration(self) -> Optional[types.FunctionDeclaration]:
def _get_declaration(
self, ignore_return_declaration: bool = False
) -> Optional[types.FunctionDeclaration]:
"""Gets the OpenAPI specification of this tool."""
function_decl = types.FunctionDeclaration.model_validate(
build_function_declaration(

Some files were not shown because too many files have changed in this diff Show More