From d3104e15aebe94a7a821fc4d9cc8f6f8ec47f50c Mon Sep 17 00:00:00 2001 From: Deon Sanchez <69873175+deon-sanchez@users.noreply.github.com> Date: Sat, 29 Mar 2025 04:12:51 -0600 Subject: [PATCH] feat: Composio Component Upgrade (#6905) * feat: Add button component and update dropdown rendering * feat: Implement ListSelectionComponent and update ButtonComponent - Add new ListSelectionComponent for selecting actions with search functionality - Refactor ButtonComponent to use ListSelectionComponent - Update ParameterRenderComponent to simplify button rendering - Modify DropdownComponent to prepare for future changes * feat: Add Sortable.js for drag-and-drop functionality and grid icon - Integrated Sortable.js library in index.html and package files - Added GridHorizontalIcon for drag handle visualization - Updated ButtonComponent with commented sortable list example - Improved input styling in ListSelectionComponent * feat: Add ButtonInput for Composio API and update input types - Introduced ButtonInput class in inputs module - Added new button input fields to ComposioAPIComponent - Updated FieldTypes enum to include BUTTON type - Modified frontend ButtonComponent layout - Removed commented-out code in DropdownComponent * [autofix.ci] apply automated fixes * feat: Enhance ButtonComponent with dynamic type and improved UI - Add type prop to ButtonComponent to support different rendering modes - Implement tool name and actions button layouts - Add sample action data for actions button type - Improve sortable list styling and interaction - Refactor ButtonComponent to handle different use cases * refactor: Improve ButtonComponent layout and Sortable configuration - Adjust Sortable animation duration - Modify list item layout and positioning - Remove unnecessary CSS classes - Simplify button and icon positioning * feat: Integrate react-sortablejs for improved drag-and-drop functionality - Replace Sortable.js with react-sortablejs library - Add TypeScript types for SortableJS - Update ButtonComponent to use ReactSortable component - Improve list item rendering and styling - Remove manual Sortable initialization * feat: Enhance ButtonComponent with authentication and dynamic action data - Add isAuthenticated state to control button visibility - Introduce initialActionData and actionData state for dynamic list management - Update button styling and icon for unauthenticated state - Enable ReactSortable to modify action data list * style: Update ButtonComponent styling with accent-themed colors * style: Refine button styling with amber accent theme * feat: Enhance ButtonComponent with dynamic rendering and improved UX * feat: Add external link to DataStax Wikipedia page on button click * feat: Implement delete action for button component action data * feat: Enhance ButtonComponent with authentication and UI improvements * feat: Enhance ListSelectionComponent with dynamic data and multi-select functionality * feat: Improve ListSelectionComponent and ButtonComponent interaction and authentication flow * refactor: Replace ButtonInput with ListSelectionInput in Composio API and input components * [autofix.ci] apply automated fixes * refactor: Optimize ListComponent with memoization and improved state management * feat: Add selection type support to Composio API and ListSelectionComponent for improved user interaction * refactor: Rename action-related props in ListSelectionComponent and ListComponent for clarity and consistency * feat: Expand ListSelectionInput options and enhance ListSelectionComponent with dynamic data and metadata support * refactor: Clean up comments and improve code readability in ListSelectionComponent and ListComponent * [autofix.ci] apply automated fixes * chore: Remove debug log from ParameterRenderComponent and add text/plain type to DRAG_EVENTS_CUSTOM_TYPES * feat: Introduce ConnectionInput and SortableListInput components, replacing ListSelectionInput; enhance ComposioAPIComponent with new input types and improve parameter rendering with connection support * [autofix.ci] apply automated fixes * feat: Enhance ConnectionComponent with new state management and selection handling; update SortableListComponent to utilize new props for improved functionality * feat: Implement SearchBarComponent to enhance ListSelectionComponent with improved search functionality and category selection * feat: Update ComposioAPIComponent with external links and enhance ConnectionComponent with improved state management and dynamic link handling * fix: Update ConnectionComponent layout and button styles for improved UI consistency * chore: Remove unused Sortable.js script from index.html to streamline frontend resources * Update index.tsx * [autofix.ci] apply automated fixes * [autofix.ci] apply automated fixes (attempt 2/3) * Add backend support for composio actions * [autofix.ci] apply automated fixes * Update composio_api.py * Update composio_api.py * [autofix.ci] apply automated fixes * fix: enhance ListSelectionComponent styling and functionality - Updated ListItem component to accept a className prop for better customization. - Improved item name display with truncation for better UI handling. - Adjusted DialogContent styling for responsive width and layout. - Enhanced overflow handling in the selection list for better user experience. * feat: enhance ListSelectionComponent and ConnectionComponent functionality - Added onSelection prop to ListSelectionComponent for handling item selection actions. - Improved item selection logic to trigger onSelection callback. - Refactored connection handling in ConnectionComponent to manage loading state and authentication. - Updated connection link handling to ensure proper link retrieval and usage. * fix: reset search input in ListSelectionComponent after selection - Added functionality to clear the search input by setting it to an empty string when an item is selected. This improves user experience by ensuring the search field is reset after a selection is made. * fix: improve selection logic and styling in ListSelectionComponent and ConnectionComponent - Enhanced ListItem component to conditionally apply a green color for validated items. - Updated selection logic in ListSelectionComponent to consider items with a validated link as selected. - Initialized authentication state in ConnectionComponent based on the connection link status. - Adjusted connection handling to update authentication state when the link changes. * fix: refine item selection handling in ConnectionComponent - Removed the setting of selectedItem state in the handleSelection function to streamline the selection process. - Updated logic to focus on handling new value updates without maintaining a separate selected item state. * fix: enhance ConnectionComponent and ListSelectionComponent functionality - Removed console log from ListSelectionComponent to clean up the code. - Added nodeId, nodeClass, and name props to ConnectionComponent for improved data handling. - Implemented polling logic in ConnectionComponent to validate connections and manage loading states effectively. - Updated loading state handling in the button to reflect polling status. * fix: refactor ConnectionComponent for improved state management and polling logic - Streamlined prop destructuring for better readability. - Enhanced state management for polling and authentication. - Added cleanup effects for polling intervals and timeouts. - Improved event handling for connection button clicks and selection dialog. - Updated comments for clarity and organization. * Support handling the auth parameter * [autofix.ci] apply automated fixes * fix: improve key handling and state management in ListSelection and SortableList components * Updated key assignment in ListSelectionComponent to include index for uniqueness. * Enhanced SortableListComponent with useEffect to initialize listData from props and improved remove handler to update state correctly. * Refactored setListData to ensure proper state updates and value handling. * fix: update selected item handling in ConnectionComponent (#7280) * Added useEffect to set selected item based on value and options. * Modified handleSelection to ensure selected item structure is consistent. * Fix output of component in actions * fix: enhance connection handling in ConnectionComponent * Updated handleConnectionButtonClick to accept a parameter for connection checking. * Modified event handler to call handleConnectionButtonClick with the appropriate argument based on connection state. * Ensured that the connection link opens in a new tab only when the connection check is true. * [autofix.ci] apply automated fixes * Remove the search categories * fix: update list data handling in SortableListComponent * Refactored useEffect to correctly set listData when value changes. * Introduced a temporary variable to hold the value before updating state. * refactor: optimize list data management in SortableListComponent * Replaced useEffect with useMemo for listData initialization. * Simplified state update logic in createRemoveHandler and setListDataHandler. * Improved key assignment for SortableListItem to handle potential undefined names. * Check if validated before passing link * [autofix.ci] apply automated fixes * refactor: improve connection handling and state management in ConnectionComponent * Enhanced state management for tracking connection status and UI states. * Updated polling management to prevent memory leaks and ensure proper cleanup. * Simplified event handlers for connection button clicks and selection handling. * Improved comments for better code clarity and understanding. * fix: improve connection link handling in ConnectionComponent * Added logging for connectionLink to aid in debugging. * Updated useEffect to set the link only if connectionLink is not empty. * Implemented authentication state change when connectionLink is validated. * Clear list of actions when changing tools * Update composio_api.py * [autofix.ci] apply automated fixes * feat: enhance error handling in ConnectionComponent * Introduced ShadTooltip for displaying error messages. * Added state management for error visibility and updated connection link handling based on error data. * Modified button behavior and icon display based on connection status. * refactor: simplify error handling in ConnectionComponent * Removed unused state for error tooltip visibility. * Updated error handling logic to include connection link status in addition to error data. * Properly indicate error status in component * Better management of tool name helper text * refactor: streamline selected item initialization in ConnectionComponent * Simplified the logic for setting the selected item based on the value prop. * Ensured that the selected item is updated correctly when value or options change. * fix: update authentication status in ConnectionComponent * Added logic to set authentication status based on the selected option. * Ensured that authentication state is updated when no option is selected. * feat: update composio package (#7325) update composio package * Update Gmail Agent.json * refactor: optimize InputGlobalComponent logic * Removed unused imports and streamlined the useEffect and useMemo hooks for better performance. * Enhanced the handling of unavailable fields and initial load completion state. * Simplified the logic for setting the selected option based on the value and load_from_db status. * refactor: improve error handling in ConnectionComponent * Removed dependency on errorData in useEffect for setting link state. * Streamlined error handling logic to focus solely on connectionLink status. * refactor: enhance connection handling in ConnectionComponent * Merged error handling logic into a single useEffect for improved clarity. * Updated link state management to reflect the selected item's validation status. * Ensured authentication state is set correctly based on the selected item. * Filter list of available tools * refactor: update ListSelectionComponent to use limit instead of selection type * Removed the SelectionMode type and replaced it with a limit prop for selection. * Adjusted selection logic to enforce the limit on selected items. * Updated related components to reflect the new limit prop instead of type. * [autofix.ci] apply automated fixes * Add limit field and santiziation * Update inputs.py * Update composio_api.py * Show actions only if a tool is selected * [autofix.ci] apply automated fixes * refactor: add limit prop to ParameterRenderComponent and SortableListComponent * Introduced a limit prop to both components to control item rendering and selection behavior. * Updated SortableListItem to conditionally render elements based on the limit value. * Adjusted button visibility and styles in SortableListComponent according to the limit prop. * fix: adjust styles in SortableListItem based on limit prop * Updated height and padding styles for SortableListItem when limit is set to 1. * Ensured consistent spacing and visibility of elements based on the limit value. * Push latest template * style: enhance SortableListItem styles for improved interaction * Updated class names to ensure proper styling for hover and group states. * Improved visibility of the remove button based on the limit prop, enhancing user experience. * Update composio_api.py * style: update SortableListItem max-width for improved layout * Adjusted max-width class for SortableListItem when limit is set to 1, enhancing the component's visual consistency. * Update Gmail Agent.json * style: enhance ListItem component for better interaction * Updated class names to improve hover effects and selected state styling. * Added rounded corners and background color changes for better visual feedback. * style: refine ListSelectionComponent and SortableListComponent layout * Simplified class names in ListItem for cleaner styling. * Moved HelperTextComponent into a dedicated div in SortableListComponent for improved layout and spacing. * refactor: clean up ConnectionComponent code * Removed unused imports and comments for better readability. * Updated polling timeout duration from 30 seconds to 9 seconds to prevent indefinite polling. * Fix bug with link on refresh * Update Gmail Agent.json * Update Gmail Agent.json * Update Gmail Agent.json --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Hare Co-authored-by: Edwin Jose --- .composio.lock | 1 + pyproject.toml | 4 +- .../components/composio/composio_api.py | 478 +++++++----------- .../starter_projects/Gmail Agent.json | 391 +++----------- src/backend/base/langflow/inputs/__init__.py | 5 +- .../base/langflow/inputs/input_mixin.py | 30 ++ src/backend/base/langflow/inputs/inputs.py | 28 + src/backend/base/langflow/utils/constants.py | 17 +- src/frontend/package-lock.json | 38 ++ src/frontend/package.json | 3 + .../ListSelectionComponent/index.tsx | 170 +++++++ .../components/connectionComponent/index.tsx | 244 +++++++++ .../components/dropdownComponent/index.tsx | 2 + .../components/helperTextComponent/index.tsx | 36 ++ .../components/inputGlobalComponent/index.tsx | 98 ++-- .../components/searchBarComponent/index.tsx | 76 +++ .../sortableListComponent/index.tsx | 184 +++++++ .../core/parameterRenderComponent/index.tsx | 33 ++ src/frontend/src/constants/constants.ts | 1 + .../GridHorizontal/GridHorizontalIcon.jsx | 26 + .../GridHorizontal/gridHorizontal-icon.svg | 1 + .../src/icons/GridHorizontal/index.tsx | 9 + src/frontend/src/style/index.css | 3 + src/frontend/src/utils/styleUtils.ts | 2 + uv.lock | 16 +- 25 files changed, 1251 insertions(+), 645 deletions(-) create mode 100644 .composio.lock create mode 100644 src/frontend/src/CustomNodes/GenericNode/components/ListSelectionComponent/index.tsx create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/connectionComponent/index.tsx create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/helperTextComponent/index.tsx create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/searchBarComponent/index.tsx create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/sortableListComponent/index.tsx create mode 100644 src/frontend/src/icons/GridHorizontal/GridHorizontalIcon.jsx create mode 100644 src/frontend/src/icons/GridHorizontal/gridHorizontal-icon.svg create mode 100644 src/frontend/src/icons/GridHorizontal/index.tsx 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" },