diff --git a/.composio.lock b/.composio.lock new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/.composio.lock @@ -0,0 +1 @@ +{} diff --git a/pyproject.toml b/pyproject.toml index cf60e8827..5fc94c13c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,8 +66,8 @@ dependencies = [ "yfinance==0.2.50", "wolframalpha==5.1.3", "astra-assistants[tools]~=2.2.11", - "composio-langchain==0.7.1", - "composio-core==0.7.1", + "composio-langchain==0.7.12", + "composio-core==0.7.12", "spider-client==0.1.24", "nltk==3.9.1", "lark==1.2.2", diff --git a/src/backend/base/langflow/components/composio/composio_api.py b/src/backend/base/langflow/components/composio/composio_api.py index fcd69e065..cef283588 100644 --- a/src/backend/base/langflow/components/composio/composio_api.py +++ b/src/backend/base/langflow/components/composio/composio_api.py @@ -2,20 +2,25 @@ from collections.abc import Sequence from typing import Any -import requests +from composio import Action, App # Third-party imports -from composio.client.collections import AppAuthScheme -from composio.client.exceptions import NoItemsFound -from composio_langchain import Action, ComposioToolSet +from composio_langchain import ComposioToolSet from langchain_core.tools import Tool -from loguru import logger # Local imports from langflow.base.langchain_utilities.model import LCToolComponent -from langflow.inputs import DropdownInput, LinkInput, MessageTextInput, MultiselectInput, SecretStrInput, StrInput +from langflow.inputs import ( + ConnectionInput, + MessageTextInput, + SecretStrInput, + SortableListInput, +) from langflow.io import Output +# TODO: We get the list from the API but we need to filter it +enabled_tools = ["confluence", "discord", "dropbox", "github", "gmail", "linkedin", "notion", "slack", "youtube"] + class ComposioAPIComponent(LCToolComponent): display_name: str = "Composio Tools" @@ -34,58 +39,28 @@ class ComposioAPIComponent(LCToolComponent): info="Refer to https://docs.composio.dev/faq/api_key/api_key", real_time_refresh=True, ), - DropdownInput( - name="app_names", - display_name="App Name", + ConnectionInput( + name="tool_name", + display_name="Tool Name", + placeholder="Select a tool...", + button_metadata={"icon": "unplug", "variant": "destructive"}, + options=[], + search_category=[], + value="", + connection_link="", + info="The name of the tool to use", + real_time_refresh=True, + ), + SortableListInput( + name="actions", + display_name="Actions", + placeholder="Select action", + helper_text="Please connect before selecting actions.", + helper_text_metadata={"icon": "OctagonAlert", "variant": "destructive"}, options=[], value="", - info="The app name to use. Please refresh after selecting app name", - refresh_button=True, - required=True, - ), - # Authentication-related inputs (initially hidden) - SecretStrInput( - name="app_credentials", - display_name="App Credentials", - required=False, - dynamic=True, - show=False, - info="Credentials for app authentication (API Key, Password, etc)", - load_from_db=False, - ), - 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=True, - options=[], - value=[], - info="The actions to pass to agent to execute", - dynamic=True, + info="The actions to use", + limit=1, show=False, ), ] @@ -94,256 +69,191 @@ class ComposioAPIComponent(LCToolComponent): Output(name="tools", display_name="Tools", method="build_tool"), ] - def _check_for_authorization(self, app: str) -> str: - """Checks if the app is authorized. + def sanitize_action_name(self, action_name: str) -> str: + # TODO: Maybe restore + return action_name - Args: - app (str): The app name to check authorization for. + # We want to use title case, and replace underscores with spaces + sanitized_name = action_name.replace("_", " ").title() - Returns: - 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 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 checking authorization" - else: - return f"{app} CONNECTED" + # Now we want to remove everything from and including the first dot + return sanitized_name.replace(self.tool_name.title() + " ", "") - def _get_auth_scheme(self, app_name: str) -> AppAuthScheme: - """Get the primary auth scheme for an app. + def desanitize_action_name(self, action_name: str) -> str: + # TODO: Maybe restore + return action_name - Args: - app_name (str): The name of the app to get auth scheme for. + # We want to reverse what we did above + unsanitized_name = action_name.replace(" ", "_").upper() - 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 + # Append the tool_name to it at the beginning, followed by a dot, in all CAPS + return f"{self.tool_name.upper()}_{unsanitized_name}" - def _get_oauth_apps(self, api_key: str) -> list[str]: - """Fetch OAuth-enabled apps from Composio API. + def validate_tool(self, build_config: dict, field_value: Any, connected_app_names: list) -> dict: + # Get the index of the selected tool in the list of options + selected_tool_index = next( + ( + ind + for ind, tool in enumerate(build_config["tool_name"]["options"]) + if tool["name"] == field_value + or ("validate" in field_value and tool["name"] == field_value["validate"]) + ), + None, + ) - Args: - api_key (str): The Composio API key. + # Set the link to be the text 'validated' + build_config["tool_name"]["options"][selected_tool_index]["link"] = "validated" - Returns: - list[str]: A list containing OAuth-enabled app names. - """ - oauth_apps = [] - try: - url = "https://backend.composio.dev/api/v1/apps" - headers = {"x-api-key": api_key} - params = { - "includeLocal": "true", - "additionalFields": "auth_schemes", - "sortBy": "alphabet", + # Set the helper text and helper text metadata field of the actions now + build_config["actions"]["helper_text"] = "" + build_config["actions"]["helper_text_metadata"] = {"icon": "Check", "variant": "success"} + + # Get the list of actions available + all_actions = list(Action.all()) + authenticated_actions = sorted( + [ + action + for action in all_actions + if action.app.lower() in list(connected_app_names) and action.app.lower() == self.tool_name.lower() + ], + key=lambda x: x.name, + ) + + # Return the list of action names + build_config["actions"]["options"] = [ + { + "name": self.sanitize_action_name(action.name), } + for action in authenticated_actions + ] - response = requests.get(url, headers=headers, params=params, timeout=20) - data = response.json() + # Lastly, we need to show the actions field + build_config["actions"]["show"] = True - for item in data.get("items", []): - for auth_scheme in item.get("auth_schemes", []): - if auth_scheme.get("mode") in {"OAUTH1", "OAUTH2"}: - oauth_apps.append(item["key"].upper()) - break - except requests.RequestException as e: - logger.error(f"Error fetching OAuth apps: {e}") - return [] - else: - return oauth_apps + return build_config - def _handle_auth_by_scheme(self, entity: Any, app: str, auth_scheme: AppAuthScheme) -> str: - """Handle authentication based on the auth scheme. + def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: + # If the list of tools is not available, always update it + if field_name == "api_key" or (self.api_key and not build_config["tool_name"]["options"]): + if field_name == "api_key" and not field_value: + # Reset the list of tools + build_config["tool_name"]["options"] = [] + build_config["tool_name"]["value"] = "" - Args: - entity (Any): The entity instance. - app (str): The app name. - auth_scheme (AppAuthScheme): The auth scheme details. + # Reset the list of actions + build_config["actions"]["show"] = False + build_config["actions"]["options"] = [] + build_config["actions"]["value"] = "" - Returns: - str: The authentication status or URL. - """ - auth_mode = auth_scheme.auth_mode + return build_config - 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" + # TODO: Re-enable dynamic tool list + # Initialize the Composio ToolSet with your API key + # toolset = ComposioToolSet(api_key=self.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" + # Get the entity (e.g., "default" for your user) + # entity = toolset.get_entity(self.entity_id) - 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" + # Get all available apps + # all_apps = entity.client.apps.get() - 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" + # Build an object with name, icon, link + build_config["tool_name"]["options"] = [ + { + "name": app.title(), # TODO: Switch to app.name + "icon": app, # TODO: Switch to app.name + "link": ( + build_config["tool_name"]["options"][ind]["link"] + if build_config["tool_name"]["options"] + else "" + ), + } + # for app in sorted(all_apps, key=lambda x: x.name) + for ind, app in enumerate(enabled_tools) + ] - 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({connection.appUniqueId for connection in connections}) - - def _get_normalized_app_name(self) -> str: - """Get app name without connection status suffix. - - Returns: - str: Normalized app name. - """ - return self.app_names.replace(" ✅", "").replace("_connected", "") - - def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: # noqa: ARG002 - # Update the available apps options from the API - if hasattr(self, "api_key") and self.api_key != "": - toolset = self._build_wrapper() - build_config["app_names"]["options"] = self._get_oauth_apps(api_key=self.api_key) - - # 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: - if build_config[field]["value"] is None or build_config[field]["value"] == "": - build_config[field]["show"] = False - build_config[field]["advanced"] = True - build_config[field]["load_from_db"] = False - else: - build_config[field]["show"] = True - build_config[field]["advanced"] = False - - if field_name == "app_names" and (not hasattr(self, "app_names") or not self.app_names): - build_config["auth_status"]["show"] = True - build_config["auth_status"]["value"] = "Please select an app first" return build_config - if field_name == "app_names" and hasattr(self, "api_key") and self.api_key != "": - # app_name = self._get_normalized_app_name() - app_name = self.app_names + # Handle the click of the Tool Name connect button + if field_name == "tool_name" and field_value: + # Get the list of apps (tools) we have connected + toolset = ComposioToolSet(api_key=self.api_key) + connected_apps = [app for app in toolset.get_connected_accounts() if app.status == "ACTIVE"] + + # Get the unique list of appName from the connected apps + connected_app_names = [app.appName.lower() for app in connected_apps] + + # Clear out the list of selected actions + build_config["actions"]["show"] = True + build_config["actions"]["options"] = [] + build_config["actions"]["value"] = "" + + # Clear out any helper text + build_config["tool_name"]["helper_text"] = "" + build_config["tool_name"]["helper_text_metadata"] = {} + + # If it's a dictionary, we need to do validation + if isinstance(field_value, dict): + # If the current field value is a dictionary, it means the user has selected a tool + if "validate" not in field_value: + return build_config + + # Check if the selected tool is connected + check_app = field_value["validate"].lower() + + # If the tool selected is NOT what we are validating, return the build config + if check_app != self.tool_name.lower(): + # Set the helper text and helper text metadata field of the actions now + build_config["actions"]["helper_text"] = "Please connect before selecting actions." + build_config["actions"]["helper_text_metadata"] = { + "icon": "OctagonAlert", + "variant": "destructive", + } + + return build_config + + # Check if the tool is already validated + if check_app not in connected_app_names: + return build_config + + # Validate the selected tool + return self.validate_tool(build_config, field_value, connected_app_names) + + # Check if the tool is already validated + if field_value.lower() in connected_app_names: + return self.validate_tool(build_config, field_value, connected_app_names) + + # Get the entity (e.g., "default" for your user) + entity = toolset.get_entity(id=self.entity_id) + + # Set the metadata for the actions + build_config["actions"]["helper_text_metadata"] = {"icon": "OctagonAlert", "variant": "destructive"} + + # Get the index of the selected tool in the list of options + selected_tool_index = next( + (ind for ind, tool in enumerate(build_config["tool_name"]["options"]) if tool["name"] == field_value), + None, + ) + + # Initiate a GitHub connection and get the redirect URL try: - toolset = self._build_wrapper() - entity = toolset.client.get_entity(id=self.entity_id) + connection_request = entity.initiate_connection(app_name=getattr(App, field_value.upper())) + except Exception as _: # noqa: BLE001 + # Indicate that there was an error connecting to the tool + build_config["tool_name"]["options"][selected_tool_index]["link"] = "error" + build_config["tool_name"]["helper_text"] = f"Error connecting to {field_value}" + build_config["tool_name"]["helper_text_metadata"] = { + "icon": "OctagonAlert", + "variant": "destructive", + } - # Always show auth_status when app is selected - build_config["auth_status"]["show"] = True - build_config["auth_status"]["advanced"] = False + return build_config - try: - # Check if already connected - entity.get_connection(app=app_name) - build_config["auth_status"]["value"] = "✅" - build_config["auth_link"]["show"] = False - # Show action selection for connected apps - build_config["action_names"]["show"] = True - build_config["action_names"]["advanced"] = False + # Print the direct HTTP link for authentication + build_config["tool_name"]["options"][selected_tool_index]["link"] = connection_request.redirectUrl - 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"] == "✅": - all_action_names = [str(action).replace("Action.", "") for action in Action.all()] - app_action_names = [ - action_name - for action_name in all_action_names - if action_name.lower().startswith(app_name.lower() + "_") - ] - if build_config["action_names"]["options"] != app_action_names: - 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}" + # Set the helper text and helper text metadata field of the actions now + build_config["actions"]["helper_text"] = "Please connect before selecting actions." return build_config @@ -354,7 +264,9 @@ class ComposioAPIComponent(LCToolComponent): Sequence[Tool]: List of configured Composio tools. """ composio_toolset = self._build_wrapper() - return composio_toolset.get_tools(actions=self.action_names) + return composio_toolset.get_tools( + actions=[self.desanitize_action_name(action["name"]) for action in self.actions] + ) def _build_wrapper(self) -> ComposioToolSet: """Build the Composio toolset wrapper. @@ -371,6 +283,6 @@ class ComposioAPIComponent(LCToolComponent): raise ValueError(msg) return ComposioToolSet(api_key=self.api_key, entity_id=self.entity_id) except ValueError as e: - logger.error(f"Error building Composio wrapper: {e}") + self.log(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/src/backend/base/langflow/initial_setup/starter_projects/Gmail Agent.json b/src/backend/base/langflow/initial_setup/starter_projects/Gmail Agent.json index 569dfd194..9f7eca95c 100644 --- a/src/backend/base/langflow/initial_setup/starter_projects/Gmail Agent.json +++ b/src/backend/base/langflow/initial_setup/starter_projects/Gmail Agent.json @@ -7,7 +7,7 @@ "data": { "sourceHandle": { "dataType": "ChatInput", - "id": "ChatInput-auIvg", + "id": "ChatInput-Vdvzc", "name": "message", "output_types": [ "Message" @@ -15,19 +15,19 @@ }, "targetHandle": { "fieldName": "input_value", - "id": "Agent-gCLrf", + "id": "Agent-chjSc", "inputTypes": [ "Message" ], "type": "str" } }, - "id": "reactflow__edge-ChatInput-auIvg{œdataTypeœ:œChatInputœ,œidœ:œChatInput-auIvgœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-Agent-gCLrf{œfieldNameœ:œinput_valueœ,œidœ:œAgent-gCLrfœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-ChatInput-Vdvzc{œdataTypeœ:œChatInputœ,œidœ:œChatInput-Vdvzcœ,œnameœ:œmessageœ,œoutput_typesœ:[œMessageœ]}-Agent-chjSc{œfieldNameœ:œinput_valueœ,œidœ:œAgent-chjScœ,œinputTypesœ:[œMessageœ],œtypeœ:œstrœ}", "selected": false, - "source": "ChatInput-auIvg", - "sourceHandle": "{œdataTypeœ: œChatInputœ, œidœ: œChatInput-auIvgœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", - "target": "Agent-gCLrf", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œAgent-gCLrfœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" + "source": "ChatInput-Vdvzc", + "sourceHandle": "{œdataTypeœ: œChatInputœ, œidœ: œChatInput-Vdvzcœ, œnameœ: œmessageœ, œoutput_typesœ: [œMessageœ]}", + "target": "Agent-chjSc", + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œAgent-chjScœ, œinputTypesœ: [œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, @@ -35,7 +35,7 @@ "data": { "sourceHandle": { "dataType": "Agent", - "id": "Agent-gCLrf", + "id": "Agent-chjSc", "name": "response", "output_types": [ "Message" @@ -43,7 +43,7 @@ }, "targetHandle": { "fieldName": "input_value", - "id": "ChatOutput-8WiQm", + "id": "ChatOutput-fXi9i", "inputTypes": [ "Data", "DataFrame", @@ -52,20 +52,19 @@ "type": "str" } }, - "id": "reactflow__edge-Agent-gCLrf{œdataTypeœ:œAgentœ,œidœ:œAgent-gCLrfœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-8WiQm{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-8WiQmœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", + "id": "reactflow__edge-Agent-chjSc{œdataTypeœ:œAgentœ,œidœ:œAgent-chjScœ,œnameœ:œresponseœ,œoutput_typesœ:[œMessageœ]}-ChatOutput-fXi9i{œfieldNameœ:œinput_valueœ,œidœ:œChatOutput-fXi9iœ,œinputTypesœ:[œDataœ,œDataFrameœ,œMessageœ],œtypeœ:œstrœ}", "selected": false, - "source": "Agent-gCLrf", - "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-gCLrfœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", - "target": "ChatOutput-8WiQm", - "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-8WiQmœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" + "source": "Agent-chjSc", + "sourceHandle": "{œdataTypeœ: œAgentœ, œidœ: œAgent-chjScœ, œnameœ: œresponseœ, œoutput_typesœ: [œMessageœ]}", + "target": "ChatOutput-fXi9i", + "targetHandle": "{œfieldNameœ: œinput_valueœ, œidœ: œChatOutput-fXi9iœ, œinputTypesœ: [œDataœ, œDataFrameœ, œMessageœ], œtypeœ: œstrœ}" }, { "animated": false, - "className": "", "data": { "sourceHandle": { "dataType": "ComposioAPI", - "id": "ComposioAPI-Tdreq", + "id": "ComposioAPI-4heel", "name": "tools", "output_types": [ "Tool" @@ -73,25 +72,25 @@ }, "targetHandle": { "fieldName": "tools", - "id": "Agent-gCLrf", + "id": "Agent-chjSc", "inputTypes": [ "Tool" ], "type": "other" } }, - "id": "reactflow__edge-ComposioAPI-Tdreq{œdataTypeœ:œComposioAPIœ,œidœ:œComposioAPI-Tdreqœ,œnameœ:œtoolsœ,œoutput_typesœ:[œToolœ]}-Agent-gCLrf{œfieldNameœ:œtoolsœ,œidœ:œAgent-gCLrfœ,œinputTypesœ:[œToolœ],œtypeœ:œotherœ}", + "id": "xy-edge__ComposioAPI-4heel{œdataTypeœ:œComposioAPIœ,œidœ:œComposioAPI-4heelœ,œnameœ:œtoolsœ,œoutput_typesœ:[œToolœ]}-Agent-chjSc{œfieldNameœ:œtoolsœ,œidœ:œAgent-chjScœ,œinputTypesœ:[œToolœ],œtypeœ:œotherœ}", "selected": false, - "source": "ComposioAPI-Tdreq", - "sourceHandle": "{œdataTypeœ: œComposioAPIœ, œidœ: œComposioAPI-Tdreqœ, œnameœ: œtoolsœ, œoutput_typesœ: [œToolœ]}", - "target": "Agent-gCLrf", - "targetHandle": "{œfieldNameœ: œtoolsœ, œidœ: œAgent-gCLrfœ, œinputTypesœ: [œToolœ], œtypeœ: œotherœ}" + "source": "ComposioAPI-4heel", + "sourceHandle": "{œdataTypeœ: œComposioAPIœ, œidœ: œComposioAPI-4heelœ, œnameœ: œtoolsœ, œoutput_typesœ: [œToolœ]}", + "target": "Agent-chjSc", + "targetHandle": "{œfieldNameœ: œtoolsœ, œidœ: œAgent-chjScœ, œinputTypesœ: [œToolœ], œtypeœ: œotherœ}" } ], "nodes": [ { "data": { - "id": "Agent-gCLrf", + "id": "Agent-chjSc", "node": { "base_classes": [ "Message" @@ -743,7 +742,7 @@ "type": "Agent" }, "dragging": false, - "id": "Agent-gCLrf", + "id": "Agent-chjSc", "measured": { "height": 624, "width": 320 @@ -757,7 +756,7 @@ }, { "data": { - "id": "ChatInput-auIvg", + "id": "ChatInput-Vdvzc", "node": { "base_classes": [ "Message" @@ -1055,7 +1054,7 @@ "type": "ChatInput" }, "dragging": false, - "id": "ChatInput-auIvg", + "id": "ChatInput-Vdvzc", "measured": { "height": 66, "width": 192 @@ -1069,7 +1068,7 @@ }, { "data": { - "id": "ChatOutput-8WiQm", + "id": "ChatOutput-fXi9i", "node": { "base_classes": [ "Message" @@ -1367,7 +1366,7 @@ "type": "ChatOutput" }, "dragging": false, - "id": "ChatOutput-8WiQm", + "id": "ChatOutput-fXi9i", "measured": { "height": 66, "width": 192 @@ -1381,9 +1380,9 @@ }, { "data": { - "id": "note-y0nez", + "id": "note-FzaUv", "node": { - "description": "# Gmail Agent\nUsing this flow you can send emails, create drafts, fetch emails and more\n\n## Instructions\n\n1. Get Composio API Key\n - Visit https://app.composio.dev\n - Enter the key in the \"Composio API Key\" field\n\n2. Authenticate Gmail Account\n - Select Gmail App from the dropdown menu in the App Names field\n - Click the refresh button next to the App Name\n - Follow the Gmail authentication link\n - After authenticating, click refresh again\n - Verify that authentication status shows as successful\n\n3. Select Actions\n - Default actions (pre-selected):\n - GMAIL_SEND_EMAIL: Send emails directly\n - GMAIL_CREATE_EMAIL_DRAFT: Create draft emails\n - Select additional actions based on your needs\n\n4. Configure OpenAI\n - Enter your OpenAI API key in the Agent OpenAI API key field\n\n5. Run Agent\n Example prompts:\n - \"Send an email to johndoe@gmail.com wishing them Happy birthday!\"\n - \"Create a draft email about project updates\"", + "description": "# Gmail Agent\nUsing this flow you can send emails, create drafts, fetch emails and more\n\n## Instructions\n\n1. Get Composio API Key\n - Visit https://app.composio.dev\n - Enter the key in the \"Composio API Key\" field\n\n2. Authenticate Gmail Account\n - Select Gmail App from the dropdown menu in the Tool Name field\n - Follow the Gmail authentication link\n - Verify that authentication status shows as successful\n\n3. Select an Action\n - GMAIL_SEND_EMAIL: Send emails directly\n - GMAIL_CREATE_EMAIL_DRAFT: Create draft emails\n - Select other actions based on your needs\n\n4. Configure OpenAI\n - Enter your OpenAI API key in the Agent OpenAI API key field\n\n5. Run Agent\n Example prompts:\n - \"Send an email to johndoe@gmail.com wishing them Happy birthday!\"\n - \"Create a draft email about project updates\"", "display_name": "", "documentation": "", "template": {} @@ -1392,7 +1391,7 @@ }, "dragging": false, "height": 842, - "id": "note-y0nez", + "id": "note-FzaUv", "measured": { "height": 842, "width": 395 @@ -1402,15 +1401,13 @@ "y": -87.30330362954265 }, "resizing": false, - "selected": false, + "selected": true, "type": "noteNode", "width": 394 }, { "data": { - "description": "Use Composio toolset to run actions with your agent", - "display_name": "Composio Tools", - "id": "ComposioAPI-Tdreq", + "id": "ComposioAPI-4heel", "node": { "base_classes": [ "Tool" @@ -1425,17 +1422,12 @@ "field_order": [ "entity_id", "api_key", - "app_names", - "app_credentials", - "username", - "auth_link", - "auth_status", - "action_names" + "tool_name", + "actions" ], "frozen": false, "icon": "Composio", "legacy": false, - "lf_version": "1.2.0", "metadata": {}, "minimized": false, "output_types": [], @@ -1460,49 +1452,29 @@ "pinned": false, "template": { "_type": "Component", - "action_names": { - "_input_type": "MultiselectInput", + "actions": { + "_input_type": "SortableListInput", "advanced": false, - "combobox": false, - "display_name": "Actions to use", - "dynamic": true, - "info": "The actions to pass to agent to execute", - "list": true, - "list_add_label": "Add More", - "load_from_db": false, - "name": "action_names", - "options": [ - "GMAIL_GET_PEOPLE", - "GMAIL_FETCH_EMAILS", - "GMAIL_FETCH_MESSAGE_BY_THREAD_ID", - "GMAIL_SEARCH_PEOPLE", - "GMAIL_SEND_EMAIL", - "GMAIL_CREATE_EMAIL_DRAFT", - "GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID", - "GMAIL_CREATE_LABEL", - "GMAIL_GET_ATTACHMENT", - "GMAIL_FIND_EMAIL_ID", - "GMAIL_REMOVE_LABEL", - "GMAIL_GET_PROFILE", - "GMAIL_ADD_LABEL_TO_EMAIL", - "GMAIL_GET_CONTACTS", - "GMAIL_REPLY_TO_THREAD", - "GMAIL_LIST_LABELS", - "GMAIL_FETCH_LAST_THREE_MESSAGES", - "GMAIL_LIST_THREADS", - "GMAIL_FETCH_EMAILS_WITH_LABEL", - "GMAIL_MODIFY_THREAD_LABELS" - ], - "placeholder": "", - "required": true, - "show": true, + "display_name": "Actions", + "dynamic": false, + "helper_text": "Please connect before selecting actions.", + "helper_text_metadata": { + "icon": "OctagonAlert", + "variant": "destructive" + }, + "info": "The actions to use", + "limit": 1, + "name": "actions", + "options": [], + "placeholder": "Select action", + "required": false, + "search_category": [], + "show": false, "title_case": false, "tool_mode": false, "trace_as_metadata": true, - "type": "str", - "value": [ - "GMAIL_GET_PEOPLE" - ] + "type": "sortableList", + "value": "" }, "api_key": { "_input_type": "SecretStrInput", @@ -1524,208 +1496,6 @@ "type": "str", "value": "COMPOSIO_API_KEY" }, - "app_credentials": { - "_input_type": "SecretStrInput", - "advanced": true, - "display_name": "App Credentials", - "dynamic": true, - "info": "Credentials for app authentication (API Key, Password, etc)", - "input_types": [ - "Message" - ], - "load_from_db": false, - "name": "app_credentials", - "password": true, - "placeholder": "", - "required": false, - "show": false, - "title_case": false, - "type": "str", - "value": "" - }, - "app_names": { - "_input_type": "DropdownInput", - "advanced": false, - "combobox": false, - "dialog_inputs": {}, - "display_name": "App Name", - "dynamic": false, - "info": "The app name to use. Please refresh after selecting app name", - "load_from_db": false, - "name": "app_names", - "options": [ - "ACCELO", - "AIRTABLE", - "AMAZON", - "APALEO", - "ASANA", - "ATLASSIAN", - "ATTIO", - "AUTH0", - "BATTLENET", - "BITBUCKET", - "BLACKBAUD", - "BLACKBOARD", - "BOLDSIGN", - "BORNEO", - "BOX", - "BRAINTREE", - "BREX", - "BREX_STAGING", - "BRIGHTPEARL", - "CALENDLY", - "CANVA", - "CANVAS", - "CHATWORK", - "CLICKUP", - "CONFLUENCE", - "CONTENTFUL", - "D2LBRIGHTSPACE", - "DEEL", - "DISCORD", - "DISCORDBOT", - "DOCUSIGN", - "DROPBOX", - "DROPBOX_SIGN", - "DYNAMICS365", - "EPIC_GAMES", - "EVENTBRITE", - "EXIST", - "FACEBOOK", - "FIGMA", - "FITBIT", - "FRESHBOOKS", - "FRONT", - "GITHUB", - "GMAIL", - "GMAIL_BETA", - "GO_TO_WEBINAR", - "GOOGLE_ANALYTICS", - "GOOGLE_DRIVE_BETA", - "GOOGLE_MAPS", - "GOOGLECALENDAR", - "GOOGLEDOCS", - "GOOGLEDRIVE", - "GOOGLEMEET", - "GOOGLEPHOTOS", - "GOOGLESHEETS", - "GOOGLETASKS", - "GORGIAS", - "GUMROAD", - "HARVEST", - "HIGHLEVEL", - "HUBSPOT", - "ICIMS_TALENT_CLOUD", - "INTERCOM", - "JIRA", - "KEAP", - "KLAVIYO", - "LASTPASS", - "LEVER", - "LEVER_SANDBOX", - "LINEAR", - "LINKEDIN", - "LINKHUT", - "MAILCHIMP", - "MICROSOFT_TEAMS", - "MICROSOFT_TENANT", - "MIRO", - "MONDAY", - "MURAL", - "NETSUITE", - "NOTION", - "ONE_DRIVE", - "OUTLOOK", - "PAGERDUTY", - "PIPEDRIVE", - "PRODUCTBOARD", - "REDDIT", - "RING_CENTRAL", - "RIPPLING", - "SAGE", - "SALESFORCE", - "SEISMIC", - "SERVICEM8", - "SHARE_POINT", - "SHOPIFY", - "SLACK", - "SLACKBOT", - "SMARTRECRUITERS", - "SPOTIFY", - "SQUARE", - "STACK_EXCHANGE", - "SURVEY_MONKEY", - "TIMELY", - "TODOIST", - "TONEDEN", - "TRELLO", - "TWITCH", - "TWITTER", - "TWITTER_MEDIA", - "WAKATIME", - "WAVE_ACCOUNTING", - "WEBEX", - "WIZ", - "WRIKE", - "XERO", - "YANDEX", - "YNAB", - "YOUTUBE", - "ZENDESK", - "ZOHO", - "ZOHO_BIGIN", - "ZOHO_BOOKS", - "ZOHO_DESK", - "ZOHO_INVENTORY", - "ZOHO_INVOICE", - "ZOHO_MAIL", - "ZOOM" - ], - "options_metadata": [], - "placeholder": "", - "refresh_button": true, - "required": true, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "str", - "value": "GMAIL" - }, - "auth_link": { - "_input_type": "LinkInput", - "advanced": true, - "display_name": "Authentication Link", - "dynamic": true, - "info": "Click to authenticate with OAuth2", - "load_from_db": false, - "name": "auth_link", - "placeholder": "Click to authenticate", - "required": false, - "show": false, - "title_case": false, - "type": "link", - "value": "" - }, - "auth_status": { - "_input_type": "StrInput", - "advanced": false, - "display_name": "Auth Status", - "dynamic": true, - "info": "Current authentication status", - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "auth_status", - "placeholder": "", - "required": false, - "show": true, - "title_case": false, - "tool_mode": false, - "trace_as_metadata": true, - "type": "str", - "value": "✅" - }, "code": { "advanced": true, "dynamic": true, @@ -1742,7 +1512,7 @@ "show": true, "title_case": false, "type": "code", - "value": "# Standard library imports\nfrom collections.abc import Sequence\nfrom typing import Any\n\nimport requests\n\n# Third-party imports\nfrom composio.client.collections import AppAuthScheme\nfrom composio.client.exceptions import NoItemsFound\nfrom composio_langchain import Action, ComposioToolSet\nfrom langchain_core.tools import Tool\nfrom loguru import logger\n\n# Local imports\nfrom langflow.base.langchain_utilities.model import LCToolComponent\nfrom langflow.inputs import DropdownInput, LinkInput, MessageTextInput, MultiselectInput, SecretStrInput, StrInput\nfrom langflow.io import Output\n\n\nclass ComposioAPIComponent(LCToolComponent):\n display_name: str = \"Composio Tools\"\n description: str = \"Use Composio toolset to run actions with your agent\"\n name = \"ComposioAPI\"\n icon = \"Composio\"\n documentation: str = \"https://docs.composio.dev\"\n\n inputs = [\n # Basic configuration inputs\n MessageTextInput(name=\"entity_id\", display_name=\"Entity ID\", value=\"default\", advanced=True),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Composio API Key\",\n required=True,\n info=\"Refer to https://docs.composio.dev/faq/api_key/api_key\",\n real_time_refresh=True,\n ),\n DropdownInput(\n name=\"app_names\",\n display_name=\"App Name\",\n options=[],\n value=\"\",\n info=\"The app name to use. Please refresh after selecting app name\",\n refresh_button=True,\n required=True,\n ),\n # Authentication-related inputs (initially hidden)\n SecretStrInput(\n name=\"app_credentials\",\n display_name=\"App Credentials\",\n required=False,\n dynamic=True,\n show=False,\n info=\"Credentials for app authentication (API Key, Password, etc)\",\n load_from_db=False,\n ),\n MessageTextInput(\n name=\"username\",\n display_name=\"Username\",\n required=False,\n dynamic=True,\n show=False,\n info=\"Username for Basic authentication\",\n ),\n LinkInput(\n name=\"auth_link\",\n display_name=\"Authentication Link\",\n value=\"\",\n info=\"Click to authenticate with OAuth2\",\n dynamic=True,\n show=False,\n placeholder=\"Click to authenticate\",\n ),\n StrInput(\n name=\"auth_status\",\n display_name=\"Auth Status\",\n value=\"Not Connected\",\n info=\"Current authentication status\",\n dynamic=True,\n show=False,\n ),\n MultiselectInput(\n name=\"action_names\",\n display_name=\"Actions to use\",\n required=True,\n options=[],\n value=[],\n info=\"The actions to pass to agent to execute\",\n dynamic=True,\n show=False,\n ),\n ]\n\n outputs = [\n Output(name=\"tools\", display_name=\"Tools\", method=\"build_tool\"),\n ]\n\n def _check_for_authorization(self, app: str) -> str:\n \"\"\"Checks if the app is authorized.\n\n Args:\n app (str): The app name to check authorization for.\n\n Returns:\n str: The authorization status or URL.\n \"\"\"\n toolset = self._build_wrapper()\n entity = toolset.client.get_entity(id=self.entity_id)\n try:\n # Check if user is already connected\n entity.get_connection(app=app)\n except NoItemsFound:\n # Get auth scheme for the app\n auth_scheme = self._get_auth_scheme(app)\n return self._handle_auth_by_scheme(entity, app, auth_scheme)\n except Exception: # noqa: BLE001\n logger.exception(\"Authorization error\")\n return \"Error checking authorization\"\n else:\n return f\"{app} CONNECTED\"\n\n def _get_auth_scheme(self, app_name: str) -> AppAuthScheme:\n \"\"\"Get the primary auth scheme for an app.\n\n Args:\n app_name (str): The name of the app to get auth scheme for.\n\n Returns:\n AppAuthScheme: The auth scheme details.\n \"\"\"\n toolset = self._build_wrapper()\n try:\n return toolset.get_auth_scheme_for_app(app=app_name.lower())\n except Exception: # noqa: BLE001\n logger.exception(f\"Error getting auth scheme for {app_name}\")\n return None\n\n def _get_oauth_apps(self, api_key: str) -> list[str]:\n \"\"\"Fetch OAuth-enabled apps from Composio API.\n\n Args:\n api_key (str): The Composio API key.\n\n Returns:\n list[str]: A list containing OAuth-enabled app names.\n \"\"\"\n oauth_apps = []\n try:\n url = \"https://backend.composio.dev/api/v1/apps\"\n headers = {\"x-api-key\": api_key}\n params = {\n \"includeLocal\": \"true\",\n \"additionalFields\": \"auth_schemes\",\n \"sortBy\": \"alphabet\",\n }\n\n response = requests.get(url, headers=headers, params=params, timeout=20)\n data = response.json()\n\n for item in data.get(\"items\", []):\n for auth_scheme in item.get(\"auth_schemes\", []):\n if auth_scheme.get(\"mode\") in {\"OAUTH1\", \"OAUTH2\"}:\n oauth_apps.append(item[\"key\"].upper())\n break\n except requests.RequestException as e:\n logger.error(f\"Error fetching OAuth apps: {e}\")\n return []\n else:\n return oauth_apps\n\n def _handle_auth_by_scheme(self, entity: Any, app: str, auth_scheme: AppAuthScheme) -> str:\n \"\"\"Handle authentication based on the auth scheme.\n\n Args:\n entity (Any): The entity instance.\n app (str): The app name.\n auth_scheme (AppAuthScheme): The auth scheme details.\n\n Returns:\n str: The authentication status or URL.\n \"\"\"\n auth_mode = auth_scheme.auth_mode\n\n try:\n # First check if already connected\n entity.get_connection(app=app)\n except NoItemsFound:\n # If not connected, handle new connection based on auth mode\n if auth_mode == \"API_KEY\":\n if hasattr(self, \"app_credentials\") and self.app_credentials:\n try:\n entity.initiate_connection(\n app_name=app,\n auth_mode=\"API_KEY\",\n auth_config={\"api_key\": self.app_credentials},\n use_composio_auth=False,\n force_new_integration=True,\n )\n except Exception as e: # noqa: BLE001\n logger.error(f\"Error connecting with API Key: {e}\")\n return \"Invalid API Key\"\n else:\n return f\"{app} CONNECTED\"\n return \"Enter API Key\"\n\n if (\n auth_mode == \"BASIC\"\n and hasattr(self, \"username\")\n and hasattr(self, \"app_credentials\")\n and self.username\n and self.app_credentials\n ):\n try:\n entity.initiate_connection(\n app_name=app,\n auth_mode=\"BASIC\",\n auth_config={\"username\": self.username, \"password\": self.app_credentials},\n use_composio_auth=False,\n force_new_integration=True,\n )\n except Exception as e: # noqa: BLE001\n logger.error(f\"Error connecting with Basic Auth: {e}\")\n return \"Invalid credentials\"\n else:\n return f\"{app} CONNECTED\"\n elif auth_mode == \"BASIC\":\n return \"Enter Username and Password\"\n\n if auth_mode == \"OAUTH2\":\n try:\n return self._initiate_default_connection(entity, app)\n except Exception as e: # noqa: BLE001\n logger.error(f\"Error initiating OAuth2: {e}\")\n return \"OAuth2 initialization failed\"\n\n return \"Unsupported auth mode\"\n except Exception as e: # noqa: BLE001\n logger.error(f\"Error checking connection status: {e}\")\n return f\"Error: {e!s}\"\n else:\n return f\"{app} CONNECTED\"\n\n def _initiate_default_connection(self, entity: Any, app: str) -> str:\n connection = entity.initiate_connection(app_name=app, use_composio_auth=True, force_new_integration=True)\n return connection.redirectUrl\n\n def _get_connected_app_names_for_entity(self) -> list[str]:\n toolset = self._build_wrapper()\n connections = toolset.client.get_entity(id=self.entity_id).get_connections()\n return list({connection.appUniqueId for connection in connections})\n\n def _get_normalized_app_name(self) -> str:\n \"\"\"Get app name without connection status suffix.\n\n Returns:\n str: Normalized app name.\n \"\"\"\n return self.app_names.replace(\" ✅\", \"\").replace(\"_connected\", \"\")\n\n def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: # noqa: ARG002\n # Update the available apps options from the API\n if hasattr(self, \"api_key\") and self.api_key != \"\":\n toolset = self._build_wrapper()\n build_config[\"app_names\"][\"options\"] = self._get_oauth_apps(api_key=self.api_key)\n\n # First, ensure all dynamic fields are hidden by default\n dynamic_fields = [\"app_credentials\", \"username\", \"auth_link\", \"auth_status\", \"action_names\"]\n for field in dynamic_fields:\n if field in build_config:\n if build_config[field][\"value\"] is None or build_config[field][\"value\"] == \"\":\n build_config[field][\"show\"] = False\n build_config[field][\"advanced\"] = True\n build_config[field][\"load_from_db\"] = False\n else:\n build_config[field][\"show\"] = True\n build_config[field][\"advanced\"] = False\n\n if field_name == \"app_names\" and (not hasattr(self, \"app_names\") or not self.app_names):\n build_config[\"auth_status\"][\"show\"] = True\n build_config[\"auth_status\"][\"value\"] = \"Please select an app first\"\n return build_config\n\n if field_name == \"app_names\" and hasattr(self, \"api_key\") and self.api_key != \"\":\n # app_name = self._get_normalized_app_name()\n app_name = self.app_names\n try:\n toolset = self._build_wrapper()\n entity = toolset.client.get_entity(id=self.entity_id)\n\n # Always show auth_status when app is selected\n build_config[\"auth_status\"][\"show\"] = True\n build_config[\"auth_status\"][\"advanced\"] = False\n\n try:\n # Check if already connected\n entity.get_connection(app=app_name)\n build_config[\"auth_status\"][\"value\"] = \"✅\"\n build_config[\"auth_link\"][\"show\"] = False\n # Show action selection for connected apps\n build_config[\"action_names\"][\"show\"] = True\n build_config[\"action_names\"][\"advanced\"] = False\n\n except NoItemsFound:\n # Get auth scheme and show relevant fields\n auth_scheme = self._get_auth_scheme(app_name)\n auth_mode = auth_scheme.auth_mode\n logger.info(f\"Auth mode for {app_name}: {auth_mode}\")\n\n if auth_mode == \"API_KEY\":\n build_config[\"app_credentials\"][\"show\"] = True\n build_config[\"app_credentials\"][\"advanced\"] = False\n build_config[\"app_credentials\"][\"display_name\"] = \"API Key\"\n build_config[\"auth_status\"][\"value\"] = \"Enter API Key\"\n\n elif auth_mode == \"BASIC\":\n build_config[\"username\"][\"show\"] = True\n build_config[\"username\"][\"advanced\"] = False\n build_config[\"app_credentials\"][\"show\"] = True\n build_config[\"app_credentials\"][\"advanced\"] = False\n build_config[\"app_credentials\"][\"display_name\"] = \"Password\"\n build_config[\"auth_status\"][\"value\"] = \"Enter Username and Password\"\n\n elif auth_mode == \"OAUTH2\":\n build_config[\"auth_link\"][\"show\"] = True\n build_config[\"auth_link\"][\"advanced\"] = False\n auth_url = self._initiate_default_connection(entity, app_name)\n build_config[\"auth_link\"][\"value\"] = auth_url\n build_config[\"auth_status\"][\"value\"] = \"Click link to authenticate\"\n\n else:\n build_config[\"auth_status\"][\"value\"] = \"Unsupported auth mode\"\n\n # Update action names if connected\n if build_config[\"auth_status\"][\"value\"] == \"✅\":\n all_action_names = [str(action).replace(\"Action.\", \"\") for action in Action.all()]\n app_action_names = [\n action_name\n for action_name in all_action_names\n if action_name.lower().startswith(app_name.lower() + \"_\")\n ]\n if build_config[\"action_names\"][\"options\"] != app_action_names:\n build_config[\"action_names\"][\"options\"] = app_action_names\n build_config[\"action_names\"][\"value\"] = [app_action_names[0]] if app_action_names else [\"\"]\n\n except Exception as e: # noqa: BLE001\n logger.error(f\"Error checking auth status: {e}, app: {app_name}\")\n build_config[\"auth_status\"][\"value\"] = f\"Error: {e!s}\"\n\n return build_config\n\n def build_tool(self) -> Sequence[Tool]:\n \"\"\"Build Composio tools based on selected actions.\n\n Returns:\n Sequence[Tool]: List of configured Composio tools.\n \"\"\"\n composio_toolset = self._build_wrapper()\n return composio_toolset.get_tools(actions=self.action_names)\n\n def _build_wrapper(self) -> ComposioToolSet:\n \"\"\"Build the Composio toolset wrapper.\n\n Returns:\n ComposioToolSet: The initialized toolset.\n\n Raises:\n ValueError: If the API key is not found or invalid.\n \"\"\"\n try:\n if not self.api_key:\n msg = \"Composio API Key is required\"\n raise ValueError(msg)\n return ComposioToolSet(api_key=self.api_key, entity_id=self.entity_id)\n except ValueError as e:\n logger.error(f\"Error building Composio wrapper: {e}\")\n msg = \"Please provide a valid Composio API Key in the component settings\"\n raise ValueError(msg) from e\n" + "value": "# Standard library imports\nfrom collections.abc import Sequence\nfrom typing import Any\n\nfrom composio import Action, App\n\n# Third-party imports\nfrom composio_langchain import ComposioToolSet\nfrom langchain_core.tools import Tool\n\n# Local imports\nfrom langflow.base.langchain_utilities.model import LCToolComponent\nfrom langflow.inputs import (\n ConnectionInput,\n MessageTextInput,\n SecretStrInput,\n SortableListInput,\n)\nfrom langflow.io import Output\n\n# TODO: We get the list from the API but we need to filter it\nenabled_tools = [\"confluence\", \"discord\", \"dropbox\", \"github\", \"gmail\", \"linkedin\", \"notion\", \"slack\", \"youtube\"]\n\n\nclass ComposioAPIComponent(LCToolComponent):\n display_name: str = \"Composio Tools\"\n description: str = \"Use Composio toolset to run actions with your agent\"\n name = \"ComposioAPI\"\n icon = \"Composio\"\n documentation: str = \"https://docs.composio.dev\"\n\n inputs = [\n # Basic configuration inputs\n MessageTextInput(name=\"entity_id\", display_name=\"Entity ID\", value=\"default\", advanced=True),\n SecretStrInput(\n name=\"api_key\",\n display_name=\"Composio API Key\",\n required=True,\n info=\"Refer to https://docs.composio.dev/faq/api_key/api_key\",\n real_time_refresh=True,\n ),\n ConnectionInput(\n name=\"tool_name\",\n display_name=\"Tool Name\",\n placeholder=\"Select a tool...\",\n button_metadata={\"icon\": \"unplug\", \"variant\": \"destructive\"},\n options=[],\n search_category=[],\n value=\"\",\n connection_link=\"\",\n info=\"The name of the tool to use\",\n real_time_refresh=True,\n ),\n SortableListInput(\n name=\"actions\",\n display_name=\"Actions\",\n placeholder=\"Select action\",\n helper_text=\"Please connect before selecting actions.\",\n helper_text_metadata={\"icon\": \"OctagonAlert\", \"variant\": \"destructive\"},\n options=[],\n value=\"\",\n info=\"The actions to use\",\n limit=1,\n show=False,\n ),\n ]\n\n outputs = [\n Output(name=\"tools\", display_name=\"Tools\", method=\"build_tool\"),\n ]\n\n def sanitize_action_name(self, action_name: str) -> str:\n # TODO: Maybe restore\n return action_name\n\n # We want to use title case, and replace underscores with spaces\n sanitized_name = action_name.replace(\"_\", \" \").title()\n\n # Now we want to remove everything from and including the first dot\n return sanitized_name.replace(self.tool_name.title() + \" \", \"\")\n\n def desanitize_action_name(self, action_name: str) -> str:\n # TODO: Maybe restore\n return action_name\n\n # We want to reverse what we did above\n unsanitized_name = action_name.replace(\" \", \"_\").upper()\n\n # Append the tool_name to it at the beginning, followed by a dot, in all CAPS\n return f\"{self.tool_name.upper()}_{unsanitized_name}\"\n\n def validate_tool(self, build_config: dict, field_value: Any, connected_app_names: list) -> dict:\n # Get the index of the selected tool in the list of options\n selected_tool_index = next(\n (\n ind\n for ind, tool in enumerate(build_config[\"tool_name\"][\"options\"])\n if tool[\"name\"] == field_value\n or (\"validate\" in field_value and tool[\"name\"] == field_value[\"validate\"])\n ),\n None,\n )\n\n # Set the link to be the text 'validated'\n build_config[\"tool_name\"][\"options\"][selected_tool_index][\"link\"] = \"validated\"\n\n # Set the helper text and helper text metadata field of the actions now\n build_config[\"actions\"][\"helper_text\"] = \"\"\n build_config[\"actions\"][\"helper_text_metadata\"] = {\"icon\": \"Check\", \"variant\": \"success\"}\n\n # Get the list of actions available\n all_actions = list(Action.all())\n authenticated_actions = sorted(\n [\n action\n for action in all_actions\n if action.app.lower() in list(connected_app_names) and action.app.lower() == self.tool_name.lower()\n ],\n key=lambda x: x.name,\n )\n\n # Return the list of action names\n build_config[\"actions\"][\"options\"] = [\n {\n \"name\": self.sanitize_action_name(action.name),\n }\n for action in authenticated_actions\n ]\n\n # Lastly, we need to show the actions field\n build_config[\"actions\"][\"show\"] = True\n\n return build_config\n\n def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:\n # If the list of tools is not available, always update it\n if field_name == \"api_key\" or (self.api_key and not build_config[\"tool_name\"][\"options\"]):\n if field_name == \"api_key\" and not field_value:\n # Reset the list of tools\n build_config[\"tool_name\"][\"options\"] = []\n build_config[\"tool_name\"][\"value\"] = \"\"\n\n # Reset the list of actions\n build_config[\"actions\"][\"show\"] = False\n build_config[\"actions\"][\"options\"] = []\n build_config[\"actions\"][\"value\"] = \"\"\n\n return build_config\n\n # TODO: Re-enable dynamic tool list\n # Initialize the Composio ToolSet with your API key\n # toolset = ComposioToolSet(api_key=self.api_key)\n\n # Get the entity (e.g., \"default\" for your user)\n # entity = toolset.get_entity(self.entity_id)\n\n # Get all available apps\n # all_apps = entity.client.apps.get()\n\n # Build an object with name, icon, link\n build_config[\"tool_name\"][\"options\"] = [\n {\n \"name\": app.title(), # TODO: Switch to app.name\n \"icon\": app, # TODO: Switch to app.name\n \"link\": (\n build_config[\"tool_name\"][\"options\"][ind][\"link\"]\n if build_config[\"tool_name\"][\"options\"]\n else \"\"\n ),\n }\n # for app in sorted(all_apps, key=lambda x: x.name)\n for ind, app in enumerate(enabled_tools)\n ]\n\n return build_config\n\n # Handle the click of the Tool Name connect button\n if field_name == \"tool_name\" and field_value:\n # Get the list of apps (tools) we have connected\n toolset = ComposioToolSet(api_key=self.api_key)\n connected_apps = [app for app in toolset.get_connected_accounts() if app.status == \"ACTIVE\"]\n\n # Get the unique list of appName from the connected apps\n connected_app_names = [app.appName.lower() for app in connected_apps]\n\n # Clear out the list of selected actions\n build_config[\"actions\"][\"show\"] = True\n build_config[\"actions\"][\"options\"] = []\n build_config[\"actions\"][\"value\"] = \"\"\n\n # Clear out any helper text\n build_config[\"tool_name\"][\"helper_text\"] = \"\"\n build_config[\"tool_name\"][\"helper_text_metadata\"] = {}\n\n # If it's a dictionary, we need to do validation\n if isinstance(field_value, dict):\n # If the current field value is a dictionary, it means the user has selected a tool\n if \"validate\" not in field_value:\n return build_config\n\n # Check if the selected tool is connected\n check_app = field_value[\"validate\"].lower()\n\n # If the tool selected is NOT what we are validating, return the build config\n if check_app != self.tool_name.lower():\n # Set the helper text and helper text metadata field of the actions now\n build_config[\"actions\"][\"helper_text\"] = \"Please connect before selecting actions.\"\n build_config[\"actions\"][\"helper_text_metadata\"] = {\n \"icon\": \"OctagonAlert\",\n \"variant\": \"destructive\",\n }\n\n return build_config\n\n # Check if the tool is already validated\n if check_app not in connected_app_names:\n return build_config\n\n # Validate the selected tool\n return self.validate_tool(build_config, field_value, connected_app_names)\n\n # Check if the tool is already validated\n if field_value.lower() in connected_app_names:\n return self.validate_tool(build_config, field_value, connected_app_names)\n\n # Get the entity (e.g., \"default\" for your user)\n entity = toolset.get_entity(id=self.entity_id)\n\n # Set the metadata for the actions\n build_config[\"actions\"][\"helper_text_metadata\"] = {\"icon\": \"OctagonAlert\", \"variant\": \"destructive\"}\n\n # Get the index of the selected tool in the list of options\n selected_tool_index = next(\n (ind for ind, tool in enumerate(build_config[\"tool_name\"][\"options\"]) if tool[\"name\"] == field_value),\n None,\n )\n\n # Initiate a GitHub connection and get the redirect URL\n try:\n connection_request = entity.initiate_connection(app_name=getattr(App, field_value.upper()))\n except Exception as _: # noqa: BLE001\n # Indicate that there was an error connecting to the tool\n build_config[\"tool_name\"][\"options\"][selected_tool_index][\"link\"] = \"error\"\n build_config[\"tool_name\"][\"helper_text\"] = f\"Error connecting to {field_value}\"\n build_config[\"tool_name\"][\"helper_text_metadata\"] = {\n \"icon\": \"OctagonAlert\",\n \"variant\": \"destructive\",\n }\n\n return build_config\n\n # Print the direct HTTP link for authentication\n build_config[\"tool_name\"][\"options\"][selected_tool_index][\"link\"] = connection_request.redirectUrl\n\n # Set the helper text and helper text metadata field of the actions now\n build_config[\"actions\"][\"helper_text\"] = \"Please connect before selecting actions.\"\n\n return build_config\n\n def build_tool(self) -> Sequence[Tool]:\n \"\"\"Build Composio tools based on selected actions.\n\n Returns:\n Sequence[Tool]: List of configured Composio tools.\n \"\"\"\n composio_toolset = self._build_wrapper()\n return composio_toolset.get_tools(\n actions=[self.desanitize_action_name(action[\"name\"]) for action in self.actions]\n )\n\n def _build_wrapper(self) -> ComposioToolSet:\n \"\"\"Build the Composio toolset wrapper.\n\n Returns:\n ComposioToolSet: The initialized toolset.\n\n Raises:\n ValueError: If the API key is not found or invalid.\n \"\"\"\n try:\n if not self.api_key:\n msg = \"Composio API Key is required\"\n raise ValueError(msg)\n return ComposioToolSet(api_key=self.api_key, entity_id=self.entity_id)\n except ValueError as e:\n self.log(f\"Error building Composio wrapper: {e}\")\n msg = \"Please provide a valid Composio API Key in the component settings\"\n raise ValueError(msg) from e\n" }, "entity_id": { "_input_type": "MessageTextInput", @@ -1767,27 +1537,28 @@ "type": "str", "value": "default" }, - "username": { - "_input_type": "MessageTextInput", - "advanced": true, - "display_name": "Username", - "dynamic": true, - "info": "Username for Basic authentication", - "input_types": [ - "Message" - ], - "list": false, - "list_add_label": "Add More", - "load_from_db": false, - "name": "username", - "placeholder": "", + "tool_name": { + "_input_type": "ConnectionInput", + "advanced": false, + "button_metadata": { + "icon": "unplug", + "variant": "destructive" + }, + "connection_link": "", + "display_name": "Tool Name", + "dynamic": false, + "info": "The name of the tool to use", + "name": "tool_name", + "options": [], + "placeholder": "Select a tool...", + "real_time_refresh": true, "required": false, - "show": false, + "search_category": [], + "show": true, "title_case": false, "tool_mode": false, - "trace_as_input": true, "trace_as_metadata": true, - "type": "str", + "type": "connect", "value": "" } }, @@ -1797,28 +1568,28 @@ "type": "ComposioAPI" }, "dragging": false, - "id": "ComposioAPI-Tdreq", + "id": "ComposioAPI-4heel", "measured": { - "height": 497, + "height": 332, "width": 320 }, "position": { - "x": -137.53986902236176, - "y": 20.325147658297382 + "x": -188.38126171451378, + "y": -74.76352834864377 }, - "selected": true, + "selected": false, "type": "genericNode" } ], "viewport": { - "x": 666.3549315745112, - "y": 178.32327136900147, - "zoom": 0.8590936972080208 + "x": 782.0226980413704, + "y": 124.34564913657198, + "zoom": 0.9609155689915785 } }, "description": "Interact with Gmail to send emails, create drafts, and fetch messages", "endpoint_name": null, - "id": "db962555-dbb0-477f-85cc-536c68b32ee8", + "id": "580794d2-0d2f-4dc3-bff0-c58dd62264ec", "is_component": false, "last_tested_version": "1.2.0", "name": "Gmail Agent", diff --git a/src/backend/base/langflow/inputs/__init__.py b/src/backend/base/langflow/inputs/__init__.py index 709b03f7c..17e6cfe21 100644 --- a/src/backend/base/langflow/inputs/__init__.py +++ b/src/backend/base/langflow/inputs/__init__.py @@ -1,6 +1,7 @@ from .inputs import ( BoolInput, CodeInput, + ConnectionInput, DataFrameInput, DataInput, DefaultPromptField, @@ -21,6 +22,7 @@ from .inputs import ( PromptInput, SecretStrInput, SliderInput, + SortableListInput, StrInput, TabInput, TableInput, @@ -29,6 +31,7 @@ from .inputs import ( __all__ = [ "BoolInput", "CodeInput", + "ConnectionInput", "DataFrameInput", "DataInput", "DefaultPromptField", @@ -42,7 +45,6 @@ __all__ = [ "Input", "IntInput", "LinkInput", - "LinkInput", "MessageInput", "MessageTextInput", "MultilineInput", @@ -52,6 +54,7 @@ __all__ = [ "PromptInput", "SecretStrInput", "SliderInput", + "SortableListInput", "StrInput", "TabInput", "TableInput", diff --git a/src/backend/base/langflow/inputs/input_mixin.py b/src/backend/base/langflow/inputs/input_mixin.py index 6558b0734..97566ec87 100644 --- a/src/backend/base/langflow/inputs/input_mixin.py +++ b/src/backend/base/langflow/inputs/input_mixin.py @@ -24,6 +24,8 @@ class FieldTypes(str, Enum): BOOLEAN = "bool" DICT = "dict" NESTED_DICT = "NestedDict" + SORTABLE_LIST = "sortableList" + CONNECTION = "connect" FILE = "file" PROMPT = "prompt" CODE = "code" @@ -191,6 +193,34 @@ class DropDownMixin(BaseModel): """Dictionary of dialog inputs for the field. Default is an empty object.""" +class SortableListMixin(BaseModel): + helper_text: str | None = None + """Adds a helper text to the field. Defaults to an empty string.""" + helper_text_metadata: dict[str, Any] | None = None + """Dictionary of metadata for the helper text.""" + search_category: list[str] = Field(default=[]) + """Specifies the category of the field. Defaults to an empty list.""" + options: list[dict[str, Any]] = Field(default_factory=list) + """List of dictionaries with metadata for each option.""" + limit: int | None = None + """Specifies the limit of the field. Defaults to None.""" + + +class ConnectionMixin(BaseModel): + helper_text: str | None = None + """Adds a helper text to the field. Defaults to an empty string.""" + helper_text_metadata: dict[str, Any] | None = None + """Dictionary of metadata for the helper text.""" + connection_link: str | None = None + """Specifies the link of the connection. Defaults to an empty string.""" + button_metadata: dict[str, Any] | None = None + """Dictionary of metadata for the button.""" + search_category: list[str] = Field(default=[]) + """Specifies the category of the field. Defaults to an empty list.""" + options: list[dict[str, Any]] = Field(default_factory=list) + """List of dictionaries with metadata for each option.""" + + class TabMixin(BaseModel): """Mixin for tab input fields that allows a maximum of 3 values, each with a maximum of 20 characters.""" diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 714b6e6f6..8a58802d8 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -13,6 +13,7 @@ from langflow.template.field.base import Input from .input_mixin import ( BaseInputMixin, + ConnectionMixin, DatabaseLoadMixin, DropDownMixin, FieldTypes, @@ -25,6 +26,7 @@ from .input_mixin import ( RangeMixin, SerializableFieldTypes, SliderMixin, + SortableListMixin, TableMixin, TabMixin, ToolModeMixin, @@ -464,6 +466,30 @@ class DropdownInput(BaseInputMixin, DropDownMixin, MetadataTraceMixin, ToolModeM dialog_inputs: dict[str, Any] = Field(default_factory=dict) +class ConnectionInput(BaseInputMixin, ConnectionMixin, MetadataTraceMixin, ToolModeMixin): + """Represents a connection input field. + + This class represents a connection input field and provides functionality for handling connection values. + It inherits from the `BaseInputMixin` and `ConnectionMixin` classes. + + """ + + field_type: SerializableFieldTypes = FieldTypes.CONNECTION + + +class SortableListInput(BaseInputMixin, SortableListMixin, MetadataTraceMixin, ToolModeMixin): + """Represents a list selection input field. + + This class represents a list selection input field and provides functionality for handling list selection values. + It inherits from the `BaseInputMixin` and `ListableInputMixin` classes. + + Attributes: + field_type (SerializableFieldTypes): The field type of the input. Defaults to FieldTypes.BUTTON. + """ + + field_type: SerializableFieldTypes = FieldTypes.SORTABLE_LIST + + class TabInput(BaseInputMixin, TabMixin, MetadataTraceMixin, ToolModeMixin): """Represents a tab input field. @@ -570,6 +596,8 @@ InputTypes: TypeAlias = ( | DictInput | DropdownInput | MultiselectInput + | SortableListInput + | ConnectionInput | FileInput | FloatInput | HandleInput diff --git a/src/backend/base/langflow/utils/constants.py b/src/backend/base/langflow/utils/constants.py index 00dd80754..4f08360e5 100644 --- a/src/backend/base/langflow/utils/constants.py +++ b/src/backend/base/langflow/utils/constants.py @@ -52,7 +52,22 @@ def python_function(text: str) -> str: PYTHON_BASIC_TYPES = [str, bool, int, float, tuple, list, dict, set] -DIRECT_TYPES = ["str", "bool", "dict", "int", "float", "Any", "prompt", "code", "NestedDict", "table", "slider", "tab"] +DIRECT_TYPES = [ + "str", + "bool", + "dict", + "int", + "float", + "Any", + "prompt", + "code", + "NestedDict", + "table", + "slider", + "tab", + "sortableList", + "connect", +] LOADERS_INFO: list[dict[str, Any]] = [ diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 1ac06ef0a..82b78bac0 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -76,6 +76,7 @@ "react-markdown": "^8.0.7", "react-pdf": "^9.0.0", "react-router-dom": "^6.23.1", + "react-sortablejs": "^6.1.4", "react-syntax-highlighter": "^15.5.0", "reactflow": "^11.11.3", "rehype-mathjax": "^4.0.3", @@ -84,6 +85,7 @@ "remark-math": "^6.0.0", "shadcn-ui": "^0.9.4", "short-unique-id": "^5.2.0", + "sortablejs": "^1.15.6", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "uuid": "^10.0.0", @@ -107,6 +109,7 @@ "@types/node": "^20.14.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/sortablejs": "^1.15.8", "@types/uuid": "^9.0.8", "@vitejs/plugin-react-swc": "^3.7.0", "autoprefixer": "^10.4.19", @@ -5562,6 +5565,11 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/sortablejs": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.8.tgz", + "integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -6669,6 +6677,11 @@ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" }, + "node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" + }, "node_modules/cli-boxes": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", @@ -13126,6 +13139,26 @@ "sisteransi": "^1.0.5" } }, + "node_modules/react-sortablejs": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/react-sortablejs/-/react-sortablejs-6.1.4.tgz", + "integrity": "sha512-fc7cBosfhnbh53Mbm6a45W+F735jwZ1UFIYSrIqcO/gRIFoDyZeMtgKlpV4DdyQfbCzdh5LoALLTDRxhMpTyXQ==", + "dependencies": { + "classnames": "2.3.1", + "tiny-invariant": "1.2.0" + }, + "peerDependencies": { + "@types/sortablejs": "1", + "react": ">=16.9.0", + "react-dom": ">=16.9.0", + "sortablejs": "1" + } + }, + "node_modules/react-sortablejs/node_modules/tiny-invariant": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", + "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -14297,6 +14330,11 @@ "node": ">=0.10.0" } }, + "node_modules/sortablejs": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==" + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 0fb72b844..2a34c8ef8 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -71,6 +71,7 @@ "react-markdown": "^8.0.7", "react-pdf": "^9.0.0", "react-router-dom": "^6.23.1", + "react-sortablejs": "^6.1.4", "react-syntax-highlighter": "^15.5.0", "reactflow": "^11.11.3", "rehype-mathjax": "^4.0.3", @@ -79,6 +80,7 @@ "remark-math": "^6.0.0", "shadcn-ui": "^0.9.4", "short-unique-id": "^5.2.0", + "sortablejs": "^1.15.6", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "uuid": "^10.0.0", @@ -130,6 +132,7 @@ "@types/node": "^20.14.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@types/sortablejs": "^1.15.8", "@types/uuid": "^9.0.8", "@vitejs/plugin-react-swc": "^3.7.0", "autoprefixer": "^10.4.19", diff --git a/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx new file mode 100644 index 000000000..0d8bd5ea3 --- /dev/null +++ b/src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx @@ -0,0 +1,170 @@ +import ForwardedIconComponent from "@/components/common/genericIconComponent"; +import SearchBarComponent from "@/components/core/parameterRenderComponent/components/searchBarComponent"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog-with-no-close"; +import { cn } from "@/utils/utils"; +import { useCallback, useMemo, useState } from "react"; + +// Update interface with better types +interface ListSelectionComponentProps { + open: boolean; + options: any[]; + onClose: () => void; + setSelectedList: (action: any[]) => void; + selectedList: any[]; + searchCategories?: string[]; + onSelection?: (action: any) => void; + limit?: number; +} + +const ListItem = ({ + item, + isSelected, + onClick, + className, +}: { + item: any; + isSelected: boolean; + onClick: () => void; + className?: string; +}) => ( + +); + +const ListSelectionComponent = ({ + open, + onClose, + searchCategories = [], + onSelection, + setSelectedList = () => {}, + selectedList = [], + options, + limit = 1, +}: ListSelectionComponentProps) => { + const [search, setSearch] = useState(""); + + const filteredList = useMemo(() => { + if (!search.trim()) { + return options; + } + const searchTerm = search.toLowerCase(); + return options.filter((item) => + item.name.toLowerCase().includes(searchTerm), + ); + }, [options, search]); + + const handleSelectAction = useCallback( + (action: any) => { + if (limit !== 1) { + // Multiple selection mode + const isAlreadySelected = selectedList.some( + (selectedItem) => selectedItem.name === action.name, + ); + + if (isAlreadySelected) { + setSelectedList( + selectedList.filter( + (selectedItem) => selectedItem.name !== action.name, + ), + ); + } else { + // Check if we've reached the selection limit + if (selectedList.length < limit) { + setSelectedList([...selectedList, action]); + } + } + } else { + // Single selection mode + setSelectedList([ + { + name: action.name, + icon: "icon" in action ? action.icon : undefined, + link: "link" in action ? action.link : undefined, + }, + ]); + onClose(); + setSearch(""); + } + }, + [selectedList, setSelectedList, onClose, limit], + ); + + const handleCloseDialog = useCallback(() => { + onClose(); + }, [onClose]); + + return ( + + +
+ + +
+ +
+ {filteredList.length > 0 ? ( + filteredList.map((item, index) => ( + selected.name === item.name, + ) || item.link === "validated" + } + onClick={() => { + handleSelectAction(item); + onSelection?.(item); + }} + /> + )) + ) : ( +
+ No items match your search +
+ )} +
+
+
+ ); +}; + +export default ListSelectionComponent; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/connectionComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/connectionComponent/index.tsx new file mode 100644 index 000000000..a7fda4c72 --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/connectionComponent/index.tsx @@ -0,0 +1,244 @@ +import ForwardedIconComponent from "@/components/common/genericIconComponent"; +import { Button } from "@/components/ui/button"; +import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value"; +import ListSelectionComponent from "@/CustomNodes/GenericNode/components/ListSelectionComponent"; +import { mutateTemplate } from "@/CustomNodes/helpers/mutate-template"; +import useAlertStore from "@/stores/alertStore"; +import { APIClassType } from "@/types/api"; +import { cn } from "@/utils/utils"; +import { memo, useEffect, useRef, useState } from "react"; +import { InputProps } from "../../types"; +import HelperTextComponent from "../helperTextComponent"; + +type ConnectionComponentProps = { + tooltip?: string; + name: string; + helperText?: string; + helperMetadata?: any; + options?: any[]; + searchCategory?: string[]; + buttonMetadata?: { variant?: string; icon?: string }; + connectionLink?: string; + nodeClass: APIClassType; + nodeId: string; +}; + +const ConnectionComponent = ({ + tooltip = "", + name, + helperText = "", + helperMetadata = { icon: undefined, variant: "muted-foreground" }, + options = [], + searchCategory = [], + buttonMetadata = { variant: "destructive", icon: "unplug" }, + connectionLink = "", + ...baseInputProps +}: InputProps) => { + const { + value, + handleOnNewValue, + handleNodeClass, + nodeClass, + nodeId, + placeholder, + } = baseInputProps; + + const setErrorData = useAlertStore((state) => state.setErrorData); + + const [isAuthenticated, setIsAuthenticated] = useState( + connectionLink === "validated", + ); + const [link, setLink] = useState(""); + const [isPolling, setIsPolling] = useState(false); + const [open, setOpen] = useState(false); + const [selectedItem, setSelectedItem] = useState([]); + + const pollingInterval = useRef(null); + const pollingTimeout = useRef(null); + + const postTemplateValue = usePostTemplateValue({ + parameterId: name, + nodeId: nodeId, + node: nodeClass, + }); + + // Initialize selected item from value on component mount + useEffect(() => { + const selectedOption = value + ? options.find((option) => option.name === value) + : null; + setSelectedItem([ + selectedOption + ? { name: selectedOption.name, icon: selectedOption.icon } + : { name: "", icon: "" }, + ]); + + // Update authentication status based on selected option + if (!selectedOption) { + setIsAuthenticated(false); + } + }, [value, options]); + + useEffect(() => { + if (connectionLink !== "") { + setLink(connectionLink); + if (connectionLink === "validated") { + setIsAuthenticated(true); + } + } + + if (connectionLink === "error") { + setLink("error"); + } + }, [connectionLink]); + + // Handles the connection button click to open connection in new tab and start polling + const handleConnectionButtonClick = () => { + if (selectedItem?.length === 0) return; + + window.open(link, "_blank"); + + startPolling(); + }; + + // Initiates polling to check connection status periodically + const startPolling = () => { + if (!selectedItem[0]?.name) return; + + setLink("loading"); + + // Initialize polling + setIsPolling(true); + + // Clear existing timers + stopPolling(); + + // Set up polling interval - check connection status every 3 seconds + pollingInterval.current = setInterval(() => { + mutateTemplate( + { validate: selectedItem[0]?.name || "" }, + nodeClass, + handleNodeClass, + postTemplateValue, + setErrorData, + name, + () => { + // Check if the connection was successful + if (connectionLink === "validated") { + stopPolling(); + setIsAuthenticated(true); + } + }, + nodeClass.tool_mode, + ); + }, 3000); + + // Set timeout to stop polling after 9 seconds to prevent indefinite polling + pollingTimeout.current = setTimeout(() => { + stopPolling(link !== ""); + // If we timed out and link is still loading, reset it + }, 9000); + }; + + // Cleans up polling timers to prevent memory leaks + const stopPolling = (resetLink = false) => { + setIsPolling(false); + if (resetLink) { + setLink(connectionLink || ""); + } + + if (pollingInterval.current) clearInterval(pollingInterval.current); + if (pollingTimeout.current) clearTimeout(pollingTimeout.current); + }; + + // Updates selected item and triggers parent component update + const handleSelection = (item: any) => { + setIsAuthenticated(false); + setSelectedItem([{ name: item.name }]); + setLink(item.link === "validated" ? "validated" : "loading"); + if (item.link === "validated") { + setIsAuthenticated(true); + } + handleOnNewValue({ value: item.name }, { skipSnapshot: true }); + }; + + // Dialog control handlers + const handleOpenListSelectionDialog = () => setOpen(true); + const handleCloseListSelectionDialog = () => setOpen(false); + + // Render component + return ( +
+
+ + + {!isAuthenticated && ( + + )} +
+ + {helperText && ( + + )} + + +
+ ); +}; + +export default memo(ConnectionComponent); diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/dropdownComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/dropdownComponent/index.tsx index 404889138..9fd140449 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/dropdownComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/dropdownComponent/index.tsx @@ -12,6 +12,8 @@ export default function DropdownComponent({ name, dialogInputs, optionsMetaData, + nodeClass, + nodeId, ...baseInputProps }: InputProps) { const onChange = (value: any, dbValue?: boolean, skipSnapshot?: boolean) => { diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/helperTextComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/helperTextComponent/index.tsx new file mode 100644 index 000000000..d4ea056e3 --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/helperTextComponent/index.tsx @@ -0,0 +1,36 @@ +import ForwardedIconComponent from "@/components/common/genericIconComponent"; +import { cn } from "@/utils/utils"; + +type HelperTextComponentProps = { + helperText: string; + helperMetadata?: { icon: string | undefined; variant: string }; +}; + +const HelperTextComponent = ({ + helperText, + helperMetadata = { icon: undefined, variant: "muted-foreground" }, +}: HelperTextComponentProps) => { + return ( +
+ {helperMetadata?.icon && ( + + )} +
+ {helperText} +
+
+ ); +}; + +export default HelperTextComponent; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/inputGlobalComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/inputGlobalComponent/index.tsx index ac6a3bfa0..17c1a41f1 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/components/inputGlobalComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/components/inputGlobalComponent/index.tsx @@ -1,13 +1,8 @@ -import { - useDeleteGlobalVariables, - useGetGlobalVariables, -} from "@/controllers/API/queries/variables"; +import { useGetGlobalVariables } from "@/controllers/API/queries/variables"; import GeneralDeleteConfirmationModal from "@/shared/components/delete-confirmation-modal"; -import GeneralGlobalVariableModal from "@/shared/components/global-variable-modal"; import { useGlobalVariablesStore } from "@/stores/globalVariablesStore/globalVariables"; -import { useEffect, useMemo } from "react"; -import DeleteConfirmationModal from "../../../../../modals/deleteConfirmationModal"; -import useAlertStore from "../../../../../stores/alertStore"; +import { useEffect, useMemo, useRef } from "react"; + import { cn } from "../../../../../utils/utils"; import ForwardedIconComponent from "../../../../common/genericIconComponent"; import { CommandItem } from "../../../../ui/command"; @@ -34,34 +29,63 @@ export default function InputGlobalComponent({ (state) => state.unavailableFields, ); - useEffect(() => { - if (globalVariables && !disabled) { - if ( - load_from_db && - !globalVariables.find((variable) => variable.name === value) - ) { - handleOnNewValue( - { value: "", load_from_db: false }, - { skipSnapshot: true }, - ); - } - if ( - !load_from_db && - value === "" && - unavailableFields && - Object.keys(unavailableFields).includes(display_name ?? "") - ) { - handleOnNewValue( - { value: unavailableFields[display_name ?? ""], load_from_db: true }, - { skipSnapshot: true }, - ); - } + const initialLoadCompleted = useRef(false); + + const valueExists = useMemo(() => { + return ( + globalVariables?.some((variable) => variable.name === value) ?? false + ); + }, [globalVariables, value]); + + const unavailableField = useMemo(() => { + if ( + display_name && + unavailableFields && + Object.keys(unavailableFields).includes(display_name) + ) { + return unavailableFields[display_name]; } - }, [globalVariables, unavailableFields, disabled]); + return null; + }, [unavailableFields, display_name]); + + useMemo(() => { + if (disabled) { + return; + } + + if (load_from_db && globalVariables && !valueExists) { + handleOnNewValue( + { value: "", load_from_db: false }, + { skipSnapshot: true }, + ); + } + }, [ + globalVariables, + unavailableFields, + disabled, + load_from_db, + valueExists, + unavailableField, + value, + handleOnNewValue, + ]); + + useEffect(() => { + if (initialLoadCompleted.current || disabled || unavailableField === null) { + return; + } + + handleOnNewValue( + { value: unavailableField, load_from_db: true }, + { skipSnapshot: true }, + ); + + initialLoadCompleted.current = true; + }, [unavailableField, disabled, load_from_db, value, handleOnNewValue]); function handleDelete(key: string) { - if (value === key && load_from_db) { - handleOnNewValue({ value: "", load_from_db: false }); + if (value === key) { + handleOnNewValue({ value: "", load_from_db: load_from_db }); } } @@ -96,13 +120,7 @@ export default function InputGlobalComponent({ onConfirmDelete={() => handleDelete(option)} /> )} - selectedOption={ - load_from_db && - globalVariables && - globalVariables?.map((variable) => variable.name).includes(value ?? "") - ? value - : "" - } + selectedOption={load_from_db && valueExists ? value : ""} setSelectedOption={(value) => { handleOnNewValue({ value: value, diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/searchBarComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/searchBarComponent/index.tsx new file mode 100644 index 000000000..e8b0305e5 --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/searchBarComponent/index.tsx @@ -0,0 +1,76 @@ +import { ForwardedIconComponent } from "@/components/common/genericIconComponent"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { useState } from "react"; + +interface SearchBarComponentProps { + searchCategories?: string[]; + search: string; + setSearch: (search: string) => void; + placeholder?: string; + onCategoryChange?: (category: string) => void; +} + +const SearchBarComponent = ({ + searchCategories, + search, + setSearch, + placeholder = "Search tools...", + onCategoryChange, +}: SearchBarComponentProps) => { + const [selectedCategory, setSelectedCategory] = useState( + searchCategories?.[0] || "All", + ); + + const handleCategoryChange = (category: string) => { + setSelectedCategory(category); + if (onCategoryChange) { + onCategoryChange(category); + } + }; + + return ( +
+ {searchCategories && searchCategories.length > 0 && ( + + + + + + {searchCategories.map((category) => ( + handleCategoryChange(category)} + className="cursor-pointer" + > + + {category} + + + ))} + + + )} + setSearch(e.target.value)} + inputClassName="border-none focus:ring-0" + /> +
+ ); +}; + +export default SearchBarComponent; diff --git a/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx new file mode 100644 index 000000000..10ff0b1e1 --- /dev/null +++ b/src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx @@ -0,0 +1,184 @@ +import ForwardedIconComponent from "@/components/common/genericIconComponent"; +import { Button } from "@/components/ui/button"; +import ListSelectionComponent from "@/CustomNodes/GenericNode/components/ListSelectionComponent"; +import { cn } from "@/utils/utils"; +import { memo, useCallback, useMemo, useState } from "react"; +import { ReactSortable } from "react-sortablejs"; +import { InputProps } from "../../types"; +import HelperTextComponent from "../helperTextComponent"; + +type SortableListComponentProps = { + tooltip?: string; + name?: string; + helperText?: string; + helperMetadata?: any; + options?: any[]; + searchCategory?: string[]; + icon?: string; + limit?: number; +}; + +const SortableListItem = memo( + ({ + data, + index, + onRemove, + limit = 1, + }: { + data: any; + index: number; + onRemove: () => void; + limit?: number; + }) => ( +
  • + {limit !== 1 && ( + + )} + +
    + {limit !== 1 && ( +
    + {index + 1} +
    + )} + + + {data.name} + +
    + +
  • + ), +); + +const SortableListComponent = ({ + tooltip = "", + name, + helperText = "", + helperMetadata = { icon: undefined, variant: "muted-foreground" }, + options = [], + searchCategory = [], + limit, + ...baseInputProps +}: InputProps) => { + const { placeholder, handleOnNewValue, value } = baseInputProps; + const [open, setOpen] = useState(false); + + // Convert value to an array if it exists, otherwise use empty array + const listData = useMemo(() => (Array.isArray(value) ? value : []), [value]); + + const createRemoveHandler = useCallback( + (index: number) => () => { + const newList = listData.filter((_, i) => i !== index); + handleOnNewValue({ value: newList }); + }, + [listData, handleOnNewValue], + ); + + const setListDataHandler = useCallback( + (newList: any[]) => { + handleOnNewValue({ value: newList }); + }, + [handleOnNewValue], + ); + + const handleCloseListSelectionDialog = useCallback(() => { + setOpen(false); + }, []); + + const handleOpenListSelectionDialog = useCallback(() => { + setOpen(true); + }, []); + + return ( +
    +
    + {!(limit === 1 && listData.length === 1) && ( + + )} +
    + + {listData.length > 0 && ( +
    + + {listData.map((data, index) => ( + + ))} + +
    + )} + + {helperText && ( +
    + +
    + )} + + +
    + ); +}; + +export default memo(SortableListComponent); diff --git a/src/frontend/src/components/core/parameterRenderComponent/index.tsx b/src/frontend/src/components/core/parameterRenderComponent/index.tsx index a6a256655..af4d4777d 100644 --- a/src/frontend/src/components/core/parameterRenderComponent/index.tsx +++ b/src/frontend/src/components/core/parameterRenderComponent/index.tsx @@ -6,6 +6,7 @@ import TabComponent from "@/components/core/parameterRenderComponent/components/ import { TEXT_FIELD_TYPES } from "@/constants/constants"; import { APIClassType, InputFieldType } from "@/types/api"; import { useMemo } from "react"; +import ConnectionComponent from "./components/connectionComponent"; import DictComponent from "./components/dictComponent"; import { EmptyParameterComponent } from "./components/emptyParameterComponent"; import FloatComponent from "./components/floatComponent"; @@ -17,6 +18,7 @@ import LinkComponent from "./components/linkComponent"; import MultiselectComponent from "./components/multiselectComponent"; import PromptAreaComponent from "./components/promptComponent"; import { RefreshParameterComponent } from "./components/refreshParameterComponent"; +import SortableListComponent from "./components/sortableListComponent"; import { StrRenderComponent } from "./components/strRenderComponent"; import ToggleShadComponent from "./components/toggleShadComponent"; import { InputProps, NodeInfoType } from "./types"; @@ -213,6 +215,37 @@ export function ParameterRenderComponent({ id={`slider_${id}`} /> ); + case "sortableList": + return ( + + ); + case "connect": + const link = + templateData?.options?.find( + (option: any) => option?.name === templateValue, + )?.link || ""; + + return ( + + ); case "tab": return ( { + return ( + + + + + + + + + ); +}; + +export default SVGGridHorizontalIcon; diff --git a/src/frontend/src/icons/GridHorizontal/gridHorizontal-icon.svg b/src/frontend/src/icons/GridHorizontal/gridHorizontal-icon.svg new file mode 100644 index 000000000..269939696 --- /dev/null +++ b/src/frontend/src/icons/GridHorizontal/gridHorizontal-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/icons/GridHorizontal/index.tsx b/src/frontend/src/icons/GridHorizontal/index.tsx new file mode 100644 index 000000000..5dc652cba --- /dev/null +++ b/src/frontend/src/icons/GridHorizontal/index.tsx @@ -0,0 +1,9 @@ +import { forwardRef } from "react"; +import SVGGridHorizontalIcon from "./GridHorizontalIcon"; + +export const GridHorizontalIcon = forwardRef< + SVGSVGElement, + React.PropsWithChildren<{}> +>((props, ref) => { + return ; +}); diff --git a/src/frontend/src/style/index.css b/src/frontend/src/style/index.css index 07b2cb13d..be932f6b0 100644 --- a/src/frontend/src/style/index.css +++ b/src/frontend/src/style/index.css @@ -28,6 +28,8 @@ --accent-foreground: 0 0% 0%; /* hsl(0, 0%, 0%) */ --destructive: 0 72% 51%; /* hsl(0, 72%, 51%) */ --destructive-foreground: 0 0% 100%; /* hsl(0, 0%, 100%) */ + --accent-amber: 26 90% 37%; /* hsl(26, 90%, 37%) */ + --accent-amber-foreground: 26 90% 37%; /* hsl(26, 90%, 37%) */ --ring: 0 0% 0%; /* hsl(0, 0%, 0%) */ --primary-hover: 240 4% 16%; /* hsl(240, 4%, 16%) */ --secondary-hover: 240 6% 90%; /* hsl(240, 6%, 90%) */ @@ -39,6 +41,7 @@ --accent-emerald-hover: 152.4 76% 80.4%; /* hsl(152.4, 76%, 80.4%) */ --accent-indigo: 226 100% 94%; /* hsl(226, 100%, 94%) */ --accent-indigo-foreground: 243 75% 59%; /* hsl(243, 75%, 59%) */ + --accent-pink: 326 78% 95%; /* hsl(326, 78%, 95%) */ --accent-pink-foreground: 333 71% 51%; /* hsl(333, 71%, 51%) */ --tooltip: 0 0% 0%; /* hsl(0, 0%, 0%) */ diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index c490c7ca1..73d713b71 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -6,6 +6,7 @@ import { DuckDuckGoIcon } from "@/icons/DuckDuckGo"; import { ExaIcon } from "@/icons/Exa"; import { GleanIcon } from "@/icons/Glean"; import { GoogleDriveIcon } from "@/icons/GoogleDrive"; +import { GridHorizontalIcon } from "@/icons/GridHorizontal"; import { JSIcon } from "@/icons/JSicon"; import { LangwatchIcon } from "@/icons/Langwatch"; import { MilvusIcon } from "@/icons/Milvus"; @@ -725,6 +726,7 @@ export const nodeIconsLucide: iconsType = { IFixitLoader: IFixIcon, CrewAI: CrewAiIcon, NotDiamond: NotDiamondIcon, + GridHorizontal: GridHorizontalIcon, Composio: ComposioIcon, Meta: MetaIcon, Midjorney: MidjourneyIcon, diff --git a/uv.lock b/uv.lock index 42afa7939..e5b643824 100644 --- a/uv.lock +++ b/uv.lock @@ -1383,7 +1383,7 @@ wheels = [ [[package]] name = "composio-core" -version = "0.7.1" +version = "0.7.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -1404,14 +1404,14 @@ dependencies = [ { name = "sentry-sdk" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/0c/c852ed97a6bd4c615447d49c56687949ee711a868e6ef992cd655a399ee6/composio_core-0.7.1.tar.gz", hash = "sha256:f51d13154df9baad6768a61b40bcab74d057da63e502ece0503b3b61666b43f0", size = 314490 } +sdist = { url = "https://files.pythonhosted.org/packages/a4/8c/baecb880d69098a17e23724ba8c35237428749d47645d353006428991ab4/composio_core-0.7.12.tar.gz", hash = "sha256:5e13db7c298bb1bbf29e40d139656c22dfde3c2a5a675962ede5673c76d376e4", size = 329357 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/03/33bbe17cca76d3ad0bb239424d5a02d7d52dc4831768cf6a5bef5cab6ac7/composio_core-0.7.1-py3-none-any.whl", hash = "sha256:cbe85a6b2e5b5326c8e067e4b88c201d076d7e46b1a35e0b803852cb001fca61", size = 477780 }, + { url = "https://files.pythonhosted.org/packages/5d/6c/71ccaaf26f399e10206dafa529e3bb6664e0eeb5f410636d7063b0eb73ee/composio_core-0.7.12-py3-none-any.whl", hash = "sha256:8904cdc47975e70542cf09499c7b90078371a9289452e941a659eb46d42f3b7a", size = 492528 }, ] [[package]] name = "composio-langchain" -version = "0.7.1" +version = "0.7.12" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "composio-core" }, @@ -1420,9 +1420,9 @@ dependencies = [ { name = "langchainhub" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/53/dec4238dd7467f3a033eeb85aefb70002464b11a5fe59659e0b90c62841a/composio_langchain-0.7.1.tar.gz", hash = "sha256:8ffd75c8a0165cc178268ad5f3bdff965886f21dc3ee580816c38c647595ed80", size = 4417 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/9d/e9e2a07eb9f582c8c7e4882f2418b32404b25540c994bd191fcc8a354823/composio_langchain-0.7.12.tar.gz", hash = "sha256:e22098542a8c2e309e79fbbd9d05b46f6b7a3bd59f13429ea1469a3bca7f3082", size = 4447 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/ce/52b0e7c72a0274e0a349f7adf931026ef248f7f5482bcf69f83f8c6cc298/composio_langchain-0.7.1-py3-none-any.whl", hash = "sha256:27ec56e6248e6ce2cde1190831323d83935f4ad62d650cbd71afb4aaf2171671", size = 4844 }, + { url = "https://files.pythonhosted.org/packages/aa/46/902dea095cf3c58c3a5269fabad7ee5d45fc303d48f5e334762a3c1dbae4/composio_langchain-0.7.12-py3-none-any.whl", hash = "sha256:5834b7b39aa1aa3400dac8ca01f372b80e999a6d57110b2d6a7c07fd7416cba5", size = 4878 }, ] [[package]] @@ -4706,8 +4706,8 @@ requires-dist = [ { name = "certifi", specifier = ">=2023.11.17,<2025.0.0" }, { name = "chromadb", specifier = "==0.5.23" }, { name = "clickhouse-connect", marker = "extra == 'clickhouse-connect'", specifier = "==0.7.19" }, - { name = "composio-core", specifier = "==0.7.1" }, - { name = "composio-langchain", specifier = "==0.7.1" }, + { name = "composio-core", specifier = "==0.7.12" }, + { name = "composio-langchain", specifier = "==0.7.12" }, { name = "couchbase", marker = "extra == 'couchbase'", specifier = ">=4.2.1" }, { name = "crewai", specifier = "==0.102.0" }, { name = "ctransformers", marker = "extra == 'local'", specifier = ">=0.2.10" },