Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
16 changes: 14 additions & 2 deletions src/google/adk/tools/function_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,20 @@ 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
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.
Expand Down
156 changes: 156 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,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
Expand Down Expand Up @@ -394,3 +403,150 @@ 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(mock_tool_context):
"""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"


@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"
},
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"
Comment thread
omarcevi marked this conversation as resolved.
Outdated


@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}


@pytest.mark.asyncio
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):
"""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"


@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"
},
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"
Comment thread
omarcevi marked this conversation as resolved.
Outdated