CrewAI Arcade (#253)
# CrewAI Integration
crewai-arcade enables you to add Arcade tools and Arcade Auth into your
CrewAI applications. Just create an `ArcadeToolManager` and add your
tools to your CrewAI Agent/Tasks.
## Initializing the ArcadeToolManager
There are two main ways to initialize your `ArcadeToolManager`
1. Default handling of tool authorization and execution:
```py
"""
When you provide a user id to the ArcadeToolManger,
it will handle the tool authorization and tool execution for you
"""
manager = ArcadeToolManager(default_user_id="me@example.com,
api_key="...")
```
2. Custom handling of tool authorization and execution
```py
"""
Provide a callback function to the `ArcadeToolManager` that handles
tool authorization and tool execution. The callback function will be
called whenever your CrewAI
application wants to call a tool.
"""
def custom_tool_executor(
manager: ArcadeToolManager, tool_name: str, **tool_input: dict[str, Any]
) -> Any:
"""Custom tool executor for the ArcadeToolManager
ArcadeToolManager's default executor handles authorization and tool
execution.
This function overrides the default executor to handle authorization and
tool execution
in a custom way.
"""
# Your custom tool auth logic goes here
# Your custom tool execution logic goes here
...
manager = ArcadeToolManager(executor=custom_tool_executor,
api_key="...")
```
## Tool Registration
1. Initialize the tools in the manager
```py
"""
Clears any existing tools in the manager and replaces them with tools
and toolkits that are provided.
"""
manager.init_tools(tools=["Google.ListEmails"], toolkits=["Slack"])
```
2. Add tools to the manager
```py
"""
Adds tools and toolkits to the manager's internal tool list.
"""
manager.add_tools(tools=["Google.ListEmails"], toolkits=["Slack"])
```
3. Retrieve tools and toolkits from the manager
```py
"""
Retrieves the provided tools and toolkits as CrewAI StructuredTools.
"""
manager.get_tools(tools=["Google.ListEmails"], toolkits=["Slack"])
```
## Auth Helpers
The `ArcadeToolManager` provides multiple helper methods for when you
need to create
a custom auth flow.
1. `authorize_tool` handles the whole authorization flow for you. This
is used internally when a custom auth flow is not needed.
2. `requires_auth(tool_name)` checks if the provided tool has
authorization requirements.
3. `authorize(tool_name, user_id)` authorizes the use of the provided
tool for the provided user ID
4. `is_authorized(tool_name, user_id)` checks if a tool is authorized
for use by the provided user ID
5. `wait_for_auth(auth_response)` waits for an authorization process to
complete before returning
## Tool Execution Helpers
1. `execute_tool` handles the whole tool execution flow for you. This is
used internally when a custom tool execution flow is not needed.
---------
Co-authored-by: lgesuellip <lgesuellipinto@uade.edu.ar>
Co-authored-by: lpetralli <123559656+lpetralli@users.noreply.github.com>
Co-authored-by: lgesuellip <102637283+lgesuellip@users.noreply.github.com>
Co-authored-by: “lgesuellip” <“lgesuellipinto@uade.edu.ar”>
This commit is contained in:
parent
8efa9a51df
commit
1e0def78df
15 changed files with 1173 additions and 0 deletions
21
contrib/crewai/LICENSE
Normal file
21
contrib/crewai/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025, Arcade AI
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
62
contrib/crewai/Makefile
Normal file
62
contrib/crewai/Makefile
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
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
|
||||||
38
contrib/crewai/README.md
Normal file
38
contrib/crewai/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<h3 align="center">
|
||||||
|
<a name="readme-top"></a>
|
||||||
|
<img
|
||||||
|
src="https://docs.arcade.dev/images/logo/arcade-logo.png"
|
||||||
|
>
|
||||||
|
</h3>
|
||||||
|
<div align="center">
|
||||||
|
<h3>CrewAI Integration</h3>
|
||||||
|
<a href="https://github.com/arcadeai/arcade-ai/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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://docs.arcade.dev" target="_blank">Docs</a> •
|
||||||
|
<a href="https://docs.arcade.dev/toolkits" target="_blank">Toolkits</a> •
|
||||||
|
<a href="https://github.com/ArcadeAI/cookbook" target="_blank">Cookbook</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>
|
||||||
|
</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-ai/tree/main/examples/crewai) for usage examples
|
||||||
3
contrib/crewai/crewai_arcade/__init__.py
Normal file
3
contrib/crewai/crewai_arcade/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .manager import ArcadeToolManager
|
||||||
|
|
||||||
|
__all__ = ["ArcadeToolManager"]
|
||||||
56
contrib/crewai/crewai_arcade/_utilities.py
Normal file
56
contrib/crewai/crewai_arcade/_utilities.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
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}")
|
||||||
363
contrib/crewai/crewai_arcade/manager.py
Normal file
363
contrib/crewai/crewai_arcade/manager.py
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
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)
|
||||||
0
contrib/crewai/crewai_arcade/py.typed
Normal file
0
contrib/crewai/crewai_arcade/py.typed
Normal file
54
contrib/crewai/crewai_arcade/structured.py
Normal file
54
contrib/crewai/crewai_arcade/structured.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
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,
|
||||||
|
)
|
||||||
45
contrib/crewai/pyproject.toml
Normal file
45
contrib/crewai/pyproject.toml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "crewai-arcade"
|
||||||
|
version = "0.1.1"
|
||||||
|
description = "An integration package connecting Arcade and CrewAI"
|
||||||
|
authors = ["Arcade <dev@arcade.dev>"]
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://github.com/arcadeai/arcade-ai/tree/main/contrib/crewai"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = ">=3.10,<3.13"
|
||||||
|
crewai = ">=0.1.0,<1.0.0"
|
||||||
|
pydantic = "^2.0.0"
|
||||||
|
arcadepy = "^1.0.0"
|
||||||
|
|
||||||
|
[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
|
||||||
0
contrib/crewai/tests/__init__.py
Normal file
0
contrib/crewai/tests/__init__.py
Normal file
258
contrib/crewai/tests/test_manager.py
Normal file
258
contrib/crewai/tests/test_manager.py
Normal file
|
|
@ -0,0 +1,258 @@
|
||||||
|
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)
|
||||||
92
contrib/crewai/tests/test_structured_tool.py
Normal file
92
contrib/crewai/tests/test_structured_tool.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
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)
|
||||||
134
examples/crewai/crewai_with_arcade_tool.py
Normal file
134
examples/crewai/crewai_with_arcade_tool.py
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
This is an example of how to use Arcade with CrewAI.
|
||||||
|
The ArcadeToolManager allows you to handle both authorization and tool execution in a custom way.
|
||||||
|
This example demonstrates how to implement a custom auth handler and a custom tool execute handler.
|
||||||
|
|
||||||
|
The example assumes the following:
|
||||||
|
1. You have an Arcade API key and have set the ARCADE_API_KEY environment variable.
|
||||||
|
2. You have an OpenAI API key and have set the OPENAI_API_KEY environment variable.
|
||||||
|
3. You have installed the necessary dependencies in the requirements.txt file: `pip install -r requirements.txt`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from crewai import Agent, Crew, Task
|
||||||
|
from crewai.crews import CrewOutput
|
||||||
|
from crewai.llm import LLM
|
||||||
|
from crewai_arcade import ArcadeToolManager
|
||||||
|
|
||||||
|
USER_ID = "user@example.com"
|
||||||
|
|
||||||
|
|
||||||
|
def custom_auth_flow(
|
||||||
|
manager: ArcadeToolManager, tool_name: str, **tool_input: dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""Custom auth flow for the ArcadeToolManager
|
||||||
|
|
||||||
|
This function is called when CrewAI needs to call a tool that requires authorization.
|
||||||
|
Authorization is handled before executing the tool.
|
||||||
|
This function overrides the ArcadeToolManager's default auth flow performed by ArcadeToolManager.authorize_tool
|
||||||
|
"""
|
||||||
|
print(f"Authorization required for tool: '{tool_name}' with inputs:")
|
||||||
|
for input_name, input_value in tool_input.items():
|
||||||
|
print(f" {input_name}: {input_value}")
|
||||||
|
|
||||||
|
# Get authorization status
|
||||||
|
auth_response = manager.authorize(tool_name, USER_ID)
|
||||||
|
|
||||||
|
# If the user is not authorized for the tool,
|
||||||
|
# then we need to handle the authorization before executing the tool
|
||||||
|
if not manager.is_authorized(auth_response.id):
|
||||||
|
# Handle authorization
|
||||||
|
print(f"\nTo authorize, visit: {auth_response.url}")
|
||||||
|
# Block until the user has completed the authorization
|
||||||
|
auth_response = manager.wait_for_auth(auth_response)
|
||||||
|
|
||||||
|
# Ensure authorization completed successfully
|
||||||
|
if not manager.is_authorized(auth_response.id):
|
||||||
|
raise ValueError(f"Authorization failed for {tool_name}. URL: {auth_response.url}")
|
||||||
|
|
||||||
|
|
||||||
|
def custom_execute_flow(
|
||||||
|
manager: ArcadeToolManager, tool_name: str, **tool_input: dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""Custom tool execution flow for the ArcadeToolManager
|
||||||
|
|
||||||
|
This function is called when CrewAI needs to execute a tool after any authorization has been handled.
|
||||||
|
This function overrides the ArcadeToolManager's default tool execution flow performed by ArcadeToolManager.execute_tool
|
||||||
|
"""
|
||||||
|
print(f"Executing tool: '{tool_name}' with inputs:")
|
||||||
|
for input_name, input_value in tool_input.items():
|
||||||
|
print(f" {input_name}: {input_value}")
|
||||||
|
|
||||||
|
# Execute the tool
|
||||||
|
response = manager._client.tools.execute(
|
||||||
|
tool_name=tool_name,
|
||||||
|
input=tool_input,
|
||||||
|
user_id=USER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle the tool error if it exists
|
||||||
|
tool_error = response.output.error if response.output else None
|
||||||
|
if tool_error:
|
||||||
|
return str(tool_error)
|
||||||
|
|
||||||
|
# Return the tool output if the tool was executed successfully
|
||||||
|
if response.success:
|
||||||
|
return response.output.value # type: ignore[union-attr]
|
||||||
|
|
||||||
|
# Return a failure message if the tool was not executed successfully
|
||||||
|
return "Failed to call " + tool_name
|
||||||
|
|
||||||
|
|
||||||
|
def custom_tool_executor(
|
||||||
|
manager: ArcadeToolManager, tool_name: str, **tool_input: dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""Custom tool executor for the ArcadeToolManager
|
||||||
|
|
||||||
|
ArcadeToolManager's default executor handles authorization and tool execution.
|
||||||
|
This function overrides the default executor to handle authorization and tool execution in a custom way.
|
||||||
|
"""
|
||||||
|
custom_auth_flow(manager, tool_name, **tool_input)
|
||||||
|
return custom_execute_flow(manager, tool_name, **tool_input)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> CrewOutput:
|
||||||
|
manager = ArcadeToolManager(
|
||||||
|
executor=custom_tool_executor,
|
||||||
|
)
|
||||||
|
tools = manager.get_tools(tools=["Google.ListEmails"])
|
||||||
|
|
||||||
|
crew_agent = Agent(
|
||||||
|
role="Main Agent",
|
||||||
|
backstory="You are a helpful assistant",
|
||||||
|
goal="Help the user with their requests",
|
||||||
|
tools=tools,
|
||||||
|
allow_delegation=False,
|
||||||
|
verbose=True,
|
||||||
|
llm=LLM(model="gpt-4o"),
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Get the 5 most recent emails from the user's inbox and summarize them and recommend a response for each.",
|
||||||
|
expected_output="A bulleted list with a one sentence summary of each email and a recommended response to the email.",
|
||||||
|
agent=crew_agent,
|
||||||
|
tools=crew_agent.tools,
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(
|
||||||
|
agents=[crew_agent],
|
||||||
|
tasks=[task],
|
||||||
|
verbose=True,
|
||||||
|
memory=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = crew.kickoff()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = main()
|
||||||
|
print("\n\n\n ------------ Result ------------ \n\n\n")
|
||||||
|
print(result)
|
||||||
1
examples/crewai/requirements.txt
Normal file
1
examples/crewai/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
crewai-arcade>=0.1.0
|
||||||
46
examples/crewai/simple_crewai_with_arcade_tool.py
Normal file
46
examples/crewai/simple_crewai_with_arcade_tool.py
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
"""
|
||||||
|
This is a simple example of how to use Arcade with CrewAI.
|
||||||
|
The example authenticates into the user's Gmail account, retrieves their 5 most recent emails, and summarizes them.
|
||||||
|
|
||||||
|
The example assumes the following:
|
||||||
|
1. You have an Arcade API key and have set the ARCADE_API_KEY environment variable.
|
||||||
|
2. You have an OpenAI API key and have set the OPENAI_API_KEY environment variable.
|
||||||
|
3. You have installed the necessary dependencies in the requirements.txt file: `pip install -r requirements.txt`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from crewai import Agent, Crew, Task
|
||||||
|
from crewai.llm import LLM
|
||||||
|
from crewai_arcade import ArcadeToolManager
|
||||||
|
|
||||||
|
manager = ArcadeToolManager(default_user_id="user@example.com")
|
||||||
|
tools = manager.get_tools(tools=["Google.ListEmails"])
|
||||||
|
|
||||||
|
crew_agent = Agent(
|
||||||
|
role="Main Agent",
|
||||||
|
backstory="You are a helpful assistant",
|
||||||
|
goal="Help the user with their requests",
|
||||||
|
tools=tools,
|
||||||
|
allow_delegation=False,
|
||||||
|
verbose=True,
|
||||||
|
llm=LLM(model="gpt-4o"),
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Get the 5 most recent emails from the user's inbox and summarize them and recommend a response for each.",
|
||||||
|
expected_output="A bulleted list with a one sentence summary of each email and a recommended response to the email.",
|
||||||
|
agent=crew_agent,
|
||||||
|
tools=crew_agent.tools,
|
||||||
|
)
|
||||||
|
|
||||||
|
crew = Crew(
|
||||||
|
agents=[crew_agent],
|
||||||
|
tasks=[task],
|
||||||
|
verbose=True,
|
||||||
|
memory=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = crew.kickoff()
|
||||||
|
|
||||||
|
print("\n\n\n ------------ Result ------------ \n\n\n")
|
||||||
|
print(result)
|
||||||
Loading…
Reference in a new issue