Compare commits

...

6 Commits

Author SHA1 Message Date
Devin AI
ce13217f0c fix: Add final type casting for remaining type-checker issues
- Add cast import and type casting in tool_utils.py for ToolUsage constructor
- Add cast import and type casting in lite_agent.py for execute_tool_and_check_finality call

These changes complete the type-checker fixes for hosted tools implementation.

Co-Authored-By: João <joao@crewai.com>
2025-08-18 14:19:38 +00:00
Devin AI
e5e6b11909 fix: Resolve final type-checker errors for hosted tools implementation
- Add cast imports and proper type casting in agent.py
- Fix Liskov substitution principle violations in langgraph_adapter.py
- Add type ignore for lite_agent.py tool parameter mismatch
- Filter tools properly in tool_utils.py for ToolUsage constructor

All type-checker issues verified locally before pushing.

Co-Authored-By: João <joao@crewai.com>
2025-08-18 14:17:24 +00:00
Devin AI
5495cccf2f fix: Resolve remaining type-checker and lint issues
- Fix ToolUsage constructor to only accept executable tools
- Remove unused imports from langgraph adapter
- Filter tools correctly for mixed type handling

Co-Authored-By: João <joao@crewai.com>
2025-08-18 14:07:07 +00:00
Devin AI
d8eaadca47 fix: Complete hosted tools implementation with remaining changes
- Finalize base_agent.py and agent_utils.py updates
- Include comprehensive test suite for hosted tools
- Update lock file with dependencies

Co-Authored-By: João <joao@crewai.com>
2025-08-18 14:03:52 +00:00
Devin AI
be09b519c4 fix: Resolve type-checker errors for mixed tool types
- Add Union types to handle both BaseTool and dict in tool parameters
- Update tool utility functions to handle mixed types safely
- Filter raw tool definitions in agent adapters
- Fix experimental evaluation metrics to handle mixed tool types
- Maintain backward compatibility while adding hosted tools support

Co-Authored-By: João <joao@crewai.com>
2025-08-18 14:03:40 +00:00
Devin AI
95d3b5dbc3 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>
2025-08-18 13:56:38 +00:00
12 changed files with 3491 additions and 3517 deletions

View File

