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
6 changes: 3 additions & 3 deletions core/src/utcp/implementations/tag_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
100 changes: 100 additions & 0 deletions core/tests/implementations/test_tag_search.py
Original file line number Diff line number Diff line change
@@ -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.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()
strategy = TagAndDescriptionWordMatchStrategy()
results = await strategy.search_tools(repo, "math", limit=0)
assert len(results) == len(tools)


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