deprecate crewai-langchain (#778)
This commit is contained in:
parent
4d783e0d31
commit
6f8db11129
10 changed files with 64 additions and 953 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in a new issue