fix: sanitize tool names in native tool call processing

- Update extract_tool_call_info to return sanitized tool names
- Fix delegation tool name matching to use sanitized names
- Add sanitization in crew_agent_executor tool call extraction
- Add sanitization in experimental agent_executor
- Add sanitization in LLM.call function lookup
- Update streaming utility to use sanitized names
- Update base_agent_executor_mixin delegation check
This commit is contained in:
Greyson LaLonde
2026-01-22 20:05:20 -05:00
parent 242757f67b
commit b104d64b39
7 changed files with 25 additions and 25 deletions

View File

@@ -10,6 +10,7 @@ from crewai.memory.long_term.long_term_memory_item import LongTermMemoryItem
from crewai.utilities.converter import ConverterError
from crewai.utilities.evaluators.task_evaluator import TaskEvaluator
from crewai.utilities.printer import Printer
from crewai.utilities.string_utils import sanitize_tool_name
if TYPE_CHECKING:
@@ -36,7 +37,7 @@ class CrewAgentExecutorMixin:
self.crew
and self.agent
and self.task
and "Action: Delegate work to coworker" not in output.text
and f"Action: {sanitize_tool_name('Delegate work to coworker')}" not in output.text
):
try:
if (

View File

@@ -576,12 +576,12 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
if hasattr(tool_call, "function"):
# OpenAI-style: has .function.name and .function.arguments
call_id = getattr(tool_call, "id", f"call_{id(tool_call)}")
func_name = tool_call.function.name
func_name = sanitize_tool_name(tool_call.function.name)
func_args = tool_call.function.arguments
elif hasattr(tool_call, "function_call") and tool_call.function_call:
# Gemini-style: has .function_call.name and .function_call.args
call_id = f"call_{id(tool_call)}"
func_name = tool_call.function_call.name
func_name = sanitize_tool_name(tool_call.function_call.name)
func_args = (
dict(tool_call.function_call.args)
if tool_call.function_call.args
@@ -590,7 +590,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
elif hasattr(tool_call, "name") and hasattr(tool_call, "input"):
# Anthropic format: has .name and .input (ToolUseBlock)
call_id = getattr(tool_call, "id", f"call_{id(tool_call)}")
func_name = tool_call.name
func_name = sanitize_tool_name(tool_call.name)
func_args = tool_call.input # Already a dict in Anthropic
elif isinstance(tool_call, dict):
# Support OpenAI "id", Bedrock "toolUseId", or generate one
@@ -600,7 +600,9 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
or f"call_{id(tool_call)}"
)
func_info = tool_call.get("function", {})
func_name = func_info.get("name", "") or tool_call.get("name", "")
func_name = sanitize_tool_name(
func_info.get("name", "") or tool_call.get("name", "")
)
func_args = func_info.get("arguments", "{}") or tool_call.get("input", {})
else:
return None

View File

@@ -56,6 +56,7 @@ from crewai.utilities.agent_utils import (
from crewai.utilities.constants import TRAINING_DATA_FILE
from crewai.utilities.i18n import I18N, get_i18n
from crewai.utilities.printer import Printer
from crewai.utilities.string_utils import sanitize_tool_name
from crewai.utilities.tool_utils import execute_tool_and_check_finality
from crewai.utilities.training_handler import CrewTrainingHandler
from crewai.utilities.types import LLMMessage
@@ -602,8 +603,6 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin):
)
# Find original tool by matching sanitized name (needed for cache_function and result_as_answer)
from crewai.utilities.string_utils import sanitize_tool_name
original_tool = None
for tool in self.original_tools or []:
if sanitize_tool_name(tool.name) == func_name:
@@ -761,14 +760,14 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin):
def _extract_tool_name(self, tool_call: Any) -> str:
"""Extract tool name from various tool call formats."""
if hasattr(tool_call, "function"):
return tool_call.function.name
return sanitize_tool_name(tool_call.function.name)
if hasattr(tool_call, "function_call") and tool_call.function_call:
return tool_call.function_call.name
return sanitize_tool_name(tool_call.function_call.name)
if hasattr(tool_call, "name"):
return tool_call.name
return sanitize_tool_name(tool_call.name)
if isinstance(tool_call, dict):
func_info = tool_call.get("function", {})
return func_info.get("name", "") or tool_call.get("name", "unknown")
return sanitize_tool_name(func_info.get("name", "") or tool_call.get("name", "unknown"))
return "unknown"
@router(execute_native_tool)

