diff --git a/src/crewai_tools/__init__.py b/src/crewai_tools/__init__.py index 1398dcfaf..1cc222ec9 100644 --- a/src/crewai_tools/__init__.py +++ b/src/crewai_tools/__init__.py @@ -1,28 +1,28 @@ -from .tools.base_tool import BaseTool, Tool, tool from .tools import ( - BrowserbaseLoadTool, - CodeDocsSearchTool, - CodeInterpreterTool, - CSVSearchTool, - DirectorySearchTool, - DOCXSearchTool, - DirectoryReadTool, - EXASearchTool, - FileReadTool, - GithubSearchTool, - SerperDevTool, - TXTSearchTool, - JSONSearchTool, - MDXSearchTool, - PDFSearchTool, - PGSearchTool, - RagTool, - ScrapeElementFromWebsiteTool, - ScrapeWebsiteTool, - SeleniumScrapingTool, - WebsiteSearchTool, - XMLSearchTool, - YoutubeChannelSearchTool, - YoutubeVideoSearchTool, - LlamaIndexTool + BrowserbaseLoadTool, + CodeDocsSearchTool, + ComposioTool, + CSVSearchTool, + DirectoryReadTool, + DirectorySearchTool, + DOCXSearchTool, + EXASearchTool, + FileReadTool, + GithubSearchTool, + JSONSearchTool, + LlamaIndexTool, + MDXSearchTool, + PDFSearchTool, + PGSearchTool, + RagTool, + ScrapeElementFromWebsiteTool, + ScrapeWebsiteTool, + SeleniumScrapingTool, + SerperDevTool, + TXTSearchTool, + WebsiteSearchTool, + XMLSearchTool, + YoutubeChannelSearchTool, + YoutubeVideoSearchTool, ) +from .tools.base_tool import BaseTool, Tool, tool \ No newline at end of file diff --git a/src/crewai_tools/tools/__init__.py b/src/crewai_tools/tools/__init__.py index 35b81396b..f5ac94052 100644 --- a/src/crewai_tools/tools/__init__.py +++ b/src/crewai_tools/tools/__init__.py @@ -1,6 +1,7 @@ from .browserbase_load_tool.browserbase_load_tool import BrowserbaseLoadTool from .code_docs_search_tool.code_docs_search_tool import CodeDocsSearchTool from .code_interpreter_tool.code_interpreter_tool import CodeInterpreterTool +from .composio_tool.composio_tool import ComposioTool from .csv_search_tool.csv_search_tool import CSVSearchTool from .directory_read_tool.directory_read_tool import DirectoryReadTool from .directory_search_tool.directory_search_tool import DirectorySearchTool diff --git a/src/crewai_tools/tools/composio_tool/README.md b/src/crewai_tools/tools/composio_tool/README.md new file mode 100644 index 000000000..18045e7f1 --- /dev/null +++ b/src/crewai_tools/tools/composio_tool/README.md @@ -0,0 +1,72 @@ +# ComposioTool Documentation + +## Description + +This tools is a wrapper around the composio toolset and gives your agent access to a wide variety of tools from the composio SDK. + +## Installation + +To incorporate this tool into your project, follow the installation instructions below: + +```shell +pip install composio-core +pip install 'crewai[tools]' +``` + +after the installation is complete, either run `composio login` or export your composio API key as `COMPOSIO_API_KEY`. + +## Example + +The following example demonstrates how to initialize the tool and execute a github action: + +1. Initialize toolset + +```python +from composio import App +from crewai_tools import ComposioTool +from crewai import Agent, Task + + +tools = [ComposioTool.from_action(action=Action.GITHUB_ACTIVITY_STAR_REPO_FOR_AUTHENTICATED_USER)] +``` + +If you don't know what action you want to use, use `from_app` and `tags` filter to get relevant actions + +```python +tools = ComposioTool.from_app(App.GITHUB, tags=["important"]) +``` + +or use `use_case` to search relevant actions + +```python +tools = ComposioTool.from_app(App.GITHUB, use_case="Star a github repository") +``` + +2. Define agent + +```python +crewai_agent = Agent( + role="Github Agent", + goal="You take action on Github using Github APIs", + backstory=( + "You are AI agent that is responsible for taking actions on Github " + "on users behalf. You need to take action on Github using Github APIs" + ), + verbose=True, + tools=tools, +) +``` + +3. Execute task + +```python +task = Task( + description="Star a repo ComposioHQ/composio on GitHub", + agent=crewai_agent, + expected_output="if the star happened", +) + +task.execute() +``` + +* More detailed list of tools can be found [here](https://app.composio.dev) diff --git a/src/crewai_tools/tools/composio_tool/composio_tool.py b/src/crewai_tools/tools/composio_tool/composio_tool.py new file mode 100644 index 000000000..62068c0bd --- /dev/null +++ b/src/crewai_tools/tools/composio_tool/composio_tool.py @@ -0,0 +1,122 @@ +""" +Composio tools wrapper. +""" + +import typing as t + +import typing_extensions as te + +from crewai_tools.tools.base_tool import BaseTool + + +class ComposioTool(BaseTool): + """Wrapper for composio tools.""" + + composio_action: t.Callable + + def _run(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Run the composio action with given arguments.""" + return self.composio_action(*args, **kwargs) + + @staticmethod + def _check_connected_account(tool: t.Any, toolset: t.Any) -> None: + """Check if connected account is required and if required it exists or not.""" + from composio import Action + from composio.client.collections import ConnectedAccountModel + + tool = t.cast(Action, tool) + if tool.no_auth: + return + + connections = t.cast( + t.List[ConnectedAccountModel], + toolset.client.connected_accounts.get(), + ) + if tool.app not in [connection.appUniqueId for connection in connections]: + raise RuntimeError( + f"No connected account found for app `{tool.app}`; " + f"Run `composio add {tool.app}` to fix this" + ) + + @classmethod + def from_action( + cls, + action: t.Any, + **kwargs: t.Any, + ) -> te.Self: + """Wrap a composio tool as crewAI tool.""" + + from composio import Action, ComposioToolSet + from composio.constants import DEFAULT_ENTITY_ID + from composio.utils.shared import json_schema_to_model + + toolset = ComposioToolSet() + if not isinstance(action, Action): + action = Action(action) + + action = t.cast(Action, action) + cls._check_connected_account( + tool=action, + toolset=toolset, + ) + + (action_schema,) = toolset.get_action_schemas(actions=[action]) + schema = action_schema.model_dump(exclude_none=True) + entity_id = kwargs.pop("entity_id", DEFAULT_ENTITY_ID) + + def function(**kwargs: t.Any) -> t.Dict: + """Wrapper function for composio action.""" + return toolset.execute_action( + action=Action(schema["name"]), + params=kwargs, + entity_id=entity_id, + ) + + function.__name__ = schema["name"] + function.__doc__ = schema["description"] + + return cls( + name=schema["name"], + description=schema["description"], + args_schema=json_schema_to_model( + action_schema.parameters.model_dump( + exclude_none=True, + ) + ), + composio_action=function, + **kwargs, + ) + + @classmethod + def from_app( + cls, + *apps: t.Any, + tags: t.Optional[t.List[str]] = None, + use_case: t.Optional[str] = None, + **kwargs: t.Any, + ) -> t.List[te.Self]: + """Create toolset from an app.""" + if len(apps) == 0: + raise ValueError("You need to provide at least one app name") + + if use_case is None and tags is None: + raise ValueError("Both `use_case` and `tags` cannot be `None`") + + if use_case is not None and tags is not None: + raise ValueError( + "Cannot use both `use_case` and `tags` to filter the actions" + ) + + from composio import ComposioToolSet + + toolset = ComposioToolSet() + if use_case is not None: + return [ + cls.from_action(action=action, **kwargs) + for action in toolset.find_actions_by_use_case(*apps, use_case=use_case) + ] + + return [ + cls.from_action(action=action, **kwargs) + for action in toolset.find_actions_by_tags(*apps, tags=tags) + ]