feat: Add support for hosted/server-side tools in agents

- Allow agents to accept both CrewAI tools and raw tool definitions (dicts)
- Raw tool definitions are passed through unchanged to LLM providers
- Maintain backward compatibility with existing CrewAI tools
- Update BaseAgent.validate_tools() to handle dict tool definitions
- Update parse_tools() to preserve raw tool definitions
- Update utility functions to handle mixed tool types
- Add comprehensive tests for new functionality

Fixes #3338

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2025-08-18 13:56:38 +00:00
parent 947c9552f0
commit 95d3b5dbc3
5 changed files with 271 additions and 20 deletions

View File

@@ -537,14 +537,14 @@ class Agent(BaseAgent):
)["output"]
def create_agent_executor(
self, tools: Optional[List[BaseTool]] = None, task=None
self, tools: Optional[List[Union[BaseTool, dict]]] = None, task=None
) -> None:
"""Create an agent executor for the agent.
Returns:
An instance of the CrewAgentExecutor class.
"""
raw_tools: List[BaseTool] = tools or self.tools or []
raw_tools: List[Union[BaseTool, dict]] = tools or self.tools or []
parsed_tools = parse_tools(raw_tools)
prompt = Prompts(

View File

@@ -2,7 +2,7 @@ import uuid
from abc import ABC, abstractmethod
from copy import copy as shallow_copy
from hashlib import md5
from typing import Any, Callable, Dict, List, Optional, TypeVar
from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
from pydantic import (
UUID4,
@@ -108,7 +108,7 @@ class BaseAgent(ABC, BaseModel):
default=False,
description="Enable agent to delegate and ask questions among each other.",
)
tools: Optional[List[BaseTool]] = Field(
tools: Optional[List[Union[BaseTool, dict]]] = Field(
default_factory=list, description="Tools at agents' disposal"
)
max_iter: int = Field(
@@ -168,13 +168,12 @@ class BaseAgent(ABC, BaseModel):
@field_validator("tools")
@classmethod
def validate_tools(cls, tools: List[Any]) -> List[BaseTool]:
def validate_tools(cls, tools: List[Any]) -> List[Union[BaseTool, dict]]:
"""Validate and process the tools provided to the agent.
This method ensures that each tool is either an instance of BaseTool
or an object with 'name', 'func', and 'description' attributes. If the
tool meets these criteria, it is processed and added to the list of
tools. Otherwise, a ValueError is raised.
This method ensures that each tool is either an instance of BaseTool,
an object with 'name', 'func', and 'description' attributes, or a
raw tool definition (dict) for hosted/server-side tools.
"""
if not tools:
return []
@@ -184,13 +183,19 @@ class BaseAgent(ABC, BaseModel):
for tool in tools:
if isinstance(tool, BaseTool):
processed_tools.append(tool)
elif isinstance(tool, dict):
if "name" not in tool:
raise ValueError(
f"Raw tool definition must have a 'name' field: {tool}"
)
processed_tools.append(tool)
elif all(hasattr(tool, attr) for attr in required_attrs):
# Tool has the required attributes, create a Tool instance
processed_tools.append(Tool.from_langchain(tool))
else:
raise ValueError(
f"Invalid tool type: {type(tool)}. "
"Tool must be an instance of BaseTool or "
"Tool must be an instance of BaseTool, a dict for hosted tools, or "
"an object with 'name', 'func', and 'description' attributes."
)
return processed_tools

View File

@@ -48,7 +48,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
agent: BaseAgent,
prompt: dict[str, str],
max_iter: int,
tools: List[CrewStructuredTool],
tools: List[Union[CrewStructuredTool, dict]],
tools_names: str,
stop_words: List[str],
tools_description: str,
@@ -84,8 +84,8 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
self.messages: List[Dict[str, str]] = []
self.iterations = 0
self.log_error_after = 3
self.tool_name_to_tool_map: Dict[str, Union[CrewStructuredTool, BaseTool]] = {
tool.name: tool for tool in self.tools
self.tool_name_to_tool_map: Dict[str, Union[CrewStructuredTool, BaseTool, dict]] = {
tool.name if hasattr(tool, 'name') else tool.get('name', 'unknown'): tool for tool in self.tools
}
existing_stop = self.llm.stop or []
self.llm.stop = list(

View File

@@ -25,26 +25,28 @@ from crewai.cli.config import Settings
console = Console()
def parse_tools(tools: List[BaseTool]) -> List[CrewStructuredTool]:
def parse_tools(tools: List[Union[BaseTool, dict]]) -> List[Union[CrewStructuredTool, dict]]:
"""Parse tools to be used for the task."""
tools_list = []
for tool in tools:
if isinstance(tool, CrewAITool):
if isinstance(tool, dict):
tools_list.append(tool)
elif isinstance(tool, BaseTool):
tools_list.append(tool.to_structured_tool())
else:
raise ValueError("Tool is not a CrewStructuredTool or BaseTool")
raise ValueError(f"Tool is not a CrewStructuredTool, BaseTool, or raw tool definition (dict): {type(tool)}")
return tools_list
def get_tool_names(tools: Sequence[Union[CrewStructuredTool, BaseTool]]) -> str:
def get_tool_names(tools: Sequence[Union[CrewStructuredTool, BaseTool, dict]]) -> str:
"""Get the names of the tools."""
return ", ".join([t.name for t in tools])
return ", ".join([t.name if hasattr(t, 'name') else t.get('name', 'unknown') for t in tools])
def render_text_description_and_args(
tools: Sequence[Union[CrewStructuredTool, BaseTool]],
tools: Sequence[Union[CrewStructuredTool, BaseTool, dict]],
) -> str:
"""Render the tool name, description, and args in plain text.
@@ -54,7 +56,12 @@ def render_text_description_and_args(
"""
tool_strings = []
for tool in tools:
tool_strings.append(tool.description)
if hasattr(tool, 'description'):
tool_strings.append(tool.description)
elif isinstance(tool, dict) and 'description' in tool:
tool_strings.append(f"Tool name: {tool.get('name', 'unknown')}\nTool description:\n{tool['description']}")
else:
tool_strings.append(f"Tool name: {tool.get('name', 'unknown') if isinstance(tool, dict) else 'unknown'}")
return "\n".join(tool_strings)