Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/google/adk/agents/llm_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ async def _convert_tool_union_to_tools(
search_engine_id=vais_tool.search_engine_id,
filter=vais_tool.filter,
max_results=vais_tool.max_results,
name=vais_tool._bypass_tool_name,
description=vais_tool._bypass_tool_description,
)
]
from ..workflow._base_node import BaseNode
Expand Down
10 changes: 10 additions & 0 deletions src/google/adk/tools/discovery_engine_search_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ def __init__(
*,
search_result_mode: Optional[SearchResultMode] = None,
location: Optional[str] = None,
name: Optional[str] = None,
description: Optional[str] = None,
):
"""Initializes the DiscoveryEngineSearchTool.

Expand All @@ -164,8 +166,16 @@ def __init__(
location: Optional endpoint location override.
Examples: "global", "us", "eu". If not specified, location is inferred
from `data_store_id` or `search_engine_id` and defaults to "global".
name: Optional custom name for the tool. Defaults to
"discovery_engine_search".
description: Optional custom description for the tool. Defaults to
the docstring of discovery_engine_search.
"""
super().__init__(self.discovery_engine_search)
if name:
self.name = name
if description:
self.description = description
if (data_store_id is None and search_engine_id is None) or (
data_store_id is not None and search_engine_id is not None
):
Expand Down
18 changes: 18 additions & 0 deletions src/google/adk/tools/vertex_ai_search_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def __init__(
filter: Optional[str] = None,
max_results: Optional[int] = None,
bypass_multi_tools_limit: bool = False,
name: Optional[str] = None,
description: Optional[str] = None,
):
"""Initializes the Vertex AI Search tool.

Expand All @@ -86,6 +88,19 @@ def __init__(
max_results: The maximum number of results to return.
bypass_multi_tools_limit: Whether to bypass the multi tools limitation,
so that the tool can be used with other tools in the same agent.
name: Optional custom name for the tool. Only used when
``bypass_multi_tools_limit=True``, in which case the tool is converted
to a client-side :class:`DiscoveryEngineSearchTool`. When ``None``
(default) the converted tool is named ``discovery_engine_search``.
Has no effect when ``bypass_multi_tools_limit=False`` because the
built-in grounding path does not expose a callable tool name to the
model. Providing a domain-specific name (e.g.
``"knowledge_base_search"``) prevents prompt-fragility issues where
lightweight models guess generic names like ``search`` and trigger a
``ValueError: Tool 'search' not found`` at runtime.
description: Optional custom description for the tool. Only used when
``bypass_multi_tools_limit=True``. When ``None`` (default) the
converted tool uses the docstring of its internal search function.

Raises:
ValueError: If both data_store_id and search_engine_id are not specified
Expand All @@ -109,6 +124,9 @@ def __init__(
self.filter = filter
self.max_results = max_results
self.bypass_multi_tools_limit = bypass_multi_tools_limit
# Stored separately so they never shadow the built-in grounding name.
self._bypass_tool_name = name
self._bypass_tool_description = description

def _build_vertex_ai_search_config(
self, readonly_context: ReadonlyContext
Expand Down
129 changes: 129 additions & 0 deletions tests/unittests/agents/test_llm_agent_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,135 @@ async def test_handle_vais_in_hierarchy_no_bypass(self):
assert tools[0].name == 'vertex_ai_search'
assert tools[0].__class__.__name__ == 'VertexAiSearchTool'

@mock.patch(
'google.auth.default',
mock.MagicMock(return_value=('credentials', 'project')),
)
async def test_vais_bypass_custom_name_forwarded(self):
"""Custom name on VertexAiSearchTool is forwarded to DiscoveryEngineSearchTool."""
agent = LlmAgent(
name='test_agent',
model='gemini-pro',
tools=[
self._my_tool,
VertexAiSearchTool(
data_store_id='test_data_store_id',
bypass_multi_tools_limit=True,
name='knowledge_base_search',
),
],
)
ctx = await _create_readonly_context(agent)
tools = await agent.canonical_tools(ctx)

assert len(tools) == 2
assert tools[1].name == 'knowledge_base_search'
assert tools[1].__class__.__name__ == 'DiscoveryEngineSearchTool'

@mock.patch(
'google.auth.default',
mock.MagicMock(return_value=('credentials', 'project')),
)
async def test_vais_bypass_custom_description_forwarded(self):
"""Custom description on VertexAiSearchTool is forwarded to DiscoveryEngineSearchTool."""
custom_desc = 'Search the internal knowledge base for product information.'
agent = LlmAgent(
name='test_agent',
model='gemini-pro',
tools=[
self._my_tool,
VertexAiSearchTool(
data_store_id='test_data_store_id',
bypass_multi_tools_limit=True,
description=custom_desc,
),
],
)
ctx = await _create_readonly_context(agent)
tools = await agent.canonical_tools(ctx)

assert len(tools) == 2
assert tools[1].description == custom_desc
assert tools[1].__class__.__name__ == 'DiscoveryEngineSearchTool'

@mock.patch(
'google.auth.default',
mock.MagicMock(return_value=('credentials', 'project')),
)
async def test_vais_bypass_custom_name_and_description_forwarded(self):
"""Both custom name and description are forwarded to DiscoveryEngineSearchTool."""
custom_name = 'product_search'
custom_desc = 'Search the product catalogue.'
agent = LlmAgent(
name='test_agent',
model='gemini-pro',
tools=[
self._my_tool,
VertexAiSearchTool(
data_store_id='test_data_store_id',
bypass_multi_tools_limit=True,
name=custom_name,
description=custom_desc,
),
],
)
ctx = await _create_readonly_context(agent)
tools = await agent.canonical_tools(ctx)

assert len(tools) == 2
assert tools[1].name == custom_name
assert tools[1].description == custom_desc
assert tools[1].__class__.__name__ == 'DiscoveryEngineSearchTool'

@mock.patch(
'google.auth.default',
mock.MagicMock(return_value=('credentials', 'project')),
)
async def test_vais_bypass_default_name_unchanged_when_no_custom_name(self):
"""Default name is still 'discovery_engine_search' when no custom name provided."""
agent = LlmAgent(
name='test_agent',
model='gemini-pro',
tools=[
self._my_tool,
VertexAiSearchTool(
data_store_id='test_data_store_id',
bypass_multi_tools_limit=True,
),
],
)
ctx = await _create_readonly_context(agent)
tools = await agent.canonical_tools(ctx)

assert len(tools) == 2
assert tools[1].name == 'discovery_engine_search'
assert tools[1].__class__.__name__ == 'DiscoveryEngineSearchTool'

@mock.patch(
'google.auth.default',
mock.MagicMock(return_value=('credentials', 'project')),
)
async def test_vais_no_bypass_custom_name_does_not_affect_builtin_name(self):
"""name param has no effect on the built-in grounding tool name (bypass=False)."""
agent = LlmAgent(
name='test_agent',
model='gemini-pro',
tools=[
VertexAiSearchTool(
data_store_id='test_data_store_id',
bypass_multi_tools_limit=False,
name='should_be_ignored',
),
],
)
ctx = await _create_readonly_context(agent)
tools = await agent.canonical_tools(ctx)

assert len(tools) == 1
# The built-in grounding tool always reports 'vertex_ai_search'
assert tools[0].name == 'vertex_ai_search'
assert tools[0].__class__.__name__ == 'VertexAiSearchTool'

async def test_handle_enterprise_web_search_in_hierarchy(self):
"""Enterprise web search without bypass remains a built-in search tool in a hierarchy."""
search_agent = LlmAgent(
Expand Down
32 changes: 32 additions & 0 deletions tests/unittests/tools/test_discovery_engine_search_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,38 @@ def test_init_with_data_store_specs_without_search_engine_id_raises_error(
data_store_id="test_data_store", data_store_specs=[{"id": "123"}]
)

def test_init_default_name(self):
"""Default name is 'discovery_engine_search' (derived from the method name)."""
tool = DiscoveryEngineSearchTool(data_store_id="test_data_store")
assert tool.name == "discovery_engine_search"

def test_init_custom_name(self):
"""Custom name overrides the default tool name."""
tool = DiscoveryEngineSearchTool(
data_store_id="test_data_store",
name="knowledge_base_search",
)
assert tool.name == "knowledge_base_search"

def test_init_custom_description(self):
"""Custom description overrides the default tool description."""
custom_desc = "Search the internal product knowledge base."
tool = DiscoveryEngineSearchTool(
data_store_id="test_data_store",
description=custom_desc,
)
assert tool.description == custom_desc

def test_init_custom_name_and_description(self):
"""Both custom name and description are applied simultaneously."""
tool = DiscoveryEngineSearchTool(
data_store_id="test_data_store",
name="product_search",
description="Search the product catalogue.",
)
assert tool.name == "product_search"
assert tool.description == "Search the product catalogue."

@pytest.mark.parametrize(
("tool_kwargs", "expected_endpoint"),
[
Expand Down