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 <ericrhare@gmail.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
Deon Sanchez
2025-03-29 04:12:51 -06:00
committed by GitHub
parent a16df4b2bf
commit d3104e15ae
25 changed files with 1251 additions and 645 deletions

1
.composio.lock Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -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",

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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",

View File

@@ -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."""

View File

@@ -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

View File

@@ -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]] = [

View File

@@ -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",

View File

@@ -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",

View File

@@ -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;
}) => (
<Button
key={item.id}
unstyled
size="sm"
className={cn("w-full rounded-md py-3 pl-3 pr-3 hover:bg-muted", className)}
onClick={onClick}
>
<div className="flex items-center gap-2">
{item.icon && (
<ForwardedIconComponent name={item.icon} className="h-5 w-5" />
)}
<span className="truncate font-semibold">{item.name}</span>
{"metaData" in item && item.metaData && (
<span className="text-gray-500">{item.metaData}</span>
)}
{isSelected ? (
<ForwardedIconComponent
name="check"
className={cn(
"ml-auto flex h-4 w-4",
item.link === "validated" && "text-green-500",
)}
/>
) : (
<span className="ml-auto flex h-4 w-4" />
)}
</div>
</Button>
);
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 (
<Dialog open={open} onOpenChange={handleCloseDialog}>
<DialogContent className="flex !w-auto w-fit min-w-[20vw] max-w-[50vw] flex-col">
<div className="flex items-center justify-between">
<SearchBarComponent
searchCategories={searchCategories}
search={search}
setSearch={setSearch}
/>
<Button
unstyled
size="icon"
className="ml-auto h-[38px]"
onClick={handleCloseDialog}
>
<ForwardedIconComponent name="x" />
</Button>
</div>
<div className="flex max-h-[80vh] flex-col gap-1 overflow-y-auto">
{filteredList.length > 0 ? (
filteredList.map((item, index) => (
<ListItem
key={`${item.name}-${index}`}
item={item}
isSelected={
selectedList.some(
(selected) => selected.name === item.name,
) || item.link === "validated"
}
onClick={() => {
handleSelectAction(item);
onSelection?.(item);
}}
/>
))
) : (
<div className="py-3 text-center text-gray-500">
No items match your search
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};
export default ListSelectionComponent;

View File

@@ -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<any, ConnectionComponentProps>) => {
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<any[]>([]);
const pollingInterval = useRef<NodeJS.Timeout | null>(null);
const pollingTimeout = useRef<NodeJS.Timeout | null>(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 (
<div className="flex w-full flex-col gap-2">
<div className="flex w-full flex-row items-center gap-2">
<Button
variant="primary"
size="xs"
role="combobox"
onClick={handleOpenListSelectionDialog}
className="dropdown-component-outline input-edit-node w-full py-2"
>
<div className={cn("flex w-full items-center justify-start text-sm")}>
{selectedItem[0]?.icon && (
<ForwardedIconComponent
name={selectedItem[0]?.icon}
className="h-5 w-5"
/>
)}
<span className="ml-2 truncate">
{selectedItem[0]?.name || placeholder}
</span>
<ForwardedIconComponent
name="ChevronsUpDown"
className="ml-auto h-5 w-5"
/>
</div>
</Button>
{!isAuthenticated && (
<Button
size="icon"
variant="ghost"
loading={link === "loading" || isPolling}
disabled={!selectedItem[0]?.name || link === "" || link === "error"}
className={cn(
"h-9 w-10 rounded-md border disabled:opacity-50",
buttonMetadata.variant && `border-${buttonMetadata.variant}`,
)}
onClick={link === "error" ? undefined : handleConnectionButtonClick}
>
<ForwardedIconComponent
name={
link === "error"
? "triangle-alert"
: buttonMetadata.icon || "unplug"
}
className={cn(
"h-5 w-5",
buttonMetadata.variant && `text-${buttonMetadata.variant}`,
)}
/>
</Button>
)}
</div>
{helperText && (
<HelperTextComponent
helperText={helperText}
helperMetadata={helperMetadata}
/>
)}
<ListSelectionComponent
open={open}
onSelection={handleSelection}
onClose={handleCloseListSelectionDialog}
searchCategories={searchCategory}
setSelectedList={setSelectedItem}
selectedList={selectedItem}
options={options}
/>
</div>
);
};
export default memo(ConnectionComponent);

View File

@@ -12,6 +12,8 @@ export default function DropdownComponent({
name,
dialogInputs,
optionsMetaData,
nodeClass,
nodeId,
...baseInputProps
}: InputProps<string, DropDownComponentType>) {
const onChange = (value: any, dbValue?: boolean, skipSnapshot?: boolean) => {

View File

@@ -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 (
<div className="flex w-full flex-row items-center gap-2">
{helperMetadata?.icon && (
<ForwardedIconComponent
name={helperMetadata?.icon}
className={cn(
`h-5 w-5`,
helperMetadata?.variant && `text-${helperMetadata?.variant}`,
)}
/>
)}
<div
className={cn(
"flex w-full flex-col text-xs",
helperMetadata?.variant && `text-${helperMetadata?.variant}`,
)}
>
{helperText}
</div>
</div>
);
};
export default HelperTextComponent;

View File

@@ -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,

View File

@@ -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 (
<div className="mr-10 flex w-full items-center rounded-md border">
{searchCategories && searchCategories.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 pl-4 text-sm">
<span className="truncate">{selectedCategory}</span>
<ForwardedIconComponent
name="chevron-down"
className="flex h-4 w-4"
/>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{searchCategories.map((category) => (
<DropdownMenuItem
key={category}
onClick={() => handleCategoryChange(category)}
className="cursor-pointer"
>
<span className="flex items-center gap-2 truncate px-2 py-1 text-sm">
{category}
</span>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)}
<Input
icon="search"
placeholder={placeholder}
value={search}
onChange={(e) => setSearch(e.target.value)}
inputClassName="border-none focus:ring-0"
/>
</div>
);
};
export default SearchBarComponent;

View File

@@ -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;
}) => (
<li
className={cn(
"inline-flex h-12 w-full items-center gap-2 text-sm font-medium text-gray-800",
limit === 1 ? "h-10 rounded-md bg-muted" : "group cursor-grab",
)}
>
{limit !== 1 && (
<ForwardedIconComponent
name="grid-horizontal"
className="h-5 w-5 fill-gray-300 text-gray-300"
/>
)}
<div className="flex w-full items-center gap-x-2">
{limit !== 1 && (
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-gray-400 text-center text-white">
{index + 1}
</div>
)}
<span
className={cn(
"truncate text-primary",
limit === 1 ? "max-w-56 pl-3" : "max-w-48",
)}
>
{data.name}
</span>
</div>
<Button
size="icon"
variant={limit !== 1 ? "outline" : "ghost"}
className={cn(
"ml-auto h-7 w-7 opacity-0 transition-opacity duration-200",
limit === 1
? "group pr-3 opacity-100"
: "hover:border hover:border-destructive hover:bg-transparent hover:opacity-100",
)}
onClick={onRemove}
>
<ForwardedIconComponent
name="x"
className={cn(
"h-6 w-6 text-red-500",
limit === 1 && "text-gray-500 group-hover:text-input",
)}
/>
</Button>
</li>
),
);
const SortableListComponent = ({
tooltip = "",
name,
helperText = "",
helperMetadata = { icon: undefined, variant: "muted-foreground" },
options = [],
searchCategory = [],
limit,
...baseInputProps
}: InputProps<any, SortableListComponentProps>) => {
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 (
<div className="flex w-full flex-col">
<div className="flex w-full flex-row gap-2">
{!(limit === 1 && listData.length === 1) && (
<Button
variant="default"
size="xs"
role="combobox"
onClick={handleOpenListSelectionDialog}
className="dropdown-component-outline input-edit-node w-full py-2"
>
<div className={cn("flex items-center text-sm font-semibold")}>
{placeholder}
</div>
</Button>
)}
</div>
{listData.length > 0 && (
<div className="flex w-full flex-col">
<ReactSortable
list={listData}
setList={setListDataHandler}
className={"flex w-full flex-col"}
>
{listData.map((data, index) => (
<SortableListItem
key={`${data?.name || "item"}-${index}`}
data={data}
index={index}
onRemove={createRemoveHandler(index)}
limit={limit}
/>
))}
</ReactSortable>
</div>
)}
{helperText && (
<div className="pt-2">
<HelperTextComponent
helperText={helperText}
helperMetadata={helperMetadata}
/>
</div>
)}
<ListSelectionComponent
open={open}
onClose={handleCloseListSelectionDialog}
searchCategories={searchCategory}
setSelectedList={setListDataHandler}
selectedList={listData}
options={options}
limit={limit}
/>
</div>
);
};
export default memo(SortableListComponent);

View File

@@ -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 (
<SortableListComponent
{...baseInputProps}
helperText={templateData?.helper_text}
helperMetadata={templateData?.helper_text_metadata}
options={templateData?.options}
searchCategory={templateData?.search_category}
limit={templateData?.limit}
/>
);
case "connect":
const link =
templateData?.options?.find(
(option: any) => option?.name === templateValue,
)?.link || "";
return (
<ConnectionComponent
{...baseInputProps}
name={name}
nodeId={nodeId}
nodeClass={nodeClass}
helperText={templateData?.helper_text}
helperMetadata={templateData?.helper_text_metadata}
options={templateData?.options}
searchCategory={templateData?.search_category}
buttonMetadata={templateData?.button_metadata}
connectionLink={link as string}
/>
);
case "tab":
return (
<TabComponent

View File

@@ -964,6 +964,7 @@ export const REFETCH_SERVER_HEALTH_INTERVAL = 20000;
export const DRAG_EVENTS_CUSTOM_TYPESS = {
genericnode: "genericNode",
notenode: "noteNode",
"text/plain": "text/plain",
};
export const NOTE_NODE_MIN_WIDTH = 324;

View File

@@ -0,0 +1,26 @@
const SVGGridHorizontalIcon = (props) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-grip-horizontal"
{...props}
>
<circle cx="12" cy="9" r="1" />
<circle cx="19" cy="9" r="1" />
<circle cx="5" cy="9" r="1" />
<circle cx="12" cy="15" r="1" />
<circle cx="19" cy="15" r="1" />
<circle cx="5" cy="15" r="1" />
</svg>
);
};
export default SVGGridHorizontalIcon;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-grip-horizontal"><circle cx="12" cy="9" r="1"/><circle cx="19" cy="9" r="1"/><circle cx="5" cy="9" r="1"/><circle cx="12" cy="15" r="1"/><circle cx="19" cy="15" r="1"/><circle cx="5" cy="15" r="1"/></svg>

After

Width:  |  Height:  |  Size: 406 B

View File

@@ -0,0 +1,9 @@
import { forwardRef } from "react";
import SVGGridHorizontalIcon from "./GridHorizontalIcon";
export const GridHorizontalIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SVGGridHorizontalIcon ref={ref} {...props} />;
});

View File

@@ -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%) */

View File

@@ -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,

16
uv.lock generated
View File

@@ -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" },