fix: remove composio Gmail component (#7319)

remove composio Gmail component
This commit is contained in:
Edwin Jose 2025-03-30 03:51:46 -04:00 committed by GitHub
commit 306f50b71f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1 additions and 746 deletions

View file

@ -1,4 +1,3 @@
from .composio_api import ComposioAPIComponent
from .gmail_api import GmailAPIComponent
__all__ = ["ComposioAPIComponent", "GmailAPIComponent"]
__all__ = ["ComposioAPIComponent"]

View file

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

View file

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