# 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”>
363 lines
14 KiB
Python
363 lines
14 KiB
Python
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)
|