@@ -1,7 +1,7 @@
import shutil
import subprocess
import time
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Type, Union
from typing import Any, Callable, Dict, List, Literal, Optional, Sequence, Tuple, Type, Union, cast
from pydantic import Field, InstanceOf, PrivateAttr, model_validator
@@ -399,8 +399,13 @@ class Agent(BaseAgent):
),
)
tools = tools or self.tools or []
self.create_agent_executor(tools=tools, task=task)
if tools is not None:
agent_tools: List[Union[BaseTool, dict]] = cast(List[Union[BaseTool, dict]], tools)
elif self.tools is not None:
agent_tools = cast(List[Union[BaseTool, dict]], self.tools)
else:
agent_tools = []
self.create_agent_executor(tools=agent_tools, task=task)
if self.crew and self.crew._train:
task_prompt = self._training_handler(task_prompt=task_prompt)
@@ -537,14 +542,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(
@@ -803,7 +808,7 @@ class Agent(BaseAgent):
goal=self.goal,
backstory=self.backstory,
llm=self.llm,
tools=self.tools or [],
tools=[tool for tool in (self.tools or []) if isinstance(tool, BaseTool)],
max_iterations=self.max_iter,
max_execution_time=self.max_execution_time,
respect_context_window=self.respect_context_window,
@@ -841,7 +846,7 @@ class Agent(BaseAgent):
goal=self.goal,
backstory=self.backstory,
llm=self.llm,
tools=self.tools or [],
tools=[tool for tool in (self.tools or []) if isinstance(tool, BaseTool)],
max_iterations=self.max_iter,
max_execution_time=self.max_execution_time,
respect_context_window=self.respect_context_window,

View File

@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
from pydantic import PrivateAttr
@@ -25,11 +25,11 @@ class BaseAgentAdapter(BaseAgent, ABC):
self._agent_config = agent_config
@abstractmethod
def configure_tools(self, tools: Optional[List[BaseTool]] = None) -> None:
def configure_tools(self, tools: Optional[List[Union[BaseTool, dict]]] = None) -> None:
"""Configure and adapt tools for the specific agent implementation.
Args:
tools: Optional list of BaseTool instances to be configured
tools: Optional list of BaseTool instances and raw tool definitions to be configured
"""
pass

View File

@@ -1,4 +1,4 @@
from typing import Any, AsyncIterable, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
from pydantic import Field, PrivateAttr
@@ -22,7 +22,6 @@ from crewai.utilities.events.agent_events import (
)
try:
from langchain_core.messages import ToolMessage
from langgraph.checkpoint.memory import MemorySaver
from langgraph.prebuilt import create_react_agent
@@ -128,7 +127,8 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
tools: Optional[List[BaseTool]] = None,
) -> str:
"""Execute a task using the LangGraph workflow."""
self.create_agent_executor(tools)
mixed_tools: Optional[List[Union[BaseTool, dict]]] = tools # type: ignore[assignment]
self.create_agent_executor(mixed_tools)
self.configure_structured_output(task)
@@ -198,17 +198,20 @@ class LangGraphAgentAdapter(BaseAgentAdapter):
)
raise
def create_agent_executor(self, tools: Optional[List[BaseTool]] = None) -> None:
def create_agent_executor(self, tools: Optional[List[Union[BaseTool, dict]]] = None) -> None:
"""Configure the LangGraph agent for execution."""
self.configure_tools(tools)
def configure_tools(self, tools: Optional[List[BaseTool]] = None) -> None:
def configure_tools(self, tools: Optional[List[Union[BaseTool, dict]]] = None) -> None:
"""Configure tools for the LangGraph agent."""
if tools:
all_tools = list(self.tools or []) + list(tools or [])
self._tool_adapter.configure_tools(all_tools)
available_tools = self._tool_adapter.tools()
self._graph.tools = available_tools
base_tools = [tool for tool in tools if isinstance(tool, BaseTool)]
existing_base_tools = [tool for tool in (self.tools or []) if isinstance(tool, BaseTool)]
all_tools = existing_base_tools + base_tools
if all_tools:
self._tool_adapter.configure_tools(all_tools)
available_tools = self._tool_adapter.tools()
self._graph.tools = available_tools
def get_delegation_tools(self, agents: List[BaseAgent]) -> List[BaseTool]:
"""Implement delegation tools support for LangGraph."""

View File

@@ -1,4 +1,4 @@
from typing import Any, List, Optional
from typing import Any, List, Optional, Union
from pydantic import Field, PrivateAttr
@@ -152,10 +152,12 @@ class OpenAIAgentAdapter(BaseAgentAdapter):
self.agent_executor = Runner
def configure_tools(self, tools: Optional[List[BaseTool]] = None) -> None:
def configure_tools(self, tools: Optional[List[Union[BaseTool, dict]]] = None) -> None:
"""Configure tools for the OpenAI Assistant"""
if tools:
self._tool_adapter.configure_tools(tools)
base_tools = [tool for tool in tools if isinstance(tool, BaseTool)]
if base_tools:
self._tool_adapter.configure_tools(base_tools)
if self._tool_adapter.converted_tools:
self._openai_agent.tools = self._tool_adapter.converted_tools

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,
@@ -25,7 +25,6 @@ from crewai.security.security_config import SecurityConfig
from crewai.tools.base_tool import BaseTool, Tool
from crewai.utilities import I18N, Logger, RPMController
from crewai.utilities.config import process_config
from crewai.utilities.converter import Converter
from crewai.utilities.string_utils import interpolate_only
T = TypeVar("T", bound="BaseAgent")
@@ -108,7 +107,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,29 +167,34 @@ 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 []
processed_tools = []
processed_tools: List[Union[BaseTool, dict]] = []
required_attrs = ["name", "func", "description"]
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

