diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py index 5864a4995e..be09088175 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py +++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent_executor_mixin.py @@ -35,23 +35,35 @@ def _create_short_term_memory(self, output) -> None: self.crew and self.agent and self.task - and "Action: Delegate work to coworker" not in output.text ): - try: - if ( - hasattr(self.crew, "_short_term_memory") - and self.crew._short_term_memory - ): - self.crew._short_term_memory.save( - value=output.text, - metadata={ - "observation": self.task.description, - }, + is_delegation = any( + pattern in output.text + for pattern in [ + "Action: Delegate work to coworker", + "Action: delegate_work", + "Action: delegate_work_to_coworker", + "Action: Ask question to coworker", + "Action: ask_question", + "Action: ask_question_to_coworker", + ] + ) + + if not is_delegation: + try: + if ( + hasattr(self.crew, "_short_term_memory") + and self.crew._short_term_memory + ): + self.crew._short_term_memory.save( + value=output.text, + metadata={ + "observation": self.task.description, + }, + ) + except Exception as e: + self.agent._logger.log( + "error", f"Failed to add to short term memory: {e}" ) - except Exception as e: - self.agent._logger.log( - "error", f"Failed to add to short term memory: {e}" - ) def _create_external_memory(self, output) -> None: """Create and save a external-term memory item if conditions are met.""" diff --git a/lib/crewai/src/crewai/tools/tool_usage.py b/lib/crewai/src/crewai/tools/tool_usage.py index 6f0e92cb8d..af2a0a980d 100644 --- a/lib/crewai/src/crewai/tools/tool_usage.py +++ b/lib/crewai/src/crewai/tools/tool_usage.py @@ -240,10 +240,18 @@ def _use( if result is None: try: - if calling.tool_name in [ + normalized_tool_name = self._normalize_tool_name(calling.tool_name) + is_delegation_tool = normalized_tool_name in [ + "delegate_work_to_coworker", + "delegate_work", + "ask_question_to_coworker", + "ask_question", + ] or calling.tool_name in [ "Delegate work to coworker", "Ask question to coworker", - ]: + ] + + if is_delegation_tool: coworker = ( calling.arguments.get("coworker") if calling.arguments else None ) @@ -400,7 +408,78 @@ def _check_usage_limit(tool: Any, tool_name: str) -> str | None: return f"Tool '{tool_name}' has reached its usage limit of {tool.max_usage_count} times and cannot be used anymore." return None + def _normalize_tool_name(self, name: str) -> str: + """Normalize tool name for language-agnostic matching. + + Converts to lowercase, removes extra whitespace, and replaces + spaces/hyphens with underscores for consistent matching. + + Args: + name: The tool name to normalize + + Returns: + Normalized tool name + """ + if not name: + return "" + normalized = name.lower().strip() + normalized = normalized.replace(" ", "_").replace("-", "_") + while "__" in normalized: + normalized = normalized.replace("__", "_") + return normalized + + def _get_tool_aliases(self, tool: Any) -> list[str]: + """Get all possible aliases for a tool including stable identifiers. + + Args: + tool: The tool object + + Returns: + List of possible tool name aliases + """ + aliases = [tool.name] # Original name + + normalized = self._normalize_tool_name(tool.name) + if normalized and normalized != tool.name: + aliases.append(normalized) + + if tool.name == "Delegate work to coworker": + aliases.extend(["delegate_work", "delegate_work_to_coworker"]) + elif tool.name == "Ask question to coworker": + aliases.extend(["ask_question", "ask_question_to_coworker"]) + + return aliases + def _select_tool(self, tool_name: str) -> Any: + """Select a tool by name with language-agnostic matching support. + + Supports matching against: + 1. Exact tool name (case-insensitive) + 2. Normalized/slugified tool name + 3. Stable short identifiers (delegate_work, ask_question) + 4. Fuzzy matching as fallback (0.85 threshold) + + Args: + tool_name: The name of the tool to select + + Returns: + The selected tool object + + Raises: + Exception: If no matching tool is found + """ + normalized_input = self._normalize_tool_name(tool_name) + + for tool in self.tools: + aliases = self._get_tool_aliases(tool) + + if tool.name.lower().strip() == tool_name.lower().strip(): + return tool + + for alias in aliases: + if self._normalize_tool_name(alias) == normalized_input: + return tool + order_tools = sorted( self.tools, key=lambda tool: SequenceMatcher( @@ -410,13 +489,13 @@ def _select_tool(self, tool_name: str) -> Any: ) for tool in order_tools: if ( - tool.name.lower().strip() == tool_name.lower().strip() - or SequenceMatcher( + SequenceMatcher( None, tool.name.lower().strip(), tool_name.lower().strip() ).ratio() > 0.85 ): return tool + if self.task: self.task.increment_tools_errors() tool_selection_data: dict[str, Any] = { diff --git a/lib/crewai/tests/tools/test_tool_usage.py b/lib/crewai/tests/tools/test_tool_usage.py index 927031302d..bbcabd32a0 100644 --- a/lib/crewai/tests/tools/test_tool_usage.py +++ b/lib/crewai/tests/tools/test_tool_usage.py @@ -742,3 +742,174 @@ def event_handler(source, event): assert isinstance(event.started_at, datetime.datetime) assert isinstance(event.finished_at, datetime.datetime) assert event.type == "tool_usage_finished" + + +def test_normalize_tool_name(): + """Test tool name normalization for language-agnostic matching.""" + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[], + task=MagicMock(), + function_calling_llm=None, + agent=MagicMock(), + action=MagicMock(), + ) + + assert tool_usage._normalize_tool_name("Delegate work to coworker") == "delegate_work_to_coworker" + assert tool_usage._normalize_tool_name("Ask question to coworker") == "ask_question_to_coworker" + assert tool_usage._normalize_tool_name("delegate_work") == "delegate_work" + assert tool_usage._normalize_tool_name("DELEGATE WORK") == "delegate_work" + assert tool_usage._normalize_tool_name("delegate-work") == "delegate_work" + assert tool_usage._normalize_tool_name(" delegate work ") == "delegate_work" + assert tool_usage._normalize_tool_name("") == "" + + +def test_get_tool_aliases_for_delegate_work(): + """Test that delegate work tool has correct aliases.""" + from crewai.tools.agent_tools.delegate_work_tool import DelegateWorkTool + + delegate_tool = DelegateWorkTool(agents=[], description="Test delegate tool") + + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[delegate_tool], + task=MagicMock(), + function_calling_llm=None, + agent=MagicMock(), + action=MagicMock(), + ) + + aliases = tool_usage._get_tool_aliases(delegate_tool) + + assert "Delegate work to coworker" in aliases + assert "delegate_work" in aliases + assert "delegate_work_to_coworker" in aliases + + +def test_get_tool_aliases_for_ask_question(): + """Test that ask question tool has correct aliases.""" + from crewai.tools.agent_tools.ask_question_tool import AskQuestionTool + + ask_tool = AskQuestionTool(agents=[], description="Test ask question tool") + + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[ask_tool], + task=MagicMock(), + function_calling_llm=None, + agent=MagicMock(), + action=MagicMock(), + ) + + aliases = tool_usage._get_tool_aliases(ask_tool) + + assert "Ask question to coworker" in aliases + assert "ask_question" in aliases + assert "ask_question_to_coworker" in aliases + + +def test_select_tool_with_short_identifier(): + """Test tool selection using short identifiers like delegate_work.""" + from crewai.tools.agent_tools.delegate_work_tool import DelegateWorkTool + from crewai.tools.agent_tools.ask_question_tool import AskQuestionTool + + delegate_tool = DelegateWorkTool(agents=[], description="Test delegate tool") + ask_tool = AskQuestionTool(agents=[], description="Test ask question tool") + + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[delegate_tool, ask_tool], + task=MagicMock(), + function_calling_llm=None, + agent=MagicMock(), + action=MagicMock(), + ) + + # Test short identifiers + selected = tool_usage._select_tool("delegate_work") + assert selected.name == "Delegate work to coworker" + + selected = tool_usage._select_tool("ask_question") + assert selected.name == "Ask question to coworker" + + # Test slugified versions + selected = tool_usage._select_tool("delegate_work_to_coworker") + assert selected.name == "Delegate work to coworker" + + selected = tool_usage._select_tool("ask_question_to_coworker") + assert selected.name == "Ask question to coworker" + + +def test_select_tool_with_exact_name(): + """Test tool selection with exact English name still works.""" + from crewai.tools.agent_tools.delegate_work_tool import DelegateWorkTool + + delegate_tool = DelegateWorkTool(agents=[], description="Test delegate tool") + + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[delegate_tool], + task=MagicMock(), + function_calling_llm=None, + agent=MagicMock(), + action=MagicMock(), + ) + + # Test exact name matching (backward compatibility) + selected = tool_usage._select_tool("Delegate work to coworker") + assert selected.name == "Delegate work to coworker" + + +def test_select_tool_case_insensitive(): + """Test tool selection is case-insensitive.""" + from crewai.tools.agent_tools.delegate_work_tool import DelegateWorkTool + + delegate_tool = DelegateWorkTool(agents=[], description="Test delegate tool") + + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[delegate_tool], + task=MagicMock(), + function_calling_llm=None, + agent=MagicMock(), + action=MagicMock(), + ) + + # Test case variations + selected = tool_usage._select_tool("DELEGATE_WORK") + assert selected.name == "Delegate work to coworker" + + selected = tool_usage._select_tool("Delegate_Work") + assert selected.name == "Delegate work to coworker" + + +def test_memory_filter_with_short_identifiers(): + """Test that memory filter recognizes short identifiers.""" + from crewai.agents.agent_builder.base_agent_executor_mixin import CrewAgentExecutorMixin + + class TestMixin(CrewAgentExecutorMixin): + def __init__(self): + self.crew = MagicMock() + self.crew._short_term_memory = MagicMock() + self.agent = MagicMock() + self.agent._logger = MagicMock() + self.task = MagicMock() + self.task.description = "test task" + + mixin = TestMixin() + + # Test with short identifier - should NOT save to memory + output = MagicMock() + output.text = "Action: delegate_work\nAction Input: {...}" + mixin._create_short_term_memory(output) + mixin.crew._short_term_memory.save.assert_not_called() + + # Test with English name - should NOT save to memory + output.text = "Action: Delegate work to coworker\nAction Input: {...}" + mixin._create_short_term_memory(output) + mixin.crew._short_term_memory.save.assert_not_called() + + # Test with non-delegation action - should save to memory + output.text = "Action: Some other tool\nAction Input: {...}" + mixin._create_short_term_memory(output) + mixin.crew._short_term_memory.save.assert_called_once()