Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/backend/base/langflow/components/composio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .composio_api import ComposioAPIComponent
from .gmail_api import GmailAPIComponent

__all__ = ["ComposioAPIComponent"]
__all__ = ["ComposioAPIComponent", "GmailAPIComponent"]
276 changes: 276 additions & 0 deletions src/backend/base/langflow/components/composio/gmail_api.py
Original file line number Diff line number Diff line change
@@ -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

Check failure on line 4 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (F401)

src/backend/base/langflow/components/composio/gmail_api.py:4:40: F401 `composio_langchain.App` imported but unused
from langchain_core.tools import Tool

Check failure on line 5 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (F401)

src/backend/base/langflow/components/composio/gmail_api.py:5:34: F401 `langchain_core.tools.Tool` imported but unused
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

Check failure on line 11 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (F401)

src/backend/base/langflow/components/composio/gmail_api.py:11:73: F401 `langflow.inputs.MultiselectInput` imported but unused
from langflow.io import Output
from langflow.schema.message import Message


class GmailAPIComponent(LCToolComponent):

Check failure on line 16 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (I001)

src/backend/base/langflow/components/composio/gmail_api.py:2:1: I001 Import block is un-sorted or un-formatted
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'):

Check failure on line 105 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (Q000)

src/backend/base/langflow/components/composio/gmail_api.py:105:30: Q000 Single quotes found but double quotes preferred

Check failure on line 105 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (Q000)

src/backend/base/langflow/components/composio/gmail_api.py:105:61: Q000 Single quotes found but double quotes preferred

Check failure on line 105 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (Q000)

src/backend/base/langflow/components/composio/gmail_api.py:105:101: Q000 Single quotes found but double quotes preferred

Check failure on line 105 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (E501)

src/backend/base/langflow/components/composio/gmail_api.py:105:121: E501 Line too long (140 > 120)

Check failure on line 105 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (Q000)

src/backend/base/langflow/components/composio/gmail_api.py:105:133: Q000 Single quotes found but double quotes preferred
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)}"

Check failure on line 123 in src/backend/base/langflow/components/composio/gmail_api.py

View workflow job for this annotation

GitHub Actions / Ruff Style Check (3.12)

Ruff (RUF010)

src/backend/base/langflow/components/composio/gmail_api.py:123:55: RUF010 Use explicit conversion flag
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
4 changes: 4 additions & 0 deletions src/frontend/src/icons/gmail/gmail.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const Icon = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="25px" height="25px"><path fill="#4caf50" d="M45,16.2l-5,2.75l-5,4.75L35,40h7c1.657,0,3-1.343,3-3V16.2z"/><path fill="#1e88e5" d="M3,16.2l3.614,1.71L13,23.7V40H6c-1.657,0-3-1.343-3-3V16.2z"/><polygon fill="#e53935" points="35,11.2 24,19.45 13,11.2 12,17 13,23.7 24,31.95 35,23.7 36,17"/><path fill="#c62828" d="M3,12.298V16.2l10,7.5V11.2L9.876,8.859C9.132,8.301,8.228,8,7.298,8h0C4.924,8,3,9.924,3,12.298z"/><path fill="#fbc02d" d="M45,12.298V16.2l-10,7.5V11.2l3.124-2.341C38.868,8.301,39.772,8,40.702,8h0 C43.076,8,45,9.924,45,12.298z"/></svg>
);
export default Icon;
1 change: 1 addition & 0 deletions src/frontend/src/icons/gmail/gmail.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/frontend/src/icons/gmail/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import GmailIconSVG from "./gmail";

export const GmailIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <GmailIconSVG ref={ref} {...props} />;
});
3 changes: 3 additions & 0 deletions src/frontend/src/utils/styleUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GmailIcon } from "../icons/Gmail";
import { AIMLIcon } from "@/icons/AIML";
import { DuckDuckGoIcon } from "@/icons/DuckDuckGo";
import { ExaIcon } from "@/icons/Exa";
Expand Down Expand Up @@ -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" },
Expand Down Expand Up @@ -597,6 +599,7 @@ export const nodeIconsLucide: iconsType = {
ChatInput: MessagesSquare,
ChatOutput: MessagesSquare,
//Integration Icons
Gmail: GmailIcon,
LMStudio: LMStudioIcon,
Notify: Bell,
ListFlows: Group,
Expand Down
Loading