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
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
87 changes: 83 additions & 4 deletions lib/crewai/src/crewai/tools/tool_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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(
Expand All @@ -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] = {
Expand Down
171 changes: 171 additions & 0 deletions lib/crewai/tests/tools/test_tool_usage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading