From 985e948e6cf15be80bc899b2035d8d16d0ee0c1b Mon Sep 17 00:00:00 2001 From: t-miyak Date: Thu, 16 Oct 2025 03:31:49 +0900 Subject: [PATCH 01/10] fix: support list of pydantic model to FunctionTool arg annotation --- src/google/adk/tools/function_tool.py | 41 ++++-- .../tools/test_function_tool_pydantic.py | 125 +++++++++++++++++- 2 files changed, 154 insertions(+), 12 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 1ab32d42b7..cd2791ffd8 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -16,17 +16,13 @@ import inspect import logging -from typing import Any -from typing import Callable -from typing import get_args -from typing import get_origin -from typing import Optional -from typing import Union +from typing import Any, Callable, Optional, Union, get_args, get_origin -from google.genai import types import pydantic from typing_extensions import override +from google.genai import types + from ..utils.context_utils import Aclosing from ._automatic_function_calling_util import build_function_declaration from .base_tool import BaseTool @@ -102,6 +98,7 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: Currently handles: - Converting JSON dictionaries to Pydantic model instances where expected + - Converting lists of JSON dictionaries to lists of Pydantic model instances Future extensions could include: - Type coercion for other complex types @@ -129,8 +126,36 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: if len(non_none_types) == 1: target_type = non_none_types[0] + # Check if the target type is a list + if get_origin(target_type) is list: + list_args = get_args(target_type) + if list_args: + element_type = list_args[0] + + # Check if the element type is a Pydantic model + if inspect.isclass(element_type) and issubclass( + element_type, pydantic.BaseModel + ): + # Skip conversion if the value is None + if args[param_name] is None: + continue + + # Convert list elements to Pydantic models + if isinstance(args[param_name], list): + converted_list = [] + for item in args[param_name]: + try: + converted_list.append(element_type.model_validate(item)) + except Exception as e: + # Skip items that fail validation + logger.warning( + f"Skipping item in '{param_name}': " + f'Failed to convert to {element_type.__name__}: {e}' + ) + converted_args[param_name] = converted_list + # Check if the target type is a Pydantic model - if inspect.isclass(target_type) and issubclass( + elif inspect.isclass(target_type) and issubclass( target_type, pydantic.BaseModel ): # Skip conversion if the value is None and the parameter is Optional diff --git a/tests/unittests/tools/test_function_tool_pydantic.py b/tests/unittests/tools/test_function_tool_pydantic.py index 1af5d68345..06b13ce177 100644 --- a/tests/unittests/tools/test_function_tool_pydantic.py +++ b/tests/unittests/tools/test_function_tool_pydantic.py @@ -17,12 +17,13 @@ from typing import Optional from unittest.mock import MagicMock +import pydantic +import pytest + from google.adk.agents.invocation_context import InvocationContext from google.adk.sessions.session import Session from google.adk.tools.function_tool import FunctionTool from google.adk.tools.tool_context import ToolContext -import pydantic -import pytest class UserModel(pydantic.BaseModel): @@ -280,5 +281,121 @@ async def test_run_async_with_optional_pydantic_models(): assert result["theme"] == "dark" assert result["notifications"] is True assert result["preferences_type"] == "PreferencesModel" - assert result["preferences_type"] == "PreferencesModel" - assert result["preferences_type"] == "PreferencesModel" + + +def function_with_list_of_pydantic_models(users: list[UserModel]) -> dict: + """Function that takes a list of Pydantic models.""" + return { + "count": len(users), + "names": [user.name for user in users], + "ages": [user.age for user in users], + "types": [type(user).__name__ for user in users], + } + + +def function_with_optional_list_of_pydantic_models( + users: Optional[list[UserModel]] = None, +) -> dict: + """Function that takes an optional list of Pydantic models.""" + if users is None: + return {"count": 0, "names": []} + return { + "count": len(users), + "names": [user.name for user in users], + } + + +def test_preprocess_args_with_list_of_dicts_to_pydantic_models(): + """Test _preprocess_args converts list of dicts to list of Pydantic models.""" + tool = FunctionTool(function_with_list_of_pydantic_models) + + input_args = { + "users": [ + {"name": "Alice", "age": 30, "email": "alice@example.com"}, + {"name": "Bob", "age": 25}, + {"name": "Charlie", "age": 35, "email": "charlie@example.com"}, + ] + } + + processed_args = tool._preprocess_args(input_args) + + # Check that the list of dicts was converted to a list of Pydantic models + assert "users" in processed_args + users = processed_args["users"] + assert isinstance(users, list) + assert len(users) == 3 + + # Check each element is a Pydantic model with correct data + assert isinstance(users[0], UserModel) + assert users[0].name == "Alice" + assert users[0].age == 30 + assert users[0].email == "alice@example.com" + + assert isinstance(users[1], UserModel) + assert users[1].name == "Bob" + assert users[1].age == 25 + assert users[1].email is None + + assert isinstance(users[2], UserModel) + assert users[2].name == "Charlie" + assert users[2].age == 35 + assert users[2].email == "charlie@example.com" + + +def test_preprocess_args_with_optional_list_of_pydantic_models_none(): + """Test _preprocess_args handles None for optional list parameter.""" + tool = FunctionTool(function_with_optional_list_of_pydantic_models) + + input_args = {"users": None} + + processed_args = tool._preprocess_args(input_args) + + # Check that None is preserved + assert "users" in processed_args + assert processed_args["users"] is None + + +def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): + """Test _preprocess_args converts list for optional list parameter.""" + tool = FunctionTool(function_with_optional_list_of_pydantic_models) + + input_args = { + "users": [ + {"name": "Alice", "age": 30}, + {"name": "Bob", "age": 25}, + ] + } + + processed_args = tool._preprocess_args(input_args) + + # Check conversion + assert "users" in processed_args + users = processed_args["users"] + assert len(users) == 2 + assert all(isinstance(user, UserModel) for user in users) + assert users[0].name == "Alice" + assert users[1].name == "Bob" + + +def test_preprocess_args_with_list_skips_invalid_items(): + """Test _preprocess_args skips items that fail validation.""" + tool = FunctionTool(function_with_list_of_pydantic_models) + + input_args = { + "users": [ + {"name": "Alice", "age": 30}, + {"name": "Invalid"}, # Missing required 'age' field + {"name": "Bob", "age": 25}, + ] + } + + processed_args = tool._preprocess_args(input_args) + + # Check that invalid item was skipped + assert "users" in processed_args + users = processed_args["users"] + assert len(users) == 2 # Only 2 valid items + assert users[0].name == "Alice" + assert users[0].age == 30 + assert users[1].name == "Bob" + assert users[1].age == 25 From f07a5aac37cc7cdd80ed5238bb12f42379c1ce59 Mon Sep 17 00:00:00 2001 From: t-miyak <19338700+t-miyak@users.noreply.github.com> Date: Sun, 19 Oct 2025 13:49:55 +0900 Subject: [PATCH 02/10] refactor: format files changed --- src/google/adk/tools/function_tool.py | 10 +++++++--- tests/unittests/tools/test_function_tool_pydantic.py | 5 ++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index cd2791ffd8..ab04b0223c 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -16,13 +16,17 @@ import inspect import logging -from typing import Any, Callable, Optional, Union, get_args, get_origin +from typing import Any +from typing import Callable +from typing import get_args +from typing import get_origin +from typing import Optional +from typing import Union +from google.genai import types import pydantic from typing_extensions import override -from google.genai import types - from ..utils.context_utils import Aclosing from ._automatic_function_calling_util import build_function_declaration from .base_tool import BaseTool diff --git a/tests/unittests/tools/test_function_tool_pydantic.py b/tests/unittests/tools/test_function_tool_pydantic.py index 06b13ce177..acacd5e8be 100644 --- a/tests/unittests/tools/test_function_tool_pydantic.py +++ b/tests/unittests/tools/test_function_tool_pydantic.py @@ -17,13 +17,12 @@ from typing import Optional from unittest.mock import MagicMock -import pydantic -import pytest - from google.adk.agents.invocation_context import InvocationContext from google.adk.sessions.session import Session from google.adk.tools.function_tool import FunctionTool from google.adk.tools.tool_context import ToolContext +import pydantic +import pytest class UserModel(pydantic.BaseModel): From 2bea40c418eacd2fa6903f51ff98c95bac5fb118 Mon Sep 17 00:00:00 2001 From: t-miyak <19338700+t-miyak@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:22:13 +0900 Subject: [PATCH 03/10] fix: use original item data if its conversion fails --- src/google/adk/tools/function_tool.py | 63 ++++++++++++++------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index ab04b0223c..22c85c8a08 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -32,7 +32,7 @@ from .base_tool import BaseTool from .tool_context import ToolContext -logger = logging.getLogger('google_adk.' + __name__) +logger = logging.getLogger("google_adk." + __name__) class FunctionTool(BaseTool): @@ -57,22 +57,22 @@ def __init__( the callable returns True, the tool will require confirmation from the user. """ - name = '' - doc = '' + name = "" + doc = "" # Handle different types of callables - if hasattr(func, '__name__'): + if hasattr(func, "__name__"): # Regular functions, unbound methods, etc. name = func.__name__ - elif hasattr(func, '__class__'): + elif hasattr(func, "__class__"): # Callable objects, bound methods, etc. name = func.__class__.__name__ # Get documentation (prioritize direct __doc__ if available) - if hasattr(func, '__doc__') and func.__doc__: + if hasattr(func, "__doc__") and func.__doc__: doc = inspect.cleandoc(func.__doc__) elif ( - hasattr(func, '__call__') - and hasattr(func.__call__, '__doc__') + hasattr(func, "__call__") + and hasattr(func.__call__, "__doc__") and func.__call__.__doc__ ): # For callable objects, try to get docstring from __call__ method @@ -80,7 +80,7 @@ def __init__( super().__init__(name=name, description=doc) self.func = func - self._ignore_params = ['tool_context', 'input_stream'] + self._ignore_params = ["tool_context", "input_stream"] self._require_confirmation = require_confirmation @override @@ -151,12 +151,13 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: try: converted_list.append(element_type.model_validate(item)) except Exception as e: - # Skip items that fail validation logger.warning( - f"Skipping item in '{param_name}': " - f'Failed to convert to {element_type.__name__}: {e}' + f"Failed to convert item in '{param_name}' to Pydantic" + f" model {element_type.__name__}: {e}" ) - converted_args[param_name] = converted_list + + # Keep the original value if conversion fails + converted_list.append(item) # Check if the target type is a Pydantic model elif inspect.isclass(target_type) and issubclass( @@ -175,7 +176,7 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: except Exception as e: logger.warning( f"Failed to convert argument '{param_name}' to Pydantic model" - f' {target_type.__name__}: {e}' + f" model {target_type.__name__}: {e}" ) # Keep the original value if conversion fails pass @@ -191,8 +192,8 @@ async def run_async( signature = inspect.signature(self.func) valid_params = {param for param in signature.parameters} - if 'tool_context' in valid_params: - args_to_call['tool_context'] = tool_context + if "tool_context" in valid_params: + args_to_call["tool_context"] = tool_context # Filter args_to_call to only include valid parameters for the function args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params} @@ -208,11 +209,11 @@ async def run_async( ] if missing_mandatory_args: - missing_mandatory_args_str = '\n'.join(missing_mandatory_args) + missing_mandatory_args_str = "\n".join(missing_mandatory_args) error_str = f"""Invoking `{self.name}()` failed as the following mandatory input parameters are not present: {missing_mandatory_args_str} You could retry calling this tool, but it is IMPORTANT for you to provide all the mandatory parameters.""" - return {'error': error_str} + return {"error": error_str} if isinstance(self._require_confirmation, Callable): require_confirmation = await self._invoke_callable( @@ -224,24 +225,24 @@ async def run_async( if require_confirmation: if not tool_context.tool_confirmation: args_to_show = args_to_call.copy() - if 'tool_context' in args_to_show: - args_to_show.pop('tool_context') + if "tool_context" in args_to_show: + args_to_show.pop("tool_context") tool_context.request_confirmation( hint=( - f'Please approve or reject the tool call {self.name}() by' - ' responding with a FunctionResponse with an expected' - ' ToolConfirmation payload.' + f"Please approve or reject the tool call {self.name}() by" + " responding with a FunctionResponse with an expected" + " ToolConfirmation payload." ), ) return { - 'error': ( - 'This tool call requires confirmation, please approve or' - ' reject.' + "error": ( + "This tool call requires confirmation, please approve or" + " reject." ) } elif not tool_context.tool_confirmation.confirmed: - return {'error': 'This tool call is rejected.'} + return {"error": "This tool call is rejected."} return await self._invoke_callable(self.func, args_to_call) @@ -254,7 +255,7 @@ async def _invoke_callable( # checking coroutine function is not enough. We also need to check whether # Callable's __call__ function is a coroutine funciton is_async = inspect.iscoroutinefunction(target) or ( - hasattr(target, '__call__') + hasattr(target, "__call__") and inspect.iscoroutinefunction(target.__call__) ) if is_async: @@ -276,11 +277,11 @@ async def _call_live( self.name in invocation_context.active_streaming_tools and invocation_context.active_streaming_tools[self.name].stream ): - args_to_call['input_stream'] = invocation_context.active_streaming_tools[ + args_to_call["input_stream"] = invocation_context.active_streaming_tools[ self.name ].stream - if 'tool_context' in signature.parameters: - args_to_call['tool_context'] = tool_context + if "tool_context" in signature.parameters: + args_to_call["tool_context"] = tool_context # TODO: support tool confirmation for live mode. async with Aclosing(self.func(**args_to_call)) as agen: From fe575f7071352b26d077ff27299aa8758f58b58b Mon Sep 17 00:00:00 2001 From: t-miyak <19338700+t-miyak@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:30:46 +0900 Subject: [PATCH 04/10] test: update pytest --- src/google/adk/tools/function_tool.py | 2 ++ .../tools/test_function_tool_pydantic.py | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 22c85c8a08..4004c5368d 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -159,6 +159,8 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: # Keep the original value if conversion fails converted_list.append(item) + converted_args[param_name] = converted_list + # Check if the target type is a Pydantic model elif inspect.isclass(target_type) and issubclass( target_type, pydantic.BaseModel diff --git a/tests/unittests/tools/test_function_tool_pydantic.py b/tests/unittests/tools/test_function_tool_pydantic.py index acacd5e8be..e83ffb5d25 100644 --- a/tests/unittests/tools/test_function_tool_pydantic.py +++ b/tests/unittests/tools/test_function_tool_pydantic.py @@ -376,8 +376,8 @@ def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): assert users[1].name == "Bob" -def test_preprocess_args_with_list_skips_invalid_items(): - """Test _preprocess_args skips items that fail validation.""" +def test_preprocess_args_with_list_keeps_invalid_items_as_original(): + """Test _preprocess_args keeps original data for items that fail validation.""" tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { @@ -390,11 +390,21 @@ def test_preprocess_args_with_list_skips_invalid_items(): processed_args = tool._preprocess_args(input_args) - # Check that invalid item was skipped + # Check that all items are preserved assert "users" in processed_args users = processed_args["users"] - assert len(users) == 2 # Only 2 valid items + assert len(users) == 3 # All items preserved + + # First item should be converted to UserModel + assert isinstance(users[0], UserModel) assert users[0].name == "Alice" assert users[0].age == 30 - assert users[1].name == "Bob" - assert users[1].age == 25 + + # Second item should remain as dict (failed validation) + assert isinstance(users[1], dict) + assert users[1] == {"name": "Invalid"} + + # Third item should be converted to UserModel + assert isinstance(users[2], UserModel) + assert users[2].name == "Bob" + assert users[2].age == 25 From 464de3b6da6323fdc829df0df34a21d302e8acdf Mon Sep 17 00:00:00 2001 From: t-miyak Date: Sat, 1 Nov 2025 17:44:42 +0900 Subject: [PATCH 05/10] fix: format in single quotes --- src/google/adk/tools/function_tool.py | 56 ++--- .../tools/test_function_tool_pydantic.py | 210 +++++++++--------- 2 files changed, 133 insertions(+), 133 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 4004c5368d..1f59e2e2dd 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -32,7 +32,7 @@ from .base_tool import BaseTool from .tool_context import ToolContext -logger = logging.getLogger("google_adk." + __name__) +logger = logging.getLogger('google_adk.' + __name__) class FunctionTool(BaseTool): @@ -57,22 +57,22 @@ def __init__( the callable returns True, the tool will require confirmation from the user. """ - name = "" - doc = "" + name = '' + doc = '' # Handle different types of callables - if hasattr(func, "__name__"): + if hasattr(func, '__name__'): # Regular functions, unbound methods, etc. name = func.__name__ - elif hasattr(func, "__class__"): + elif hasattr(func, '__class__'): # Callable objects, bound methods, etc. name = func.__class__.__name__ # Get documentation (prioritize direct __doc__ if available) - if hasattr(func, "__doc__") and func.__doc__: + if hasattr(func, '__doc__') and func.__doc__: doc = inspect.cleandoc(func.__doc__) elif ( - hasattr(func, "__call__") - and hasattr(func.__call__, "__doc__") + hasattr(func, '__call__') + and hasattr(func.__call__, '__doc__') and func.__call__.__doc__ ): # For callable objects, try to get docstring from __call__ method @@ -80,7 +80,7 @@ def __init__( super().__init__(name=name, description=doc) self.func = func - self._ignore_params = ["tool_context", "input_stream"] + self._ignore_params = ['tool_context', 'input_stream'] self._require_confirmation = require_confirmation @override @@ -153,7 +153,7 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: except Exception as e: logger.warning( f"Failed to convert item in '{param_name}' to Pydantic" - f" model {element_type.__name__}: {e}" + f' model {element_type.__name__}: {e}' ) # Keep the original value if conversion fails @@ -178,7 +178,7 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: except Exception as e: logger.warning( f"Failed to convert argument '{param_name}' to Pydantic model" - f" model {target_type.__name__}: {e}" + f' model {target_type.__name__}: {e}' ) # Keep the original value if conversion fails pass @@ -194,8 +194,8 @@ async def run_async( signature = inspect.signature(self.func) valid_params = {param for param in signature.parameters} - if "tool_context" in valid_params: - args_to_call["tool_context"] = tool_context + if 'tool_context' in valid_params: + args_to_call['tool_context'] = tool_context # Filter args_to_call to only include valid parameters for the function args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params} @@ -211,11 +211,11 @@ async def run_async( ] if missing_mandatory_args: - missing_mandatory_args_str = "\n".join(missing_mandatory_args) + missing_mandatory_args_str = '\n'.join(missing_mandatory_args) error_str = f"""Invoking `{self.name}()` failed as the following mandatory input parameters are not present: {missing_mandatory_args_str} You could retry calling this tool, but it is IMPORTANT for you to provide all the mandatory parameters.""" - return {"error": error_str} + return {'error': error_str} if isinstance(self._require_confirmation, Callable): require_confirmation = await self._invoke_callable( @@ -227,24 +227,24 @@ async def run_async( if require_confirmation: if not tool_context.tool_confirmation: args_to_show = args_to_call.copy() - if "tool_context" in args_to_show: - args_to_show.pop("tool_context") + if 'tool_context' in args_to_show: + args_to_show.pop('tool_context') tool_context.request_confirmation( hint=( - f"Please approve or reject the tool call {self.name}() by" - " responding with a FunctionResponse with an expected" - " ToolConfirmation payload." + f'Please approve or reject the tool call {self.name}() by' + ' responding with a FunctionResponse with an expected' + ' ToolConfirmation payload.' ), ) return { - "error": ( - "This tool call requires confirmation, please approve or" - " reject." + 'error': ( + 'This tool call requires confirmation, please approve or' + ' reject.' ) } elif not tool_context.tool_confirmation.confirmed: - return {"error": "This tool call is rejected."} + return {'error': 'This tool call is rejected.'} return await self._invoke_callable(self.func, args_to_call) @@ -257,7 +257,7 @@ async def _invoke_callable( # checking coroutine function is not enough. We also need to check whether # Callable's __call__ function is a coroutine funciton is_async = inspect.iscoroutinefunction(target) or ( - hasattr(target, "__call__") + hasattr(target, '__call__') and inspect.iscoroutinefunction(target.__call__) ) if is_async: @@ -279,11 +279,11 @@ async def _call_live( self.name in invocation_context.active_streaming_tools and invocation_context.active_streaming_tools[self.name].stream ): - args_to_call["input_stream"] = invocation_context.active_streaming_tools[ + args_to_call['input_stream'] = invocation_context.active_streaming_tools[ self.name ].stream - if "tool_context" in signature.parameters: - args_to_call["tool_context"] = tool_context + if 'tool_context' in signature.parameters: + args_to_call['tool_context'] = tool_context # TODO: support tool confirmation for live mode. async with Aclosing(self.func(**args_to_call)) as agen: diff --git a/tests/unittests/tools/test_function_tool_pydantic.py b/tests/unittests/tools/test_function_tool_pydantic.py index e83ffb5d25..34456a7c44 100644 --- a/tests/unittests/tools/test_function_tool_pydantic.py +++ b/tests/unittests/tools/test_function_tool_pydantic.py @@ -36,27 +36,27 @@ class UserModel(pydantic.BaseModel): class PreferencesModel(pydantic.BaseModel): """Test Pydantic model for preferences.""" - theme: str = "light" + theme: str = 'light' notifications: bool = True def sync_function_with_pydantic_model(user: UserModel) -> dict: """Sync function that takes a Pydantic model.""" return { - "name": user.name, - "age": user.age, - "email": user.email, - "type": str(type(user).__name__), + 'name': user.name, + 'age': user.age, + 'email': user.email, + 'type': str(type(user).__name__), } async def async_function_with_pydantic_model(user: UserModel) -> dict: """Async function that takes a Pydantic model.""" return { - "name": user.name, - "age": user.age, - "email": user.email, - "type": str(type(user).__name__), + 'name': user.name, + 'age': user.age, + 'email': user.email, + 'type': str(type(user).__name__), } @@ -65,14 +65,14 @@ def function_with_optional_pydantic_model( ) -> dict: """Function with required and optional Pydantic models.""" result = { - "user_name": user.name, - "user_type": str(type(user).__name__), + 'user_name': user.name, + 'user_type': str(type(user).__name__), } if preferences: result.update({ - "theme": preferences.theme, - "notifications": preferences.notifications, - "preferences_type": str(type(preferences).__name__), + 'theme': preferences.theme, + 'notifications': preferences.notifications, + 'preferences_type': str(type(preferences).__name__), }) return result @@ -82,10 +82,10 @@ def function_with_mixed_args( ) -> dict: """Function with mixed argument types including Pydantic model.""" return { - "name": name, - "user_name": user.name, - "user_type": str(type(user).__name__), - "count": count, + 'name': name, + 'user_name': user.name, + 'user_type': str(type(user).__name__), + 'count': count, } @@ -94,18 +94,18 @@ def test_preprocess_args_with_dict_to_pydantic_conversion(): tool = FunctionTool(sync_function_with_pydantic_model) input_args = { - "user": {"name": "Alice", "age": 30, "email": "alice@example.com"} + 'user': {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'} } processed_args = tool._preprocess_args(input_args) # Check that the dict was converted to a Pydantic model - assert "user" in processed_args - user = processed_args["user"] + assert 'user' in processed_args + user = processed_args['user'] assert isinstance(user, UserModel) - assert user.name == "Alice" + assert user.name == 'Alice' assert user.age == 30 - assert user.email == "alice@example.com" + assert user.email == 'alice@example.com' def test_preprocess_args_with_existing_pydantic_model(): @@ -113,33 +113,33 @@ def test_preprocess_args_with_existing_pydantic_model(): tool = FunctionTool(sync_function_with_pydantic_model) # Create an existing Pydantic model - existing_user = UserModel(name="Bob", age=25) - input_args = {"user": existing_user} + existing_user = UserModel(name='Bob', age=25) + input_args = {'user': existing_user} processed_args = tool._preprocess_args(input_args) # Check that the existing model was not changed (same object) - assert "user" in processed_args - user = processed_args["user"] + assert 'user' in processed_args + user = processed_args['user'] assert user is existing_user assert isinstance(user, UserModel) - assert user.name == "Bob" + assert user.name == 'Bob' def test_preprocess_args_with_optional_pydantic_model_none(): """Test _preprocess_args handles None for optional Pydantic models.""" tool = FunctionTool(function_with_optional_pydantic_model) - input_args = {"user": {"name": "Charlie", "age": 35}, "preferences": None} + input_args = {'user': {'name': 'Charlie', 'age': 35}, 'preferences': None} processed_args = tool._preprocess_args(input_args) # Check user conversion - assert isinstance(processed_args["user"], UserModel) - assert processed_args["user"].name == "Charlie" + assert isinstance(processed_args['user'], UserModel) + assert processed_args['user'].name == 'Charlie' # Check preferences remains None - assert processed_args["preferences"] is None + assert processed_args['preferences'] is None def test_preprocess_args_with_optional_pydantic_model_dict(): @@ -147,19 +147,19 @@ def test_preprocess_args_with_optional_pydantic_model_dict(): tool = FunctionTool(function_with_optional_pydantic_model) input_args = { - "user": {"name": "Diana", "age": 28}, - "preferences": {"theme": "dark", "notifications": False}, + 'user': {'name': 'Diana', 'age': 28}, + 'preferences': {'theme': 'dark', 'notifications': False}, } processed_args = tool._preprocess_args(input_args) # Check both conversions - assert isinstance(processed_args["user"], UserModel) - assert processed_args["user"].name == "Diana" + assert isinstance(processed_args['user'], UserModel) + assert processed_args['user'].name == 'Diana' - assert isinstance(processed_args["preferences"], PreferencesModel) - assert processed_args["preferences"].theme == "dark" - assert processed_args["preferences"].notifications is False + assert isinstance(processed_args['preferences'], PreferencesModel) + assert processed_args['preferences'].theme == 'dark' + assert processed_args['preferences'].notifications is False def test_preprocess_args_with_mixed_types(): @@ -167,21 +167,21 @@ def test_preprocess_args_with_mixed_types(): tool = FunctionTool(function_with_mixed_args) input_args = { - "name": "test_name", - "user": {"name": "Eve", "age": 40}, - "count": 10, + 'name': 'test_name', + 'user': {'name': 'Eve', 'age': 40}, + 'count': 10, } processed_args = tool._preprocess_args(input_args) # Check that only Pydantic model was converted - assert processed_args["name"] == "test_name" # string unchanged - assert processed_args["count"] == 10 # int unchanged + assert processed_args['name'] == 'test_name' # string unchanged + assert processed_args['count'] == 10 # int unchanged # Check Pydantic model conversion - assert isinstance(processed_args["user"], UserModel) - assert processed_args["user"].name == "Eve" - assert processed_args["user"].age == 40 + assert isinstance(processed_args['user'], UserModel) + assert processed_args['user'].name == 'Eve' + assert processed_args['user'].age == 40 def test_preprocess_args_with_invalid_data_graceful_failure(): @@ -189,23 +189,23 @@ def test_preprocess_args_with_invalid_data_graceful_failure(): tool = FunctionTool(sync_function_with_pydantic_model) # Invalid data that can't be converted to UserModel - input_args = {"user": "invalid_string"} # string instead of dict/model + input_args = {'user': 'invalid_string'} # string instead of dict/model processed_args = tool._preprocess_args(input_args) # Should keep original value when conversion fails - assert processed_args["user"] == "invalid_string" + assert processed_args['user'] == 'invalid_string' def test_preprocess_args_with_non_pydantic_parameters(): """Test _preprocess_args ignores non-Pydantic parameters.""" def simple_function(name: str, age: int) -> dict: - return {"name": name, "age": age} + return {'name': name, 'age': age} tool = FunctionTool(simple_function) - input_args = {"name": "test", "age": 25} + input_args = {'name': 'test', 'age': 25} processed_args = tool._preprocess_args(input_args) # Should remain unchanged (no Pydantic models to convert) @@ -223,15 +223,15 @@ async def test_run_async_with_pydantic_model_conversion_sync_function(): invocation_context_mock.session = session_mock tool_context_mock.invocation_context = invocation_context_mock - args = {"user": {"name": "Frank", "age": 45, "email": "frank@example.com"}} + args = {'user': {'name': 'Frank', 'age': 45, 'email': 'frank@example.com'}} result = await tool.run_async(args=args, tool_context=tool_context_mock) # Verify the function received a proper Pydantic model - assert result["name"] == "Frank" - assert result["age"] == 45 - assert result["email"] == "frank@example.com" - assert result["type"] == "UserModel" + assert result['name'] == 'Frank' + assert result['age'] == 45 + assert result['email'] == 'frank@example.com' + assert result['type'] == 'UserModel' @pytest.mark.asyncio @@ -245,15 +245,15 @@ async def test_run_async_with_pydantic_model_conversion_async_function(): invocation_context_mock.session = session_mock tool_context_mock.invocation_context = invocation_context_mock - args = {"user": {"name": "Grace", "age": 32}} + args = {'user': {'name': 'Grace', 'age': 32}} result = await tool.run_async(args=args, tool_context=tool_context_mock) # Verify the function received a proper Pydantic model - assert result["name"] == "Grace" - assert result["age"] == 32 - assert result["email"] is None # default value - assert result["type"] == "UserModel" + assert result['name'] == 'Grace' + assert result['age'] == 32 + assert result['email'] is None # default value + assert result['type'] == 'UserModel' @pytest.mark.asyncio @@ -269,26 +269,26 @@ async def test_run_async_with_optional_pydantic_models(): # Test with both required and optional models args = { - "user": {"name": "Henry", "age": 50}, - "preferences": {"theme": "dark", "notifications": True}, + 'user': {'name': 'Henry', 'age': 50}, + 'preferences': {'theme': 'dark', 'notifications': True}, } result = await tool.run_async(args=args, tool_context=tool_context_mock) - assert result["user_name"] == "Henry" - assert result["user_type"] == "UserModel" - assert result["theme"] == "dark" - assert result["notifications"] is True - assert result["preferences_type"] == "PreferencesModel" + assert result['user_name'] == 'Henry' + assert result['user_type'] == 'UserModel' + assert result['theme'] == 'dark' + assert result['notifications'] is True + assert result['preferences_type'] == 'PreferencesModel' def function_with_list_of_pydantic_models(users: list[UserModel]) -> dict: """Function that takes a list of Pydantic models.""" return { - "count": len(users), - "names": [user.name for user in users], - "ages": [user.age for user in users], - "types": [type(user).__name__ for user in users], + 'count': len(users), + 'names': [user.name for user in users], + 'ages': [user.age for user in users], + 'types': [type(user).__name__ for user in users], } @@ -297,10 +297,10 @@ def function_with_optional_list_of_pydantic_models( ) -> dict: """Function that takes an optional list of Pydantic models.""" if users is None: - return {"count": 0, "names": []} + return {'count': 0, 'names': []} return { - "count": len(users), - "names": [user.name for user in users], + 'count': len(users), + 'names': [user.name for user in users], } @@ -309,49 +309,49 @@ def test_preprocess_args_with_list_of_dicts_to_pydantic_models(): tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { - "users": [ - {"name": "Alice", "age": 30, "email": "alice@example.com"}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35, "email": "charlie@example.com"}, + 'users': [ + {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}, + {'name': 'Bob', 'age': 25}, + {'name': 'Charlie', 'age': 35, 'email': 'charlie@example.com'}, ] } processed_args = tool._preprocess_args(input_args) # Check that the list of dicts was converted to a list of Pydantic models - assert "users" in processed_args - users = processed_args["users"] + assert 'users' in processed_args + users = processed_args['users'] assert isinstance(users, list) assert len(users) == 3 # Check each element is a Pydantic model with correct data assert isinstance(users[0], UserModel) - assert users[0].name == "Alice" + assert users[0].name == 'Alice' assert users[0].age == 30 - assert users[0].email == "alice@example.com" + assert users[0].email == 'alice@example.com' assert isinstance(users[1], UserModel) - assert users[1].name == "Bob" + assert users[1].name == 'Bob' assert users[1].age == 25 assert users[1].email is None assert isinstance(users[2], UserModel) - assert users[2].name == "Charlie" + assert users[2].name == 'Charlie' assert users[2].age == 35 - assert users[2].email == "charlie@example.com" + assert users[2].email == 'charlie@example.com' def test_preprocess_args_with_optional_list_of_pydantic_models_none(): """Test _preprocess_args handles None for optional list parameter.""" tool = FunctionTool(function_with_optional_list_of_pydantic_models) - input_args = {"users": None} + input_args = {'users': None} processed_args = tool._preprocess_args(input_args) # Check that None is preserved - assert "users" in processed_args - assert processed_args["users"] is None + assert 'users' in processed_args + assert processed_args['users'] is None def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): @@ -359,21 +359,21 @@ def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): tool = FunctionTool(function_with_optional_list_of_pydantic_models) input_args = { - "users": [ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, + 'users': [ + {'name': 'Alice', 'age': 30}, + {'name': 'Bob', 'age': 25}, ] } processed_args = tool._preprocess_args(input_args) # Check conversion - assert "users" in processed_args - users = processed_args["users"] + assert 'users' in processed_args + users = processed_args['users'] assert len(users) == 2 assert all(isinstance(user, UserModel) for user in users) - assert users[0].name == "Alice" - assert users[1].name == "Bob" + assert users[0].name == 'Alice' + assert users[1].name == 'Bob' def test_preprocess_args_with_list_keeps_invalid_items_as_original(): @@ -381,30 +381,30 @@ def test_preprocess_args_with_list_keeps_invalid_items_as_original(): tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { - "users": [ - {"name": "Alice", "age": 30}, - {"name": "Invalid"}, # Missing required 'age' field - {"name": "Bob", "age": 25}, + 'users': [ + {'name': 'Alice', 'age': 30}, + {'name': 'Invalid'}, # Missing required 'age' field + {'name': 'Bob', 'age': 25}, ] } processed_args = tool._preprocess_args(input_args) # Check that all items are preserved - assert "users" in processed_args - users = processed_args["users"] + assert 'users' in processed_args + users = processed_args['users'] assert len(users) == 3 # All items preserved # First item should be converted to UserModel assert isinstance(users[0], UserModel) - assert users[0].name == "Alice" + assert users[0].name == 'Alice' assert users[0].age == 30 # Second item should remain as dict (failed validation) assert isinstance(users[1], dict) - assert users[1] == {"name": "Invalid"} + assert users[1] == {'name': 'Invalid'} # Third item should be converted to UserModel assert isinstance(users[2], UserModel) - assert users[2].name == "Bob" + assert users[2].name == 'Bob' assert users[2].age == 25 From 2ecf493b6e4a4c0fe44c38d2140d71fb40235275 Mon Sep 17 00:00:00 2001 From: t-miyak Date: Sat, 1 Nov 2025 17:46:25 +0900 Subject: [PATCH 06/10] fix: format in single quotes --- .../tools/test_function_tool_pydantic.py | 210 +++++++++--------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/tests/unittests/tools/test_function_tool_pydantic.py b/tests/unittests/tools/test_function_tool_pydantic.py index 34456a7c44..e83ffb5d25 100644 --- a/tests/unittests/tools/test_function_tool_pydantic.py +++ b/tests/unittests/tools/test_function_tool_pydantic.py @@ -36,27 +36,27 @@ class UserModel(pydantic.BaseModel): class PreferencesModel(pydantic.BaseModel): """Test Pydantic model for preferences.""" - theme: str = 'light' + theme: str = "light" notifications: bool = True def sync_function_with_pydantic_model(user: UserModel) -> dict: """Sync function that takes a Pydantic model.""" return { - 'name': user.name, - 'age': user.age, - 'email': user.email, - 'type': str(type(user).__name__), + "name": user.name, + "age": user.age, + "email": user.email, + "type": str(type(user).__name__), } async def async_function_with_pydantic_model(user: UserModel) -> dict: """Async function that takes a Pydantic model.""" return { - 'name': user.name, - 'age': user.age, - 'email': user.email, - 'type': str(type(user).__name__), + "name": user.name, + "age": user.age, + "email": user.email, + "type": str(type(user).__name__), } @@ -65,14 +65,14 @@ def function_with_optional_pydantic_model( ) -> dict: """Function with required and optional Pydantic models.""" result = { - 'user_name': user.name, - 'user_type': str(type(user).__name__), + "user_name": user.name, + "user_type": str(type(user).__name__), } if preferences: result.update({ - 'theme': preferences.theme, - 'notifications': preferences.notifications, - 'preferences_type': str(type(preferences).__name__), + "theme": preferences.theme, + "notifications": preferences.notifications, + "preferences_type": str(type(preferences).__name__), }) return result @@ -82,10 +82,10 @@ def function_with_mixed_args( ) -> dict: """Function with mixed argument types including Pydantic model.""" return { - 'name': name, - 'user_name': user.name, - 'user_type': str(type(user).__name__), - 'count': count, + "name": name, + "user_name": user.name, + "user_type": str(type(user).__name__), + "count": count, } @@ -94,18 +94,18 @@ def test_preprocess_args_with_dict_to_pydantic_conversion(): tool = FunctionTool(sync_function_with_pydantic_model) input_args = { - 'user': {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'} + "user": {"name": "Alice", "age": 30, "email": "alice@example.com"} } processed_args = tool._preprocess_args(input_args) # Check that the dict was converted to a Pydantic model - assert 'user' in processed_args - user = processed_args['user'] + assert "user" in processed_args + user = processed_args["user"] assert isinstance(user, UserModel) - assert user.name == 'Alice' + assert user.name == "Alice" assert user.age == 30 - assert user.email == 'alice@example.com' + assert user.email == "alice@example.com" def test_preprocess_args_with_existing_pydantic_model(): @@ -113,33 +113,33 @@ def test_preprocess_args_with_existing_pydantic_model(): tool = FunctionTool(sync_function_with_pydantic_model) # Create an existing Pydantic model - existing_user = UserModel(name='Bob', age=25) - input_args = {'user': existing_user} + existing_user = UserModel(name="Bob", age=25) + input_args = {"user": existing_user} processed_args = tool._preprocess_args(input_args) # Check that the existing model was not changed (same object) - assert 'user' in processed_args - user = processed_args['user'] + assert "user" in processed_args + user = processed_args["user"] assert user is existing_user assert isinstance(user, UserModel) - assert user.name == 'Bob' + assert user.name == "Bob" def test_preprocess_args_with_optional_pydantic_model_none(): """Test _preprocess_args handles None for optional Pydantic models.""" tool = FunctionTool(function_with_optional_pydantic_model) - input_args = {'user': {'name': 'Charlie', 'age': 35}, 'preferences': None} + input_args = {"user": {"name": "Charlie", "age": 35}, "preferences": None} processed_args = tool._preprocess_args(input_args) # Check user conversion - assert isinstance(processed_args['user'], UserModel) - assert processed_args['user'].name == 'Charlie' + assert isinstance(processed_args["user"], UserModel) + assert processed_args["user"].name == "Charlie" # Check preferences remains None - assert processed_args['preferences'] is None + assert processed_args["preferences"] is None def test_preprocess_args_with_optional_pydantic_model_dict(): @@ -147,19 +147,19 @@ def test_preprocess_args_with_optional_pydantic_model_dict(): tool = FunctionTool(function_with_optional_pydantic_model) input_args = { - 'user': {'name': 'Diana', 'age': 28}, - 'preferences': {'theme': 'dark', 'notifications': False}, + "user": {"name": "Diana", "age": 28}, + "preferences": {"theme": "dark", "notifications": False}, } processed_args = tool._preprocess_args(input_args) # Check both conversions - assert isinstance(processed_args['user'], UserModel) - assert processed_args['user'].name == 'Diana' + assert isinstance(processed_args["user"], UserModel) + assert processed_args["user"].name == "Diana" - assert isinstance(processed_args['preferences'], PreferencesModel) - assert processed_args['preferences'].theme == 'dark' - assert processed_args['preferences'].notifications is False + assert isinstance(processed_args["preferences"], PreferencesModel) + assert processed_args["preferences"].theme == "dark" + assert processed_args["preferences"].notifications is False def test_preprocess_args_with_mixed_types(): @@ -167,21 +167,21 @@ def test_preprocess_args_with_mixed_types(): tool = FunctionTool(function_with_mixed_args) input_args = { - 'name': 'test_name', - 'user': {'name': 'Eve', 'age': 40}, - 'count': 10, + "name": "test_name", + "user": {"name": "Eve", "age": 40}, + "count": 10, } processed_args = tool._preprocess_args(input_args) # Check that only Pydantic model was converted - assert processed_args['name'] == 'test_name' # string unchanged - assert processed_args['count'] == 10 # int unchanged + assert processed_args["name"] == "test_name" # string unchanged + assert processed_args["count"] == 10 # int unchanged # Check Pydantic model conversion - assert isinstance(processed_args['user'], UserModel) - assert processed_args['user'].name == 'Eve' - assert processed_args['user'].age == 40 + assert isinstance(processed_args["user"], UserModel) + assert processed_args["user"].name == "Eve" + assert processed_args["user"].age == 40 def test_preprocess_args_with_invalid_data_graceful_failure(): @@ -189,23 +189,23 @@ def test_preprocess_args_with_invalid_data_graceful_failure(): tool = FunctionTool(sync_function_with_pydantic_model) # Invalid data that can't be converted to UserModel - input_args = {'user': 'invalid_string'} # string instead of dict/model + input_args = {"user": "invalid_string"} # string instead of dict/model processed_args = tool._preprocess_args(input_args) # Should keep original value when conversion fails - assert processed_args['user'] == 'invalid_string' + assert processed_args["user"] == "invalid_string" def test_preprocess_args_with_non_pydantic_parameters(): """Test _preprocess_args ignores non-Pydantic parameters.""" def simple_function(name: str, age: int) -> dict: - return {'name': name, 'age': age} + return {"name": name, "age": age} tool = FunctionTool(simple_function) - input_args = {'name': 'test', 'age': 25} + input_args = {"name": "test", "age": 25} processed_args = tool._preprocess_args(input_args) # Should remain unchanged (no Pydantic models to convert) @@ -223,15 +223,15 @@ async def test_run_async_with_pydantic_model_conversion_sync_function(): invocation_context_mock.session = session_mock tool_context_mock.invocation_context = invocation_context_mock - args = {'user': {'name': 'Frank', 'age': 45, 'email': 'frank@example.com'}} + args = {"user": {"name": "Frank", "age": 45, "email": "frank@example.com"}} result = await tool.run_async(args=args, tool_context=tool_context_mock) # Verify the function received a proper Pydantic model - assert result['name'] == 'Frank' - assert result['age'] == 45 - assert result['email'] == 'frank@example.com' - assert result['type'] == 'UserModel' + assert result["name"] == "Frank" + assert result["age"] == 45 + assert result["email"] == "frank@example.com" + assert result["type"] == "UserModel" @pytest.mark.asyncio @@ -245,15 +245,15 @@ async def test_run_async_with_pydantic_model_conversion_async_function(): invocation_context_mock.session = session_mock tool_context_mock.invocation_context = invocation_context_mock - args = {'user': {'name': 'Grace', 'age': 32}} + args = {"user": {"name": "Grace", "age": 32}} result = await tool.run_async(args=args, tool_context=tool_context_mock) # Verify the function received a proper Pydantic model - assert result['name'] == 'Grace' - assert result['age'] == 32 - assert result['email'] is None # default value - assert result['type'] == 'UserModel' + assert result["name"] == "Grace" + assert result["age"] == 32 + assert result["email"] is None # default value + assert result["type"] == "UserModel" @pytest.mark.asyncio @@ -269,26 +269,26 @@ async def test_run_async_with_optional_pydantic_models(): # Test with both required and optional models args = { - 'user': {'name': 'Henry', 'age': 50}, - 'preferences': {'theme': 'dark', 'notifications': True}, + "user": {"name": "Henry", "age": 50}, + "preferences": {"theme": "dark", "notifications": True}, } result = await tool.run_async(args=args, tool_context=tool_context_mock) - assert result['user_name'] == 'Henry' - assert result['user_type'] == 'UserModel' - assert result['theme'] == 'dark' - assert result['notifications'] is True - assert result['preferences_type'] == 'PreferencesModel' + assert result["user_name"] == "Henry" + assert result["user_type"] == "UserModel" + assert result["theme"] == "dark" + assert result["notifications"] is True + assert result["preferences_type"] == "PreferencesModel" def function_with_list_of_pydantic_models(users: list[UserModel]) -> dict: """Function that takes a list of Pydantic models.""" return { - 'count': len(users), - 'names': [user.name for user in users], - 'ages': [user.age for user in users], - 'types': [type(user).__name__ for user in users], + "count": len(users), + "names": [user.name for user in users], + "ages": [user.age for user in users], + "types": [type(user).__name__ for user in users], } @@ -297,10 +297,10 @@ def function_with_optional_list_of_pydantic_models( ) -> dict: """Function that takes an optional list of Pydantic models.""" if users is None: - return {'count': 0, 'names': []} + return {"count": 0, "names": []} return { - 'count': len(users), - 'names': [user.name for user in users], + "count": len(users), + "names": [user.name for user in users], } @@ -309,49 +309,49 @@ def test_preprocess_args_with_list_of_dicts_to_pydantic_models(): tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { - 'users': [ - {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}, - {'name': 'Bob', 'age': 25}, - {'name': 'Charlie', 'age': 35, 'email': 'charlie@example.com'}, + "users": [ + {"name": "Alice", "age": 30, "email": "alice@example.com"}, + {"name": "Bob", "age": 25}, + {"name": "Charlie", "age": 35, "email": "charlie@example.com"}, ] } processed_args = tool._preprocess_args(input_args) # Check that the list of dicts was converted to a list of Pydantic models - assert 'users' in processed_args - users = processed_args['users'] + assert "users" in processed_args + users = processed_args["users"] assert isinstance(users, list) assert len(users) == 3 # Check each element is a Pydantic model with correct data assert isinstance(users[0], UserModel) - assert users[0].name == 'Alice' + assert users[0].name == "Alice" assert users[0].age == 30 - assert users[0].email == 'alice@example.com' + assert users[0].email == "alice@example.com" assert isinstance(users[1], UserModel) - assert users[1].name == 'Bob' + assert users[1].name == "Bob" assert users[1].age == 25 assert users[1].email is None assert isinstance(users[2], UserModel) - assert users[2].name == 'Charlie' + assert users[2].name == "Charlie" assert users[2].age == 35 - assert users[2].email == 'charlie@example.com' + assert users[2].email == "charlie@example.com" def test_preprocess_args_with_optional_list_of_pydantic_models_none(): """Test _preprocess_args handles None for optional list parameter.""" tool = FunctionTool(function_with_optional_list_of_pydantic_models) - input_args = {'users': None} + input_args = {"users": None} processed_args = tool._preprocess_args(input_args) # Check that None is preserved - assert 'users' in processed_args - assert processed_args['users'] is None + assert "users" in processed_args + assert processed_args["users"] is None def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): @@ -359,21 +359,21 @@ def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): tool = FunctionTool(function_with_optional_list_of_pydantic_models) input_args = { - 'users': [ - {'name': 'Alice', 'age': 30}, - {'name': 'Bob', 'age': 25}, + "users": [ + {"name": "Alice", "age": 30}, + {"name": "Bob", "age": 25}, ] } processed_args = tool._preprocess_args(input_args) # Check conversion - assert 'users' in processed_args - users = processed_args['users'] + assert "users" in processed_args + users = processed_args["users"] assert len(users) == 2 assert all(isinstance(user, UserModel) for user in users) - assert users[0].name == 'Alice' - assert users[1].name == 'Bob' + assert users[0].name == "Alice" + assert users[1].name == "Bob" def test_preprocess_args_with_list_keeps_invalid_items_as_original(): @@ -381,30 +381,30 @@ def test_preprocess_args_with_list_keeps_invalid_items_as_original(): tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { - 'users': [ - {'name': 'Alice', 'age': 30}, - {'name': 'Invalid'}, # Missing required 'age' field - {'name': 'Bob', 'age': 25}, + "users": [ + {"name": "Alice", "age": 30}, + {"name": "Invalid"}, # Missing required 'age' field + {"name": "Bob", "age": 25}, ] } processed_args = tool._preprocess_args(input_args) # Check that all items are preserved - assert 'users' in processed_args - users = processed_args['users'] + assert "users" in processed_args + users = processed_args["users"] assert len(users) == 3 # All items preserved # First item should be converted to UserModel assert isinstance(users[0], UserModel) - assert users[0].name == 'Alice' + assert users[0].name == "Alice" assert users[0].age == 30 # Second item should remain as dict (failed validation) assert isinstance(users[1], dict) - assert users[1] == {'name': 'Invalid'} + assert users[1] == {"name": "Invalid"} # Third item should be converted to UserModel assert isinstance(users[2], UserModel) - assert users[2].name == 'Bob' + assert users[2].name == "Bob" assert users[2].age == 25 From 7a4aa5c678bf4137f6c06bf3a36fa23c2f054e4b Mon Sep 17 00:00:00 2001 From: t-miyak Date: Mon, 3 Nov 2025 18:34:41 +0900 Subject: [PATCH 07/10] Revert "fix: format in single quotes" This reverts commit 2ecf493b6e4a4c0fe44c38d2140d71fb40235275. --- .../tools/test_function_tool_pydantic.py | 210 +++++++++--------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/tests/unittests/tools/test_function_tool_pydantic.py b/tests/unittests/tools/test_function_tool_pydantic.py index e83ffb5d25..34456a7c44 100644 --- a/tests/unittests/tools/test_function_tool_pydantic.py +++ b/tests/unittests/tools/test_function_tool_pydantic.py @@ -36,27 +36,27 @@ class UserModel(pydantic.BaseModel): class PreferencesModel(pydantic.BaseModel): """Test Pydantic model for preferences.""" - theme: str = "light" + theme: str = 'light' notifications: bool = True def sync_function_with_pydantic_model(user: UserModel) -> dict: """Sync function that takes a Pydantic model.""" return { - "name": user.name, - "age": user.age, - "email": user.email, - "type": str(type(user).__name__), + 'name': user.name, + 'age': user.age, + 'email': user.email, + 'type': str(type(user).__name__), } async def async_function_with_pydantic_model(user: UserModel) -> dict: """Async function that takes a Pydantic model.""" return { - "name": user.name, - "age": user.age, - "email": user.email, - "type": str(type(user).__name__), + 'name': user.name, + 'age': user.age, + 'email': user.email, + 'type': str(type(user).__name__), } @@ -65,14 +65,14 @@ def function_with_optional_pydantic_model( ) -> dict: """Function with required and optional Pydantic models.""" result = { - "user_name": user.name, - "user_type": str(type(user).__name__), + 'user_name': user.name, + 'user_type': str(type(user).__name__), } if preferences: result.update({ - "theme": preferences.theme, - "notifications": preferences.notifications, - "preferences_type": str(type(preferences).__name__), + 'theme': preferences.theme, + 'notifications': preferences.notifications, + 'preferences_type': str(type(preferences).__name__), }) return result @@ -82,10 +82,10 @@ def function_with_mixed_args( ) -> dict: """Function with mixed argument types including Pydantic model.""" return { - "name": name, - "user_name": user.name, - "user_type": str(type(user).__name__), - "count": count, + 'name': name, + 'user_name': user.name, + 'user_type': str(type(user).__name__), + 'count': count, } @@ -94,18 +94,18 @@ def test_preprocess_args_with_dict_to_pydantic_conversion(): tool = FunctionTool(sync_function_with_pydantic_model) input_args = { - "user": {"name": "Alice", "age": 30, "email": "alice@example.com"} + 'user': {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'} } processed_args = tool._preprocess_args(input_args) # Check that the dict was converted to a Pydantic model - assert "user" in processed_args - user = processed_args["user"] + assert 'user' in processed_args + user = processed_args['user'] assert isinstance(user, UserModel) - assert user.name == "Alice" + assert user.name == 'Alice' assert user.age == 30 - assert user.email == "alice@example.com" + assert user.email == 'alice@example.com' def test_preprocess_args_with_existing_pydantic_model(): @@ -113,33 +113,33 @@ def test_preprocess_args_with_existing_pydantic_model(): tool = FunctionTool(sync_function_with_pydantic_model) # Create an existing Pydantic model - existing_user = UserModel(name="Bob", age=25) - input_args = {"user": existing_user} + existing_user = UserModel(name='Bob', age=25) + input_args = {'user': existing_user} processed_args = tool._preprocess_args(input_args) # Check that the existing model was not changed (same object) - assert "user" in processed_args - user = processed_args["user"] + assert 'user' in processed_args + user = processed_args['user'] assert user is existing_user assert isinstance(user, UserModel) - assert user.name == "Bob" + assert user.name == 'Bob' def test_preprocess_args_with_optional_pydantic_model_none(): """Test _preprocess_args handles None for optional Pydantic models.""" tool = FunctionTool(function_with_optional_pydantic_model) - input_args = {"user": {"name": "Charlie", "age": 35}, "preferences": None} + input_args = {'user': {'name': 'Charlie', 'age': 35}, 'preferences': None} processed_args = tool._preprocess_args(input_args) # Check user conversion - assert isinstance(processed_args["user"], UserModel) - assert processed_args["user"].name == "Charlie" + assert isinstance(processed_args['user'], UserModel) + assert processed_args['user'].name == 'Charlie' # Check preferences remains None - assert processed_args["preferences"] is None + assert processed_args['preferences'] is None def test_preprocess_args_with_optional_pydantic_model_dict(): @@ -147,19 +147,19 @@ def test_preprocess_args_with_optional_pydantic_model_dict(): tool = FunctionTool(function_with_optional_pydantic_model) input_args = { - "user": {"name": "Diana", "age": 28}, - "preferences": {"theme": "dark", "notifications": False}, + 'user': {'name': 'Diana', 'age': 28}, + 'preferences': {'theme': 'dark', 'notifications': False}, } processed_args = tool._preprocess_args(input_args) # Check both conversions - assert isinstance(processed_args["user"], UserModel) - assert processed_args["user"].name == "Diana" + assert isinstance(processed_args['user'], UserModel) + assert processed_args['user'].name == 'Diana' - assert isinstance(processed_args["preferences"], PreferencesModel) - assert processed_args["preferences"].theme == "dark" - assert processed_args["preferences"].notifications is False + assert isinstance(processed_args['preferences'], PreferencesModel) + assert processed_args['preferences'].theme == 'dark' + assert processed_args['preferences'].notifications is False def test_preprocess_args_with_mixed_types(): @@ -167,21 +167,21 @@ def test_preprocess_args_with_mixed_types(): tool = FunctionTool(function_with_mixed_args) input_args = { - "name": "test_name", - "user": {"name": "Eve", "age": 40}, - "count": 10, + 'name': 'test_name', + 'user': {'name': 'Eve', 'age': 40}, + 'count': 10, } processed_args = tool._preprocess_args(input_args) # Check that only Pydantic model was converted - assert processed_args["name"] == "test_name" # string unchanged - assert processed_args["count"] == 10 # int unchanged + assert processed_args['name'] == 'test_name' # string unchanged + assert processed_args['count'] == 10 # int unchanged # Check Pydantic model conversion - assert isinstance(processed_args["user"], UserModel) - assert processed_args["user"].name == "Eve" - assert processed_args["user"].age == 40 + assert isinstance(processed_args['user'], UserModel) + assert processed_args['user'].name == 'Eve' + assert processed_args['user'].age == 40 def test_preprocess_args_with_invalid_data_graceful_failure(): @@ -189,23 +189,23 @@ def test_preprocess_args_with_invalid_data_graceful_failure(): tool = FunctionTool(sync_function_with_pydantic_model) # Invalid data that can't be converted to UserModel - input_args = {"user": "invalid_string"} # string instead of dict/model + input_args = {'user': 'invalid_string'} # string instead of dict/model processed_args = tool._preprocess_args(input_args) # Should keep original value when conversion fails - assert processed_args["user"] == "invalid_string" + assert processed_args['user'] == 'invalid_string' def test_preprocess_args_with_non_pydantic_parameters(): """Test _preprocess_args ignores non-Pydantic parameters.""" def simple_function(name: str, age: int) -> dict: - return {"name": name, "age": age} + return {'name': name, 'age': age} tool = FunctionTool(simple_function) - input_args = {"name": "test", "age": 25} + input_args = {'name': 'test', 'age': 25} processed_args = tool._preprocess_args(input_args) # Should remain unchanged (no Pydantic models to convert) @@ -223,15 +223,15 @@ async def test_run_async_with_pydantic_model_conversion_sync_function(): invocation_context_mock.session = session_mock tool_context_mock.invocation_context = invocation_context_mock - args = {"user": {"name": "Frank", "age": 45, "email": "frank@example.com"}} + args = {'user': {'name': 'Frank', 'age': 45, 'email': 'frank@example.com'}} result = await tool.run_async(args=args, tool_context=tool_context_mock) # Verify the function received a proper Pydantic model - assert result["name"] == "Frank" - assert result["age"] == 45 - assert result["email"] == "frank@example.com" - assert result["type"] == "UserModel" + assert result['name'] == 'Frank' + assert result['age'] == 45 + assert result['email'] == 'frank@example.com' + assert result['type'] == 'UserModel' @pytest.mark.asyncio @@ -245,15 +245,15 @@ async def test_run_async_with_pydantic_model_conversion_async_function(): invocation_context_mock.session = session_mock tool_context_mock.invocation_context = invocation_context_mock - args = {"user": {"name": "Grace", "age": 32}} + args = {'user': {'name': 'Grace', 'age': 32}} result = await tool.run_async(args=args, tool_context=tool_context_mock) # Verify the function received a proper Pydantic model - assert result["name"] == "Grace" - assert result["age"] == 32 - assert result["email"] is None # default value - assert result["type"] == "UserModel" + assert result['name'] == 'Grace' + assert result['age'] == 32 + assert result['email'] is None # default value + assert result['type'] == 'UserModel' @pytest.mark.asyncio @@ -269,26 +269,26 @@ async def test_run_async_with_optional_pydantic_models(): # Test with both required and optional models args = { - "user": {"name": "Henry", "age": 50}, - "preferences": {"theme": "dark", "notifications": True}, + 'user': {'name': 'Henry', 'age': 50}, + 'preferences': {'theme': 'dark', 'notifications': True}, } result = await tool.run_async(args=args, tool_context=tool_context_mock) - assert result["user_name"] == "Henry" - assert result["user_type"] == "UserModel" - assert result["theme"] == "dark" - assert result["notifications"] is True - assert result["preferences_type"] == "PreferencesModel" + assert result['user_name'] == 'Henry' + assert result['user_type'] == 'UserModel' + assert result['theme'] == 'dark' + assert result['notifications'] is True + assert result['preferences_type'] == 'PreferencesModel' def function_with_list_of_pydantic_models(users: list[UserModel]) -> dict: """Function that takes a list of Pydantic models.""" return { - "count": len(users), - "names": [user.name for user in users], - "ages": [user.age for user in users], - "types": [type(user).__name__ for user in users], + 'count': len(users), + 'names': [user.name for user in users], + 'ages': [user.age for user in users], + 'types': [type(user).__name__ for user in users], } @@ -297,10 +297,10 @@ def function_with_optional_list_of_pydantic_models( ) -> dict: """Function that takes an optional list of Pydantic models.""" if users is None: - return {"count": 0, "names": []} + return {'count': 0, 'names': []} return { - "count": len(users), - "names": [user.name for user in users], + 'count': len(users), + 'names': [user.name for user in users], } @@ -309,49 +309,49 @@ def test_preprocess_args_with_list_of_dicts_to_pydantic_models(): tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { - "users": [ - {"name": "Alice", "age": 30, "email": "alice@example.com"}, - {"name": "Bob", "age": 25}, - {"name": "Charlie", "age": 35, "email": "charlie@example.com"}, + 'users': [ + {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}, + {'name': 'Bob', 'age': 25}, + {'name': 'Charlie', 'age': 35, 'email': 'charlie@example.com'}, ] } processed_args = tool._preprocess_args(input_args) # Check that the list of dicts was converted to a list of Pydantic models - assert "users" in processed_args - users = processed_args["users"] + assert 'users' in processed_args + users = processed_args['users'] assert isinstance(users, list) assert len(users) == 3 # Check each element is a Pydantic model with correct data assert isinstance(users[0], UserModel) - assert users[0].name == "Alice" + assert users[0].name == 'Alice' assert users[0].age == 30 - assert users[0].email == "alice@example.com" + assert users[0].email == 'alice@example.com' assert isinstance(users[1], UserModel) - assert users[1].name == "Bob" + assert users[1].name == 'Bob' assert users[1].age == 25 assert users[1].email is None assert isinstance(users[2], UserModel) - assert users[2].name == "Charlie" + assert users[2].name == 'Charlie' assert users[2].age == 35 - assert users[2].email == "charlie@example.com" + assert users[2].email == 'charlie@example.com' def test_preprocess_args_with_optional_list_of_pydantic_models_none(): """Test _preprocess_args handles None for optional list parameter.""" tool = FunctionTool(function_with_optional_list_of_pydantic_models) - input_args = {"users": None} + input_args = {'users': None} processed_args = tool._preprocess_args(input_args) # Check that None is preserved - assert "users" in processed_args - assert processed_args["users"] is None + assert 'users' in processed_args + assert processed_args['users'] is None def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): @@ -359,21 +359,21 @@ def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): tool = FunctionTool(function_with_optional_list_of_pydantic_models) input_args = { - "users": [ - {"name": "Alice", "age": 30}, - {"name": "Bob", "age": 25}, + 'users': [ + {'name': 'Alice', 'age': 30}, + {'name': 'Bob', 'age': 25}, ] } processed_args = tool._preprocess_args(input_args) # Check conversion - assert "users" in processed_args - users = processed_args["users"] + assert 'users' in processed_args + users = processed_args['users'] assert len(users) == 2 assert all(isinstance(user, UserModel) for user in users) - assert users[0].name == "Alice" - assert users[1].name == "Bob" + assert users[0].name == 'Alice' + assert users[1].name == 'Bob' def test_preprocess_args_with_list_keeps_invalid_items_as_original(): @@ -381,30 +381,30 @@ def test_preprocess_args_with_list_keeps_invalid_items_as_original(): tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { - "users": [ - {"name": "Alice", "age": 30}, - {"name": "Invalid"}, # Missing required 'age' field - {"name": "Bob", "age": 25}, + 'users': [ + {'name': 'Alice', 'age': 30}, + {'name': 'Invalid'}, # Missing required 'age' field + {'name': 'Bob', 'age': 25}, ] } processed_args = tool._preprocess_args(input_args) # Check that all items are preserved - assert "users" in processed_args - users = processed_args["users"] + assert 'users' in processed_args + users = processed_args['users'] assert len(users) == 3 # All items preserved # First item should be converted to UserModel assert isinstance(users[0], UserModel) - assert users[0].name == "Alice" + assert users[0].name == 'Alice' assert users[0].age == 30 # Second item should remain as dict (failed validation) assert isinstance(users[1], dict) - assert users[1] == {"name": "Invalid"} + assert users[1] == {'name': 'Invalid'} # Third item should be converted to UserModel assert isinstance(users[2], UserModel) - assert users[2].name == "Bob" + assert users[2].name == 'Bob' assert users[2].age == 25 From efa45acf99cdcce9e5b66a5b4f9f31b18b66ade8 Mon Sep 17 00:00:00 2001 From: t-miyak Date: Mon, 3 Nov 2025 18:34:42 +0900 Subject: [PATCH 08/10] Revert "fix: format in single quotes" This reverts commit 464de3b6da6323fdc829df0df34a21d302e8acdf. --- src/google/adk/tools/function_tool.py | 56 ++--- .../tools/test_function_tool_pydantic.py | 210 +++++++++--------- 2 files changed, 133 insertions(+), 133 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 1f59e2e2dd..4004c5368d 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -32,7 +32,7 @@ from .base_tool import BaseTool from .tool_context import ToolContext -logger = logging.getLogger('google_adk.' + __name__) +logger = logging.getLogger("google_adk." + __name__) class FunctionTool(BaseTool): @@ -57,22 +57,22 @@ def __init__( the callable returns True, the tool will require confirmation from the user. """ - name = '' - doc = '' + name = "" + doc = "" # Handle different types of callables - if hasattr(func, '__name__'): + if hasattr(func, "__name__"): # Regular functions, unbound methods, etc. name = func.__name__ - elif hasattr(func, '__class__'): + elif hasattr(func, "__class__"): # Callable objects, bound methods, etc. name = func.__class__.__name__ # Get documentation (prioritize direct __doc__ if available) - if hasattr(func, '__doc__') and func.__doc__: + if hasattr(func, "__doc__") and func.__doc__: doc = inspect.cleandoc(func.__doc__) elif ( - hasattr(func, '__call__') - and hasattr(func.__call__, '__doc__') + hasattr(func, "__call__") + and hasattr(func.__call__, "__doc__") and func.__call__.__doc__ ): # For callable objects, try to get docstring from __call__ method @@ -80,7 +80,7 @@ def __init__( super().__init__(name=name, description=doc) self.func = func - self._ignore_params = ['tool_context', 'input_stream'] + self._ignore_params = ["tool_context", "input_stream"] self._require_confirmation = require_confirmation @override @@ -153,7 +153,7 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: except Exception as e: logger.warning( f"Failed to convert item in '{param_name}' to Pydantic" - f' model {element_type.__name__}: {e}' + f" model {element_type.__name__}: {e}" ) # Keep the original value if conversion fails @@ -178,7 +178,7 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: except Exception as e: logger.warning( f"Failed to convert argument '{param_name}' to Pydantic model" - f' model {target_type.__name__}: {e}' + f" model {target_type.__name__}: {e}" ) # Keep the original value if conversion fails pass @@ -194,8 +194,8 @@ async def run_async( signature = inspect.signature(self.func) valid_params = {param for param in signature.parameters} - if 'tool_context' in valid_params: - args_to_call['tool_context'] = tool_context + if "tool_context" in valid_params: + args_to_call["tool_context"] = tool_context # Filter args_to_call to only include valid parameters for the function args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params} @@ -211,11 +211,11 @@ async def run_async( ] if missing_mandatory_args: - missing_mandatory_args_str = '\n'.join(missing_mandatory_args) + missing_mandatory_args_str = "\n".join(missing_mandatory_args) error_str = f"""Invoking `{self.name}()` failed as the following mandatory input parameters are not present: {missing_mandatory_args_str} You could retry calling this tool, but it is IMPORTANT for you to provide all the mandatory parameters.""" - return {'error': error_str} + return {"error": error_str} if isinstance(self._require_confirmation, Callable): require_confirmation = await self._invoke_callable( @@ -227,24 +227,24 @@ async def run_async( if require_confirmation: if not tool_context.tool_confirmation: args_to_show = args_to_call.copy() - if 'tool_context' in args_to_show: - args_to_show.pop('tool_context') + if "tool_context" in args_to_show: + args_to_show.pop("tool_context") tool_context.request_confirmation( hint=( - f'Please approve or reject the tool call {self.name}() by' - ' responding with a FunctionResponse with an expected' - ' ToolConfirmation payload.' + f"Please approve or reject the tool call {self.name}() by" + " responding with a FunctionResponse with an expected" + " ToolConfirmation payload." ), ) return { - 'error': ( - 'This tool call requires confirmation, please approve or' - ' reject.' + "error": ( + "This tool call requires confirmation, please approve or" + " reject." ) } elif not tool_context.tool_confirmation.confirmed: - return {'error': 'This tool call is rejected.'} + return {"error": "This tool call is rejected."} return await self._invoke_callable(self.func, args_to_call) @@ -257,7 +257,7 @@ async def _invoke_callable( # checking coroutine function is not enough. We also need to check whether # Callable's __call__ function is a coroutine funciton is_async = inspect.iscoroutinefunction(target) or ( - hasattr(target, '__call__') + hasattr(target, "__call__") and inspect.iscoroutinefunction(target.__call__) ) if is_async: @@ -279,11 +279,11 @@ async def _call_live( self.name in invocation_context.active_streaming_tools and invocation_context.active_streaming_tools[self.name].stream ): - args_to_call['input_stream'] = invocation_context.active_streaming_tools[ + args_to_call["input_stream"] = invocation_context.active_streaming_tools[ self.name ].stream - if 'tool_context' in signature.parameters: - args_to_call['tool_context'] = tool_context + if "tool_context" in signature.parameters: + args_to_call["tool_context"] = tool_context # TODO: support tool confirmation for live mode. async with Aclosing(self.func(**args_to_call)) as agen: diff --git a/tests/unittests/tools/test_function_tool_pydantic.py b/tests/unittests/tools/test_function_tool_pydantic.py index 34456a7c44..e83ffb5d25 100644 --- a/tests/unittests/tools/test_function_tool_pydantic.py +++ b/tests/unittests/tools/test_function_tool_pydantic.py @@ -36,27 +36,27 @@ class UserModel(pydantic.BaseModel): class PreferencesModel(pydantic.BaseModel): """Test Pydantic model for preferences.""" - theme: str = 'light' + theme: str = "light" notifications: bool = True def sync_function_with_pydantic_model(user: UserModel) -> dict: """Sync function that takes a Pydantic model.""" return { - 'name': user.name, - 'age': user.age, - 'email': user.email, - 'type': str(type(user).__name__), + "name": user.name, + "age": user.age, + "email": user.email, + "type": str(type(user).__name__), } async def async_function_with_pydantic_model(user: UserModel) -> dict: """Async function that takes a Pydantic model.""" return { - 'name': user.name, - 'age': user.age, - 'email': user.email, - 'type': str(type(user).__name__), + "name": user.name, + "age": user.age, + "email": user.email, + "type": str(type(user).__name__), } @@ -65,14 +65,14 @@ def function_with_optional_pydantic_model( ) -> dict: """Function with required and optional Pydantic models.""" result = { - 'user_name': user.name, - 'user_type': str(type(user).__name__), + "user_name": user.name, + "user_type": str(type(user).__name__), } if preferences: result.update({ - 'theme': preferences.theme, - 'notifications': preferences.notifications, - 'preferences_type': str(type(preferences).__name__), + "theme": preferences.theme, + "notifications": preferences.notifications, + "preferences_type": str(type(preferences).__name__), }) return result @@ -82,10 +82,10 @@ def function_with_mixed_args( ) -> dict: """Function with mixed argument types including Pydantic model.""" return { - 'name': name, - 'user_name': user.name, - 'user_type': str(type(user).__name__), - 'count': count, + "name": name, + "user_name": user.name, + "user_type": str(type(user).__name__), + "count": count, } @@ -94,18 +94,18 @@ def test_preprocess_args_with_dict_to_pydantic_conversion(): tool = FunctionTool(sync_function_with_pydantic_model) input_args = { - 'user': {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'} + "user": {"name": "Alice", "age": 30, "email": "alice@example.com"} } processed_args = tool._preprocess_args(input_args) # Check that the dict was converted to a Pydantic model - assert 'user' in processed_args - user = processed_args['user'] + assert "user" in processed_args + user = processed_args["user"] assert isinstance(user, UserModel) - assert user.name == 'Alice' + assert user.name == "Alice" assert user.age == 30 - assert user.email == 'alice@example.com' + assert user.email == "alice@example.com" def test_preprocess_args_with_existing_pydantic_model(): @@ -113,33 +113,33 @@ def test_preprocess_args_with_existing_pydantic_model(): tool = FunctionTool(sync_function_with_pydantic_model) # Create an existing Pydantic model - existing_user = UserModel(name='Bob', age=25) - input_args = {'user': existing_user} + existing_user = UserModel(name="Bob", age=25) + input_args = {"user": existing_user} processed_args = tool._preprocess_args(input_args) # Check that the existing model was not changed (same object) - assert 'user' in processed_args - user = processed_args['user'] + assert "user" in processed_args + user = processed_args["user"] assert user is existing_user assert isinstance(user, UserModel) - assert user.name == 'Bob' + assert user.name == "Bob" def test_preprocess_args_with_optional_pydantic_model_none(): """Test _preprocess_args handles None for optional Pydantic models.""" tool = FunctionTool(function_with_optional_pydantic_model) - input_args = {'user': {'name': 'Charlie', 'age': 35}, 'preferences': None} + input_args = {"user": {"name": "Charlie", "age": 35}, "preferences": None} processed_args = tool._preprocess_args(input_args) # Check user conversion - assert isinstance(processed_args['user'], UserModel) - assert processed_args['user'].name == 'Charlie' + assert isinstance(processed_args["user"], UserModel) + assert processed_args["user"].name == "Charlie" # Check preferences remains None - assert processed_args['preferences'] is None + assert processed_args["preferences"] is None def test_preprocess_args_with_optional_pydantic_model_dict(): @@ -147,19 +147,19 @@ def test_preprocess_args_with_optional_pydantic_model_dict(): tool = FunctionTool(function_with_optional_pydantic_model) input_args = { - 'user': {'name': 'Diana', 'age': 28}, - 'preferences': {'theme': 'dark', 'notifications': False}, + "user": {"name": "Diana", "age": 28}, + "preferences": {"theme": "dark", "notifications": False}, } processed_args = tool._preprocess_args(input_args) # Check both conversions - assert isinstance(processed_args['user'], UserModel) - assert processed_args['user'].name == 'Diana' + assert isinstance(processed_args["user"], UserModel) + assert processed_args["user"].name == "Diana" - assert isinstance(processed_args['preferences'], PreferencesModel) - assert processed_args['preferences'].theme == 'dark' - assert processed_args['preferences'].notifications is False + assert isinstance(processed_args["preferences"], PreferencesModel) + assert processed_args["preferences"].theme == "dark" + assert processed_args["preferences"].notifications is False def test_preprocess_args_with_mixed_types(): @@ -167,21 +167,21 @@ def test_preprocess_args_with_mixed_types(): tool = FunctionTool(function_with_mixed_args) input_args = { - 'name': 'test_name', - 'user': {'name': 'Eve', 'age': 40}, - 'count': 10, + "name": "test_name", + "user": {"name": "Eve", "age": 40}, + "count": 10, } processed_args = tool._preprocess_args(input_args) # Check that only Pydantic model was converted - assert processed_args['name'] == 'test_name' # string unchanged - assert processed_args['count'] == 10 # int unchanged + assert processed_args["name"] == "test_name" # string unchanged + assert processed_args["count"] == 10 # int unchanged # Check Pydantic model conversion - assert isinstance(processed_args['user'], UserModel) - assert processed_args['user'].name == 'Eve' - assert processed_args['user'].age == 40 + assert isinstance(processed_args["user"], UserModel) + assert processed_args["user"].name == "Eve" + assert processed_args["user"].age == 40 def test_preprocess_args_with_invalid_data_graceful_failure(): @@ -189,23 +189,23 @@ def test_preprocess_args_with_invalid_data_graceful_failure(): tool = FunctionTool(sync_function_with_pydantic_model) # Invalid data that can't be converted to UserModel - input_args = {'user': 'invalid_string'} # string instead of dict/model + input_args = {"user": "invalid_string"} # string instead of dict/model processed_args = tool._preprocess_args(input_args) # Should keep original value when conversion fails - assert processed_args['user'] == 'invalid_string' + assert processed_args["user"] == "invalid_string" def test_preprocess_args_with_non_pydantic_parameters(): """Test _preprocess_args ignores non-Pydantic parameters.""" def simple_function(name: str, age: int) -> dict: - return {'name': name, 'age': age} + return {"name": name, "age": age} tool = FunctionTool(simple_function) - input_args = {'name': 'test', 'age': 25} + input_args = {"name": "test", "age": 25} processed_args = tool._preprocess_args(input_args) # Should remain unchanged (no Pydantic models to convert) @@ -223,15 +223,15 @@ async def test_run_async_with_pydantic_model_conversion_sync_function(): invocation_context_mock.session = session_mock tool_context_mock.invocation_context = invocation_context_mock - args = {'user': {'name': 'Frank', 'age': 45, 'email': 'frank@example.com'}} + args = {"user": {"name": "Frank", "age": 45, "email": "frank@example.com"}} result = await tool.run_async(args=args, tool_context=tool_context_mock) # Verify the function received a proper Pydantic model - assert result['name'] == 'Frank' - assert result['age'] == 45 - assert result['email'] == 'frank@example.com' - assert result['type'] == 'UserModel' + assert result["name"] == "Frank" + assert result["age"] == 45 + assert result["email"] == "frank@example.com" + assert result["type"] == "UserModel" @pytest.mark.asyncio @@ -245,15 +245,15 @@ async def test_run_async_with_pydantic_model_conversion_async_function(): invocation_context_mock.session = session_mock tool_context_mock.invocation_context = invocation_context_mock - args = {'user': {'name': 'Grace', 'age': 32}} + args = {"user": {"name": "Grace", "age": 32}} result = await tool.run_async(args=args, tool_context=tool_context_mock) # Verify the function received a proper Pydantic model - assert result['name'] == 'Grace' - assert result['age'] == 32 - assert result['email'] is None # default value - assert result['type'] == 'UserModel' + assert result["name"] == "Grace" + assert result["age"] == 32 + assert result["email"] is None # default value + assert result["type"] == "UserModel" @pytest.mark.asyncio @@ -269,26 +269,26 @@ async def test_run_async_with_optional_pydantic_models(): # Test with both required and optional models args = { - 'user': {'name': 'Henry', 'age': 50}, - 'preferences': {'theme': 'dark', 'notifications': True}, + "user": {"name": "Henry", "age": 50}, + "preferences": {"theme": "dark", "notifications": True}, } result = await tool.run_async(args=args, tool_context=tool_context_mock) - assert result['user_name'] == 'Henry' - assert result['user_type'] == 'UserModel' - assert result['theme'] == 'dark' - assert result['notifications'] is True - assert result['preferences_type'] == 'PreferencesModel' + assert result["user_name"] == "Henry" + assert result["user_type"] == "UserModel" + assert result["theme"] == "dark" + assert result["notifications"] is True + assert result["preferences_type"] == "PreferencesModel" def function_with_list_of_pydantic_models(users: list[UserModel]) -> dict: """Function that takes a list of Pydantic models.""" return { - 'count': len(users), - 'names': [user.name for user in users], - 'ages': [user.age for user in users], - 'types': [type(user).__name__ for user in users], + "count": len(users), + "names": [user.name for user in users], + "ages": [user.age for user in users], + "types": [type(user).__name__ for user in users], } @@ -297,10 +297,10 @@ def function_with_optional_list_of_pydantic_models( ) -> dict: """Function that takes an optional list of Pydantic models.""" if users is None: - return {'count': 0, 'names': []} + return {"count": 0, "names": []} return { - 'count': len(users), - 'names': [user.name for user in users], + "count": len(users), + "names": [user.name for user in users], } @@ -309,49 +309,49 @@ def test_preprocess_args_with_list_of_dicts_to_pydantic_models(): tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { - 'users': [ - {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}, - {'name': 'Bob', 'age': 25}, - {'name': 'Charlie', 'age': 35, 'email': 'charlie@example.com'}, + "users": [ + {"name": "Alice", "age": 30, "email": "alice@example.com"}, + {"name": "Bob", "age": 25}, + {"name": "Charlie", "age": 35, "email": "charlie@example.com"}, ] } processed_args = tool._preprocess_args(input_args) # Check that the list of dicts was converted to a list of Pydantic models - assert 'users' in processed_args - users = processed_args['users'] + assert "users" in processed_args + users = processed_args["users"] assert isinstance(users, list) assert len(users) == 3 # Check each element is a Pydantic model with correct data assert isinstance(users[0], UserModel) - assert users[0].name == 'Alice' + assert users[0].name == "Alice" assert users[0].age == 30 - assert users[0].email == 'alice@example.com' + assert users[0].email == "alice@example.com" assert isinstance(users[1], UserModel) - assert users[1].name == 'Bob' + assert users[1].name == "Bob" assert users[1].age == 25 assert users[1].email is None assert isinstance(users[2], UserModel) - assert users[2].name == 'Charlie' + assert users[2].name == "Charlie" assert users[2].age == 35 - assert users[2].email == 'charlie@example.com' + assert users[2].email == "charlie@example.com" def test_preprocess_args_with_optional_list_of_pydantic_models_none(): """Test _preprocess_args handles None for optional list parameter.""" tool = FunctionTool(function_with_optional_list_of_pydantic_models) - input_args = {'users': None} + input_args = {"users": None} processed_args = tool._preprocess_args(input_args) # Check that None is preserved - assert 'users' in processed_args - assert processed_args['users'] is None + assert "users" in processed_args + assert processed_args["users"] is None def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): @@ -359,21 +359,21 @@ def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): tool = FunctionTool(function_with_optional_list_of_pydantic_models) input_args = { - 'users': [ - {'name': 'Alice', 'age': 30}, - {'name': 'Bob', 'age': 25}, + "users": [ + {"name": "Alice", "age": 30}, + {"name": "Bob", "age": 25}, ] } processed_args = tool._preprocess_args(input_args) # Check conversion - assert 'users' in processed_args - users = processed_args['users'] + assert "users" in processed_args + users = processed_args["users"] assert len(users) == 2 assert all(isinstance(user, UserModel) for user in users) - assert users[0].name == 'Alice' - assert users[1].name == 'Bob' + assert users[0].name == "Alice" + assert users[1].name == "Bob" def test_preprocess_args_with_list_keeps_invalid_items_as_original(): @@ -381,30 +381,30 @@ def test_preprocess_args_with_list_keeps_invalid_items_as_original(): tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { - 'users': [ - {'name': 'Alice', 'age': 30}, - {'name': 'Invalid'}, # Missing required 'age' field - {'name': 'Bob', 'age': 25}, + "users": [ + {"name": "Alice", "age": 30}, + {"name": "Invalid"}, # Missing required 'age' field + {"name": "Bob", "age": 25}, ] } processed_args = tool._preprocess_args(input_args) # Check that all items are preserved - assert 'users' in processed_args - users = processed_args['users'] + assert "users" in processed_args + users = processed_args["users"] assert len(users) == 3 # All items preserved # First item should be converted to UserModel assert isinstance(users[0], UserModel) - assert users[0].name == 'Alice' + assert users[0].name == "Alice" assert users[0].age == 30 # Second item should remain as dict (failed validation) assert isinstance(users[1], dict) - assert users[1] == {'name': 'Invalid'} + assert users[1] == {"name": "Invalid"} # Third item should be converted to UserModel assert isinstance(users[2], UserModel) - assert users[2].name == 'Bob' + assert users[2].name == "Bob" assert users[2].age == 25 From 49f3987047b813763dee50778603251e52d9b4d0 Mon Sep 17 00:00:00 2001 From: t-miyak Date: Mon, 3 Nov 2025 18:34:44 +0900 Subject: [PATCH 09/10] Revert "test: update pytest" This reverts commit fe575f7071352b26d077ff27299aa8758f58b58b. --- src/google/adk/tools/function_tool.py | 2 -- .../tools/test_function_tool_pydantic.py | 22 +++++-------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 4004c5368d..22c85c8a08 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -159,8 +159,6 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: # Keep the original value if conversion fails converted_list.append(item) - converted_args[param_name] = converted_list - # Check if the target type is a Pydantic model elif inspect.isclass(target_type) and issubclass( target_type, pydantic.BaseModel diff --git a/tests/unittests/tools/test_function_tool_pydantic.py b/tests/unittests/tools/test_function_tool_pydantic.py index e83ffb5d25..acacd5e8be 100644 --- a/tests/unittests/tools/test_function_tool_pydantic.py +++ b/tests/unittests/tools/test_function_tool_pydantic.py @@ -376,8 +376,8 @@ def test_preprocess_args_with_optional_list_of_pydantic_models_with_data(): assert users[1].name == "Bob" -def test_preprocess_args_with_list_keeps_invalid_items_as_original(): - """Test _preprocess_args keeps original data for items that fail validation.""" +def test_preprocess_args_with_list_skips_invalid_items(): + """Test _preprocess_args skips items that fail validation.""" tool = FunctionTool(function_with_list_of_pydantic_models) input_args = { @@ -390,21 +390,11 @@ def test_preprocess_args_with_list_keeps_invalid_items_as_original(): processed_args = tool._preprocess_args(input_args) - # Check that all items are preserved + # Check that invalid item was skipped assert "users" in processed_args users = processed_args["users"] - assert len(users) == 3 # All items preserved - - # First item should be converted to UserModel - assert isinstance(users[0], UserModel) + assert len(users) == 2 # Only 2 valid items assert users[0].name == "Alice" assert users[0].age == 30 - - # Second item should remain as dict (failed validation) - assert isinstance(users[1], dict) - assert users[1] == {"name": "Invalid"} - - # Third item should be converted to UserModel - assert isinstance(users[2], UserModel) - assert users[2].name == "Bob" - assert users[2].age == 25 + assert users[1].name == "Bob" + assert users[1].age == 25 From 7589882fd8f4628594b85ac79eb1b0638b55c58c Mon Sep 17 00:00:00 2001 From: t-miyak Date: Mon, 3 Nov 2025 18:35:00 +0900 Subject: [PATCH 10/10] Revert "fix: use original item data if its conversion fails" This reverts commit 2bea40c418eacd2fa6903f51ff98c95bac5fb118. --- src/google/adk/tools/function_tool.py | 63 +++++++++++++-------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 22c85c8a08..ab04b0223c 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -32,7 +32,7 @@ from .base_tool import BaseTool from .tool_context import ToolContext -logger = logging.getLogger("google_adk." + __name__) +logger = logging.getLogger('google_adk.' + __name__) class FunctionTool(BaseTool): @@ -57,22 +57,22 @@ def __init__( the callable returns True, the tool will require confirmation from the user. """ - name = "" - doc = "" + name = '' + doc = '' # Handle different types of callables - if hasattr(func, "__name__"): + if hasattr(func, '__name__'): # Regular functions, unbound methods, etc. name = func.__name__ - elif hasattr(func, "__class__"): + elif hasattr(func, '__class__'): # Callable objects, bound methods, etc. name = func.__class__.__name__ # Get documentation (prioritize direct __doc__ if available) - if hasattr(func, "__doc__") and func.__doc__: + if hasattr(func, '__doc__') and func.__doc__: doc = inspect.cleandoc(func.__doc__) elif ( - hasattr(func, "__call__") - and hasattr(func.__call__, "__doc__") + hasattr(func, '__call__') + and hasattr(func.__call__, '__doc__') and func.__call__.__doc__ ): # For callable objects, try to get docstring from __call__ method @@ -80,7 +80,7 @@ def __init__( super().__init__(name=name, description=doc) self.func = func - self._ignore_params = ["tool_context", "input_stream"] + self._ignore_params = ['tool_context', 'input_stream'] self._require_confirmation = require_confirmation @override @@ -151,13 +151,12 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: try: converted_list.append(element_type.model_validate(item)) except Exception as e: + # Skip items that fail validation logger.warning( - f"Failed to convert item in '{param_name}' to Pydantic" - f" model {element_type.__name__}: {e}" + f"Skipping item in '{param_name}': " + f'Failed to convert to {element_type.__name__}: {e}' ) - - # Keep the original value if conversion fails - converted_list.append(item) + converted_args[param_name] = converted_list # Check if the target type is a Pydantic model elif inspect.isclass(target_type) and issubclass( @@ -176,7 +175,7 @@ def _preprocess_args(self, args: dict[str, Any]) -> dict[str, Any]: except Exception as e: logger.warning( f"Failed to convert argument '{param_name}' to Pydantic model" - f" model {target_type.__name__}: {e}" + f' {target_type.__name__}: {e}' ) # Keep the original value if conversion fails pass @@ -192,8 +191,8 @@ async def run_async( signature = inspect.signature(self.func) valid_params = {param for param in signature.parameters} - if "tool_context" in valid_params: - args_to_call["tool_context"] = tool_context + if 'tool_context' in valid_params: + args_to_call['tool_context'] = tool_context # Filter args_to_call to only include valid parameters for the function args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params} @@ -209,11 +208,11 @@ async def run_async( ] if missing_mandatory_args: - missing_mandatory_args_str = "\n".join(missing_mandatory_args) + missing_mandatory_args_str = '\n'.join(missing_mandatory_args) error_str = f"""Invoking `{self.name}()` failed as the following mandatory input parameters are not present: {missing_mandatory_args_str} You could retry calling this tool, but it is IMPORTANT for you to provide all the mandatory parameters.""" - return {"error": error_str} + return {'error': error_str} if isinstance(self._require_confirmation, Callable): require_confirmation = await self._invoke_callable( @@ -225,24 +224,24 @@ async def run_async( if require_confirmation: if not tool_context.tool_confirmation: args_to_show = args_to_call.copy() - if "tool_context" in args_to_show: - args_to_show.pop("tool_context") + if 'tool_context' in args_to_show: + args_to_show.pop('tool_context') tool_context.request_confirmation( hint=( - f"Please approve or reject the tool call {self.name}() by" - " responding with a FunctionResponse with an expected" - " ToolConfirmation payload." + f'Please approve or reject the tool call {self.name}() by' + ' responding with a FunctionResponse with an expected' + ' ToolConfirmation payload.' ), ) return { - "error": ( - "This tool call requires confirmation, please approve or" - " reject." + 'error': ( + 'This tool call requires confirmation, please approve or' + ' reject.' ) } elif not tool_context.tool_confirmation.confirmed: - return {"error": "This tool call is rejected."} + return {'error': 'This tool call is rejected.'} return await self._invoke_callable(self.func, args_to_call) @@ -255,7 +254,7 @@ async def _invoke_callable( # checking coroutine function is not enough. We also need to check whether # Callable's __call__ function is a coroutine funciton is_async = inspect.iscoroutinefunction(target) or ( - hasattr(target, "__call__") + hasattr(target, '__call__') and inspect.iscoroutinefunction(target.__call__) ) if is_async: @@ -277,11 +276,11 @@ async def _call_live( self.name in invocation_context.active_streaming_tools and invocation_context.active_streaming_tools[self.name].stream ): - args_to_call["input_stream"] = invocation_context.active_streaming_tools[ + args_to_call['input_stream'] = invocation_context.active_streaming_tools[ self.name ].stream - if "tool_context" in signature.parameters: - args_to_call["tool_context"] = tool_context + if 'tool_context' in signature.parameters: + args_to_call['tool_context'] = tool_context # TODO: support tool confirmation for live mode. async with Aclosing(self.func(**args_to_call)) as agen: