diff --git a/src/crewai/agent.py b/src/crewai/agent.py index ec7ff03d8..317ba24fc 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -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( diff --git a/src/crewai/agents/agent_builder/base_agent.py b/src/crewai/agents/agent_builder/base_agent.py index ba2596f63..99a6eeb9b 100644 --- a/src/crewai/agents/agent_builder/base_agent.py +++ b/src/crewai/agents/agent_builder/base_agent.py @@ -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 diff --git a/src/crewai/agents/crew_agent_executor.py b/src/crewai/agents/crew_agent_executor.py index 858a4bf09..44c2f04eb 100644 --- a/src/crewai/agents/crew_agent_executor.py +++ b/src/crewai/agents/crew_agent_executor.py @@ -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( diff --git a/src/crewai/utilities/agent_utils.py b/src/crewai/utilities/agent_utils.py index 4289e85a8..71d0ec186 100644 --- a/src/crewai/utilities/agent_utils.py +++ b/src/crewai/utilities/agent_utils.py @@ -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) diff --git a/tests/test_hosted_tools.py b/tests/test_hosted_tools.py new file mode 100644 index 000000000..07789416d --- /dev/null +++ b/tests/test_hosted_tools.py @@ -0,0 +1,239 @@ +import pytest +from unittest.mock import Mock, patch +from crewai import Agent, Task, Crew +from crewai.tools import BaseTool +from crewai.llm import LLM + + +class MockTool(BaseTool): + name: str = "mock_tool" + description: str = "A mock tool for testing" + + def _run(self, query: str) -> str: + return f"Mock result for: {query}" + + +def test_agent_with_crewai_tools_only(): + """Test backward compatibility with CrewAI tools only.""" + mock_tool = MockTool() + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[mock_tool] + ) + + assert len(agent.tools) == 1 + assert isinstance(agent.tools[0], BaseTool) + + +def test_agent_with_raw_tools_only(): + """Test agent with raw tool definitions only.""" + raw_tool = { + "name": "hosted_search", + "description": "Search the web using hosted search", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"} + }, + "required": ["query"] + } + } + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[raw_tool] + ) + + assert len(agent.tools) == 1 + assert isinstance(agent.tools[0], dict) + assert agent.tools[0]["name"] == "hosted_search" + + +def test_agent_with_mixed_tools(): + """Test agent with both CrewAI tools and raw tool definitions.""" + mock_tool = MockTool() + raw_tool = { + "name": "hosted_calculator", + "description": "Hosted calculator tool", + "parameters": { + "type": "object", + "properties": { + "expression": {"type": "string"} + } + } + } + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[mock_tool, raw_tool] + ) + + assert len(agent.tools) == 2 + assert isinstance(agent.tools[0], BaseTool) + assert isinstance(agent.tools[1], dict) + + +def test_invalid_raw_tool_definition(): + """Test error handling for invalid raw tool definitions.""" + invalid_tool = {"description": "Missing name field"} + + with pytest.raises(ValueError, match="Raw tool definition must have a 'name' field"): + Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[invalid_tool] + ) + + +def test_parse_tools_with_mixed_types(): + """Test parse_tools function with mixed tool types.""" + from crewai.utilities.agent_utils import parse_tools + + mock_tool = MockTool() + raw_tool = { + "name": "hosted_tool", + "description": "A hosted tool", + "parameters": {"type": "object"} + } + + parsed = parse_tools([mock_tool, raw_tool]) + + assert len(parsed) == 2 + assert hasattr(parsed[0], 'name') + assert parsed[0].name == "mock_tool" + assert isinstance(parsed[1], dict) + assert parsed[1]["name"] == "hosted_tool" + + +def test_get_tool_names_with_mixed_types(): + """Test get_tool_names function with mixed tool types.""" + from crewai.utilities.agent_utils import get_tool_names + + mock_tool = MockTool() + raw_tool = {"name": "hosted_tool", "description": "A hosted tool"} + + names = get_tool_names([mock_tool, raw_tool]) + assert "mock_tool" in names + assert "hosted_tool" in names + + +def test_render_text_description_with_mixed_types(): + """Test render_text_description_and_args function with mixed tool types.""" + from crewai.utilities.agent_utils import render_text_description_and_args + + mock_tool = MockTool() + raw_tool = {"name": "hosted_tool", "description": "A hosted tool"} + + description = render_text_description_and_args([mock_tool, raw_tool]) + assert "A mock tool for testing" in description + assert "A hosted tool" in description + + +def test_agent_executor_with_mixed_tools(): + """Test CrewAgentExecutor initialization with mixed tool types.""" + mock_tool = MockTool() + raw_tool = {"name": "hosted_tool", "description": "A hosted tool"} + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[mock_tool, raw_tool] + ) + + agent.create_agent_executor() + + assert len(agent.agent_executor.tool_name_to_tool_map) == 2 + assert "mock_tool" in agent.agent_executor.tool_name_to_tool_map + assert "hosted_tool" in agent.agent_executor.tool_name_to_tool_map + + +def test_empty_tools_list(): + """Test agent with empty tools list.""" + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[] + ) + + assert len(agent.tools) == 0 + + +def test_none_tools(): + """Test agent with None tools.""" + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=None + ) + + assert agent.tools == [] + + +def test_raw_tool_without_description(): + """Test raw tool definition without description field.""" + raw_tool = {"name": "minimal_tool"} + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[raw_tool] + ) + + assert len(agent.tools) == 1 + assert isinstance(agent.tools[0], dict) + assert agent.tools[0]["name"] == "minimal_tool" + + +def test_complex_raw_tool_definition(): + """Test complex raw tool definition with nested parameters.""" + raw_tool = { + "name": "complex_search", + "description": "Advanced search with multiple parameters", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + }, + "filters": { + "type": "object", + "properties": { + "date_range": {"type": "string"}, + "category": {"type": "string"} + } + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 10 + } + }, + "required": ["query"] + } + } + + agent = Agent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + tools=[raw_tool] + ) + + assert len(agent.tools) == 1 + assert isinstance(agent.tools[0], dict) + assert agent.tools[0]["name"] == "complex_search" + assert "parameters" in agent.tools[0] + assert agent.tools[0]["parameters"]["type"] == "object"