View File

@@ -50,6 +50,7 @@ from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
)
from crewai.utilities.logger_utils import suppress_warnings
from crewai.utilities.string_utils import sanitize_tool_name
if TYPE_CHECKING:
@@ -1540,7 +1541,7 @@ class LLM(BaseLLM):
# --- 2) Extract function name from first tool call
tool_call = tool_calls[0]
function_name = tool_call.function.name
function_name = sanitize_tool_name(tool_call.function.name)
function_args = {} # Initialize to empty dict to avoid unbound variable
# --- 3) Check if function is available

View File

@@ -296,6 +296,4 @@ class CrewStructuredTool:
return self.args_schema.model_json_schema()["properties"]
def __repr__(self) -> str:
return (
f"CrewStructuredTool(name='{self.name}', description='{self.description}')"
)
return f"CrewStructuredTool(name='{sanitize_tool_name(self.name)}', description='{self.description}')"

View File

@@ -821,10 +821,8 @@ def load_agent_from_repository(from_repository: str) -> dict[str, Any]:
DELEGATION_TOOL_NAMES: Final[frozenset[str]] = frozenset(
[
"Delegate work to coworker",
"Delegate_work_to_coworker",
"Ask question to coworker",
"Ask_question_to_coworker",
sanitize_tool_name("Delegate work to coworker"),
sanitize_tool_name("Ask question to coworker"),
]
)
@@ -842,7 +840,7 @@ def track_delegation_if_needed(
tool_args: Arguments passed to the tool.
task: The task being executed (used to track delegations).
"""
if tool_name in DELEGATION_TOOL_NAMES and task is not None:
if sanitize_tool_name(tool_name) in DELEGATION_TOOL_NAMES and task is not None:
coworker = tool_args.get("coworker")
task.increment_delegations(coworker)
@@ -861,19 +859,19 @@ def extract_tool_call_info(
if hasattr(tool_call, "function"):
# OpenAI-style: has .function.name and .function.arguments
call_id = getattr(tool_call, "id", f"call_{id(tool_call)}")
return call_id, tool_call.function.name, tool_call.function.arguments
return call_id, sanitize_tool_name(tool_call.function.name), tool_call.function.arguments
if hasattr(tool_call, "function_call") and tool_call.function_call:
# Gemini-style: has .function_call.name and .function_call.args
call_id = f"call_{id(tool_call)}"
return (
call_id,
tool_call.function_call.name,
sanitize_tool_name(tool_call.function_call.name),
dict(tool_call.function_call.args) if tool_call.function_call.args else {},
)
if hasattr(tool_call, "name") and hasattr(tool_call, "input"):
# Anthropic format: has .name and .input (ToolUseBlock)
call_id = getattr(tool_call, "id", f"call_{id(tool_call)}")
return call_id, tool_call.name, tool_call.input
return call_id, sanitize_tool_name(tool_call.name), tool_call.input
if isinstance(tool_call, dict):
# Support OpenAI "id", Bedrock "toolUseId", or generate one
call_id = (
@@ -882,7 +880,7 @@ def extract_tool_call_info(
func_info = tool_call.get("function", {})
func_name = func_info.get("name", "") or tool_call.get("name", "")
func_args = func_info.get("arguments", "{}") or tool_call.get("input", {})
return call_id, func_name, func_args
return call_id, sanitize_tool_name(func_name), func_args
return None

View File

@@ -18,6 +18,7 @@ from crewai.types.streaming import (
StreamChunkType,
ToolCallChunk,
)
from crewai.utilities.string_utils import sanitize_tool_name
class TaskInfo(TypedDict):
@@ -58,7 +59,7 @@ def _extract_tool_call_info(
StreamChunkType.TOOL_CALL,
ToolCallChunk(
tool_id=event.tool_call.id,
tool_name=event.tool_call.function.name,
tool_name=sanitize_tool_name(event.tool_call.function.name),
arguments=event.tool_call.function.arguments,
index=event.tool_call.index,
),