From 8f0f51e5ea07de6d730d127eeb9515279365e6b9 Mon Sep 17 00:00:00 2001 From: Sooriya04 Date: Sat, 31 Oct 2026 22:29:54 +0530 Subject: [PATCH 1/2] fix(core): return all tools when search_tools limit is 0 - In TagAndDescriptionWordMatchStrategy, slicing sorted_tools[:limit] with limit=0 returned an empty list instead of all matching tools as specified by ToolSearchStrategy. - Update slice to return all tools when limit == 0: sorted_tools[:limit] if limit > 0 else sorted_tools. - Update search_tools docstrings in TagAndDescriptionWordMatchStrategy and UtcpClientImplementation to document limit=0 as no limit. - Add comprehensive unit test suite in core/tests/implementations/test_tag_search.py verifying limit=0, positive limits, negative limits, and tag filtering. --- core/src/utcp/implementations/tag_search.py | 6 +- .../utcp_client_implementation.py | 2 +- core/tests/implementations/test_tag_search.py | 100 ++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 core/tests/implementations/test_tag_search.py diff --git a/core/src/utcp/implementations/tag_search.py b/core/src/utcp/implementations/tag_search.py index f11bf40..7113f51 100644 --- a/core/src/utcp/implementations/tag_search.py +++ b/core/src/utcp/implementations/tag_search.py @@ -39,7 +39,7 @@ async def search_tools(self, tool_repository: ConcurrentToolRepository, query: s Args: tool_repository: The tool repository to search in. query: The query to search for. - limit: The maximum number of results to return. + limit: The maximum number of results to return. Use 0 for no limit. any_of_tags_required: A list of tags that must be present in the tool. Returns: @@ -91,8 +91,8 @@ async def search_tools(self, tool_repository: ConcurrentToolRepository, query: s # Sort tools by score in descending order sorted_tools = [tool for tool, score in sorted(tool_scores, key=lambda x: x[1], reverse=True)] - # Return up to 'limit' tools - return sorted_tools[:limit] + # Return up to 'limit' tools (0 for no limit) + return sorted_tools[:limit] if limit > 0 else sorted_tools class TagAndDescriptionWordMatchStrategyConfigSerializer(Serializer[TagAndDescriptionWordMatchStrategy]): """REQUIRED diff --git a/core/src/utcp/implementations/utcp_client_implementation.py b/core/src/utcp/implementations/utcp_client_implementation.py index e494b6e..9d62826 100644 --- a/core/src/utcp/implementations/utcp_client_implementation.py +++ b/core/src/utcp/implementations/utcp_client_implementation.py @@ -442,7 +442,7 @@ async def search_tools(self, query: str, limit: int = 10, any_of_tags_required: Args: query: The query to search for. - limit: The maximum number of results to return. + limit: The maximum number of results to return. 0 for no limit. any_of_tags_required: A list of tags that must be present in the tool. Returns: diff --git a/core/tests/implementations/test_tag_search.py b/core/tests/implementations/test_tag_search.py new file mode 100644 index 0000000..a601078 --- /dev/null +++ b/core/tests/implementations/test_tag_search.py @@ -0,0 +1,100 @@ +"""Unit tests for TagAndDescriptionWordMatchStrategy.""" + +import pytest +from utcp.data.call_template import CallTemplate +from utcp.data.tool import Tool, JsonSchema +from utcp.data.utcp_manual import UtcpManual +from utcp.implementations.in_mem_tool_repository import InMemToolRepository +from utcp.implementations.tag_search import TagAndDescriptionWordMatchStrategy + + +def get_sample_tools(): + tpl = CallTemplate(name="test_manual", call_template_type="test") + return [ + Tool( + name="add_numbers", + description="Add two numbers together in arithmetic", + inputs=JsonSchema(type="object"), + outputs=JsonSchema(type="object"), + tool_call_template=tpl, + tags=["math", "addition", "calculator"], + ), + Tool( + name="subtract_numbers", + description="Subtract two numbers in arithmetic", + inputs=JsonSchema(type="object"), + outputs=JsonSchema(type="object"), + tool_call_template=tpl, + tags=["math", "subtraction", "calculator"], + ), + Tool( + name="send_email", + description="Send an email to a recipient", + inputs=JsonSchema(type="object"), + outputs=JsonSchema(type="object"), + tool_call_template=tpl, + tags=["communication", "email"], + ), + ] + + +async def create_populated_repo(): + tools = get_sample_tools() + repo = InMemToolRepository() + tpl = CallTemplate(name="test_manual", call_template_type="test") + manual = UtcpManual(tools=tools) + await repo.save_manual(tpl, manual) + return repo, tools + + +@pytest.mark.anyio +async def test_search_tools_limit_zero_returns_all_tools(): + """Verify limit=0 returns all matching tools without slicing to empty.""" + repo, tools = await create_populated_repo() + strategy = TagAndDescriptionWordMatchStrategy() + results = await strategy.search_tools(repo, "math", limit=0) + assert len(results) == len(tools) + + +@pytest.mark.anyio +async def test_search_tools_positive_limit_respected(): + """Verify limit > 0 returns at most `limit` results.""" + repo, _ = await create_populated_repo() + strategy = TagAndDescriptionWordMatchStrategy() + results = await strategy.search_tools(repo, "math", limit=1) + assert len(results) == 1 + assert results[0].name in ("add_numbers", "subtract_numbers") + + +@pytest.mark.anyio +async def test_search_tools_negative_limit_raises_error(): + """Verify negative limit raises ValueError.""" + repo, _ = await create_populated_repo() + strategy = TagAndDescriptionWordMatchStrategy() + with pytest.raises(ValueError, match="limit must be non-negative"): + await strategy.search_tools(repo, "math", limit=-1) + + +@pytest.mark.anyio +async def test_search_tools_tag_and_description_matching(): + """Verify tools with matching tags rank highest.""" + repo, _ = await create_populated_repo() + strategy = TagAndDescriptionWordMatchStrategy() + results = await strategy.search_tools(repo, "email", limit=10) + assert len(results) > 0 + assert results[0].name == "send_email" + + +@pytest.mark.anyio +async def test_search_tools_any_of_tags_required(): + """Verify any_of_tags_required filters out non-matching tools.""" + repo, _ = await create_populated_repo() + strategy = TagAndDescriptionWordMatchStrategy() + results = await strategy.search_tools( + repo, + "numbers", + limit=0, + any_of_tags_required=["addition"], + ) + assert len(results) == 1 + assert results[0].name == "add_numbers" From 389f3ceefd37716eebda9c2983a25486a07875cf Mon Sep 17 00:00:00 2001 From: Sooriya04 Date: Sat, 31 Oct 2026 22:39:05 +0530 Subject: [PATCH 2/2] test(core): replace anyio test markers with pytest-asyncio in test_tag_search --- core/tests/implementations/test_tag_search.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/tests/implementations/test_tag_search.py b/core/tests/implementations/test_tag_search.py index a601078..94be080 100644 --- a/core/tests/implementations/test_tag_search.py +++ b/core/tests/implementations/test_tag_search.py @@ -47,7 +47,7 @@ async def create_populated_repo(): return repo, tools -@pytest.mark.anyio +@pytest.mark.asyncio async def test_search_tools_limit_zero_returns_all_tools(): """Verify limit=0 returns all matching tools without slicing to empty.""" repo, tools = await create_populated_repo() @@ -56,7 +56,7 @@ async def test_search_tools_limit_zero_returns_all_tools(): assert len(results) == len(tools) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_search_tools_positive_limit_respected(): """Verify limit > 0 returns at most `limit` results.""" repo, _ = await create_populated_repo() @@ -66,7 +66,7 @@ async def test_search_tools_positive_limit_respected(): assert results[0].name in ("add_numbers", "subtract_numbers") -@pytest.mark.anyio +@pytest.mark.asyncio async def test_search_tools_negative_limit_raises_error(): """Verify negative limit raises ValueError.""" repo, _ = await create_populated_repo() @@ -75,7 +75,7 @@ async def test_search_tools_negative_limit_raises_error(): await strategy.search_tools(repo, "math", limit=-1) -@pytest.mark.anyio +@pytest.mark.asyncio async def test_search_tools_tag_and_description_matching(): """Verify tools with matching tags rank highest.""" repo, _ = await create_populated_repo() @@ -85,7 +85,7 @@ async def test_search_tools_tag_and_description_matching(): assert results[0].name == "send_email" -@pytest.mark.anyio +@pytest.mark.asyncio async def test_search_tools_any_of_tags_required(): """Verify any_of_tags_required filters out non-matching tools.""" repo, _ = await create_populated_repo()