From 306f50b71f803cc2e5f1e74e7a47cb81a9ab9202 Mon Sep 17 00:00:00 2001 From: Edwin Jose Date: Sun, 30 Mar 2025 03:51:46 -0400 Subject: [PATCH] fix: remove composio Gmail component (#7319) remove composio Gmail component --- .../langflow/components/composio/__init__.py | 3 +- .../langflow/components/composio/gmail_api.py | 476 ------------------ .../bundles/composio/test_gmail_api.py | 268 ---------- 3 files changed, 1 insertion(+), 746 deletions(-) delete mode 100644 src/backend/base/langflow/components/composio/gmail_api.py delete mode 100644 src/backend/tests/unit/components/bundles/composio/test_gmail_api.py diff --git a/src/backend/base/langflow/components/composio/__init__.py b/src/backend/base/langflow/components/composio/__init__.py index 12d4ecac7..24e438134 100644 --- a/src/backend/base/langflow/components/composio/__init__.py +++ b/src/backend/base/langflow/components/composio/__init__.py @@ -1,4 +1,3 @@ from .composio_api import ComposioAPIComponent -from .gmail_api import GmailAPIComponent -__all__ = ["ComposioAPIComponent", "GmailAPIComponent"] +__all__ = ["ComposioAPIComponent"] diff --git a/src/backend/base/langflow/components/composio/gmail_api.py b/src/backend/base/langflow/components/composio/gmail_api.py deleted file mode 100644 index f397bf40d..000000000 --- a/src/backend/base/langflow/components/composio/gmail_api.py +++ /dev/null @@ -1,476 +0,0 @@ -from typing import Any - -from composio.client.collections import AppAuthScheme -from composio.client.exceptions import NoItemsFound -from composio_langchain import Action, ComposioToolSet -from langchain_core.tools import Tool -from loguru import logger - -from langflow.base.langchain_utilities.model import LCToolComponent -from langflow.inputs import ( - BoolInput, - DropdownInput, - FileInput, - IntInput, - LinkInput, - MessageTextInput, - SecretStrInput, - StrInput, -) -from langflow.io import Output -from langflow.schema.message import Message - - -class GmailAPIComponent(LCToolComponent): - display_name: str = "Gmail" - description: str = "Gmail API" - name = "GmailAPI" - icon = "Gmail" - documentation: str = "https://docs.composio.dev" - - _actions_data: dict = { - "GMAIL_SEND_EMAIL": { - "display_name": "Send Email", - "actions": ["recipient_email", "subject", "body", "cc", "bcc", "is_html"], - }, - "GMAIL_FETCH_EMAILS": { - "display_name": "Fetch Emails", - "actions": ["max_results", "query"], - }, - "GMAIL_GET_PROFILE": { - "display_name": "Get User Profile", - "actions": [], - }, - "GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID": { - "display_name": "Get Email By ID", - "actions": ["message_id"], - }, - "GMAIL_CREATE_EMAIL_DRAFT": { - "display_name": "Create Draft Email", - "actions": ["recipient_email", "subject", "body", "cc", "bcc", "is_html"], - }, - "GMAIL_FETCH_MESSAGE_BY_THREAD_ID": { - "display_name": "Get Message By Thread ID", - "actions": ["thread_id"], - }, - "GMAIL_LIST_THREADS": { - "display_name": "List Email Threads", - "actions": ["max_results", "query"], - }, - "GMAIL_REPLY_TO_THREAD": { - "display_name": "Reply To Thread", - "actions": ["thread_id", "message_body", "recipient_email"], - }, - "GMAIL_LIST_LABELS": { - "display_name": "List Email Labels", - "actions": [], - }, - "GMAIL_CREATE_LABEL": { - "display_name": "Create Email Label", - "actions": ["label_name"], - }, - "GMAIL_GET_PEOPLE": { - "display_name": "Get Contacts", - "actions": [], - }, - "GMAIL_REMOVE_LABEL": { - "display_name": "Delete Email Label", - "actions": ["label_id"], - }, - } - - _bool_variables = {"is_html", "include_spam_trash"} - - inputs = [ - MessageTextInput( - name="entity_id", - display_name="Entity ID", - value="default", - advanced=True, - tool_mode=True, # Intentionally setting tool_mode=True to make this Component support both tool and non-tool functionality # noqa: E501 - ), - SecretStrInput( - name="api_key", - display_name="Composio API Key", - required=True, - info="Refer to https://docs.composio.dev/faq/api_key/api_key", - real_time_refresh=True, - ), - 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, - refresh_button=True, - ), - # Non tool-mode input fields - DropdownInput( - name="action", - display_name="Action", - options=[], - value="", - info="Select Gmail action to pass to the agent", - show=True, - real_time_refresh=True, - required=True, - ), - MessageTextInput( - name="recipient_email", - display_name="Recipient Email", - info="Email address of the recipient", - show=False, - required=True, - ), - MessageTextInput( - name="subject", - display_name="Subject", - info="Subject of the email", - show=False, - required=True, - ), - MessageTextInput( - name="body", - display_name="Body", - required=True, - info="Content of the email", - show=False, - ), - IntInput( - name="max_results", - display_name="Max Results", - required=True, - info="Maximum number of emails to be returned", - show=False, - ), - MessageTextInput( - name="message_id", - display_name="Message ID", - info="The ID of the specific email message", - show=False, - required=True, - ), - StrInput( - name="thread_id", - display_name="Thread ID", - info="The ID of the email thread", - show=False, - required=True, - ), - MessageTextInput( - name="query", - display_name="Query", - info="Search query to filter emails (e.g., 'from:someone@email.com' or 'subject:hello')", - show=False, - ), - MessageTextInput( - name="message_body", - display_name="Message Body", - info="The body content of the message to be sent", - show=False, - ), - MessageTextInput( - name="label_name", - display_name="Label Name", - info="Name of the Gmail label to create, modify, or filter by", - show=False, - ), - MessageTextInput( - name="label_id", - display_name="Label ID", - info="The ID of the Gmail label", - show=False, - ), - MessageTextInput( - name="cc", - display_name="CC", - info="Email addresses to CC (Carbon Copy) in the email, separated by commas", - show=False, - ), - MessageTextInput( - name="bcc", - display_name="BCC", - info="Email addresses to BCC (Blid Carbon Copy) in the email, separated by commas", - show=False, - ), - BoolInput( - name="is_html", - display_name="Is HTML", - info="Specify whether the email body contains HTML content (true/false)", - show=False, - value=False, - ), - MessageTextInput( - name="page_token", - display_name="Page Token", - info="Token for retrieving the next page of results", - show=False, - ), - MessageTextInput( - name="label_ids", - display_name="Label Ids", - info="Comma-separated list of label IDs to filter messages", - show=False, - ), - BoolInput( - name="include_spam_trash", - display_name="Include messages from Spam/Trash", - info="Include messages from SPAM and TRASH in the results", - show=False, - value=False, - ), - MessageTextInput( - name="format", - display_name="Format", - info="The format to return the message in. Possible values: minimal, full, raw, metadata", - show=False, - ), - MessageTextInput( - name="label_list_visibility", - display_name="Label List Visibility", - info="The visibility of the label in the label list in the Gmail web interface. Possible values: 'labelShow' to show the label in the label list, 'labelShowIfUnread' to show the label if there are any unread messages with that label, 'labelHide' to not show the label in the label list", # noqa: E501 - show=False, - ), - MessageTextInput( - name="message_list_visibility", - display_name="Message List Visibility", - info="The visibility of the label in the message list in the Gmail web interface. Possible values: 'show' to show the label in the message list, 'hide' to not show the label in the message list", # noqa: E501 - show=False, - ), - MessageTextInput( - name="resource_name", - display_name="Resource Name", - info="The resource name of the person to provide information about. To get information about a google account, specify 'people/account_id'", # noqa: E501 - show=False, - ), - MessageTextInput( - name="person_fields", - display_name="Person fields", - info="A field mask to restrict which fields on the person are returned. Multiple fields can be specified by separating them with commas.Valid values are: addresses, ageRanges, biographies, birthdays, calendarUrls, clientData, coverPhotos, email Addresses etc", # noqa: E501 - show=False, - ), - MessageTextInput( - name="attachment_id", - display_name="Attachment ID", - info="Id of the attachment", - show=False, - required=True, - ), - MessageTextInput( - name="file_name", - display_name="File name", - info="File name of the attachment file", - show=False, - required=True, - ), - FileInput( - name="attachment", - display_name="Add Attachment", - file_types=[ - "csv", - "txt", - "doc", - "docx", - "xls", - "xlsx", - "pdf", - "png", - "jpg", - "jpeg", - "gif", - "zip", - "rar", - "ppt", - "pptx", - ], - info="Add an attachment", - show=False, - ), - ] - - outputs = [ - Output(name="text", display_name="Response", method="execute_action"), - ] - - def execute_action(self) -> Message: - """Execute Gmail action and return response as Message.""" - toolset = self._build_wrapper() - - try: - action_key = self.action - if action_key not in self._actions_data: - for key, data in self._actions_data.items(): - if data["display_name"] == action_key: - action_key = key - break - - enum_name = getattr(Action, action_key) - params = {} - if action_key in self._actions_data: - for field in self._actions_data[action_key]["actions"]: - value = getattr(self, field) - - # Skip empty values - if value is None or value == "": - continue - - # Handle comma-separated fields that should be converted to lists - if field in ["cc", "bcc", "label_ids"] and value: - value = [item.strip() for item in value.split(",")] - - # Handle boolean fields - if field in self._bool_variables: - value = bool(value) - - params[field] = value - - result = toolset.execute_action( - action=enum_name, - params=params, - ) - self.status = result - return Message(text=str(result)) - except Exception as e: - logger.error(f"Error executing action: {e}") - display_name = self.action - if self.action in self._actions_data: - display_name = self._actions_data[self.action]["display_name"] - msg = f"Failed to execute {display_name}: {e!s}" - raise ValueError(msg) from e - - def show_hide_fields(self, build_config: dict, field_value: Any): - all_fields = set() - for action_data in self._actions_data.values(): - all_fields.update(action_data["actions"]) - - for field in all_fields: - build_config[field]["show"] = False - - if field in self._bool_variables: - build_config[field]["value"] = False - else: - build_config[field]["value"] = "" - - action_key = field_value - if action_key not in self._actions_data: - for key, data in self._actions_data.items(): - if data["display_name"] == action_key: - action_key = key - break - - if action_key in self._actions_data: - for field in self._actions_data[action_key]["actions"]: - build_config[field]["show"] = True - - def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: - build_config["auth_status"]["show"] = True - build_config["auth_status"]["advanced"] = False - - if field_name == "tool_mode": - if field_value: - build_config["action"]["show"] = False - - all_fields = set() - for action_data in self._actions_data.values(): - all_fields.update(action_data["actions"]) - for field in all_fields: - build_config[field]["show"] = False - - else: - build_config["action"]["show"] = True - - if field_name == "action": - self.show_hide_fields(build_config, field_value) - - if hasattr(self, "api_key") and self.api_key != "": - gmail_display_names = [ - self._actions_data[action]["display_name"] for action in list(self._actions_data.keys()) - ] - build_config["action"]["options"] = gmail_display_names - - try: - toolset = self._build_wrapper() - entity = toolset.client.get_entity(id=self.entity_id) - - try: - entity.get_connection(app="gmail") - build_config["auth_status"]["value"] = "✅" - build_config["auth_link"]["show"] = False - - except NoItemsFound: - auth_scheme = self._get_auth_scheme("gmail") - if auth_scheme.auth_mode == "OAUTH2": - build_config["auth_link"]["show"] = True - build_config["auth_link"]["advanced"] = False - auth_url = self._initiate_default_connection(entity, "gmail") - build_config["auth_link"]["value"] = auth_url - build_config["auth_status"]["value"] = "Click link to authenticate" - - except (ValueError, ConnectionError) as e: - logger.error(f"Error checking auth status: {e}") - build_config["auth_status"]["value"] = f"Error: {e!s}" - - return build_config - - def _get_auth_scheme(self, app_name: str) -> AppAuthScheme: - """Get the primary auth scheme for an app. - - Args: - app_name (str): The name of the app to get auth scheme for. - - Returns: - AppAuthScheme: The auth scheme details. - """ - toolset = self._build_wrapper() - try: - return toolset.get_auth_scheme_for_app(app=app_name.lower()) - except Exception: # noqa: BLE001 - logger.exception(f"Error getting auth scheme for {app_name}") - return None - - def _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 _build_wrapper(self) -> ComposioToolSet: - """Build the Composio toolset wrapper. - - Returns: - ComposioToolSet: The initialized toolset. - - Raises: - ValueError: If the API key is not found or invalid. - """ - try: - if not self.api_key: - msg = "Composio API Key is required" - raise ValueError(msg) - return ComposioToolSet(api_key=self.api_key) - except ValueError as e: - logger.error(f"Error building Composio wrapper: {e}") - msg = "Please provide a valid Composio API Key in the component settings" - raise ValueError(msg) from e - - async def _get_tools(self) -> list[Tool]: - toolset = self._build_wrapper() - tools = toolset.get_tools(actions=self._actions_data.keys()) - for tool in tools: - tool.tags = [tool.name] # Assigning tags directly - return tools - - @property - def enabled_tools(self): - return [ - "GMAIL_SEND_EMAIL", - "GMAIL_FETCH_EMAILS", - ] diff --git a/src/backend/tests/unit/components/bundles/composio/test_gmail_api.py b/src/backend/tests/unit/components/bundles/composio/test_gmail_api.py deleted file mode 100644 index 1b5a42a76..000000000 --- a/src/backend/tests/unit/components/bundles/composio/test_gmail_api.py +++ /dev/null @@ -1,268 +0,0 @@ -from unittest.mock import MagicMock, patch - -import pytest -from composio.client.exceptions import NoItemsFound -from langflow.components.composio.gmail_api import GmailAPIComponent -from langflow.schema.message import Message - -from tests.base import ComponentTestBaseWithoutClient - - -class TestGmailAPIComponent(ComponentTestBaseWithoutClient): - @pytest.fixture - def component_class(self): - """Return the component class to test.""" - return GmailAPIComponent - - @pytest.fixture - def default_kwargs(self): - """Return the default kwargs for the component.""" - return { - "api_key": "test_api_key", - "entity_id": "default", - "action": "GMAIL_SEND_EMAIL", - "recipient_email": "test@example.com", - "subject": "Test Subject", - "body": "Test Body", - } - - @pytest.fixture - def file_names_mapping(self): - """Return an empty list since this component doesn't have version-specific files.""" - return [] - - @patch("langflow.components.composio.gmail_api.ComposioToolSet") - def test_execute_action_send_email_success(self, mock_toolset): - # Setup mock - mock_instance = mock_toolset.return_value - mock_instance.execute_action.return_value = "Email sent successfully" - - # Create component with required parameters - component = GmailAPIComponent( - api_key="test_api_key", - action="Send Email", # Using display name - recipient_email="test@example.com", - subject="Test Subject", - body="Test Body", - ) - - # Execute the action - result = component.execute_action() - - # Verify the result - assert isinstance(result, Message) - assert result.text == "Email sent successfully" - - # Verify the mock was called with correct parameters - mock_instance.execute_action.assert_called_once() - call_args = mock_instance.execute_action.call_args[1] - assert call_args["params"]["recipient_email"] == "test@example.com" - assert call_args["params"]["subject"] == "Test Subject" - assert call_args["params"]["body"] == "Test Body" - - @patch("langflow.components.composio.gmail_api.ComposioToolSet") - def test_execute_action_fetch_emails(self, mock_toolset): - # Setup mock - mock_instance = mock_toolset.return_value - mock_instance.execute_action.return_value = "Retrieved 5 emails" - - # Create component - component = GmailAPIComponent( - api_key="test_api_key", action="Fetch Emails", max_results=5, query="from:test@example.com" - ) - - # Execute the action - result = component.execute_action() - - # Verify the result - assert isinstance(result, Message) - assert result.text == "Retrieved 5 emails" - - # Verify the mock was called with correct parameters - mock_instance.execute_action.assert_called_once() - call_args = mock_instance.execute_action.call_args[1] - assert call_args["params"]["max_results"] == 5 - assert call_args["params"]["query"] == "from:test@example.com" - - @patch("langflow.components.composio.gmail_api.ComposioToolSet") - def test_execute_action_error(self, mock_toolset): - # Setup mock to raise an exception - mock_instance = mock_toolset.return_value - mock_instance.execute_action.side_effect = Exception("API Error") - - # Create component - component = GmailAPIComponent( - api_key="test_api_key", - action="Send Email", - recipient_email="test@example.com", - subject="Test Subject", - body="Test Body", - ) - - # Execute the action and expect an error - with pytest.raises(ValueError, match="Failed to execute Send Email: API Error"): - component.execute_action() - - @patch("langflow.components.composio.gmail_api.ComposioToolSet") - def test_update_build_config_with_valid_api_key(self, mock_toolset): - # Setup mocks - mock_instance = mock_toolset.return_value - mock_entity = MagicMock() - mock_instance.client.get_entity.return_value = mock_entity - - # Mock successful connection - mock_entity.get_connection.return_value = "connected" - - # Create component - component = GmailAPIComponent(api_key="test_api_key") - - # Test update_build_config - build_config = { - "auth_status": {"show": False, "value": "Not Connected", "advanced": True}, - "auth_link": {"show": False, "value": "", "advanced": True}, - "action": {"show": True, "options": []}, - # Add all action fields with default show=False - "recipient_email": {"show": False, "value": ""}, - "subject": {"show": False, "value": ""}, - "body": {"show": False, "value": ""}, - "max_results": {"show": False, "value": ""}, - "query": {"show": False, "value": ""}, - "message_id": {"show": False, "value": ""}, - "thread_id": {"show": False, "value": ""}, - "message_body": {"show": False, "value": ""}, - "label_name": {"show": False, "value": ""}, - "label_id": {"show": False, "value": ""}, - "cc": {"show": False, "value": ""}, - "bcc": {"show": False, "value": ""}, - "is_html": {"show": False, "value": False}, - } - - result = component.update_build_config(build_config, "Send Email", "action") - - # Verify the result - assert result["auth_status"]["value"] == "✅" - assert result["auth_link"]["show"] is False - assert "Send Email" in result["action"]["options"] - - # Verify fields for Send Email are shown - assert result["recipient_email"]["show"] is True - assert result["subject"]["show"] is True - assert result["body"]["show"] is True - - @patch("langflow.components.composio.gmail_api.ComposioToolSet") - def test_update_build_config_needs_authentication(self, mock_toolset): - # Setup mocks - mock_instance = mock_toolset.return_value - mock_entity = MagicMock() - mock_instance.client.get_entity.return_value = mock_entity - - # Mock connection not found - mock_entity.get_connection.side_effect = NoItemsFound("Connection not found") - - # Mock auth scheme - mock_auth_scheme = MagicMock() - mock_auth_scheme.auth_mode = "OAUTH2" - component = GmailAPIComponent(api_key="test_api_key") - component._get_auth_scheme = MagicMock(return_value=mock_auth_scheme) - - # Mock initiate connection - component._initiate_default_connection = MagicMock(return_value="https://auth.example.com") - - # Test update_build_config - build_config = { - "auth_status": {"show": False, "value": "Not Connected", "advanced": True}, - "auth_link": {"show": False, "value": "", "advanced": True}, - "action": {"show": True, "options": []}, - # Add all action fields with default show=False - "recipient_email": {"show": False, "value": ""}, - "subject": {"show": False, "value": ""}, - "body": {"show": False, "value": ""}, - } - - result = component.update_build_config(build_config, None, None) - - # Verify the result - assert result["auth_status"]["value"] == "Click link to authenticate" - assert result["auth_link"]["show"] is True - assert result["auth_link"]["value"] == "https://auth.example.com" - - def test_show_hide_fields(self): - # Create component - component = GmailAPIComponent() - - # Create a build config with all fields - build_config = { - "recipient_email": {"show": False, "value": ""}, - "subject": {"show": False, "value": ""}, - "body": {"show": False, "value": ""}, - "max_results": {"show": False, "value": ""}, - "query": {"show": False, "value": ""}, - "message_id": {"show": False, "value": ""}, - "thread_id": {"show": False, "value": ""}, - "message_body": {"show": False, "value": ""}, - "label_name": {"show": False, "value": ""}, - "label_id": {"show": False, "value": ""}, - "cc": {"show": False, "value": ""}, - "bcc": {"show": False, "value": ""}, - "is_html": {"show": False, "value": False}, - } - - # Test with Send Email action - component.show_hide_fields(build_config, "Send Email") - - # Verify Send Email fields are shown - assert build_config["recipient_email"]["show"] is True - assert build_config["subject"]["show"] is True - assert build_config["body"]["show"] is True - assert build_config["cc"]["show"] is True - assert build_config["bcc"]["show"] is True - assert build_config["is_html"]["show"] is True - - # Verify other fields are hidden - assert build_config["max_results"]["show"] is False - assert build_config["query"]["show"] is False - - # Reset and test with Fetch Emails action - for config in build_config.values(): - config["show"] = False - - component.show_hide_fields(build_config, "Fetch Emails") - - # Verify Fetch Emails fields are shown - assert build_config["max_results"]["show"] is True - assert build_config["query"]["show"] is True - - # Verify other fields are hidden - assert build_config["recipient_email"]["show"] is False - assert build_config["subject"]["show"] is False - - @patch("langflow.components.composio.gmail_api.ComposioToolSet") - async def test_get_tools(self, mock_toolset): - # Setup mock - mock_instance = mock_toolset.return_value - mock_tools = [MagicMock(), MagicMock()] - # Configure the mock tools to have name attributes - mock_tools[0].name = "GMAIL_SEND_EMAIL" - mock_tools[1].name = "GMAIL_FETCH_EMAILS" - mock_instance.get_tools.return_value = mock_tools - - # Create component - component = GmailAPIComponent(api_key="test_api_key") - - # Get tools - tools = await component._get_tools() - - # Verify the result - assert tools == mock_tools - assert all(hasattr(tool, "tags") for tool in tools) - - # Verify that each tool has tags that are a list containing a string - for tool in tools: - assert isinstance(tool.tags, list), f"Tool tags should be a list, got {type(tool.tags)}" - assert len(tool.tags) == 1, f"Tool tags should have exactly one element, got {len(tool.tags)}" - assert isinstance(tool.tags[0], str), f"Tool tag should be a string, got {type(tool.tags[0])}" - assert tool.tags[0] == tool.name, f"Tool tag should be the tool name, got {tool.tags[0]}" - - # Verify the mock was called with correct parameters - mock_instance.get_tools.assert_called_once() - assert set(mock_instance.get_tools.call_args[1]["actions"]) == set(component._actions_data.keys())