From f5949f67bcaebc25fa440370cd8a92a8fd309d98 Mon Sep 17 00:00:00 2001 From: Omar Elcircevi Date: Sun, 28 Sep 2025 17:19:09 +0300 Subject: [PATCH 1/7] fix: improve parameter filtering for functions with **kwargs - Fix FunctionTool parameter filtering to support CrewAI-style tools - Functions with **kwargs now receive all parameters except 'self' and 'tool_context' - Maintains backward compatibility with explicit parameter functions - Add comprehensive tests for **kwargs functionality Fixes parameter filtering issue where CrewAI tools using **kwargs pattern would receive empty parameter dictionaries, causing search_query and other parameters to be None. #non-breaking --- src/google/adk/tools/function_tool.py | 17 +++- tests/unittests/tools/test_function_tool.py | 95 +++++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 088b2fe137..010dc38735 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -105,8 +105,21 @@ async def run_async( 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} + # Check if function accepts **kwargs + has_kwargs = any( + param.kind == inspect.Parameter.VAR_KEYWORD + for param in signature.parameters.values() + ) + + if has_kwargs: + # For functions with **kwargs, pass all arguments except 'self' and 'tool_context' + args_to_call = { + k: v for k, v in args_to_call.items() + if k not in ('self', 'tool_context') + } + else: + # For functions without **kwargs, use the original filtering + args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params} # Before invoking the function, we check for if the list of args passed in # has all the mandatory arguments or not. diff --git a/tests/unittests/tools/test_function_tool.py b/tests/unittests/tools/test_function_tool.py index e7854a2c87..3c4339d43e 100644 --- a/tests/unittests/tools/test_function_tool.py +++ b/tests/unittests/tools/test_function_tool.py @@ -394,3 +394,98 @@ def sample_func(arg1: str): tool_context=tool_context_mock, ) assert result == {"received_arg": "hello"} + + +@pytest.mark.asyncio +async def test_run_async_with_kwargs_crewai_style(): + """Test that run_async works with CrewAI-style functions that use **kwargs.""" + + def crewai_style_tool(*args, **kwargs): + """CrewAI-style tool that accepts any keyword arguments.""" + return { + "received_args": args, + "received_kwargs": kwargs, + "search_query": kwargs.get("search_query"), + "other_param": kwargs.get("other_param") + } + + tool = FunctionTool(crewai_style_tool) + mock_invocation_context = MagicMock(spec=InvocationContext) + mock_invocation_context.session = MagicMock(spec=Session) + mock_invocation_context.session.state = MagicMock() + tool_context_mock = ToolContext(invocation_context=mock_invocation_context) + + # Test with CrewAI-style parameters that should be passed through + result = await tool.run_async( + args={ + "search_query": "test_query", + "other_param": "test_value" + }, + tool_context=tool_context_mock, + ) + + assert result["search_query"] == "test_query" + assert result["other_param"] == "test_value" + assert result["received_kwargs"]["search_query"] == "test_query" + assert result["received_kwargs"]["other_param"] == "test_value" + + +@pytest.mark.asyncio +async def test_run_async_with_kwargs_crewai_style_async(): + """Test that run_async works with async CrewAI-style functions that use **kwargs.""" + + async def async_crewai_style_tool(*args, **kwargs): + """Async CrewAI-style tool that accepts any keyword arguments.""" + return { + "received_args": args, + "received_kwargs": kwargs, + "search_query": kwargs.get("search_query"), + "other_param": kwargs.get("other_param") + } + + tool = FunctionTool(async_crewai_style_tool) + mock_invocation_context = MagicMock(spec=InvocationContext) + mock_invocation_context.session = MagicMock(spec=Session) + mock_invocation_context.session.state = MagicMock() + tool_context_mock = ToolContext(invocation_context=mock_invocation_context) + + # Test with CrewAI-style parameters that should be passed through + result = await tool.run_async( + args={ + "search_query": "async test query", + "other_param": "async test value" + }, + tool_context=tool_context_mock, + ) + + assert result["search_query"] == "async test query" + assert result["other_param"] == "async test value" + assert result["received_kwargs"]["search_query"] == "async test query" + assert result["received_kwargs"]["other_param"] == "async test value" + + +@pytest.mark.asyncio +async def test_run_async_with_kwargs_backward_compatibility(): + """Test that the **kwargs fix maintains backward compatibility with explicit parameters.""" + + def explicit_params_func(arg1: str, arg2: int): + """Function with explicit parameters (no **kwargs).""" + return {"arg1": arg1, "arg2": arg2} + + tool = FunctionTool(explicit_params_func) + mock_invocation_context = MagicMock(spec=InvocationContext) + mock_invocation_context.session = MagicMock(spec=Session) + mock_invocation_context.session.state = MagicMock() + tool_context_mock = ToolContext(invocation_context=mock_invocation_context) + + # Test that unexpected parameters are still filtered out for non-kwargs functions + result = await tool.run_async( + args={ + "arg1": "test", + "arg2": 42, + "unexpected_param": "should_be_filtered" + }, + tool_context=tool_context_mock, + ) + + assert result == {"arg1": "test", "arg2": 42} From 1e97f351720573f9bd2b4ab2b5e959612003e175 Mon Sep 17 00:00:00 2001 From: Omar Elcircevi Date: Sun, 28 Sep 2025 17:26:41 +0300 Subject: [PATCH 2/7] fix: correct tool_context handling in **kwargs parameter filtering - Fix critical bug where tool_context was being removed for functions with both explicit tool_context and **kwargs - Start with original args instead of args_to_call to avoid removing injected tool_context - Re-inject tool_context only if it's an explicit parameter in the function signature - Add comprehensive tests for edge case: functions with both tool_context and **kwargs - Maintains all existing functionality while fixing the TypeError issue This fixes the scenario where functions like: def my_func(tool_context: ToolContext, **kwargs): would fail with TypeError due to missing tool_context argument. #non-breaking --- src/google/adk/tools/function_tool.py | 10 +-- tests/unittests/tools/test_function_tool.py | 72 +++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 010dc38735..cd12bdadb9 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -112,11 +112,11 @@ async def run_async( ) if has_kwargs: - # For functions with **kwargs, pass all arguments except 'self' and 'tool_context' - args_to_call = { - k: v for k, v in args_to_call.items() - if k not in ('self', 'tool_context') - } + # For functions with **kwargs, start with the original LLM args and filter reserved names. + args_to_call = {k: v for k, v in args.items() if k not in ('self', 'tool_context')} + # Then, add back the injected `tool_context` if it's an explicit parameter. + if 'tool_context' in valid_params: + args_to_call['tool_context'] = tool_context else: # For functions without **kwargs, use the original filtering args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params} diff --git a/tests/unittests/tools/test_function_tool.py b/tests/unittests/tools/test_function_tool.py index 3c4339d43e..c1d8b3ad0a 100644 --- a/tests/unittests/tools/test_function_tool.py +++ b/tests/unittests/tools/test_function_tool.py @@ -489,3 +489,75 @@ def explicit_params_func(arg1: str, arg2: int): ) assert result == {"arg1": "test", "arg2": 42} + + +@pytest.mark.asyncio +async def test_run_async_with_kwargs_and_tool_context(): + """Test that run_async works with functions that have both tool_context and **kwargs.""" + + def func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs): + """Function with explicit tool_context parameter and **kwargs.""" + return { + "arg1": arg1, + "tool_context_present": bool(tool_context), + "search_query": kwargs.get("search_query"), + "received_kwargs": kwargs + } + + tool = FunctionTool(func_with_context_and_kwargs) + mock_invocation_context = MagicMock(spec=InvocationContext) + mock_invocation_context.session = MagicMock(spec=Session) + mock_invocation_context.session.state = MagicMock() + tool_context_mock = ToolContext(invocation_context=mock_invocation_context) + + # Test that both tool_context and **kwargs parameters work together + result = await tool.run_async( + args={ + "arg1": "test_value", + "search_query": "omar elcircevi speaker", + "other_param": "test_value" + }, + tool_context=tool_context_mock, + ) + + assert result["arg1"] == "test_value" + assert result["tool_context_present"] == True + assert result["search_query"] == "omar elcircevi speaker" + assert result["received_kwargs"]["search_query"] == "omar elcircevi speaker" + assert result["received_kwargs"]["other_param"] == "test_value" + + +@pytest.mark.asyncio +async def test_run_async_with_kwargs_and_tool_context_async(): + """Test that run_async works with async functions that have both tool_context and **kwargs.""" + + async def async_func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs): + """Async function with explicit tool_context parameter and **kwargs.""" + return { + "arg1": arg1, + "tool_context_present": bool(tool_context), + "search_query": kwargs.get("search_query"), + "received_kwargs": kwargs + } + + tool = FunctionTool(async_func_with_context_and_kwargs) + mock_invocation_context = MagicMock(spec=InvocationContext) + mock_invocation_context.session = MagicMock(spec=Session) + mock_invocation_context.session.state = MagicMock() + tool_context_mock = ToolContext(invocation_context=mock_invocation_context) + + # Test that both tool_context and **kwargs parameters work together + result = await tool.run_async( + args={ + "arg1": "async_test_value", + "search_query": "async test query", + "other_param": "async test value" + }, + tool_context=tool_context_mock, + ) + + assert result["arg1"] == "async_test_value" + assert result["tool_context_present"] == True + assert result["search_query"] == "async test query" + assert result["received_kwargs"]["search_query"] == "async test query" + assert result["received_kwargs"]["other_param"] == "async test value" From 31b7ec2d55c5cbf5ef300fdf7c0d8adcf5c300cd Mon Sep 17 00:00:00 2001 From: Omar Elcircevi Date: Thu, 2 Oct 2025 18:32:23 +0300 Subject: [PATCH 3/7] fix: Improve handling of **kwargs in FunctionTool by removing 'self' and updating tests with mock ToolContext - Updated FunctionTool to defensively remove 'self' from arguments when handling **kwargs. - Refactored tests to utilize a mock ToolContext for better isolation and clarity. - Ensured backward compatibility with existing functionality while enhancing test coverage. #non-breaking --- src/google/adk/tools/function_tool.py | 10 ++--- tests/unittests/tools/test_function_tool.py | 49 ++++++++------------- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index cd12bdadb9..82e50211aa 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -112,13 +112,11 @@ async def run_async( ) if has_kwargs: - # For functions with **kwargs, start with the original LLM args and filter reserved names. - args_to_call = {k: v for k, v in args.items() if k not in ('self', 'tool_context')} - # Then, add back the injected `tool_context` if it's an explicit parameter. - if 'tool_context' in valid_params: - args_to_call['tool_context'] = tool_context + # For functions with **kwargs, we pass all arguments. `args_to_call` is + # already correctly populated. We just defensively remove `self`. + args_to_call.pop('self', None) else: - # For functions without **kwargs, use the original filtering + # For functions without **kwargs, use the original filtering. args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params} # Before invoking the function, we check for if the list of args passed in diff --git a/tests/unittests/tools/test_function_tool.py b/tests/unittests/tools/test_function_tool.py index c1d8b3ad0a..2817fa0edb 100644 --- a/tests/unittests/tools/test_function_tool.py +++ b/tests/unittests/tools/test_function_tool.py @@ -22,6 +22,15 @@ import pytest +@pytest.fixture +def mock_tool_context() -> ToolContext: + """Fixture that provides a mock ToolContext for testing.""" + mock_invocation_context = MagicMock(spec=InvocationContext) + mock_invocation_context.session = MagicMock(spec=Session) + mock_invocation_context.session.state = MagicMock() + return ToolContext(invocation_context=mock_invocation_context) + + def function_for_testing_with_no_args(): """Function for testing with no args.""" pass @@ -397,7 +406,7 @@ def sample_func(arg1: str): @pytest.mark.asyncio -async def test_run_async_with_kwargs_crewai_style(): +async def test_run_async_with_kwargs_crewai_style(mock_tool_context): """Test that run_async works with CrewAI-style functions that use **kwargs.""" def crewai_style_tool(*args, **kwargs): @@ -410,10 +419,6 @@ def crewai_style_tool(*args, **kwargs): } tool = FunctionTool(crewai_style_tool) - mock_invocation_context = MagicMock(spec=InvocationContext) - mock_invocation_context.session = MagicMock(spec=Session) - mock_invocation_context.session.state = MagicMock() - tool_context_mock = ToolContext(invocation_context=mock_invocation_context) # Test with CrewAI-style parameters that should be passed through result = await tool.run_async( @@ -421,7 +426,7 @@ def crewai_style_tool(*args, **kwargs): "search_query": "test_query", "other_param": "test_value" }, - tool_context=tool_context_mock, + tool_context=mock_tool_context, ) assert result["search_query"] == "test_query" @@ -431,7 +436,7 @@ def crewai_style_tool(*args, **kwargs): @pytest.mark.asyncio -async def test_run_async_with_kwargs_crewai_style_async(): +async def test_run_async_with_kwargs_crewai_style_async(mock_tool_context): """Test that run_async works with async CrewAI-style functions that use **kwargs.""" async def async_crewai_style_tool(*args, **kwargs): @@ -444,10 +449,6 @@ async def async_crewai_style_tool(*args, **kwargs): } tool = FunctionTool(async_crewai_style_tool) - mock_invocation_context = MagicMock(spec=InvocationContext) - mock_invocation_context.session = MagicMock(spec=Session) - mock_invocation_context.session.state = MagicMock() - tool_context_mock = ToolContext(invocation_context=mock_invocation_context) # Test with CrewAI-style parameters that should be passed through result = await tool.run_async( @@ -455,7 +456,7 @@ async def async_crewai_style_tool(*args, **kwargs): "search_query": "async test query", "other_param": "async test value" }, - tool_context=tool_context_mock, + tool_context=mock_tool_context, ) assert result["search_query"] == "async test query" @@ -465,7 +466,7 @@ async def async_crewai_style_tool(*args, **kwargs): @pytest.mark.asyncio -async def test_run_async_with_kwargs_backward_compatibility(): +async def test_run_async_with_kwargs_backward_compatibility(mock_tool_context): """Test that the **kwargs fix maintains backward compatibility with explicit parameters.""" def explicit_params_func(arg1: str, arg2: int): @@ -473,10 +474,6 @@ def explicit_params_func(arg1: str, arg2: int): return {"arg1": arg1, "arg2": arg2} tool = FunctionTool(explicit_params_func) - mock_invocation_context = MagicMock(spec=InvocationContext) - mock_invocation_context.session = MagicMock(spec=Session) - mock_invocation_context.session.state = MagicMock() - tool_context_mock = ToolContext(invocation_context=mock_invocation_context) # Test that unexpected parameters are still filtered out for non-kwargs functions result = await tool.run_async( @@ -485,14 +482,14 @@ def explicit_params_func(arg1: str, arg2: int): "arg2": 42, "unexpected_param": "should_be_filtered" }, - tool_context=tool_context_mock, + tool_context=mock_tool_context, ) assert result == {"arg1": "test", "arg2": 42} @pytest.mark.asyncio -async def test_run_async_with_kwargs_and_tool_context(): +async def test_run_async_with_kwargs_and_tool_context(mock_tool_context): """Test that run_async works with functions that have both tool_context and **kwargs.""" def func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs): @@ -505,10 +502,6 @@ def func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs) } tool = FunctionTool(func_with_context_and_kwargs) - mock_invocation_context = MagicMock(spec=InvocationContext) - mock_invocation_context.session = MagicMock(spec=Session) - mock_invocation_context.session.state = MagicMock() - tool_context_mock = ToolContext(invocation_context=mock_invocation_context) # Test that both tool_context and **kwargs parameters work together result = await tool.run_async( @@ -517,7 +510,7 @@ def func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs) "search_query": "omar elcircevi speaker", "other_param": "test_value" }, - tool_context=tool_context_mock, + tool_context=mock_tool_context, ) assert result["arg1"] == "test_value" @@ -528,7 +521,7 @@ def func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs) @pytest.mark.asyncio -async def test_run_async_with_kwargs_and_tool_context_async(): +async def test_run_async_with_kwargs_and_tool_context_async(mock_tool_context): """Test that run_async works with async functions that have both tool_context and **kwargs.""" async def async_func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs): @@ -541,10 +534,6 @@ async def async_func_with_context_and_kwargs(arg1: str, tool_context: ToolContex } tool = FunctionTool(async_func_with_context_and_kwargs) - mock_invocation_context = MagicMock(spec=InvocationContext) - mock_invocation_context.session = MagicMock(spec=Session) - mock_invocation_context.session.state = MagicMock() - tool_context_mock = ToolContext(invocation_context=mock_invocation_context) # Test that both tool_context and **kwargs parameters work together result = await tool.run_async( @@ -553,7 +542,7 @@ async def async_func_with_context_and_kwargs(arg1: str, tool_context: ToolContex "search_query": "async test query", "other_param": "async test value" }, - tool_context=tool_context_mock, + tool_context=mock_tool_context, ) assert result["arg1"] == "async_test_value" From 619d5a1ec521218685e4a52bc1b1dbafa1d03aba Mon Sep 17 00:00:00 2001 From: Omar Elcircevi Date: Thu, 2 Oct 2025 18:35:52 +0300 Subject: [PATCH 4/7] Update src/google/adk/tools/function_tool.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/google/adk/tools/function_tool.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 82e50211aa..663ea4dee6 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -112,8 +112,9 @@ async def run_async( ) if has_kwargs: - # For functions with **kwargs, we pass all arguments. `args_to_call` is - # already correctly populated. We just defensively remove `self`. + # For functions with **kwargs, we pass all arguments. We defensively + # remove the `self` argument, which may be injected by some tool + # frameworks but is not intended for the wrapped function. args_to_call.pop('self', None) else: # For functions without **kwargs, use the original filtering. From 574db75fe7f31afad021fc23a125e90163c681c9 Mon Sep 17 00:00:00 2001 From: Omar Elcircevi Date: Thu, 2 Oct 2025 18:49:35 +0300 Subject: [PATCH 5/7] test: Enhance FunctionTool tests with CrewAI-style functions and context handling - Introduced synchronous and asynchronous CrewAI-style tools for testing. - Refactored tests to utilize parameterized inputs for better coverage of **kwargs handling. - Ensured both synchronous and asynchronous functions are tested with tool_context and **kwargs. - Improved clarity and maintainability of test cases while preserving existing functionality. #non-breaking --- tests/unittests/tools/test_function_tool.py | 199 +++++++++----------- 1 file changed, 93 insertions(+), 106 deletions(-) diff --git a/tests/unittests/tools/test_function_tool.py b/tests/unittests/tools/test_function_tool.py index 2817fa0edb..d20c0c02c4 100644 --- a/tests/unittests/tools/test_function_tool.py +++ b/tests/unittests/tools/test_function_tool.py @@ -31,6 +31,48 @@ def mock_tool_context() -> ToolContext: return ToolContext(invocation_context=mock_invocation_context) +def _crewai_style_tool_sync(*args, **kwargs): + """CrewAI-style tool that accepts any keyword arguments.""" + return { + "received_args": args, + "received_kwargs": kwargs, + "search_query": kwargs.get("search_query"), + "other_param": kwargs.get("other_param"), + } + + +async def _crewai_style_tool_async(*args, **kwargs): + """Async CrewAI-style tool that accepts any keyword arguments.""" + return { + "received_args": args, + "received_kwargs": kwargs, + "search_query": kwargs.get("search_query"), + "other_param": kwargs.get("other_param"), + } + + +def _func_with_context_and_kwargs_sync(arg1: str, tool_context: ToolContext, **kwargs): + """Function with explicit tool_context parameter and **kwargs.""" + return { + "arg1": arg1, + "tool_context_present": bool(tool_context), + "search_query": kwargs.get("search_query"), + "received_kwargs": kwargs, + } + + +async def _func_with_context_and_kwargs_async( + arg1: str, tool_context: ToolContext, **kwargs +): + """Async function with explicit tool_context parameter and **kwargs.""" + return { + "arg1": arg1, + "tool_context_present": bool(tool_context), + "search_query": kwargs.get("search_query"), + "received_kwargs": kwargs, + } + + def function_for_testing_with_no_args(): """Function for testing with no args.""" pass @@ -406,63 +448,30 @@ def sample_func(arg1: str): @pytest.mark.asyncio -async def test_run_async_with_kwargs_crewai_style(mock_tool_context): +@pytest.mark.parametrize( + "tool_function, search_query, other_param", + [ + (_crewai_style_tool_sync, "test_query", "test_value"), + (_crewai_style_tool_async, "async test query", "async test value"), + ], + ids=["sync", "async"], +) +async def test_run_async_with_kwargs_crewai_style( + mock_tool_context, tool_function, search_query, other_param +): """Test that run_async works with CrewAI-style functions that use **kwargs.""" - - def crewai_style_tool(*args, **kwargs): - """CrewAI-style tool that accepts any keyword arguments.""" - return { - "received_args": args, - "received_kwargs": kwargs, - "search_query": kwargs.get("search_query"), - "other_param": kwargs.get("other_param") - } - - tool = FunctionTool(crewai_style_tool) - - # Test with CrewAI-style parameters that should be passed through - result = await tool.run_async( - args={ - "search_query": "test_query", - "other_param": "test_value" - }, - tool_context=mock_tool_context, - ) - - assert result["search_query"] == "test_query" - assert result["other_param"] == "test_value" - assert result["received_kwargs"]["search_query"] == "test_query" - assert result["received_kwargs"]["other_param"] == "test_value" + tool = FunctionTool(tool_function) - -@pytest.mark.asyncio -async def test_run_async_with_kwargs_crewai_style_async(mock_tool_context): - """Test that run_async works with async CrewAI-style functions that use **kwargs.""" - - async def async_crewai_style_tool(*args, **kwargs): - """Async CrewAI-style tool that accepts any keyword arguments.""" - return { - "received_args": args, - "received_kwargs": kwargs, - "search_query": kwargs.get("search_query"), - "other_param": kwargs.get("other_param") - } - - tool = FunctionTool(async_crewai_style_tool) - # Test with CrewAI-style parameters that should be passed through result = await tool.run_async( - args={ - "search_query": "async test query", - "other_param": "async test value" - }, + args={"search_query": search_query, "other_param": other_param}, tool_context=mock_tool_context, ) - - assert result["search_query"] == "async test query" - assert result["other_param"] == "async test value" - assert result["received_kwargs"]["search_query"] == "async test query" - assert result["received_kwargs"]["other_param"] == "async test value" + + assert result["search_query"] == search_query + assert result["other_param"] == other_param + assert result["received_kwargs"]["search_query"] == search_query + assert result["received_kwargs"]["other_param"] == other_param @pytest.mark.asyncio @@ -489,64 +498,42 @@ def explicit_params_func(arg1: str, arg2: int): @pytest.mark.asyncio -async def test_run_async_with_kwargs_and_tool_context(mock_tool_context): +@pytest.mark.parametrize( + "tool_function, args", + [ + ( + _func_with_context_and_kwargs_sync, + { + "arg1": "test_value", + "search_query": "omar elcircevi speaker", + "other_param": "test_value", + }, + ), + ( + _func_with_context_and_kwargs_async, + { + "arg1": "async_test_value", + "search_query": "async test query", + "other_param": "async test value", + }, + ), + ], + ids=["sync", "async"], +) +async def test_run_async_with_kwargs_and_tool_context( + mock_tool_context, tool_function, args +): """Test that run_async works with functions that have both tool_context and **kwargs.""" - - def func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs): - """Function with explicit tool_context parameter and **kwargs.""" - return { - "arg1": arg1, - "tool_context_present": bool(tool_context), - "search_query": kwargs.get("search_query"), - "received_kwargs": kwargs - } - - tool = FunctionTool(func_with_context_and_kwargs) - - # Test that both tool_context and **kwargs parameters work together - result = await tool.run_async( - args={ - "arg1": "test_value", - "search_query": "omar elcircevi speaker", - "other_param": "test_value" - }, - tool_context=mock_tool_context, - ) - - assert result["arg1"] == "test_value" - assert result["tool_context_present"] == True - assert result["search_query"] == "omar elcircevi speaker" - assert result["received_kwargs"]["search_query"] == "omar elcircevi speaker" - assert result["received_kwargs"]["other_param"] == "test_value" + tool = FunctionTool(tool_function) - -@pytest.mark.asyncio -async def test_run_async_with_kwargs_and_tool_context_async(mock_tool_context): - """Test that run_async works with async functions that have both tool_context and **kwargs.""" - - async def async_func_with_context_and_kwargs(arg1: str, tool_context: ToolContext, **kwargs): - """Async function with explicit tool_context parameter and **kwargs.""" - return { - "arg1": arg1, - "tool_context_present": bool(tool_context), - "search_query": kwargs.get("search_query"), - "received_kwargs": kwargs - } - - tool = FunctionTool(async_func_with_context_and_kwargs) - # Test that both tool_context and **kwargs parameters work together result = await tool.run_async( - args={ - "arg1": "async_test_value", - "search_query": "async test query", - "other_param": "async test value" - }, + args=args, tool_context=mock_tool_context, ) - - assert result["arg1"] == "async_test_value" - assert result["tool_context_present"] == True - assert result["search_query"] == "async test query" - assert result["received_kwargs"]["search_query"] == "async test query" - assert result["received_kwargs"]["other_param"] == "async test value" + + assert result["arg1"] == args["arg1"] + assert result["tool_context_present"] is True + assert result["search_query"] == args["search_query"] + assert result["received_kwargs"]["search_query"] == args["search_query"] + assert result["received_kwargs"]["other_param"] == args["other_param"] From 45d114294c570984fab2c2b287cbf44ced6d8e23 Mon Sep 17 00:00:00 2001 From: Omar Elcircevi Date: Thu, 2 Oct 2025 18:54:41 +0300 Subject: [PATCH 6/7] Update src/google/adk/tools/function_tool.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/google/adk/tools/function_tool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/google/adk/tools/function_tool.py b/src/google/adk/tools/function_tool.py index 663ea4dee6..629f1c81af 100644 --- a/src/google/adk/tools/function_tool.py +++ b/src/google/adk/tools/function_tool.py @@ -116,6 +116,8 @@ async def run_async( # remove the `self` argument, which may be injected by some tool # frameworks but is not intended for the wrapped function. args_to_call.pop('self', None) + if 'tool_context' not in valid_params: + args_to_call.pop('tool_context', None) else: # For functions without **kwargs, use the original filtering. args_to_call = {k: v for k, v in args_to_call.items() if k in valid_params} From d1234aa2990e1ec89e14c2f98dfe3c7dc7dc8563 Mon Sep 17 00:00:00 2001 From: Omar Elcircevi Date: Thu, 2 Oct 2025 19:01:59 +0300 Subject: [PATCH 7/7] test: Verify filtering of unexpected parameters in FunctionTool tests - Added an assertion to ensure that unexpected parameters are filtered out and not passed to the function in the test for backward compatibility with **kwargs. - This enhancement improves the robustness of the tests by explicitly checking for parameter handling. #non-breaking --- tests/unittests/tools/test_function_tool.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unittests/tools/test_function_tool.py b/tests/unittests/tools/test_function_tool.py index d20c0c02c4..cfdbb2a21b 100644 --- a/tests/unittests/tools/test_function_tool.py +++ b/tests/unittests/tools/test_function_tool.py @@ -495,6 +495,8 @@ def explicit_params_func(arg1: str, arg2: int): ) assert result == {"arg1": "test", "arg2": 42} + # Explicitly verify that unexpected_param was filtered out and not passed to the function + assert "unexpected_param" not in result @pytest.mark.asyncio