diff --git a/src/backend/base/langflow/components/composio/__init__.py b/src/backend/base/langflow/components/composio/__init__.py index 24e438134a44..12d4ecac72af 100644 --- a/src/backend/base/langflow/components/composio/__init__.py +++ b/src/backend/base/langflow/components/composio/__init__.py @@ -1,3 +1,4 @@ from .composio_api import ComposioAPIComponent +from .gmail_api import GmailAPIComponent -__all__ = ["ComposioAPIComponent"] +__all__ = ["ComposioAPIComponent", "GmailAPIComponent"] diff --git a/src/backend/base/langflow/components/composio/gmail_api.py b/src/backend/base/langflow/components/composio/gmail_api.py new file mode 100644 index 000000000000..c24cdb86141f --- /dev/null +++ b/src/backend/base/langflow/components/composio/gmail_api.py @@ -0,0 +1,276 @@ +# Third-party imports +from composio.client.collections import AppAuthScheme +from composio.client.exceptions import NoItemsFound +from composio_langchain import Action, App, ComposioToolSet +from langchain_core.tools import Tool +from loguru import logger +from typing import Any + +# Local imports +from langflow.base.langchain_utilities.model import LCToolComponent +from langflow.inputs import DropdownInput, LinkInput, MessageTextInput, MultiselectInput, SecretStrInput, StrInput +from langflow.io import Output +from langflow.schema.message import Message + + +class GmailAPIComponent(LCToolComponent): + display_name: str = "Gmail" + description: str = "Use Gmail API to send emails and create drafts" + name = "GmailAPI" + icon = "Gmail" + documentation: str = "https://docs.composio.dev" + + inputs = [ + MessageTextInput( + name="entity_id", + display_name="Entity ID", + value="default", + advanced=True, + tool_mode=True, # Enable tool mode toggle + ), + 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 inputs - explicitly set show=True + DropdownInput( + name="action", + display_name="Action", + # options=["GMAIL_SEND_EMAIL", "GMAIL_CREATE_EMAIL_DRAFT"], + options=[], + value="", + info="Select Gmail action to perform", + show=True, + real_time_refresh=True, + ), + MessageTextInput( + name="recipient_email", + display_name="Recipient Email", + required=True, + info="Email address of the recipient", + show=False, + tool_mode=True + ), + MessageTextInput( + name="subject", + display_name="Subject", + required=True, + info="Subject of the email", + show=False, + tool_mode=True, + ), + MessageTextInput( + name="body", + display_name="Body", + required=True, + info="Content of the email", + show=False, + tool_mode=True, + ) + ] + + outputs = [ + Output( + name="text", + display_name="Result", + method="process_action" + ), + ] + + def process_action(self) -> Message: + """Process Gmail action and return result as Message.""" + toolset = self._build_wrapper() + + if not hasattr(self, 'action') or not hasattr(self, 'recipient_email') or not hasattr(self, 'subject') or not hasattr(self, 'body'): + msg = "Missing required fields" + raise ValueError(msg) + + try: + enum_name = getattr(Action, self.action) + result = toolset.execute_action( + action=enum_name, + params={ + "recipient_email": self.recipient_email, + "subject": self.subject, + "body": self.body + } + ) + self.status = result + return Message(text=str(result)) + except Exception as e: + logger.error(f"Error executing action: {e}") + msg = f"Failed to execute {self.action}: {str(e)}" + raise ValueError(msg) from e + + def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict: + # Always show auth status + build_config["auth_status"]["show"] = True + build_config["auth_status"]["advanced"] = False + + # Handle action selection changes + if field_name == "action": + if field_value != "": + build_config["recipient_email"]["show"] = True + build_config["subject"]["show"] = True + build_config["body"]["show"] = True + + # Handle authentication checks if API key is present + if hasattr(self, "api_key") and self.api_key != "": + build_config["action"]["options"] = ["GMAIL_SEND_EMAIL", "GMAIL_CREATE_EMAIL_DRAFT"] + try: + toolset = self._build_wrapper() + entity = toolset.client.get_entity(id=self.entity_id) + + try: + # Check if already connected + entity.get_connection(app="gmail") + build_config["auth_status"]["value"] = "✅" + build_config["auth_link"]["show"] = False + + except NoItemsFound: + # Handle authentication + 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 Exception 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 _handle_auth_by_scheme(self, entity: Any, app: str, auth_scheme: AppAuthScheme) -> str: + """Handle authentication based on the auth scheme. + + Args: + entity (Any): The entity instance. + app (str): The app name. + auth_scheme (AppAuthScheme): The auth scheme details. + + Returns: + str: The authentication status or URL. + """ + auth_mode = auth_scheme.auth_mode + + 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" + + 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" + + 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" + + 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" + + 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 \ No newline at end of file diff --git a/src/frontend/src/icons/gmail/gmail.jsx b/src/frontend/src/icons/gmail/gmail.jsx new file mode 100644 index 000000000000..80eeade3e45e --- /dev/null +++ b/src/frontend/src/icons/gmail/gmail.jsx @@ -0,0 +1,4 @@ +const Icon = (props) => ( + +); +export default Icon; diff --git a/src/frontend/src/icons/gmail/gmail.svg b/src/frontend/src/icons/gmail/gmail.svg new file mode 100644 index 000000000000..ad845176a0a2 --- /dev/null +++ b/src/frontend/src/icons/gmail/gmail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/frontend/src/icons/gmail/index.tsx b/src/frontend/src/icons/gmail/index.tsx new file mode 100644 index 000000000000..3e5a81c2b1aa --- /dev/null +++ b/src/frontend/src/icons/gmail/index.tsx @@ -0,0 +1,9 @@ +import React, { forwardRef } from "react"; +import GmailIconSVG from "./gmail"; + +export const GmailIcon = forwardRef< + SVGSVGElement, + React.PropsWithChildren<{}> +>((props, ref) => { + return ; +}); \ No newline at end of file diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts index 2fcfeb84b5cf..b70f6a89780f 100644 --- a/src/frontend/src/utils/styleUtils.ts +++ b/src/frontend/src/utils/styleUtils.ts @@ -1,3 +1,4 @@ +import { GmailIcon } from "../icons/Gmail"; import { AIMLIcon } from "@/icons/AIML"; import { DuckDuckGoIcon } from "@/icons/DuckDuckGo"; import { ExaIcon } from "@/icons/Exa"; @@ -515,6 +516,7 @@ export const SIDEBAR_CATEGORIES = [ ]; export const SIDEBAR_BUNDLES = [ + { display_name: "Gmail", name: "gmail", icon: "Gmail" }, { display_name: "LangChain", name: "langchain_utilities", icon: "LangChain" }, { display_name: "AgentQL", name: "agentql", icon: "AgentQL" }, { display_name: "AssemblyAI", name: "assemblyai", icon: "AssemblyAI" }, @@ -597,6 +599,7 @@ export const nodeIconsLucide: iconsType = { ChatInput: MessagesSquare, ChatOutput: MessagesSquare, //Integration Icons + Gmail: GmailIcon, LMStudio: LMStudioIcon, Notify: Bell, ListFlows: Group,