From a56bf0c193236c7f2e563f1057b6fcc7403e17f9 Mon Sep 17 00:00:00 2001 From: HumphreySun98 Date: Wed, 24 Jun 2026 12:15:16 -0500 Subject: [PATCH] perf(tools): add exact-match fast path to _select_tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _select_tool sorted ALL tools by fuzzy SequenceMatcher ratio (an O(L^2) string diff) on every tool-call iteration, even when the LLM emitted a valid, exactly-matching tool name — the overwhelmingly common case. An exact match has ratio 1.0 and would always win that sort anyway. Resolve exact (sanitized) name matches with a single O(n) scan before falling back to the fuzzy sort, which is retained unchanged for typo tolerance. Scanning self.tools in order preserves the prior stable tie-break. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/crewai/src/crewai/tools/tool_usage.py | 9 +++ lib/crewai/tests/tools/test_tool_usage.py | 87 +++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/lib/crewai/src/crewai/tools/tool_usage.py b/lib/crewai/src/crewai/tools/tool_usage.py index e92ba03eed..fa54c6dfad 100644 --- a/lib/crewai/src/crewai/tools/tool_usage.py +++ b/lib/crewai/src/crewai/tools/tool_usage.py @@ -758,6 +758,15 @@ def _check_usage_limit(tool: Any, tool_name: str) -> str | None: def _select_tool(self, tool_name: str) -> Any: sanitized_input = sanitize_tool_name(tool_name) + # Fast path: an exact (sanitized) name match is the overwhelmingly + # common case — the LLM emitted a valid tool name — so resolve it in a + # single O(n) scan and skip the O(n log n) fuzzy SequenceMatcher sort + # below. An exact match has ratio 1.0, so it would always win that sort + # anyway; scanning self.tools in order preserves the same tie-break. + for tool in self.tools: + if sanitize_tool_name(tool.name) == sanitized_input: + return tool + # Fuzzy fallback: tolerate minor typos / near-misses in the tool name. order_tools = sorted( self.tools, key=lambda tool: SequenceMatcher( diff --git a/lib/crewai/tests/tools/test_tool_usage.py b/lib/crewai/tests/tools/test_tool_usage.py index 9d61c93a9b..793a4df271 100644 --- a/lib/crewai/tests/tools/test_tool_usage.py +++ b/lib/crewai/tests/tools/test_tool_usage.py @@ -113,6 +113,93 @@ def test_random_number_tool_schema(): ) +def _make_tool_usage(tools): + return ToolUsage( + tools_handler=MagicMock(), + tools=tools, + task=MagicMock(), + function_calling_llm=MagicMock(), + agent=MagicMock(), + action=MagicMock(), + ) + + +def _legacy_select_tool(tool_usage, tool_name): + """Reference implementation of the pre-fast-path _select_tool matching + logic (fuzzy sort + threshold), used to prove behavior is preserved.""" + from difflib import SequenceMatcher + + from crewai.utilities.string_utils import sanitize_tool_name + + sanitized_input = sanitize_tool_name(tool_name) + order_tools = sorted( + tool_usage.tools, + key=lambda tool: SequenceMatcher( + None, sanitize_tool_name(tool.name), sanitized_input + ).ratio(), + reverse=True, + ) + for tool in order_tools: + sanitized_tool = sanitize_tool_name(tool.name) + if ( + sanitized_tool == sanitized_input + or SequenceMatcher(None, sanitized_tool, sanitized_input).ratio() > 0.85 + ): + return tool + return None + + +class _NamedTool(BaseTool): + description: str = "A tool" + + def _run(self) -> str: + return "ok" + + +def test_select_tool_exact_match_by_raw_and_sanitized_name(): + calc = _NamedTool(name="Calculator Tool") + weather = _NamedTool(name="Weather Lookup") + tool_usage = _make_tool_usage([calc, weather]) + + # Raw name and its sanitized form both resolve to the same tool. + assert tool_usage._select_tool("Calculator Tool") is calc + assert tool_usage._select_tool("calculator_tool") is calc + assert tool_usage._select_tool("Weather Lookup") is weather + + +def test_select_tool_fuzzy_fallback_for_typo(): + calc = _NamedTool(name="Calculator Tool") + weather = _NamedTool(name="Weather Lookup") + tool_usage = _make_tool_usage([calc, weather]) + + # A small typo (no exact match) still resolves via the fuzzy fallback. + assert tool_usage._select_tool("calculater_tool") is calc + + +def test_select_tool_matches_legacy_behavior(): + tools = [ + _NamedTool(name="Calculator Tool"), + _NamedTool(name="Weather Lookup"), + _NamedTool(name="Search The Web"), + ] + tool_usage = _make_tool_usage(tools) + + # Across exact names, sanitized names, and near-miss typos, the fast-path + # selection must return exactly what the legacy fuzzy-sort logic returned. + for query in [ + "Calculator Tool", + "calculator_tool", + "Weather Lookup", + "weather_lookup", + "Search The Web", + "search_the_webb", # typo, fuzzy + "calculater_tool", # typo, fuzzy + ]: + assert tool_usage._select_tool(query) is _legacy_select_tool( + tool_usage, query + ), f"mismatch for {query!r}" + + def test_tool_usage_render(): tool = RandomNumberTool()