From 818a17d0db6bb164d4f5a3b272de8ea0a2805083 Mon Sep 17 00:00:00 2001 From: Himanshu Dixit Date: Wed, 31 Jul 2024 02:44:32 +0530 Subject: [PATCH] feat: add composio toolset (#3034) * feat: add composio toolset * feat: v1 * feat: format code * feat: add support for multi tools * feat: make methods private * [autofix.ci] apply automated fixes * feat: use logger * refactor(ComposioAPI.py): reorganize import statement * refactor: update typing import in langchain_utilities/model.py * refactor: update typing import in ComposioAPI.py --------- Co-authored-by: Gabriel Luiz Freitas Almeida Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- poetry.lock | 231 +++++++++++++++++- pyproject.toml | 1 + .../base/langchain_utilities/model.py | 4 +- .../components/toolkits/ComposioAPI.py | 172 +++++++++++++ .../langflow/components/toolkits/__init__.py | 2 + src/frontend/src/icons/Composio/Composio.svg | 68 ++++++ src/frontend/src/icons/Composio/composio.jsx | 74 ++++++ src/frontend/src/icons/Composio/index.tsx | 9 + src/frontend/src/utils/styleUtils.ts | 2 + 9 files changed, 559 insertions(+), 4 deletions(-) create mode 100644 src/backend/base/langflow/components/toolkits/ComposioAPI.py create mode 100644 src/frontend/src/icons/Composio/Composio.svg create mode 100644 src/frontend/src/icons/Composio/composio.jsx create mode 100644 src/frontend/src/icons/Composio/index.tsx diff --git a/poetry.lock b/poetry.lock index 50acc9d31..0fb42acd5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,16 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "aenum" +version = "3.1.15" +description = "Advanced Enumerations (compatible with Python's stdlib Enum), NamedTuples, and NamedConstants" +optional = false +python-versions = "*" +files = [ + {file = "aenum-3.1.15-py2-none-any.whl", hash = "sha256:27b1710b9d084de6e2e695dab78fe9f269de924b51ae2850170ee7e1ca6288a5"}, + {file = "aenum-3.1.15-py3-none-any.whl", hash = "sha256:e0dfaeea4c2bd362144b87377e2c61d91958c5ed0b4daf89cb6f45ae23af6288"}, + {file = "aenum-3.1.15.tar.gz", hash = "sha256:8cbd76cd18c4f870ff39b24284d3ea028fbe8731a58df3aa581e434c575b9559"}, +] [[package]] name = "aiofiles" @@ -1306,6 +1318,58 @@ traitlets = ">=4" [package.extras] test = ["pytest"] +[[package]] +name = "composio-core" +version = "0.3.28" +description = "Core package to act as a bridge between composio platform and other services." +optional = false +python-versions = "<4,>=3.9" +files = [ + {file = "composio_core-0.3.28-py3-none-any.whl", hash = "sha256:2c863fbf60096df4593a2bfa12f776318a2c6c852c12555e45e8d1c9977f36a9"}, + {file = "composio_core-0.3.28.tar.gz", hash = "sha256:31453edd3818518144709ad456be05cf19718c6e7d703c6bdb277e5491a1514a"}, +] + +[package.dependencies] +aiohttp = "*" +click = "*" +docker = ">=7.1.0" +e2b-code-interpreter = "*" +fastapi = "*" +gql = "*" +importlib-metadata = ">=4.8.1" +inflection = ">=0.5.1" +jsonref = ">=1.1.0" +jsonschema = ">=4.21.1,<5" +paramiko = "*" +pydantic = ">=2.6.4,<3" +pyperclip = ">=1.8.2,<2" +pysher = "1.0.8" +requests = ">=2.31.0,<3" +requests-toolbelt = "*" +rich = ">=13.7.1,<14" +sentry-sdk = ">=2.0.0" + +[package.extras] +all = ["diskcache", "flake8", "networkx", "pathspec", "pygments", "ruff", "transformers", "tree-sitter (==0.21.3)", "tree-sitter-languages"] + +[[package]] +name = "composio-langchain" +version = "0.3.28" +description = "Use Composio to get an array of tools with your LangChain agent." +optional = false +python-versions = "<4,>=3.9" +files = [ + {file = "composio_langchain-0.3.28-py3-none-any.whl", hash = "sha256:b75b52e2ffd02eef0f63f714719c96eaaddd7ba63ab6dfe1a213add8c3d94dab"}, + {file = "composio_langchain-0.3.28.tar.gz", hash = "sha256:638b8b17140b5af96be848f1e701f5b1fcae6a47dac700c87208365f7445f315"}, +] + +[package.dependencies] +composio-core = "0.3.28" +langchain = ">=0.1.0" +langchain-openai = ">=0.0.2.post1" +langchainhub = ">=0.1.15" +pydantic = ">=2.6.4" + [[package]] name = "configargparse" version = "1.7" @@ -1949,6 +2013,44 @@ files = [ {file = "duckdb-1.0.0.tar.gz", hash = "sha256:a2a059b77bc7d5b76ae9d88e267372deff19c291048d59450c431e166233d453"}, ] +[[package]] +name = "e2b" +version = "0.17.1" +description = "E2B SDK that give agents cloud environments" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "e2b-0.17.1-py3-none-any.whl", hash = "sha256:c0698fd03b639f4dd88eed167a98af4d450668c0ae9805122a98f62f36f2491f"}, + {file = "e2b-0.17.1.tar.gz", hash = "sha256:9e69a059cb73334bac7db189287552af9321fb3ac8ced52557907e10c4310733"}, +] + +[package.dependencies] +aenum = ">=3.1.11" +aiohttp = ">=3.8.4" +jsonrpcclient = ">=4.0.3" +pydantic = "*" +python-dateutil = ">=2.8.2" +requests = ">=2.31.0" +typing-extensions = ">=4.8.0" +urllib3 = ">=1.25.3" +websockets = ">=11.0.3" + +[[package]] +name = "e2b-code-interpreter" +version = "0.0.10" +description = "E2B Code Interpreter - Stateful code execution" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "e2b_code_interpreter-0.0.10-py3-none-any.whl", hash = "sha256:85700fad734334678a11e6b8cfea9dfd5af7f2f16b8f9a5950cf06b1877c02da"}, + {file = "e2b_code_interpreter-0.0.10.tar.gz", hash = "sha256:2882197b819e657c5b03083b2330c8e06117e7a584ca93e6d1acded9da517622"}, +] + +[package.dependencies] +e2b = ">=0.17.1" +pydantic = "*" +websocket-client = ">=1.7.0,<2.0.0" + [[package]] name = "ecdsa" version = "0.19.0" @@ -3274,6 +3376,45 @@ cachetools = "*" numpy = "*" requests = "*" +[[package]] +name = "gql" +version = "3.5.0" +description = "GraphQL client for Python" +optional = false +python-versions = "*" +files = [ + {file = "gql-3.5.0-py2.py3-none-any.whl", hash = "sha256:70dda5694a5b194a8441f077aa5fb70cc94e4ec08016117523f013680901ecb7"}, + {file = "gql-3.5.0.tar.gz", hash = "sha256:ccb9c5db543682b28f577069950488218ed65d4ac70bb03b6929aaadaf636de9"}, +] + +[package.dependencies] +anyio = ">=3.0,<5" +backoff = ">=1.11.1,<3.0" +graphql-core = ">=3.2,<3.3" +yarl = ">=1.6,<2.0" + +[package.extras] +aiohttp = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)"] +all = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "websockets (>=10,<12)"] +botocore = ["botocore (>=1.21,<2)"] +dev = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "httpx (>=0.23.1,<1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "sphinx (>=5.3.0,<6)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "vcrpy (==4.4.0)", "websockets (>=10,<12)"] +httpx = ["httpx (>=0.23.1,<1)"] +requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)"] +test = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "vcrpy (==4.4.0)", "websockets (>=10,<12)"] +test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0)"] +websockets = ["websockets (>=10,<12)"] + +[[package]] +name = "graphql-core" +version = "3.2.3" +description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, + {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, +] + [[package]] name = "greenlet" version = "3.0.3" @@ -3972,6 +4113,17 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] +[[package]] +name = "inflection" +version = "0.5.1" +description = "A port of Ruby on Rails inflector to Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, + {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, +] + [[package]] name = "iniconfig" version = "2.0.0" @@ -4383,6 +4535,19 @@ files = [ {file = "jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552"}, ] +[[package]] +name = "jsonrpcclient" +version = "4.0.3" +description = "Send JSON-RPC requests" +optional = false +python-versions = ">=3.6" +files = [ + {file = "jsonrpcclient-4.0.3-py3-none-any.whl", hash = "sha256:3cbb9e27e1be29821becf135ea183144a836215422727e1ffe5056a49a670f0d"}, +] + +[package.extras] +qa = ["pytest", "pytest-cov", "tox"] + [[package]] name = "jsonschema" version = "4.23.0" @@ -6308,6 +6473,7 @@ description = "Nvidia JIT LTO Library" optional = true python-versions = ">=3" files = [ + {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-manylinux2014_aarch64.whl", hash = "sha256:98103729cc5226e13ca319a10bbf9433bbbd44ef64fe72f45f067cacc14b8d27"}, {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f9b37bc5c8cf7509665cb6ada5aaa0ce65618f2332b7d3e78e9790511f111212"}, {file = "nvidia_nvjitlink_cu12-12.5.82-py3-none-win_amd64.whl", hash = "sha256:e782564d705ff0bf61ac3e1bf730166da66dd2fe9012f111ede5fc49b64ae697"}, ] @@ -6844,6 +7010,27 @@ files = [ [package.extras] dev = ["jinja2"] +[[package]] +name = "paramiko" +version = "3.4.0" +description = "SSH2 protocol library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "paramiko-3.4.0-py3-none-any.whl", hash = "sha256:43f0b51115a896f9c00f59618023484cb3a14b98bbceab43394a39c6739b7ee7"}, + {file = "paramiko-3.4.0.tar.gz", hash = "sha256:aac08f26a31dc4dffd92821527d1682d99d52f9ef6851968114a8728f3c274d3"}, +] + +[package.dependencies] +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" + +[package.extras] +all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +invoke = ["invoke (>=2.0)"] + [[package]] name = "parso" version = "0.8.4" @@ -7953,6 +8140,32 @@ snappy = ["python-snappy"] test = ["pytest (>=7)"] zstd = ["zstandard"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + [[package]] name = "pyparsing" version = "3.1.2" @@ -8058,6 +8271,20 @@ files = [ {file = "pysbd-0.3.4-py3-none-any.whl", hash = "sha256:cd838939b7b0b185fcf86b0baf6636667dfb6e474743beeff878e9f42e022953"}, ] +[[package]] +name = "pysher" +version = "1.0.8" +description = "Pusher websocket client for python, based on Erik Kulyk's PythonPusherClient" +optional = false +python-versions = "*" +files = [ + {file = "Pysher-1.0.8.tar.gz", hash = "sha256:7849c56032b208e49df67d7bd8d49029a69042ab0bb45b2ed59fa08f11ac5988"}, +] + +[package.dependencies] +requests = ">=2.26.0" +websocket-client = "!=0.49" + [[package]] name = "pysocks" version = "1.7.1" @@ -11768,4 +11995,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "d5e5f7e03a57022c0548ff9042b68e43d23a923eafb75384e8132af1113e3d9d" +content-hash = "3d81751b63f97649e1b46a0d626c055303df6ab3bfce2e6b49941400aad08793" diff --git a/pyproject.toml b/pyproject.toml index c11fa5356..995c1fc80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,7 @@ yfinance = "^0.2.40" langchain-google-community = "1.0.7" wolframalpha = "^5.1.3" astra-assistants = "^2.0.15" +composio-langchain = "^0.3.28" [tool.poetry.group.dev.dependencies] diff --git a/src/backend/base/langflow/base/langchain_utilities/model.py b/src/backend/base/langflow/base/langchain_utilities/model.py index cea2090e5..e2c1c0321 100644 --- a/src/backend/base/langflow/base/langchain_utilities/model.py +++ b/src/backend/base/langflow/base/langchain_utilities/model.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Union +from typing import Sequence, Union from langflow.custom import Component from langflow.field_typing import Tool @@ -31,7 +31,7 @@ class LCToolComponent(Component): pass @abstractmethod - def build_tool(self) -> Tool: + def build_tool(self) -> Tool | Sequence[Tool]: """ Build the tool. """ diff --git a/src/backend/base/langflow/components/toolkits/ComposioAPI.py b/src/backend/base/langflow/components/toolkits/ComposioAPI.py new file mode 100644 index 000000000..cf8dfa2be --- /dev/null +++ b/src/backend/base/langflow/components/toolkits/ComposioAPI.py @@ -0,0 +1,172 @@ +from typing import Any, Sequence + +from composio_langchain import Action, App, ComposioToolSet # type: ignore +from langchain_core.tools import Tool +from loguru import logger + +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import DropdownInput, MessageTextInput, MultiselectInput, SecretStrInput, StrInput + + +class ComposioAPIComponent(LCToolComponent): + display_name: str = "Composio Tools" + description: str = "Use Composio toolset to run actions with your agent" + name = "ComposioAPI" + icon = "Composio" + documentation: str = "https://docs.composio.dev" + + inputs = [ + MessageTextInput(name="entity_id", display_name="Entity ID", value="default", advanced=True), + SecretStrInput( + name="api_key", + display_name="Composio API Key", + required=True, + refresh_button=True, + info="Refer to https://docs.composio.dev/introduction/foundations/howtos/get_api_key", + ), + DropdownInput( + name="app_names", + display_name="App Name", + options=[app_name for app_name in App.__annotations__], + value="", + info="The app name to use. Please refresh after selecting app name", + refresh_button=True, + ), + MultiselectInput( + name="action_names", + display_name="Actions to use", + required=False, + options=[], + value=[], + info="The actions to pass to agent to execute", + ), + StrInput( + name="auth_status_config", + display_name="Auth status", + value="", + refresh_button=True, + info="Open link or enter api key. Then refresh button", + ), + ] + + def _check_for_authorization(self, app: str) -> str: + """ + Checks if the app is authorized. + + Args: + app (str): The app name to check authorization for. + + Returns: + str: The authorization status. + """ + toolset = self._build_wrapper() + entity = toolset.client.get_entity(id=self.entity_id) + try: + entity.get_connection(app=app) + return f"{app} CONNECTED" + except Exception: + return self._handle_authorization_failure(toolset, entity, app) + + def _handle_authorization_failure(self, toolset: ComposioToolSet, entity: Any, app: str) -> str: + """ + Handles the authorization failure by attempting to process API key auth or initiate default connection. + + Args: + toolset (ComposioToolSet): The toolset instance. + entity (Any): The entity instance. + app (str): The app name. + + Returns: + str: The result of the authorization failure message. + """ + try: + auth_schemes = toolset.client.apps.get(app).auth_schemes + if auth_schemes[0].auth_mode == "API_KEY": + return self._process_api_key_auth(entity, app) + else: + return self._initiate_default_connection(entity, app) + except Exception as exc: + logger.error(f"Authorization error: {str(exc)}") + return "Error" + + def _process_api_key_auth(self, entity: Any, app: str) -> str: + """ + Processes the API key authentication. + + Args: + entity (Any): The entity instance. + app (str): The app name. + + Returns: + str: The status of the API key authentication. + """ + auth_status_config = self.auth_status_config + is_url = "http" in auth_status_config or "https" in auth_status_config + is_different_app = "CONNECTED" in auth_status_config and app not in auth_status_config + is_default_api_key_message = "API Key" in auth_status_config + + if is_different_app or is_url or is_default_api_key_message: + return "Enter API Key" + else: + if not is_default_api_key_message: + entity.initiate_connection( + app_name=app, + auth_mode="API_KEY", + auth_config={"api_key": self.auth_status_config}, + use_composio_auth=False, + force_new_integration=True, + ) + return f"{app} CONNECTED" + else: + return "Enter API Key" + + def _initiate_default_connection(self, entity: Any, app: str) -> str: + connection = entity.initiate_connection(app_name=app, use_composio_auth=True, force_new_integration=True) + return connection.redirectUrl + + def _get_connected_app_names_for_entity(self) -> list[str]: + toolset = self._build_wrapper() + connections = toolset.client.get_entity(id=self.entity_id).get_connections() + return list(set(connection.appUniqueId for connection in connections)) + + def _update_app_names_with_connected_status(self, build_config: dict) -> dict: + connected_app_names = self._get_connected_app_names_for_entity() + + app_names = [ + f"{app_name}_CONNECTED" for app_name in App.__annotations__ if app_name.lower() in connected_app_names + ] + non_connected_app_names = [ + app_name for app_name in App.__annotations__ if app_name.lower() not in connected_app_names + ] + build_config["app_names"]["options"] = app_names + non_connected_app_names + build_config["app_names"]["value"] = app_names[0] if app_names else "" + return build_config + + def _get_normalized_app_name(self) -> str: + return self.app_names.replace("_CONNECTED", "").replace("_connected", "") + + def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: + if field_name == "api_key": + build_config = self._update_app_names_with_connected_status(build_config) + return build_config + + if field_name in {"app_names", "auth_status_config"}: + build_config["auth_status_config"]["value"] = self._check_for_authorization(self._get_normalized_app_name()) + + all_action_names = [action_name for action_name in Action.__annotations__] + app_action_names = [ + action_name + for action_name in all_action_names + if action_name.lower().startswith(self._get_normalized_app_name().lower() + "_") + ] + build_config["action_names"]["options"] = app_action_names + build_config["action_names"]["value"] = [app_action_names[0]] if app_action_names else [""] + return build_config + + def build_tool(self) -> Sequence[Tool]: + composio_toolset = self._build_wrapper() + composio_tools = composio_toolset.get_actions(actions=self.action_names) + return composio_tools + + def _build_wrapper(self) -> ComposioToolSet: + return ComposioToolSet(api_key=self.api_key) diff --git a/src/backend/base/langflow/components/toolkits/__init__.py b/src/backend/base/langflow/components/toolkits/__init__.py index abded990c..8d3e5c8cb 100644 --- a/src/backend/base/langflow/components/toolkits/__init__.py +++ b/src/backend/base/langflow/components/toolkits/__init__.py @@ -1,7 +1,9 @@ from .Metaphor import MetaphorToolkit from .VectorStoreInfo import VectorStoreInfoComponent +from .ComposioAPI import ComposioAPIComponent __all__ = [ "MetaphorToolkit", "VectorStoreInfoComponent", + "ComposioAPIComponent", ] diff --git a/src/frontend/src/icons/Composio/Composio.svg b/src/frontend/src/icons/Composio/Composio.svg new file mode 100644 index 000000000..7ac76eb76 --- /dev/null +++ b/src/frontend/src/icons/Composio/Composio.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/frontend/src/icons/Composio/composio.jsx b/src/frontend/src/icons/Composio/composio.jsx new file mode 100644 index 000000000..2d683ac06 --- /dev/null +++ b/src/frontend/src/icons/Composio/composio.jsx @@ -0,0 +1,74 @@ +const Icon = (props) => ( + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default Icon; diff --git a/src/frontend/src/icons/Composio/index.tsx b/src/frontend/src/icons/Composio/index.tsx new file mode 100644 index 000000000..91424b153 --- /dev/null +++ b/src/frontend/src/icons/Composio/index.tsx @@ -0,0 +1,9 @@ +import React, { forwardRef } from "react"; +import ComposioIconSVG from "./composio"; + +export const ComposioIcon = forwardRef< + SVGSVGElement, + React.PropsWithChildren<{}> +>((props, ref) => { + return ; +}); diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index bef662795..eb37edf57 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -172,6 +172,7 @@ import { BotMessageSquareIcon } from "../icons/BotMessageSquare"; import { CassandraIcon } from "../icons/Cassandra"; import { ChromaIcon } from "../icons/ChromaIcon"; import { CohereIcon } from "../icons/Cohere"; +import { ComposioIcon } from "../icons/Composio"; import { ConfluenceIcon } from "../icons/Confluence"; import { CouchbaseIcon } from "../icons/Couchbase"; import { CrewAiIcon } from "../icons/CrewAI"; @@ -392,6 +393,7 @@ export const nodeIconsLucide: iconsType = { HuggingFaceEmbeddings: HuggingFaceIcon, IFixitLoader: IFixIcon, CrewAI: CrewAiIcon, + Composio: ComposioIcon, Meta: MetaIcon, CheckCheck, Midjorney: MidjourneyIcon,