diff --git a/pyproject.toml b/pyproject.toml index bd16677a2..c14019ba9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,7 @@ dependencies = [ "yfinance>=0.2.40,<1.0.0", "wolframalpha>=5.1.3,<6.0.0", "astra-assistants[tools]~=2.2.6", - "composio-langchain==0.5.9", + "composio-langchain==0.5.42", "spider-client>=0.0.27,<1.0.0", "nltk>=3.9.1,<4.0.0", "lark>=1.2.2,<2.0.0", diff --git a/src/backend/base/langflow/components/composio/composio_api.py b/src/backend/base/langflow/components/composio/composio_api.py index d830b8c36..2959f2950 100644 --- a/src/backend/base/langflow/components/composio/composio_api.py +++ b/src/backend/base/langflow/components/composio/composio_api.py @@ -1,13 +1,15 @@ from collections.abc import Sequence from typing import Any +from composio.client.collections import AppAuthScheme +from composio.client.exceptions import NoItemsFound from composio_langchain import Action, App, ComposioToolSet from langchain_core.tools import Tool from loguru import logger from typing_extensions import override from langflow.base.langchain_utilities.model import LCToolComponent -from langflow.inputs import DropdownInput, MessageTextInput, MultiselectInput, SecretStrInput, StrInput +from langflow.inputs import DropdownInput, LinkInput, MessageTextInput, MultiselectInput, SecretStrInput, StrInput class ComposioAPIComponent(LCToolComponent): @@ -34,20 +36,49 @@ class ComposioAPIComponent(LCToolComponent): info="The app name to use. Please refresh after selecting app name", refresh_button=True, ), + # Initially hidden fields for different auth types + SecretStrInput( + name="app_credentials", + display_name="App Credentials", + required=False, + dynamic=True, + show=False, + info="Credentials for app authentication (API Key, Password, etc)", + ), + MessageTextInput( + name="username", + display_name="Username", + required=False, + dynamic=True, + show=False, + info="Username for Basic authentication", + ), + LinkInput( + name="auth_link", + display_name="Authentication Link", + value="", + info="Click to authenticate with OAuth2", + dynamic=True, + show=False, + placeholder="Click to authenticate", + ), + StrInput( + name="auth_status", + display_name="Auth Status", + value="Not Connected", + info="Current authentication status", + dynamic=True, + show=False, + ), MultiselectInput( name="action_names", display_name="Actions to use", - required=False, + required=True, 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", + dynamic=True, + show=False, ), ] @@ -58,65 +89,110 @@ class ComposioAPIComponent(LCToolComponent): app (str): The app name to check authorization for. Returns: - str: The authorization status. + str: The authorization status or URL. """ toolset = self._build_wrapper() entity = toolset.client.get_entity(id=self.entity_id) try: + # Check if user is already connected entity.get_connection(app=app) - except Exception: # noqa: BLE001 - logger.opt(exception=True).debug("Authorization error") - return self._handle_authorization_failure(toolset, entity, app) - - return f"{app} CONNECTED" - - 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) - return self._initiate_default_connection(entity, app) + except NoItemsFound: + # Get auth scheme for the app + auth_scheme = self._get_auth_scheme(app) + return self._handle_auth_by_scheme(entity, app, auth_scheme) except Exception: # noqa: BLE001 logger.exception("Authorization error") - return "Error" + return "Error checking authorization" + else: + return f"{app} CONNECTED" - def _process_api_key_auth(self, entity: Any, app: str) -> str: - """Processes the API key authentication. + def _get_auth_scheme(self, app_name: str) -> AppAuthScheme: + """Get the primary auth scheme for an app. + + Args: + app_name (str): The name of the app to get auth scheme for. + + Returns: + AppAuthScheme: The auth scheme details. + """ + toolset = self._build_wrapper() + try: + return toolset.get_auth_scheme_for_app(app=app_name.lower()) + except Exception: # noqa: BLE001 + logger.exception(f"Error getting auth scheme for {app_name}") + return None + + def _handle_auth_by_scheme(self, entity: Any, app: str, auth_scheme: AppAuthScheme) -> str: + """Handle authentication based on the auth scheme. Args: entity (Any): The entity instance. app (str): The app name. + auth_scheme (AppAuthScheme): The auth scheme details. Returns: - str: The status of the API key authentication. + str: The authentication status or URL. """ - 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 + auth_mode = auth_scheme.auth_mode - if is_different_app or is_url or is_default_api_key_message: - return "Enter API Key" - 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, - ) + try: + # First check if already connected + entity.get_connection(app=app) + except NoItemsFound: + # If not connected, handle new connection based on auth mode + if auth_mode == "API_KEY": + if hasattr(self, "app_credentials") and self.app_credentials: + try: + entity.initiate_connection( + app_name=app, + auth_mode="API_KEY", + auth_config={"api_key": self.app_credentials}, + use_composio_auth=False, + force_new_integration=True, + ) + except Exception as e: # noqa: BLE001 + logger.error(f"Error connecting with API Key: {e}") + return "Invalid API Key" + else: + return f"{app} CONNECTED" + return "Enter API Key" + + if ( + auth_mode == "BASIC" + and hasattr(self, "username") + and hasattr(self, "app_credentials") + and self.username + and self.app_credentials + ): + try: + entity.initiate_connection( + app_name=app, + auth_mode="BASIC", + auth_config={"username": self.username, "password": self.app_credentials}, + use_composio_auth=False, + force_new_integration=True, + ) + except Exception as e: # noqa: BLE001 + logger.error(f"Error connecting with Basic Auth: {e}") + return "Invalid credentials" + else: + return f"{app} CONNECTED" + elif auth_mode == "BASIC": + return "Enter Username and Password" + + if auth_mode == "OAUTH2": + try: + return self._initiate_default_connection(entity, app) + except Exception as e: # noqa: BLE001 + logger.error(f"Error initiating OAuth2: {e}") + return "OAuth2 initialization failed" + + return "Unsupported auth mode" + except Exception as e: # noqa: BLE001 + logger.error(f"Error checking connection status: {e}") + return f"Error: {e!s}" + else: return f"{app} CONNECTED" - 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) @@ -145,24 +221,86 @@ class ComposioAPIComponent(LCToolComponent): @override def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: + # First, ensure all dynamic fields are hidden by default + dynamic_fields = ["app_credentials", "username", "auth_link", "auth_status", "action_names"] + for field in dynamic_fields: + if field in build_config: + build_config[field]["show"] = False + build_config[field]["advanced"] = True # Hide from main view + if field_name == "api_key": if hasattr(self, "api_key") and self.api_key != "": build_config = self._update_app_names_with_connected_status(build_config) + # Show app_names when API key is provided + build_config["app_names"]["show"] = True + build_config["app_names"]["advanced"] = False return build_config - if field_name in {"app_names", "auth_status_config"}: - if hasattr(self, "api_key") and self.api_key != "": - build_config["auth_status_config"]["value"] = self._check_for_authorization( - self._get_normalized_app_name() - ) - all_action_names = list(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 [""] + if field_name in {"app_names"} and hasattr(self, "api_key") and self.api_key != "": + app_name = self._get_normalized_app_name() + + try: + toolset = self._build_wrapper() + entity = toolset.client.get_entity(id=self.entity_id) + + # Always show auth_status when app is selected + build_config["auth_status"]["show"] = True + build_config["auth_status"]["advanced"] = False + + try: + # Check if already connected + entity.get_connection(app=app_name) + build_config["auth_status"]["value"] = f"{app_name} CONNECTED" + + # Show action selection for connected apps + build_config["action_names"]["show"] = True + build_config["action_names"]["advanced"] = False + + except NoItemsFound: + # Get auth scheme and show relevant fields + auth_scheme = self._get_auth_scheme(app_name) + auth_mode = auth_scheme.auth_mode + logger.info(f"Auth mode for {app_name}: {auth_mode}") + + if auth_mode == "API_KEY": + build_config["app_credentials"]["show"] = True + build_config["app_credentials"]["advanced"] = False + build_config["app_credentials"]["display_name"] = "API Key" + build_config["auth_status"]["value"] = "Enter API Key" + + elif auth_mode == "BASIC": + build_config["username"]["show"] = True + build_config["username"]["advanced"] = False + build_config["app_credentials"]["show"] = True + build_config["app_credentials"]["advanced"] = False + build_config["app_credentials"]["display_name"] = "Password" + build_config["auth_status"]["value"] = "Enter Username and Password" + + elif auth_mode == "OAUTH2": + build_config["auth_link"]["show"] = True + build_config["auth_link"]["advanced"] = False + auth_url = self._initiate_default_connection(entity, app_name) + build_config["auth_link"]["value"] = auth_url + build_config["auth_status"]["value"] = "Click link to authenticate" + + else: + build_config["auth_status"]["value"] = "Unsupported auth mode" + + # Update action names if connected + if build_config["auth_status"]["value"] == f"{app_name} CONNECTED": + all_action_names = list(Action.__annotations__) + app_action_names = [ + action_name + for action_name in all_action_names + if action_name.lower().startswith(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 [""] + + except Exception as e: # noqa: BLE001 + logger.error(f"Error checking auth status: {e}, app: {app_name}") + build_config["auth_status"]["value"] = f"Error: {e!s}" + return build_config def build_tool(self) -> Sequence[Tool]: @@ -170,4 +308,20 @@ class ComposioAPIComponent(LCToolComponent): return composio_toolset.get_tools(actions=self.action_names) def _build_wrapper(self) -> ComposioToolSet: - return ComposioToolSet(api_key=self.api_key) + """Build the Composio toolset wrapper. + + Returns: + ComposioToolSet: The initialized toolset. + + Raises: + ValueError: If the API key is not found or invalid. + """ + try: + if not self.api_key: + msg = "Composio API Key is required" + raise ValueError(msg) + return ComposioToolSet(api_key=self.api_key) + except ValueError as e: + logger.error(f"Error building Composio wrapper: {e}") + msg = "Please provide a valid Composio API Key in the component settings" + raise ValueError(msg) from e diff --git a/uv.lock b/uv.lock index cee245848..5c2ee0d4e 100644 --- a/uv.lock +++ b/uv.lock @@ -980,15 +980,12 @@ wheels = [ [[package]] name = "composio-core" -version = "0.5.9" +version = "0.5.42" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "click" }, - { name = "docker" }, - { name = "e2b-code-interpreter" }, { name = "fastapi" }, - { name = "gql" }, { name = "importlib-metadata" }, { name = "inflection" }, { name = "jsonref" }, @@ -998,20 +995,19 @@ dependencies = [ { name = "pyperclip" }, { name = "pysher" }, { name = "requests" }, - { name = "requests-toolbelt" }, { name = "rich" }, { name = "semver" }, { name = "sentry-sdk" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/a0/c1496ce0fb28171e4713499ef9bc849df44e5bcf3a14442f4069e66fe1da/composio_core-0.5.9.tar.gz", hash = "sha256:c401a86c72b4dae1d0e3075d52ec3a2b66ef6fdc46d232486e0992d8b4e97602", size = 252849 } +sdist = { url = "https://files.pythonhosted.org/packages/83/b9/a66a7646564f91937e2ac9a72c12308a37aac76dea88ea1d727a061bf153/composio_core-0.5.42.tar.gz", hash = "sha256:b38ea94e25807a6da813efb2c53ea8e01f71e54f0275ecfa55225887b36b6537", size = 295930 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/8f/96fe624e94121edaf852f526bf923f437554631a160363b33ab3338ca237/composio_core-0.5.9-py3-none-any.whl", hash = "sha256:a9f013b3f152fa9e730ad8e71bef07965ac40bbf8ecd6e4b439e1ff1b4a12474", size = 410561 }, + { url = "https://files.pythonhosted.org/packages/00/7a/45acb7b6bcca477712f89bd43888393f365dca0270062e97389e3653aec8/composio_core-0.5.42-py3-none-any.whl", hash = "sha256:351ddee8ac8203ea8039098c8afce7c43b1cf4a97eccbb2046c489a3a53afa54", size = 457643 }, ] [[package]] name = "composio-langchain" -version = "0.5.9" +version = "0.5.42" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "composio-core" }, @@ -1020,9 +1016,9 @@ dependencies = [ { name = "langchainhub" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/7c/f95092b589677c2a5bda9bf4cc8ff6b652c7c2c151a723c25a875d70a893/composio_langchain-0.5.9.tar.gz", hash = "sha256:9b0c5dff2545615180c5892d622fd38aa3a61a987871725dff555413b45078f9", size = 3973 } +sdist = { url = "https://files.pythonhosted.org/packages/18/3e/fa76297c590405d17ca8183396a51ae461845f82e232e64f68f383e2f804/composio_langchain-0.5.42.tar.gz", hash = "sha256:ed95acce4603099eccc022cd42f75df876dc566ecd0ed509bb33d18d0c541358", size = 4281 } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/a8/c117bc0201a82c2b2c9e66f59aa54b20d6327cdbc0cfb676e0dd78143ebc/composio_langchain-0.5.9-py3-none-any.whl", hash = "sha256:245caaefb769a98dbfb7a715d283f811b459c16a54e1c8579be5b23acc2e55ae", size = 4389 }, + { url = "https://files.pythonhosted.org/packages/da/a4/4d65985c49db8d828ef39843003c443919fa9a285d3244dba3aaedbd4fd1/composio_langchain-0.5.42-py3-none-any.whl", hash = "sha256:ddbffa94c5ad7105bdc877e0103a5633cca8c736a7b6853230078e143e718700", size = 4701 }, ] [[package]] @@ -2257,21 +2253,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/87/8dde0a3757bc207805f751b47878888b09db4a464ae48a55f386f091b488/gptcache-0.1.44-py3-none-any.whl", hash = "sha256:11ddd63b173dc3822b8c2eb7588ea947c825845ed0737b043038a238286bfec4", size = 131634 }, ] -[[package]] -name = "gql" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "backoff" }, - { name = "graphql-core" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/85/feda24b33adcc6c8463a62a8e2ca2cc3425dc6d687388ff728ceae231204/gql-3.5.0.tar.gz", hash = "sha256:ccb9c5db543682b28f577069950488218ed65d4ac70bb03b6929aaadaf636de9", size = 179939 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/fb/01a200e1c31b79690427c8e983014e4220d2652b4372a46fe4598e1d7a8e/gql-3.5.0-py2.py3-none-any.whl", hash = "sha256:70dda5694a5b194a8441f077aa5fb70cc94e4ec08016117523f013680901ecb7", size = 74001 }, -] - [[package]] name = "grandalf" version = "0.8" @@ -2284,15 +2265,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/30/44c7eb0a952478dbb5f2f67df806686d6a7e4b19f6204e091c4f49dc7c69/grandalf-0.8-py3-none-any.whl", hash = "sha256:793ca254442f4a79252ea9ff1ab998e852c1e071b863593e5383afee906b4185", size = 41802 }, ] -[[package]] -name = "graphql-core" -version = "3.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/9e/aa527fb09a9d7399d5d7d2aa2da490e4580707652d3b4fc156996ae88a5b/graphql-core-3.2.4.tar.gz", hash = "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264", size = 504611 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/cc72c4c658c6316f188a60bc4e5a91cd4ceaaa8c3e7e691ac9297e4e72c7/graphql_core-3.2.4-py3-none-any.whl", hash = "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0", size = 203179 }, -] - [[package]] name = "greenlet" version = "3.1.1" @@ -3673,8 +3645,8 @@ requires-dist = [ { name = "certifi", specifier = ">=2023.11.17,<2025.0.0" }, { name = "chromadb", specifier = ">=0.4,<1.0.0" }, { name = "clickhouse-connect", marker = "extra == 'clickhouse-connect'", specifier = "==0.7.19" }, + { name = "composio-langchain", specifier = "==0.5.42" }, { name = "cohere", specifier = ">=5.5.3,<6.0.0" }, - { name = "composio-langchain", specifier = "==0.5.9" }, { name = "couchbase", marker = "extra == 'couchbase'", specifier = ">=4.2.1" }, { name = "ctransformers", marker = "extra == 'local'", specifier = ">=0.2.10" }, { name = "dspy-ai", specifier = ">=2.4.0,<3.0.0" },