Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f5949f6
fix: improve parameter filtering for functions with **kwargs
omarcevi Sep 28, 2025
1e97f35
fix: correct tool_context handling in **kwargs parameter filtering
omarcevi Sep 28, 2025
dffd106
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 2, 2025
31b7ec2
fix: Improve handling of **kwargs in FunctionTool by removing 'self' …
omarcevi Oct 2, 2025
619d5a1
Update src/google/adk/tools/function_tool.py
omarcevi Oct 2, 2025
574db75
test: Enhance FunctionTool tests with CrewAI-style functions and cont…
omarcevi Oct 2, 2025
45d1142
Update src/google/adk/tools/function_tool.py
omarcevi Oct 2, 2025
d1234aa
test: Verify filtering of unexpected parameters in FunctionTool tests
omarcevi Oct 2, 2025
dc6a5f4
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 4, 2025
f7f5a38
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 5, 2025
3c251b2
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 11, 2025
9ef73e0
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 15, 2025
14b89ed
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 15, 2025
ef010b8
feat: Enhance CrewaiTool with async run method and parameter filtering
omarcevi Oct 17, 2025
8d81f2c
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 17, 2025
9de9de4
docs: Update CrewaiTool docstring for parameter filtering clarification
omarcevi Oct 17, 2025
75467d7
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 20, 2025
2bd9e55
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 21, 2025
012bbfc
Merge branch 'main' into fix/function-tool-kwargs-parameter-filtering
omarcevi Oct 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/google/adk/tools/function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,22 @@ 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()
Comment thread
omarcevi marked this conversation as resolved.
Outdated
)

if has_kwargs:
# 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)
Comment thread
omarcevi marked this conversation as resolved.
Outdated
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}
Comment thread
omarcevi marked this conversation as resolved.
Outdated

# Before invoking the function, we check for if the list of args passed in
# has all the mandatory arguments or not.
Expand Down
145 changes: 145 additions & 0 deletions tests/unittests/tools/test_function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,57 @@
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 _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
Expand Down Expand Up @@ -394,3 +445,97 @@ def sample_func(arg1: str):
tool_context=tool_context_mock,
)
assert result == {"received_arg": "hello"}


@pytest.mark.asyncio
@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."""
tool = FunctionTool(tool_function)

# Test with CrewAI-style parameters that should be passed through
result = await tool.run_async(
args={"search_query": search_query, "other_param": other_param},
tool_context=mock_tool_context,
)

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
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):
"""Function with explicit parameters (no **kwargs)."""
return {"arg1": arg1, "arg2": arg2}

tool = FunctionTool(explicit_params_func)

# 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"
},
Comment thread
omarcevi marked this conversation as resolved.
tool_context=mock_tool_context,
)

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
Comment thread
omarcevi marked this conversation as resolved.


@pytest.mark.asyncio
@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."""
tool = FunctionTool(tool_function)

# Test that both tool_context and **kwargs parameters work together
result = await tool.run_async(
args=args,
tool_context=mock_tool_context,
)

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"]