Spaces:
Paused
Paused
| """ | |
| Tests for FunctionCallingCache - specifically for tool name extraction and validation. | |
| """ | |
| import time | |
| from unittest.mock import MagicMock | |
| import pytest | |
| from api_utils.utils_ext.function_calling_cache import ( | |
| FunctionCallingCache, | |
| FunctionCallingCacheEntry, | |
| ) | |
| class TestFunctionCallingCacheEntry: | |
| """Tests for FunctionCallingCacheEntry dataclass.""" | |
| def test_default_values(self): | |
| """Test default values are correct.""" | |
| entry = FunctionCallingCacheEntry( | |
| tools_digest="abc123", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| timestamp=time.time(), | |
| ) | |
| assert entry.tools_digest == "abc123" | |
| assert entry.toggle_enabled is True | |
| assert entry.declarations_set is True | |
| assert entry.model_name is None | |
| assert entry.tool_names == set() | |
| def test_with_tool_names(self): | |
| """Test entry with tool names.""" | |
| names = {"get_weather", "search_web", "calculate"} | |
| entry = FunctionCallingCacheEntry( | |
| tools_digest="xyz789", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| timestamp=time.time(), | |
| tool_names=names, | |
| ) | |
| assert entry.tool_names == names | |
| assert "get_weather" in entry.tool_names | |
| assert "search_web" in entry.tool_names | |
| class TestExtractToolNames: | |
| """Tests for _extract_tool_names method.""" | |
| def cache(self): | |
| """Create a cache instance.""" | |
| return FunctionCallingCache(logger=MagicMock()) | |
| def test_extract_from_openai_format(self, cache): | |
| """Test extracting names from OpenAI tool format (nested function.name).""" | |
| tools = [ | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "get_weather", | |
| "description": "Get weather info", | |
| "parameters": {"type": "object", "properties": {}}, | |
| }, | |
| }, | |
| { | |
| "type": "function", | |
| "function": { | |
| "name": "search_web", | |
| "description": "Search the web", | |
| "parameters": {"type": "object", "properties": {}}, | |
| }, | |
| }, | |
| ] | |
| names = cache._extract_tool_names(tools) | |
| assert names == {"get_weather", "search_web"} | |
| def test_extract_from_flat_format(self, cache): | |
| """Test extracting names from flat format (name at top level).""" | |
| tools = [ | |
| {"name": "get_weather", "description": "Get weather"}, | |
| {"name": "calculate", "description": "Calculate math"}, | |
| ] | |
| names = cache._extract_tool_names(tools) | |
| assert names == {"get_weather", "calculate"} | |
| def test_extract_from_mixed_format(self, cache): | |
| """Test extracting names from mixed formats.""" | |
| tools = [ | |
| {"type": "function", "function": {"name": "openai_tool"}}, | |
| {"name": "flat_tool"}, | |
| ] | |
| names = cache._extract_tool_names(tools) | |
| assert names == {"openai_tool", "flat_tool"} | |
| def test_extract_empty_list(self, cache): | |
| """Test extracting from empty list.""" | |
| names = cache._extract_tool_names([]) | |
| assert names == set() | |
| def test_extract_invalid_tools(self, cache): | |
| """Test extracting from invalid tool definitions.""" | |
| tools = [ | |
| None, | |
| "not a dict", | |
| {"no_name_key": "value"}, | |
| {"function": "not a dict"}, | |
| {"function": {"no_name": "value"}}, | |
| ] | |
| names = cache._extract_tool_names(tools) | |
| assert names == set() | |
| def test_extract_with_special_characters(self, cache): | |
| """Test extracting names with special characters.""" | |
| tools = [ | |
| {"name": "gh_grep_searchGitHub"}, | |
| {"name": "tavily_tavily_search"}, | |
| {"name": "context7_get-library-docs"}, | |
| ] | |
| names = cache._extract_tool_names(tools) | |
| assert names == { | |
| "gh_grep_searchGitHub", | |
| "tavily_tavily_search", | |
| "context7_get-library-docs", | |
| } | |
| class TestValidateFunctionName: | |
| """Tests for validate_function_name method (fuzzy matching).""" | |
| def cache_with_tools(self): | |
| """Create a cache with registered tools.""" | |
| cache = FunctionCallingCache(logger=MagicMock()) | |
| # Manually set up cache with tool names | |
| cache._cache = FunctionCallingCacheEntry( | |
| tools_digest="test123", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| timestamp=time.time(), | |
| tool_names={ | |
| "gh_grep_searchGitHub", | |
| "tavily_tavily_search", | |
| "context7_get-library-docs", | |
| "chrome_devtools_click", | |
| "short", | |
| }, | |
| ) | |
| return cache | |
| def test_exact_match(self, cache_with_tools): | |
| """Test exact name match returns the name unchanged.""" | |
| corrected, was_corrected, confidence = cache_with_tools.validate_function_name( | |
| "gh_grep_searchGitHub" | |
| ) | |
| assert corrected == "gh_grep_searchGitHub" | |
| assert was_corrected is False | |
| assert confidence == 1.0 | |
| def test_prefix_match_truncated_name(self, cache_with_tools): | |
| """Test fuzzy matching corrects truncated names.""" | |
| # Simulate truncated name from model hallucination | |
| corrected, was_corrected, confidence = cache_with_tools.validate_function_name( | |
| "gh_grep_searchGitH" | |
| ) | |
| assert corrected == "gh_grep_searchGitHub" | |
| assert was_corrected is True | |
| assert 0.7 < confidence < 1.0 # High confidence but not exact | |
| def test_prefix_match_another_truncated(self, cache_with_tools): | |
| """Test another truncated name gets corrected.""" | |
| corrected, was_corrected, confidence = cache_with_tools.validate_function_name( | |
| "tavily_tavily_sear" | |
| ) | |
| assert corrected == "tavily_tavily_search" | |
| assert was_corrected is True | |
| assert 0.7 < confidence < 1.0 | |
| def test_prefix_too_short(self, cache_with_tools): | |
| """Test that very short prefixes still match (no minimum threshold).""" | |
| # "gh" matches "gh_grep_searchGitHub" via prefix | |
| corrected, was_corrected, confidence = cache_with_tools.validate_function_name( | |
| "gh" | |
| ) | |
| # It will match but with low confidence | |
| assert was_corrected is True | |
| assert confidence < 0.2 # Very low confidence for short prefix | |
| def test_no_match_invalid_name(self, cache_with_tools): | |
| """Test that completely invalid names are not matched.""" | |
| corrected, was_corrected, confidence = cache_with_tools.validate_function_name( | |
| "completely_unknown_function" | |
| ) | |
| assert was_corrected is False | |
| assert corrected == "completely_unknown_function" | |
| assert confidence == 0.0 | |
| def test_empty_cache(self): | |
| """Test validation with empty cache returns original name.""" | |
| cache = FunctionCallingCache(logger=MagicMock()) | |
| # No cache set | |
| corrected, was_corrected, confidence = cache.validate_function_name( | |
| "any_function" | |
| ) | |
| assert was_corrected is False | |
| assert corrected == "any_function" | |
| assert confidence == 0.0 | |
| def test_empty_tool_names(self): | |
| """Test validation with empty tool_names set.""" | |
| cache = FunctionCallingCache(logger=MagicMock()) | |
| cache._cache = FunctionCallingCacheEntry( | |
| tools_digest="test", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| timestamp=time.time(), | |
| tool_names=set(), | |
| ) | |
| corrected, was_corrected, confidence = cache.validate_function_name( | |
| "any_function" | |
| ) | |
| assert was_corrected is False | |
| assert corrected == "any_function" | |
| assert confidence == 0.0 | |
| def test_ambiguous_prefix(self, cache_with_tools): | |
| """Test that ambiguous prefixes (matching multiple tools) return first match.""" | |
| # Add another tool with similar prefix | |
| cache_with_tools._cache.tool_names.add("chrome_devtools_screenshot") | |
| # "chrome_devtools_c" matches "chrome_devtools_click" | |
| corrected, was_corrected, confidence = cache_with_tools.validate_function_name( | |
| "chrome_devtools_c" | |
| ) | |
| assert was_corrected is True | |
| assert corrected == "chrome_devtools_click" | |
| class TestGetRegisteredToolNames: | |
| """Tests for get_registered_tool_names method.""" | |
| def test_no_cache(self): | |
| """Test returns empty set when no cache exists.""" | |
| cache = FunctionCallingCache(logger=MagicMock()) | |
| names = cache.get_registered_tool_names() | |
| assert names == set() | |
| def test_with_cache(self): | |
| """Test returns tool names from cache.""" | |
| cache = FunctionCallingCache(logger=MagicMock()) | |
| cache._cache = FunctionCallingCacheEntry( | |
| tools_digest="test", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| timestamp=time.time(), | |
| tool_names={"func1", "func2", "func3"}, | |
| ) | |
| names = cache.get_registered_tool_names() | |
| assert names == {"func1", "func2", "func3"} | |
| class TestUpdateCacheWithTools: | |
| """Tests for update_cache with tools parameter.""" | |
| def cache(self): | |
| """Create a cache instance.""" | |
| cache = FunctionCallingCache(logger=MagicMock()) | |
| cache._enabled = True | |
| return cache | |
| def test_update_cache_with_openai_tools(self, cache): | |
| """Test that update_cache extracts and stores tool names.""" | |
| tools = [ | |
| {"type": "function", "function": {"name": "get_weather"}}, | |
| {"type": "function", "function": {"name": "search_web"}}, | |
| ] | |
| cache.update_cache( | |
| tools_digest="digest123", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| tools=tools, | |
| ) | |
| assert cache._cache is not None | |
| assert cache._cache.tool_names == {"get_weather", "search_web"} | |
| def test_update_cache_without_tools(self, cache): | |
| """Test that update_cache works without tools (empty set).""" | |
| cache.update_cache( | |
| tools_digest="digest123", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| ) | |
| assert cache._cache is not None | |
| assert cache._cache.tool_names == set() | |
| def test_update_cache_replaces_previous_tools(self, cache): | |
| """Test that update_cache replaces previous tool names.""" | |
| # First update | |
| cache.update_cache( | |
| tools_digest="digest1", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| tools=[{"name": "old_tool"}], | |
| ) | |
| assert cache._cache.tool_names == {"old_tool"} | |
| # Second update replaces | |
| cache.update_cache( | |
| tools_digest="digest2", | |
| toggle_enabled=True, | |
| declarations_set=True, | |
| tools=[{"name": "new_tool"}], | |
| ) | |
| assert cache._cache.tool_names == {"new_tool"} | |
| assert "old_tool" not in cache._cache.tool_names | |