From d4449ee5f0044e9324325bd77acc788c7d72ed1f Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Mon, 24 Jun 2024 20:48:52 +0530 Subject: [PATCH 1/7] feat: add composio CrewAI tool wrapper --- .../tools/composio_tool/README.md | 30 +++++++++ .../tools/composio_tool/composio_tool.py | 62 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/crewai_tools/tools/composio_tool/README.md create mode 100644 src/crewai_tools/tools/composio_tool/composio_tool.py 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..ef7cf1edb --- /dev/null +++ b/src/crewai_tools/tools/composio_tool/README.md @@ -0,0 +1,30 @@ +# 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]' +``` + +## Example + +The following example demonstrates how to initialize the tool and execute a mathematical operation: + +```python + +from composio import Action + +from crewai_tools.tools.composio_tool.composio_tool import ComposioTool + +tool = ComposioTool.from_tool( + tool=Action.MATHEMATICAL_CALCULATOR, +) +``` + 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..e08fbde31 --- /dev/null +++ b/src/crewai_tools/tools/composio_tool/composio_tool.py @@ -0,0 +1,62 @@ +""" +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) + + @classmethod + def from_tool(cls, tool: 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(tool, Action): + tool = Action.from_action(name=tool) + + tool = t.cast(Action, tool) + (action,) = toolset.get_action_schemas(actions=[tool]) + schema = action.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.from_app_and_action( + app=schema["appName"], + name=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.parameters.model_dump( + exclude_none=True, + ) + ), + composio_action=function, + **kwargs + ) From 41478abdf5665e9a912716e5b7d62a36db1e6d69 Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 27 Jun 2024 11:36:52 +0530 Subject: [PATCH 2/7] feat: define `ComposioTool` in the top level imports --- src/crewai_tools/__init__.py | 53 +++++++++++++++--------------- src/crewai_tools/tools/__init__.py | 17 ++++++---- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/crewai_tools/__init__.py b/src/crewai_tools/__init__.py index a51d70449..214dbbb31 100644 --- a/src/crewai_tools/__init__.py +++ b/src/crewai_tools/__init__.py @@ -1,27 +1,28 @@ -from .tools.base_tool import BaseTool, Tool, tool from .tools import ( - BrowserbaseLoadTool, - CodeDocsSearchTool, - CSVSearchTool, - DirectorySearchTool, - DOCXSearchTool, - DirectoryReadTool, - EXASearchTool, - FileReadTool, - GithubSearchTool, - SerperDevTool, - TXTSearchTool, - JSONSearchTool, - MDXSearchTool, - PDFSearchTool, - PGSearchTool, - RagTool, - ScrapeElementFromWebsiteTool, - ScrapeWebsiteTool, - SeleniumScrapingTool, - WebsiteSearchTool, - XMLSearchTool, - YoutubeChannelSearchTool, - YoutubeVideoSearchTool, - LlamaIndexTool -) \ No newline at end of file + 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 diff --git a/src/crewai_tools/tools/__init__.py b/src/crewai_tools/tools/__init__.py index 4da0c0337..df0ec7286 100644 --- a/src/crewai_tools/tools/__init__.py +++ b/src/crewai_tools/tools/__init__.py @@ -1,24 +1,29 @@ from .browserbase_load_tool.browserbase_load_tool import BrowserbaseLoadTool from .code_docs_search_tool.code_docs_search_tool import CodeDocsSearchTool +from .composio_tool.composio_tool import ComposioTool from .csv_search_tool.csv_search_tool import CSVSearchTool -from .directory_search_tool.directory_search_tool import DirectorySearchTool from .directory_read_tool.directory_read_tool import DirectoryReadTool +from .directory_search_tool.directory_search_tool import DirectorySearchTool from .docx_search_tool.docx_search_tool import DOCXSearchTool from .exa_tools.exa_search_tool import EXASearchTool from .file_read_tool.file_read_tool import FileReadTool from .github_search_tool.github_search_tool import GithubSearchTool -from .serper_dev_tool.serper_dev_tool import SerperDevTool -from .txt_search_tool.txt_search_tool import TXTSearchTool from .json_search_tool.json_search_tool import JSONSearchTool +from .llamaindex_tool.llamaindex_tool import LlamaIndexTool from .mdx_seach_tool.mdx_search_tool import MDXSearchTool from .pdf_search_tool.pdf_search_tool import PDFSearchTool from .pg_seach_tool.pg_search_tool import PGSearchTool from .rag.rag_tool import RagTool -from .scrape_element_from_website.scrape_element_from_website import ScrapeElementFromWebsiteTool +from .scrape_element_from_website.scrape_element_from_website import ( + ScrapeElementFromWebsiteTool, +) from .scrape_website_tool.scrape_website_tool import ScrapeWebsiteTool from .selenium_scraping_tool.selenium_scraping_tool import SeleniumScrapingTool +from .serper_dev_tool.serper_dev_tool import SerperDevTool +from .txt_search_tool.txt_search_tool import TXTSearchTool from .website_search.website_search_tool import WebsiteSearchTool from .xml_search_tool.xml_search_tool import XMLSearchTool -from .youtube_channel_search_tool.youtube_channel_search_tool import YoutubeChannelSearchTool +from .youtube_channel_search_tool.youtube_channel_search_tool import ( + YoutubeChannelSearchTool, +) from .youtube_video_search_tool.youtube_video_search_tool import YoutubeVideoSearchTool -from .llamaindex_tool.llamaindex_tool import LlamaIndexTool From be6e1a79dd136ed8b3e36947203e1e7b91f95eee Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 27 Jun 2024 11:39:42 +0530 Subject: [PATCH 3/7] feat: add search utility methods --- .../tools/composio_tool/composio_tool.py | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/crewai_tools/tools/composio_tool/composio_tool.py b/src/crewai_tools/tools/composio_tool/composio_tool.py index e08fbde31..664898ce3 100644 --- a/src/crewai_tools/tools/composio_tool/composio_tool.py +++ b/src/crewai_tools/tools/composio_tool/composio_tool.py @@ -19,7 +19,11 @@ class ComposioTool(BaseTool): return self.composio_action(*args, **kwargs) @classmethod - def from_tool(cls, tool: t.Any, **kwargs: t.Any) -> te.Self: + def from_tool( + cls, + tool: t.Any, + **kwargs: t.Any, + ) -> te.Self: """Wrap a composio tool as crewAI tool.""" from composio import Action, ComposioToolSet @@ -28,7 +32,7 @@ class ComposioTool(BaseTool): toolset = ComposioToolSet() if not isinstance(tool, Action): - tool = Action.from_action(name=tool) + tool = Action(tool) tool = t.cast(Action, tool) (action,) = toolset.get_action_schemas(actions=[tool]) @@ -38,10 +42,7 @@ class ComposioTool(BaseTool): def function(**kwargs: t.Any) -> t.Dict: """Wrapper function for composio action.""" return toolset.execute_action( - action=Action.from_app_and_action( - app=schema["appName"], - name=schema["name"], - ), + action=Action(schema["name"]), params=kwargs, entity_id=entity_id, ) @@ -58,5 +59,42 @@ class ComposioTool(BaseTool): ) ), composio_action=function, - **kwargs + **kwargs, ) + + @classmethod + def from_app( + cls, + app: t.Any, + tags: t.Optional[t.List[str]] = None, + **kwargs: t.Any, + ) -> t.List[te.Self]: + """Create toolset from an app.""" + from composio import App + + if not isinstance(app, App): + app = App(app) + + return [ + cls.from_tool(tool=action, **kwargs) + for action in app.get_actions(tags=tags) + ] + + @classmethod + def from_use_case( + cls, + *apps: t.Any, + use_case: str, + **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 to search by use case" + ) + + from composio import ComposioToolSet + + toolset = ComposioToolSet() + actions = toolset.find_actions_by_use_case(*apps, use_case=use_case) + return [cls.from_tool(tool=action, **kwargs) for action in actions] From ab484172ef2d3237fb14416ca2894db3568e216b Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 27 Jun 2024 11:40:02 +0530 Subject: [PATCH 4/7] chore: update readme --- .../tools/composio_tool/README.md | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/crewai_tools/tools/composio_tool/README.md b/src/crewai_tools/tools/composio_tool/README.md index ef7cf1edb..4a4db85d5 100644 --- a/src/crewai_tools/tools/composio_tool/README.md +++ b/src/crewai_tools/tools/composio_tool/README.md @@ -15,16 +15,55 @@ pip install 'crewai[tools]' ## Example -The following example demonstrates how to initialize the tool and execute a mathematical operation: +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 -from composio import Action -from crewai_tools.tools.composio_tool.composio_tool import ComposioTool +tools = [ComposioTool.from_tool(tool=Action.GITHUB_ACTIVITY_STAR_REPO_FOR_AUTHENTICATED_USER)] +``` -tool = ComposioTool.from_tool( - tool=Action.MATHEMATICAL_CALCULATOR, +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=App.GITHUB, tags=["important"]) +``` + +or use `from_use_case` to search relevant actions + +```python +tools = ComposioTool.from_use_case(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() +``` + From 369c03a257c6c11c5620d3bcf4e66310a649c193 Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 27 Jun 2024 12:16:22 +0530 Subject: [PATCH 5/7] feat: add check for auth accounts --- .../tools/composio_tool/composio_tool.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/crewai_tools/tools/composio_tool/composio_tool.py b/src/crewai_tools/tools/composio_tool/composio_tool.py index 664898ce3..fd478eeb6 100644 --- a/src/crewai_tools/tools/composio_tool/composio_tool.py +++ b/src/crewai_tools/tools/composio_tool/composio_tool.py @@ -18,6 +18,26 @@ class ComposioTool(BaseTool): """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_tool( cls, @@ -35,6 +55,11 @@ class ComposioTool(BaseTool): tool = Action(tool) tool = t.cast(Action, tool) + cls._check_connected_account( + tool=tool, + toolset=toolset, + ) + (action,) = toolset.get_action_schemas(actions=[tool]) schema = action.model_dump(exclude_none=True) entity_id = kwargs.pop("entity_id", DEFAULT_ENTITY_ID) From 58354ec638326a4b406aa97d6038d2fc7d6df418 Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 27 Jun 2024 12:23:05 +0530 Subject: [PATCH 6/7] chore: update README --- src/crewai_tools/tools/composio_tool/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/crewai_tools/tools/composio_tool/README.md b/src/crewai_tools/tools/composio_tool/README.md index 4a4db85d5..fe030cbd2 100644 --- a/src/crewai_tools/tools/composio_tool/README.md +++ b/src/crewai_tools/tools/composio_tool/README.md @@ -13,6 +13,8 @@ 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: @@ -45,7 +47,7 @@ tools = ComposioTool.from_use_case(App.GITHUB, use_case="Star a github repositor ```python crewai_agent = Agent( role="Github Agent", - goal="""You take action on Github using Github APIs""", + 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" @@ -67,3 +69,4 @@ task = Task( task.execute() ``` +* More detailed list of tools can be found [here](https://app.composio.dev) From 9a8d88b8aa3890897d75814fd4b916d3dba706b7 Mon Sep 17 00:00:00 2001 From: angrybayblade Date: Thu, 27 Jun 2024 13:35:57 +0530 Subject: [PATCH 7/7] fix: merge `from_app` and `from_use_case` --- .../tools/composio_tool/README.md | 8 +-- .../tools/composio_tool/composio_tool.py | 59 +++++++++---------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/crewai_tools/tools/composio_tool/README.md b/src/crewai_tools/tools/composio_tool/README.md index fe030cbd2..18045e7f1 100644 --- a/src/crewai_tools/tools/composio_tool/README.md +++ b/src/crewai_tools/tools/composio_tool/README.md @@ -27,19 +27,19 @@ from crewai_tools import ComposioTool from crewai import Agent, Task -tools = [ComposioTool.from_tool(tool=Action.GITHUB_ACTIVITY_STAR_REPO_FOR_AUTHENTICATED_USER)] +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=App.GITHUB, tags=["important"]) +tools = ComposioTool.from_app(App.GITHUB, tags=["important"]) ``` -or use `from_use_case` to search relevant actions +or use `use_case` to search relevant actions ```python -tools = ComposioTool.from_use_case(App.GITHUB, use_case="Star a github repository") +tools = ComposioTool.from_app(App.GITHUB, use_case="Star a github repository") ``` 2. Define agent diff --git a/src/crewai_tools/tools/composio_tool/composio_tool.py b/src/crewai_tools/tools/composio_tool/composio_tool.py index fd478eeb6..62068c0bd 100644 --- a/src/crewai_tools/tools/composio_tool/composio_tool.py +++ b/src/crewai_tools/tools/composio_tool/composio_tool.py @@ -39,9 +39,9 @@ class ComposioTool(BaseTool): ) @classmethod - def from_tool( + def from_action( cls, - tool: t.Any, + action: t.Any, **kwargs: t.Any, ) -> te.Self: """Wrap a composio tool as crewAI tool.""" @@ -51,17 +51,17 @@ class ComposioTool(BaseTool): from composio.utils.shared import json_schema_to_model toolset = ComposioToolSet() - if not isinstance(tool, Action): - tool = Action(tool) + if not isinstance(action, Action): + action = Action(action) - tool = t.cast(Action, tool) + action = t.cast(Action, action) cls._check_connected_account( - tool=tool, + tool=action, toolset=toolset, ) - (action,) = toolset.get_action_schemas(actions=[tool]) - schema = action.model_dump(exclude_none=True) + (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: @@ -79,7 +79,7 @@ class ComposioTool(BaseTool): name=schema["name"], description=schema["description"], args_schema=json_schema_to_model( - action.parameters.model_dump( + action_schema.parameters.model_dump( exclude_none=True, ) ), @@ -89,37 +89,34 @@ class ComposioTool(BaseTool): @classmethod def from_app( - cls, - app: t.Any, - tags: t.Optional[t.List[str]] = None, - **kwargs: t.Any, - ) -> t.List[te.Self]: - """Create toolset from an app.""" - from composio import App - - if not isinstance(app, App): - app = App(app) - - return [ - cls.from_tool(tool=action, **kwargs) - for action in app.get_actions(tags=tags) - ] - - @classmethod - def from_use_case( cls, *apps: t.Any, - use_case: str, + 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( - "You need to provide at least one app name to search by use case" + "Cannot use both `use_case` and `tags` to filter the actions" ) from composio import ComposioToolSet toolset = ComposioToolSet() - actions = toolset.find_actions_by_use_case(*apps, use_case=use_case) - return [cls.from_tool(tool=action, **kwargs) for action in actions] + 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) + ]