deprecate crewai-langchain (#778)

This commit is contained in:
Mateo Torres 2026-02-21 00:11:46 +00:00 committed by GitHub
parent 4d783e0d31
commit 6f8db11129
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 64 additions and 953 deletions

View file

@ -1,62 +0,0 @@
VERSION ?= "0.1.1"
.PHONY: install
install: ## Install the poetry environment and install the pre-commit hooks
@if ! command -v poetry >/dev/null 2>&1; then \
echo "🚫 Poetry is not installed. Please install poetry before proceeding."; \
exit 1; \
fi
@echo "🚀 Creating virtual environment using pyenv and poetry"
@poetry install --all-extras
@poetry run pre-commit install
.PHONY: check
check: ## Run code quality tools.
@echo "🚀 Checking Poetry lock file consistency with 'pyproject.toml': Running poetry check --lock"
@poetry check --lock
@echo "🚀 Linting code: Running pre-commit"
@poetry run pre-commit run -a
@echo "🚀 Static type checking: Running mypy"
@poetry run mypy $(git ls-files '*.py')
.PHONY: test
test: ## Test the code with pytest
@echo "🚀 Testing code: Running pytest"
@poetry run pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml
.PHONY: set-version
set-version: ## Set the version in the pyproject.toml file
@echo "🚀 Setting version in pyproject.toml"
@poetry version $(VERSION)
.PHONY: unset-version
unset-version: ## Set the version in the pyproject.toml file
@echo "🚀 Setting version in pyproject.toml"
@poetry version 0.1.0
.PHONY: build
build: clean-build ## Build wheel file using poetry
@echo "🚀 Creating wheel file"
@poetry build
.PHONY: clean-build
clean-build: ## clean build artifacts
@rm -rf dist
.PHONY: publish
publish: ## publish a release to pypi.
@echo "🚀 Publishing: Dry run."
@poetry config pypi-token.pypi $(PYPI_TOKEN)
@poetry publish --dry-run
@echo "🚀 Publishing."
@poetry publish
.PHONY: build-and-publish
build-and-publish: build publish ## Build and publish.
.PHONY: help
help:
@echo "🛠️ Arcade AI Dev Commands:\n"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.DEFAULT_GOAL := help

View file

@ -5,33 +5,30 @@
>
</h3>
<div align="center">
<h3>CrewAI Integration</h3>
<a href="https://github.com/arcadeai/arcade-mcp/blob/main/LICENSE">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
</a>
<a href="https://pepy.tech/project/crewai-arcade">
<img src="https://static.pepy.tech/badge/crewai-arcade" alt="Downloads">
</a>
<h1>⚠️ DEPRECATED ⚠️</h1>
<h3>crewai-arcade is no longer maintained</h3>
</div>
---
## Important Notice
**This package has been deprecated and is no longer maintained.**
The `crewai-arcade` package is no longer needed. Arcade now provides better ways to integrate with your AI applications.
## What Should I Use Instead?
Please visit **[docs.arcade.dev](https://docs.arcade.dev)** for the latest documentation on how to integrate Arcade tools into your applications.
## Migration Guide
If you were previously using `crewai-arcade`, we recommend:
1. Visit [docs.arcade.dev](https://docs.arcade.dev) to learn about the new integration options
---
<p align="center">
<a href="https://docs.arcade.dev" target="_blank">Docs</a>
<a href="https://docs.arcade.dev/en/resources/integrations" target="_blank">Servers</a>
<a href="https://github.com/ArcadeAI/arcade-py" target="_blank">Python Client</a>
<a href="https://github.com/ArcadeAI/arcade-js" target="_blank">JavaScript Client</a>
Thank you for using crewai-arcade. We hope to see you using Arcade's new integrations!
</p>
## Overview
`crewai-arcade` allows you to use Arcade tools in your CrewAI applications.
## Installation
```bash
pip install crewai-arcade
```
## Usage
See the [examples](https://github.com/ArcadeAI/arcade-mcp/tree/main/examples/crewai) for usage examples

View file

@ -1,3 +1,17 @@
from .manager import ArcadeToolManager
import warnings
__all__ = ["ArcadeToolManager"]
warnings.warn(
"\n" + "=" * 70 + "\n"
"DEPRECATION NOTICE: crewai-arcade is no longer maintained.\n"
"\n"
"This package has been deprecated. Please visit https://docs.arcade.dev\n"
"for the latest documentation on integrating Arcade tools into your\n"
"applications.\n"
"\n"
"Arcade now supports MCP (Model Context Protocol) and direct API\n"
"integration via the Arcade Python SDK.\n" + "=" * 70,
DeprecationWarning,
stacklevel=2,
)
__all__: list[str] = []

View file

@ -1,56 +0,0 @@
from typing import Any
from arcadepy.types import ToolDefinition
from pydantic import BaseModel, Field, create_model
# Mapping of Arcade value types to Python types
TYPE_MAPPING = {
"string": str,
"number": float,
"integer": int,
"boolean": bool,
"array": list,
"json": dict,
}
def get_python_type(val_type: str) -> Any:
"""Map Arcade value types to Python types.
Args:
val_type: The value type as a string.
Returns:
Corresponding Python type.
"""
_type = TYPE_MAPPING.get(val_type)
if _type is None:
raise ValueError(f"Invalid value type: {val_type}")
return _type
def tool_definition_to_pydantic_model(tool_def: ToolDefinition) -> type[BaseModel]:
"""Convert a ToolDefinition's inputs into a Pydantic BaseModel.
Args:
tool_def: The ToolDefinition object to convert.
Returns:
A Pydantic BaseModel class representing the tool's input schema.
"""
try:
fields: dict[str, Any] = {}
for param in tool_def.input.parameters or []:
param_type = get_python_type(param.value_schema.val_type)
if param_type == list and param.value_schema.inner_val_type: # noqa: E721
inner_type: type[Any] = get_python_type(param.value_schema.inner_val_type)
param_type = list[inner_type] # type: ignore[valid-type]
param_description = param.description or "No description provided."
default = ... if param.required else None
fields[param.name] = (
param_type,
Field(default=default, description=param_description),
)
return create_model(f"{tool_def.name}Args", **fields)
except ValueError as e:
raise ValueError(f"Error converting {tool_def.name} parameters into pydantic model: {e}")

View file

@ -1,363 +0,0 @@
from collections.abc import Iterator
from typing import Any, Callable, Optional, Protocol
from arcadepy import Arcade
from arcadepy.types import ToolDefinition
from arcadepy.types.shared import AuthorizationResponse
from crewai_arcade._utilities import tool_definition_to_pydantic_model
from crewai_arcade.structured import StructuredTool
TOOL_NAME_SEPARATOR = "_"
class ArcadeToolExecutorProtocol(Protocol):
"""Protocol for Arcade tool executor callback."""
def __call__(
self,
manager: "ArcadeToolManager",
name: str,
**input: dict[str, Any], # noqa: A002
) -> Any: ...
class ArcadeToolManager:
"""Arcade tool manager for CrewAI
Wraps Arcade tools as CrewAI StructuredTools
"""
def __init__(
self,
client: Optional[Arcade] = None,
executor: Optional[ArcadeToolExecutorProtocol] = None,
*,
default_user_id: Optional[str] = None,
**kwargs: dict[str, Any],
) -> None:
"""Initialize the ArcadeToolManager.
Example:
>>> manager = ArcadeToolManager(default_user_id="me@example.com", api_key="...")
>>>
>>> # retrieve a specific Arcade tool as a CrewAI tool and add it to the manager
>>> manager.get_tools(tools=["Search.SearchGoogle"])
>>>
>>> # retrieve all Arcade tools in a toolkit as CrewAI tools and add them to the manager
>>> manager.get_tools(toolkits=["Search"])
>>>
>>> # retrieve all tools in the manager as CrewAI tools
>>> manager.get_tools()
>>>
>>> # clear and initialize new tools in the manager
>>> manager.init_tools(tools=["Search.SearchGoogle"], toolkits=["Search"])
Args:
client: Arcade client instance.
executor: Optional custom executor callback. Useful for customizing the authorization and execution flow.
default_user_id: The default user id used for tool authorization and execution
when no custom executor is provided.
**kwargs: Additional keyword arguments for the Arcade client if the client is not provided.
Note:
If no executor is provided, `default_user_id` must be specified so that the default
executor can call authorize and execute with that id.
"""
if not client:
api_key = kwargs.get("api_key")
base_url = kwargs.get("base_url")
arcade_kwargs = {"api_key": api_key, "base_url": base_url, **kwargs}
client = Arcade(**arcade_kwargs) # type: ignore[arg-type]
self._client = client
self._tools: dict[str, ToolDefinition] = {}
self.default_user_id = default_user_id
# Use the default executor if none is provided.
if executor is None and default_user_id is None:
raise ValueError("A default_user_id must be provided if no executor is specified.")
self.executor = executor or self._default_executor
if not callable(self.executor):
raise TypeError(
"executor must be callable and adhere to the ArcadeToolExecutorProtocol signature"
)
@property
def tools(self) -> list[str]:
return list(self._tools.keys())
def __iter__(self) -> Iterator[tuple[str, ToolDefinition]]:
yield from self._tools.items()
def __len__(self) -> int:
return len(self._tools)
def __getitem__(self, tool_name: str) -> ToolDefinition:
return self._tools[tool_name]
def init_tools(
self,
tools: Optional[list[str]] = None,
toolkits: Optional[list[str]] = None,
) -> None:
"""Initialize the tools in the manager.
This method clears any existing tools in the manager and replaces them
with tools and toolkits that are provided. If no tools or toolkits are
provided, then all tools in the Arcade client will be added.
Example:
>>> manager = ArcadeToolManager(default_user_id="me@example.com", api_key="...")
>>> manager.init_tools(tools=["Search.SearchGoogle"])
>>> manager.get_tools()
Args:
tools: Optional list of tool names to include.
toolkits: Optional list of toolkits to include.
"""
self._tools = self._retrieve_tool_definitions(tools, toolkits)
def add_tools(
self, tools: Optional[list[str]] = None, toolkits: Optional[list[str]] = None
) -> None:
"""Add tools to the manager.
This method adds tools to the manager's internal tool list. If no tools or
toolkits are provided, all tools in the Arcade client will be added.
Example:
>>> manager = ArcadeToolManager(default_user_id="me@example.com", api_key="...")
>>> manager.init_tools(tools=["Search.SearchGoogle"])
>>> manager.add_tools(tools=["Google.ListEmails"], toolkits=["Slack"])
>>> manager.get_tools()
Args:
tools: List of tool names to add.
toolkits: List of toolkits to add tools from.
"""
new_tool_definitions = self._retrieve_tool_definitions(tools, toolkits)
self._tools.update(new_tool_definitions)
def get_tools(
self, tools: Optional[list[str]] = None, toolkits: Optional[list[str]] = None
) -> list[StructuredTool]:
"""Retrieves the requested tools or toolkits from the manager.
This method retrieves the provided tools or toolkits as CrewAI StructuredTools.
If any provided tools or toolkits are not already present in the
internal tool list, then they are added to the manager.
If no tools or toolkits are provided, then all tools in the manager's
internal tool list are returned as CrewAI StructuredTools.
Example:
>>> manager = ArcadeToolManager(default_user_id="me@example.com", api_key="...")
>>>
>>> # Retrieve a specific tool as a CrewAI tool
>>> manager.get_tools(tools=["Search.SearchGoogle"])
>>>
>>> # Retrieve all tools in a toolkit as CrewAI tools
>>> manager.get_tools(toolkits=["Search"])
>>>
>>> # Retrieve all tools in the manager as CrewAI tools
>>> manager.get_tools()
Args:
tools: An optional list of tool names to retrieve and wrap. If any of these
tools are missing from the internal list, they will be added.
toolkits: An optional list of toolkits from which to retrieve and wrap tools.
Tools from these toolkits will be added if they are not already present.
Returns:
A list of StructuredTool instances adapted from the specified tools.
"""
if tools or toolkits:
if len(self) == 0:
self.init_tools(tools, toolkits)
else:
new_tools = self._retrieve_tool_definitions(tools, toolkits)
self._tools.update(new_tools)
# Wrap the requested tools as CrewAI StructuredTools
crewai_tools: list[StructuredTool] = []
for tool_name, tool_def in self:
crewai_tools.append(self._wrap_arcade_tool(tool_name, tool_def))
return crewai_tools
def authorize_tool(self, user_id: str, name: str) -> None:
"""Handle the authorization flow.
Args:
user_id: The user ID to authorize the tool for.
name: The name of the tool to authorize.
"""
if self.requires_auth(name):
# Get authorization status
auth_response = self.authorize(name, user_id)
if not self.is_authorized(auth_response.id): # type: ignore[arg-type]
# Handle authorization
print(f"Please use the following link to authorize: {auth_response.url}")
auth_response = self.wait_for_auth(auth_response)
# Ensure authorization completed successfully
if not self.is_authorized(auth_response.id): # type: ignore[arg-type]
raise ValueError(f"Authorization failed for {name}. URL: {auth_response.url}")
def execute_tool(self, user_id: str, name: str, **input: Any) -> Any: # noqa: A002
"""Handle the tool execution flow.
Args:
user_id: The user ID to execute the tool for.
name: The name of the tool to execute.
**input: Dictionary of input arguments for the tool.
Returns:
The output of the tool.
"""
response = self._client.tools.execute(
tool_name=name,
input=input,
user_id=user_id,
)
tool_error = response.output.error if response.output else None
if tool_error:
return str(tool_error)
if response.success:
return response.output.value # type: ignore[union-attr]
return "Failed to call " + name
def requires_auth(self, tool_name: str) -> bool:
"""Check if a tool requires authorization."""
cleaned_tool_name = tool_name.replace(".", TOOL_NAME_SEPARATOR)
tool_def = self._tools.get(cleaned_tool_name)
if tool_def is None:
raise ValueError(f"Tool '{tool_name}' not found in this ArcadeToolManager instance")
if tool_def.requirements is None:
return False
return tool_def.requirements.authorization is not None
def authorize(self, tool_name: str, user_id: str) -> AuthorizationResponse:
"""Authorize a user for a tool.
Args:
tool_name: The name of the tool to authorize.
user_id: The user ID to authorize.
Returns:
AuthorizationResponse
"""
return self._client.tools.authorize(tool_name=tool_name, user_id=user_id)
def is_authorized(self, authorization_id: str) -> bool:
"""Check if a tool authorization is complete."""
return self._client.auth.status(id=authorization_id).status == "completed"
def wait_for_auth(self, auth_response: AuthorizationResponse) -> AuthorizationResponse:
"""Wait for an authorization process to complete.
Args:
auth_response: The authorization response from the initial authorize call.
Returns:
AuthorizationResponse with completed status
"""
return self._client.auth.wait_for_completion(auth_response)
def _wrap_arcade_tool(self, name: str, tool_def: ToolDefinition) -> StructuredTool:
"""Wrap an Arcade tool as a CrewAI StructuredTool.
Args:
name: The name of the tool to wrap.
tool_def: The definition of the tool to wrap.
Returns:
A StructuredTool instance.
"""
description = tool_def.description or "No description provided."
args_schema = tool_definition_to_pydantic_model(tool_def)
tool_function = self._create_tool_function(name)
return StructuredTool.from_function(
func=tool_function,
name=name,
description=description,
args_schema=args_schema,
)
def _retrieve_tool_definitions(
self, tools: Optional[list[str]] = None, toolkits: Optional[list[str]] = None
) -> dict[str, ToolDefinition]:
"""Retrieve tool definitions from the Arcade client.
This method fetches tool definitions based on the provided tool names or toolkits.
If no specific tools or toolkits are provided, the method will fetch and return
all tools available in the Arcade client.
Args:
tools: Optional list of tool names to retrieve.
toolkits: Optional list of toolkits to retrieve tools from.
Returns:
A dictionary mapping full tool names to their corresponding ToolDefinition objects
"""
all_tools: list[ToolDefinition] = []
if tools:
single_tools = [self._client.tools.get(name=tool_id) for tool_id in tools]
all_tools.extend(single_tools)
if toolkits:
for tk in toolkits:
all_tools.extend(self._client.tools.list(toolkit=tk))
if not tools and not toolkits:
# Retrieve all Arcade tools.
page_iterator = self._client.tools.list()
all_tools.extend(page_iterator)
tool_definitions: dict[str, ToolDefinition] = {}
for tool in all_tools:
full_tool_name = f"{tool.toolkit.name}{TOOL_NAME_SEPARATOR}{tool.name}"
tool_definitions[full_tool_name] = tool
return tool_definitions
def _create_tool_function(self, tool_name: str) -> Callable[..., Any]:
"""Creates a function wrapper for an Arcade tool.
Args:
tool_name: The name of the tool to create a function for.
Returns:
A callable function that executes the tool.
"""
def tool_function(**kwargs: Any) -> Any:
return self.executor(self, tool_name, **kwargs)
return tool_function
@staticmethod
def _default_executor(
manager: "ArcadeToolManager", name: str, **tool_input: dict[str, Any]
) -> Any:
"""
Default executor that performs authorization followed
by tool execution using the manager's default_user_id.
"""
if manager.default_user_id is None:
raise ValueError("default_user_id is not set in ArcadeToolManager.")
user_id = manager.default_user_id
manager.authorize_tool(user_id, name)
return manager.execute_tool(user_id, name, **tool_input)

View file

@ -1,54 +0,0 @@
from textwrap import dedent
from typing import Any, Callable, Optional
from crewai.tools.base_tool import BaseTool
from pydantic import BaseModel as PydanticBaseModel
class StructuredTool(BaseTool): # type: ignore[no-any-unimported]
"""A tool that executes functions with structured inputs using a schema."""
func: Callable[..., Any]
"""The callable function that implements the tool's functionality."""
def _run(self, *args: Any, **kwargs: Any) -> Any:
"""Execute the tool's function with the provided arguments."""
return self.func(*args, **kwargs)
@classmethod
def from_function(
cls,
func: Callable,
args_schema: type[PydanticBaseModel],
name: Optional[str] = None,
description: Optional[str] = None,
**kwargs: Any,
) -> "StructuredTool":
"""Create a new StructuredTool instance from a function.
Args:
func: Function to wrap as a CrewAI tool
name: Custom name for the tool
description: Custom description of the tool's functionality
args_schema: Pydantic model defining the expected argument structure
**kwargs: Additional tool configuration parameters, like cache_function
Returns:
StructuredTool: A new tool instance wrapping the provided function
"""
name = name or func.__name__
if description is None:
description = func.__doc__
if description is None:
raise ValueError("Function must have a docstring if description not provided.")
# Clean up docstring
description = dedent(description).strip()
return cls(
func=func,
args_schema=args_schema,
name=name,
description=description,
**kwargs,
)

View file

@ -1,45 +1,30 @@
[tool.poetry]
[build-system]
requires = [ "hatchling",]
build-backend = "hatchling.build"
[project]
name = "crewai-arcade"
version = "0.1.1"
description = "An integration package connecting Arcade and CrewAI"
authors = ["Arcade <dev@arcade.dev>"]
version = "2.0.0"
description = "This package is no longer maintained. Please visit https://docs.arcade.dev for the latest Arcade integrations."
readme = "README.md"
repository = "https://github.com/arcadeai/arcade-mcp/tree/main/contrib/crewai"
license = "MIT"
requires-python = ">=3.10"
keywords = ["deprecated", "arcade", "crewai"]
classifiers = [
"Development Status :: 7 - Inactive",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = []
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
crewai = ">=0.1.0,<1.0.0"
pydantic = "^2.0.0"
arcadepy = "^1.0.0"
[project.urls]
Homepage = "https://docs.arcade.dev"
Documentation = "https://docs.arcade.dev"
Repository = "https://github.com/arcadeai/arcade-mcp"
[tool.poetry.group.dev.dependencies]
pytest = "^8.1.2"
pytest-cov = "^4.0.0"
mypy = "^1.5.1"
pre-commit = "^3.4.0"
tox = "^4.11.1"
[tool.mypy]
files = ["crewai_arcade"]
python_version = "3.10"
disallow_untyped_defs = "True"
disallow_any_unimported = "True"
no_implicit_optional = "True"
check_untyped_defs = "True"
warn_return_any = "True"
warn_unused_ignores = "True"
show_error_codes = "True"
ignore_missing_imports = "True"
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.coverage.run]
branch = true
source = ["crewai_arcade"]
[tool.coverage.report]
skip_empty = true
[tool.hatch.build.targets.wheel]
packages = ["crewai_arcade"]

View file

@ -1,258 +0,0 @@
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from arcadepy.types import ToolDefinition
from crewai_arcade.manager import TOOL_NAME_SEPARATOR, ArcadeToolManager
# --- Custom executor ---
def custom_executor(manager: ArcadeToolManager, name: str, **tool_input: dict[str, Any]) -> Any:
"""Custom executor for testing purposes."""
return "Tool executed"
# --- Fixtures ---
@pytest.fixture
def mock_client():
"""Create a fake Arcade client fixture."""
return MagicMock()
@pytest.fixture
def manager_with_default_executor(mock_client):
"""Return an ArcadeToolManager with a test default_user_id and fake client."""
return ArcadeToolManager(default_user_id="test_user", client=mock_client)
@pytest.fixture
def manager_with_custom_executor(mock_client):
"""Return an ArcadeToolManager with a test default_user_id and fake client using a custom executor."""
return ArcadeToolManager(
default_user_id="test_user", client=mock_client, executor=custom_executor
)
@pytest.fixture
def fake_tool_definition():
"""Return a fake tool definition for testing purposes."""
fake_tool = MagicMock(spec=ToolDefinition)
fake_tool.name = "SearchGoogle"
fake_tool.description = "Test tool description"
fake_tool.toolkit = MagicMock()
fake_tool.toolkit.name = "Search"
fake_tool.requirements = None
fake_tool.input = MagicMock()
fake_tool.input.parameters = []
return fake_tool
# --- Tests for _create_tool_function ---
def test_create_tool_function_success_custom(manager_with_custom_executor):
"""
Test that the tool function executes successfully using the custom executor.
The custom executor simply returns "Tool executed".
"""
tool_function = manager_with_custom_executor._create_tool_function("test_tool")
result = tool_function()
assert result == "Tool executed"
def test_create_tool_function_forwards_kwargs_custom(manager_with_custom_executor):
"""
Test that extra keyword arguments are forwarded correctly to the custom executor.
The custom executor ignores the kwargs and returns "Tool executed".
"""
tool_function = manager_with_custom_executor._create_tool_function("test_tool")
result = tool_function(param1="value1", param2=2)
assert result == "Tool executed"
def test_create_tool_function_unauthorized(manager_with_default_executor):
"""
Test that the tool function raises a ValueError when authorization fails using the default executor.
"""
# Mock an authorization failure by having authorize_tool raise ValueError.
manager_with_default_executor.authorize_tool = MagicMock(
side_effect=ValueError("Authorization failed for test_tool")
)
manager_with_default_executor.execute_tool = MagicMock()
tool_function = manager_with_default_executor._create_tool_function("test_tool")
with pytest.raises(ValueError, match="Authorization failed for test_tool"):
tool_function()
manager_with_default_executor.authorize_tool.assert_called_once_with("test_user", "test_tool")
manager_with_default_executor.execute_tool.assert_not_called() # auth fails before this is called
def test_create_tool_function_execution_failure(manager_with_default_executor):
"""
Test that when tool execution returns a failing value, that value is returned.
"""
manager_with_default_executor.authorize_tool = MagicMock() # auth passes
manager_with_default_executor.execute_tool = MagicMock(return_value="error")
tool_function = manager_with_default_executor._create_tool_function("test_tool")
result = tool_function()
assert result == "error"
manager_with_default_executor.authorize_tool.assert_called_once_with("test_user", "test_tool")
manager_with_default_executor.execute_tool.assert_called_once_with("test_user", "test_tool")
# --- Test for _wrap_arcade_tool ---
def test_wrap_arcade_tool(manager_with_default_executor, fake_tool_definition):
"""
Test that _wrap_arcade_tool correctly creates a StructuredTool.
"""
fake_tool_definition.description = "Test tool"
tool_name = "test_tool"
# Patch the conversion utilities. Also, override _create_tool_function to return a dummy function.
with (
patch(
"crewai_arcade.manager.tool_definition_to_pydantic_model", return_value="args_schema"
) as mock_to_model,
patch(
"crewai_arcade.structured.StructuredTool.from_function", return_value="structured_tool"
) as mock_from_function,
patch.object(
manager_with_default_executor,
"_create_tool_function",
return_value=lambda *a, **kw: None,
) as mock_create_tool,
):
result = manager_with_default_executor._wrap_arcade_tool(tool_name, fake_tool_definition)
assert result == "structured_tool"
mock_to_model.assert_called_once_with(fake_tool_definition)
mock_from_function.assert_called_once_with(
func=mock_create_tool.return_value,
name=tool_name,
description="Test tool",
args_schema="args_schema",
)
# --- Tests for tool registration (init_tools, add_tools, get_tools) ---
def test_init_tools_with_tool(manager_with_default_executor, fake_tool_definition):
"""
Test that init_tools clears and initializes the tools in the manager using tool names.
"""
manager_with_default_executor._client.tools.get.return_value = fake_tool_definition
manager_with_default_executor.init_tools(tools=["Search.SearchGoogle"])
expected_key = (
f"{fake_tool_definition.toolkit.name}{TOOL_NAME_SEPARATOR}{fake_tool_definition.name}"
)
assert expected_key in manager_with_default_executor._tools
assert len(manager_with_default_executor._tools) == 1
def test_init_tools_with_toolkit(manager_with_default_executor, fake_tool_definition):
"""
Test that init_tools correctly fetches tools using a toolkit.
"""
# Simulate that listing a toolkit returns a list with the fake tool
manager_with_default_executor._client.tools.list.return_value = [fake_tool_definition]
manager_with_default_executor.init_tools(toolkits=["Search"])
expected_key = (
f"{fake_tool_definition.toolkit.name}{TOOL_NAME_SEPARATOR}{fake_tool_definition.name}"
)
assert expected_key in manager_with_default_executor._tools
assert len(manager_with_default_executor._tools) == 1
def test_init_tools_with_none(manager_with_default_executor, fake_tool_definition):
"""
Test that init_tools with no arguments retrieves all tools.
"""
manager_with_default_executor._client.tools.list.return_value = [fake_tool_definition]
manager_with_default_executor.init_tools()
expected_key = (
f"{fake_tool_definition.toolkit.name}{TOOL_NAME_SEPARATOR}{fake_tool_definition.name}"
)
assert expected_key in manager_with_default_executor._tools
assert len(manager_with_default_executor._tools) == 1
def test_add_tools(manager_with_default_executor, fake_tool_definition):
"""
Test that add_tools supplements the manager's existing tool dictionary.
"""
# Set an initial tool in _tools.
fake_initial_tool = MagicMock(spec=ToolDefinition)
fake_initial_tool.name = "InitialTool"
fake_initial_tool.toolkit = MagicMock()
fake_initial_tool.toolkit.name = "InitialToolkit"
initial_key = f"{fake_initial_tool.toolkit.name}{TOOL_NAME_SEPARATOR}{fake_initial_tool.name}"
manager_with_default_executor._tools[initial_key] = fake_initial_tool
# Mock retrieval of a new tool.
manager_with_default_executor._client.tools.get.return_value = fake_tool_definition
manager_with_default_executor.add_tools(tools=["Search.SearchGoogle"])
new_key = f"{fake_tool_definition.toolkit.name}{TOOL_NAME_SEPARATOR}{fake_tool_definition.name}"
assert initial_key in manager_with_default_executor._tools
assert new_key in manager_with_default_executor._tools
def test_get_tools_with_existing_tools(manager_with_default_executor, fake_tool_definition):
"""
Test that get_tools wraps existing tools if they are already registered.
"""
manager_with_default_executor._client.tools.get.return_value = fake_tool_definition
manager_with_default_executor.init_tools(tools=["Search.SearchGoogle"])
expected_key = (
f"{fake_tool_definition.toolkit.name}{TOOL_NAME_SEPARATOR}{fake_tool_definition.name}"
)
# Patch _wrap_arcade_tool to verify that it is called.
with patch.object(
manager_with_default_executor, "_wrap_arcade_tool", side_effect=lambda name, td: (name, td)
) as mock_wrap:
crewai_tools = manager_with_default_executor.get_tools()
assert len(crewai_tools) == 1
assert crewai_tools[0] == (expected_key, fake_tool_definition)
mock_wrap.assert_called_once_with(expected_key, fake_tool_definition)
def test_get_tools_with_missing_tool_and_toolkit(
manager_with_default_executor, fake_tool_definition
):
"""
Test that get_tools adds missing tools and toolkits when not already registered.
"""
manager_with_default_executor._tools = {}
manager_with_default_executor._client.tools.get.return_value = fake_tool_definition
manager_with_default_executor._client.tools.list.return_value = [fake_tool_definition]
with patch.object(
manager_with_default_executor, "_wrap_arcade_tool", side_effect=lambda name, td: (name, td)
) as mock_wrap:
crewai_tools = manager_with_default_executor.get_tools(
tools=["Search.SearchGoogle"], toolkits=["Search"]
)
expected_key = (
f"{fake_tool_definition.toolkit.name}{TOOL_NAME_SEPARATOR}{fake_tool_definition.name}"
)
assert expected_key in manager_with_default_executor._tools
assert len(crewai_tools) == 1
assert crewai_tools[0] == (expected_key, fake_tool_definition)
mock_wrap.assert_called_once_with(expected_key, fake_tool_definition)

View file

@ -1,92 +0,0 @@
import pytest
from crewai import Agent, Crew, Task
from crewai_arcade.structured import StructuredTool
from pydantic import BaseModel
class CalculatorInput(BaseModel):
x: float
y: float
def add_numbers(x: float, y: float) -> float:
"""Add two numbers together."""
return x + y
def unnamed_function(x: float, y: float) -> float:
return x + y
def test_structured_tool_basic():
# Test basic functionality with explicit name and description
calculator_tool = StructuredTool.from_function(
func=add_numbers,
args_schema=CalculatorInput,
name="Calculator",
description="A tool that adds two numbers together",
)
expected_description = (
"Tool Name: Calculator\n"
"Tool Arguments: {'x': {'description': None, 'type': 'float'}, 'y': {'description': None, 'type': 'float'}}\n"
"Tool Description: A tool that adds two numbers together"
)
assert calculator_tool.description == expected_description
assert calculator_tool.func(2, 3) == 5
def test_structured_tool_auto_name_description():
# Test automatic name and description generation from function
calculator_tool = StructuredTool.from_function(func=add_numbers, args_schema=CalculatorInput)
expected_description = (
"Tool Name: add_numbers\n"
"Tool Arguments: {'x': {'description': None, 'type': 'float'}, 'y': {'description': None, 'type': 'float'}}\n"
"Tool Description: Add two numbers together."
)
assert calculator_tool.description == expected_description
def test_structured_tool_validation_errors():
# Test missing docstring
with pytest.raises(
ValueError, match="Function must have a docstring if description not provided."
):
StructuredTool.from_function(func=unnamed_function, args_schema=CalculatorInput)
def test_structured_tool_in_crew():
calculator_tool = StructuredTool.from_function(
func=add_numbers,
args_schema=CalculatorInput,
)
calculator_agent = Agent(
role="Math Expert",
goal="Perform mathematical calculations accurately",
backstory="An expert mathematician who specializes in calculations",
tools=[calculator_tool],
verbose=True,
cache=False,
)
addition_task = Task(
description="Add the numbers 5 and 3 together",
expected_output="The sum of 5 and 3 is 8",
agent=calculator_agent,
)
crew = Crew(
agents=[calculator_agent],
tasks=[addition_task],
)
# Assert crew structure
assert len(crew.agents) == 1
assert len(crew.tasks) == 1
assert crew.agents[0] == calculator_agent
assert crew.tasks[0] == addition_task
assert crew.tasks[0].agent == calculator_agent
assert len(crew.agents[0].tools) == 1
assert isinstance(crew.agents[0].tools[0], StructuredTool)