@@ -43,7 +43,9 @@ class ToolSelectionEvaluator(BaseEvaluator):
available_tools_info = ""
if agent.tools:
for tool in agent.tools:
available_tools_info += f"- {tool.name}: {tool.description}\n"
tool_name = tool.name if hasattr(tool, 'name') else tool.get('name', 'unknown')
tool_desc = tool.description if hasattr(tool, 'description') else tool.get('description', 'No description')
available_tools_info += f"- {tool_name}: {tool_desc}\n"
else:
available_tools_info = "No tools available"

View File

@@ -564,9 +564,10 @@ class LiteAgent(FlowTrackable, BaseModel):
if isinstance(formatted_answer, AgentAction):
try:
from typing import cast
tool_result = execute_tool_and_check_finality(
agent_action=formatted_answer,
tools=self._parsed_tools,
tools=cast(List[Union[CrewStructuredTool, dict]], self._parsed_tools),
i18n=self.i18n,
agent_key=self.key,
agent_role=self.role,

View File

@@ -11,7 +11,6 @@ from crewai.agents.parser import (
)
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM
from crewai.tools import BaseTool as CrewAITool
from crewai.tools.base_tool import BaseTool
from crewai.tools.structured_tool import CrewStructuredTool
from crewai.tools.tool_types import ToolResult
@@ -25,26 +24,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 = []
tools_list: List[Union[CrewStructuredTool, dict]] = []
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 +55,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)

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
from crewai.agents.parser import AgentAction
from crewai.security import Fingerprint
@@ -10,7 +10,7 @@ from crewai.utilities.i18n import I18N
def execute_tool_and_check_finality(
agent_action: AgentAction,
tools: List[CrewStructuredTool],
tools: List[Union[CrewStructuredTool, dict]],
i18n: I18N,
agent_key: Optional[str] = None,
agent_role: Optional[str] = None,
@@ -37,7 +37,8 @@ def execute_tool_and_check_finality(
ToolResult containing the execution result and whether it should be treated as a final answer
"""
try:
tool_name_to_tool_map = {tool.name: tool for tool in tools}
executable_tools = [tool for tool in tools if hasattr(tool, 'name') and hasattr(tool, 'result_as_answer')]
tool_name_to_tool_map = {tool.name: tool for tool in executable_tools}
if agent_key and agent_role and agent:
fingerprint_context = fingerprint_context or {}
@@ -52,10 +53,12 @@ def execute_tool_and_check_finality(
except Exception as e:
raise ValueError(f"Failed to set fingerprint: {e}")
# Create tool usage instance
# Create tool usage instance - filter to only CrewStructuredTool instances for ToolUsage
from typing import cast
crew_structured_tools = [tool for tool in tools if hasattr(tool, 'name') and hasattr(tool, 'result_as_answer') and not isinstance(tool, dict)]
tool_usage = ToolUsage(
tools_handler=tools_handler,
tools=tools,
tools=cast(List[CrewStructuredTool], crew_structured_tools),
function_calling_llm=function_calling_llm,
task=task,
agent=agent,
@@ -82,7 +85,7 @@ def execute_tool_and_check_finality(
# Handle invalid tool name
tool_result = i18n.errors("wrong_tool_name").format(
tool=tool_calling.tool_name,
tools=", ".join([tool.name.casefold() for tool in tools]),
tools=", ".join([tool.name.casefold() for tool in executable_tools]),
)
return ToolResult(tool_result, False)

237
tests/test_hosted_tools.py Normal file
View File

@@ -0,0 +1,237 @@
import pytest
from crewai import Agent
from crewai.tools import BaseTool
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"

6641
uv.lock generated

File diff suppressed because it is too large Load Diff