feat: update Composio components (#8868)

Co-authored-by: Uday-sidagana <uday.sidgana@gmail.com>
Co-authored-by: Uday Sidagana <udaysidagana@Mac.lan>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
Co-authored-by: Uday Sidagana <129588963+Uday-sidagana@users.noreply.github.com>
This commit is contained in:
Abhishek Patil 2025-08-18 21:27:13 +05:30 committed by GitHub
commit f6e6edbc37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1749 additions and 4627 deletions

View file

@ -66,8 +66,8 @@ dependencies = [
"yfinance==0.2.50",
"wolframalpha==5.1.3",
"astra-assistants[tools]~=2.2.12",
"composio-langchain==0.7.15",
"composio-core==0.7.15",
"composio-langchain==0.8.5",
"composio==0.8.5",
"spider-client==0.1.24",
"nltk==3.9.1",
"lark==1.2.2",
@ -329,4 +329,4 @@ ignore_missing_imports = true
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
build-backend = "hatchling.build"

View file

@ -724,7 +724,7 @@ async def custom_component_update(
field_value=code_request.field_value,
field_name=code_request.field,
)
if "code" not in updated_build_config:
if "code" not in updated_build_config or not updated_build_config.get("code", {}).get("value"):
updated_build_config = add_code_field_to_build_config(updated_build_config, code_request.code)
component_node["template"] = updated_build_config

File diff suppressed because it is too large Load diff

View file

@ -9,16 +9,32 @@ if TYPE_CHECKING:
from .github_composio import ComposioGitHubAPIComponent
from .gmail_composio import ComposioGmailAPIComponent
from .googlecalendar_composio import ComposioGoogleCalendarAPIComponent
from .googlemeet_composio import ComposioGooglemeetAPIComponent
from .googletasks_composio import ComposioGoogleTasksAPIComponent
from .linear_composio import ComposioLinearAPIComponent
from .outlook_composio import ComposioOutlookAPIComponent
from .reddit_composio import ComposioRedditAPIComponent
from .slack_composio import ComposioSlackAPIComponent
from .slackbot_composio import ComposioSlackbotAPIComponent
from .supabase_composio import ComposioSupabaseAPIComponent
from .todoist_composio import ComposioTodoistAPIComponent
from .youtube_composio import ComposioYoutubeAPIComponent
_dynamic_imports = {
"ComposioAPIComponent": "composio_api",
"ComposioGitHubAPIComponent": "github_composio",
"ComposioGmailAPIComponent": "gmail_composio",
"ComposioGoogleCalendarAPIComponent": "googlecalendar_composio",
"ComposioGooglemeetAPIComponent": "googlemeet_composio",
"ComposioOutlookAPIComponent": "outlook_composio",
"ComposioSlackAPIComponent": "slack_composio",
"ComposioGoogleTasksAPIComponent": "googletasks_composio",
"ComposioLinearAPIComponent": "linear_composio",
"ComposioRedditAPIComponent": "reddit_composio",
"ComposioSlackbotAPIComponent": "slackbot_composio",
"ComposioSupabaseAPIComponent": "supabase_composio",
"ComposioTodoistAPIComponent": "todoist_composio",
"ComposioYoutubeAPIComponent": "youtube_composio",
}
__all__ = [
@ -26,8 +42,16 @@ __all__ = [
"ComposioGitHubAPIComponent",
"ComposioGmailAPIComponent",
"ComposioGoogleCalendarAPIComponent",
"ComposioGoogleTasksAPIComponent",
"ComposioGooglemeetAPIComponent",
"ComposioLinearAPIComponent",
"ComposioOutlookAPIComponent",
"ComposioRedditAPIComponent",
"ComposioSlackAPIComponent",
"ComposioSlackbotAPIComponent",
"ComposioSupabaseAPIComponent",
"ComposioTodoistAPIComponent",
"ComposioYoutubeAPIComponent",
]

View file

@ -2,10 +2,10 @@
from collections.abc import Sequence
from typing import Any
from composio import Action, App
from composio import Composio
from composio_langchain import LangchainProvider
# Third-party imports
from composio_langchain import ComposioToolSet
from langchain_core.tools import Tool
# Local imports
@ -69,27 +69,7 @@ class ComposioAPIComponent(LCToolComponent):
Output(name="tools", display_name="Tools", method="build_tool"),
]
def sanitize_action_name(self, action_name: str) -> str:
# TODO: Maybe restore
return action_name
# We want to use title case, and replace underscores with spaces
sanitized_name = action_name.replace("_", " ").title()
# Now we want to remove everything from and including the first dot
return sanitized_name.replace(self.tool_name.title() + " ", "")
def desanitize_action_name(self, action_name: str) -> str:
# TODO: Maybe restore
return action_name
# We want to reverse what we did above
unsanitized_name = action_name.replace(" ", "_").upper()
# 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 validate_tool(self, build_config: dict, field_value: Any, connected_app_names: list) -> dict:
def validate_tool(self, build_config: dict, field_value: Any, tool_name: str | None = None) -> dict:
# Get the index of the selected tool in the list of options
selected_tool_index = next(
(
@ -108,35 +88,40 @@ class ComposioAPIComponent(LCToolComponent):
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,
)
try:
composio = self._build_wrapper()
current_tool = tool_name or getattr(self, "tool_name", None)
if not current_tool:
self.log("No tool name available for validate_tool")
return build_config
toolkit_slug = current_tool.lower()
tools = composio.tools.get(user_id=self.entity_id, toolkits=[toolkit_slug])
authenticated_actions = []
for tool in tools:
if hasattr(tool, "name"):
action_name = tool.name
display_name = action_name.replace("_", " ").title()
authenticated_actions.append({"name": action_name, "display_name": display_name})
except (ValueError, ConnectionError, AttributeError) as e:
self.log(f"Error getting actions for {current_tool or 'unknown tool'}: {e}")
authenticated_actions = []
# Return the list of action names
build_config["actions"]["options"] = [
{
"name": self.sanitize_action_name(action.name),
"name": action["name"],
}
for action in authenticated_actions
]
# Lastly, we need to show the actions field
build_config["actions"]["show"] = True
return build_config
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"] = ""
@ -147,113 +132,94 @@ class ComposioAPIComponent(LCToolComponent):
return build_config
# TODO: Re-enable dynamic tool list
# Initialize the Composio ToolSet with your API key
# toolset = ComposioToolSet(api_key=self.api_key)
# Get the entity (e.g., "default" for your user)
# entity = toolset.get_entity(self.entity_id)
# Get all available apps
# all_apps = entity.client.apps.get()
# Build an object with name, icon, link
# Build the list of available tools
build_config["tool_name"]["options"] = [
{
"name": app.title(), # TODO: Switch to app.name
"icon": app, # TODO: Switch to app.name
"name": app.title(),
"icon": app,
"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)
]
return build_config
# 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"]
composio = self._build_wrapper()
# 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,
current_tool_name = (
field_value
if isinstance(field_value, str)
else field_value.get("validate")
if isinstance(field_value, dict) and "validate" in field_value
else getattr(self, "tool_name", None)
)
# Initiate a GitHub connection and get the redirect URL
try:
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",
}
if not current_tool_name:
self.log("No tool name available for connection check")
return build_config
# Print the direct HTTP link for authentication
build_config["tool_name"]["options"][selected_tool_index]["link"] = connection_request.redirectUrl
try:
toolkit_slug = current_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."
connection_list = composio.connected_accounts.list(
user_ids=[self.entity_id], toolkit_slugs=[toolkit_slug]
)
# Check for active connections
has_active_connections = False
if (
connection_list
and hasattr(connection_list, "items")
and connection_list.items
and isinstance(connection_list.items, list)
and len(connection_list.items) > 0
):
for connection in connection_list.items:
if getattr(connection, "status", None) == "ACTIVE":
has_active_connections = True
break
# 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"] == current_tool_name.title()
),
None,
)
if has_active_connections:
# User has active connection
if selected_tool_index is not None:
build_config["tool_name"]["options"][selected_tool_index]["link"] = "validated"
# If it's a validation request, validate the tool
if (isinstance(field_value, dict) and "validate" in field_value) or isinstance(field_value, str):
return self.validate_tool(build_config, field_value, current_tool_name)
else:
# No active connection - create OAuth connection
try:
connection = composio.toolkits.authorize(user_id=self.entity_id, toolkit=toolkit_slug)
redirect_url = getattr(connection, "redirect_url", None)
if redirect_url and redirect_url.startswith(("http://", "https://")):
if selected_tool_index is not None:
build_config["tool_name"]["options"][selected_tool_index]["link"] = redirect_url
elif selected_tool_index is not None:
build_config["tool_name"]["options"][selected_tool_index]["link"] = "error"
except (ValueError, ConnectionError, AttributeError) as e:
self.log(f"Error creating OAuth connection: {e}")
if selected_tool_index is not None:
build_config["tool_name"]["options"][selected_tool_index]["link"] = "error"
except (ValueError, ConnectionError, AttributeError) as e:
self.log(f"Error checking connection status: {e}")
return build_config
@ -263,16 +229,30 @@ class ComposioAPIComponent(LCToolComponent):
Returns:
Sequence[Tool]: List of configured Composio tools.
"""
composio_toolset = self._build_wrapper()
return composio_toolset.get_tools(
actions=[self.desanitize_action_name(action["name"]) for action in self.actions]
)
composio = self._build_wrapper()
action_names = [action["name"] for action in self.actions]
def _build_wrapper(self) -> ComposioToolSet:
"""Build the Composio toolset wrapper.
# Get toolkits from action names
toolkits = set()
for action_name in action_names:
if "_" in action_name:
toolkit = action_name.split("_")[0].lower()
toolkits.add(toolkit)
if not toolkits:
return []
# Get all tools for the relevant toolkits
all_tools = composio.tools.get(user_id=self.entity_id, toolkits=list(toolkits))
# Filter to only the specific actions we want using list comprehension
return [tool for tool in all_tools if hasattr(tool, "name") and tool.name in action_names]
def _build_wrapper(self) -> Composio:
"""Build the Composio wrapper using new SDK.
Returns:
ComposioToolSet: The initialized toolset.
Composio: The initialized Composio client.
Raises:
ValueError: If the API key is not found or invalid.
@ -281,7 +261,7 @@ class ComposioAPIComponent(LCToolComponent):
if not self.api_key:
msg = "Composio API Key is required"
raise ValueError(msg)
return ComposioToolSet(api_key=self.api_key, entity_id=self.entity_id)
return Composio(api_key=self.api_key, provider=LangchainProvider())
except ValueError as e:
self.log(f"Error building Composio wrapper: {e}")
msg = "Please provide a valid Composio API Key in the component settings"

View file

@ -0,0 +1,11 @@
from langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioDropboxAPIComponent(ComposioBaseComponent):
display_name: str = "Dropbox"
icon = "Dropbox"
documentation: str = "https://docs.composio.dev"
app_name = "dropbox"
def set_default_tools(self):
"""Set the default tools for Dropbox component."""

View file

@ -1,649 +1,11 @@
import json
from typing import Any
from composio import Action
from langflow.base.composio.composio_base import ComposioBaseComponent
from langflow.inputs import (
BoolInput,
IntInput,
MessageTextInput,
)
from langflow.logging import logger
class ComposioGitHubAPIComponent(ComposioBaseComponent):
"""GitHub API component for interacting with GitHub services."""
display_name: str = "GitHub"
description: str = "GitHub API"
icon = "Github"
documentation: str = "https://docs.composio.dev"
app_name = "github"
# GitHub-specific actions
_actions_data: dict = {
"GITHUB_CREATE_A_PULL_REQUEST": {
"display_name": "Create A Pull Request",
"action_fields": [
"GITHUB_CREATE_A_PULL_REQUEST_owner",
"GITHUB_CREATE_A_PULL_REQUEST_repo",
"GITHUB_CREATE_A_PULL_REQUEST_title",
"GITHUB_CREATE_A_PULL_REQUEST_head",
"GITHUB_CREATE_A_PULL_REQUEST_head_repo",
"GITHUB_CREATE_A_PULL_REQUEST_base",
"GITHUB_CREATE_A_PULL_REQUEST_body",
"GITHUB_CREATE_A_PULL_REQUEST_maintainer_can_modify",
"GITHUB_CREATE_A_PULL_REQUEST_draft",
"GITHUB_CREATE_A_PULL_REQUEST_issue",
],
},
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER": {
"display_name": "Star A Repository",
"action_fields": [
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER_owner",
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER_repo",
],
},
"GITHUB_LIST_COMMITS": {
"display_name": "List Commits",
"action_fields": [
"GITHUB_LIST_COMMITS_owner",
"GITHUB_LIST_COMMITS_repo",
"GITHUB_LIST_COMMITS_sha",
"GITHUB_LIST_COMMITS_path",
"GITHUB_LIST_COMMITS_author",
"GITHUB_LIST_COMMITS_committer",
"GITHUB_LIST_COMMITS_since",
"GITHUB_LIST_COMMITS_until",
"GITHUB_LIST_COMMITS_per_page",
"GITHUB_LIST_COMMITS_page",
],
},
"GITHUB_GET_A_PULL_REQUEST": {
"display_name": "Get A Pull Request",
"action_fields": [
"GITHUB_GET_A_PULL_REQUEST_owner",
"GITHUB_GET_A_PULL_REQUEST_repo",
"GITHUB_GET_A_PULL_REQUEST_pull_number",
],
},
"GITHUB_CREATE_AN_ISSUE": {
"display_name": "Create An Issue",
"action_fields": [
"GITHUB_CREATE_AN_ISSUE_owner",
"GITHUB_CREATE_AN_ISSUE_repo",
"GITHUB_CREATE_AN_ISSUE_title",
"GITHUB_CREATE_AN_ISSUE_body",
"GITHUB_CREATE_AN_ISSUE_assignee",
"GITHUB_CREATE_AN_ISSUE_milestone",
"GITHUB_CREATE_AN_ISSUE_labels",
"GITHUB_CREATE_AN_ISSUE_assignees",
],
},
"GITHUB_LIST_REPOSITORY_ISSUES": {
"display_name": "List Repository Issues",
"action_fields": [
"GITHUB_LIST_REPOSITORY_ISSUES_owner",
"GITHUB_LIST_REPOSITORY_ISSUES_repo",
"GITHUB_LIST_REPOSITORY_ISSUES_milestone",
"GITHUB_LIST_REPOSITORY_ISSUES_state",
"GITHUB_LIST_REPOSITORY_ISSUES_assignee",
"GITHUB_LIST_REPOSITORY_ISSUES_creator",
"GITHUB_LIST_REPOSITORY_ISSUES_mentioned",
"GITHUB_LIST_REPOSITORY_ISSUES_labels",
"GITHUB_LIST_REPOSITORY_ISSUES_sort",
"GITHUB_LIST_REPOSITORY_ISSUES_direction",
"GITHUB_LIST_REPOSITORY_ISSUES_since",
"GITHUB_LIST_REPOSITORY_ISSUES_per_page",
"GITHUB_LIST_REPOSITORY_ISSUES_page",
],
},
"GITHUB_LIST_BRANCHES": {
"display_name": "List Branches",
"action_fields": [
"GITHUB_LIST_BRANCHES_owner",
"GITHUB_LIST_BRANCHES_repo",
"GITHUB_LIST_BRANCHES_protected",
"GITHUB_LIST_BRANCHES_per_page",
"GITHUB_LIST_BRANCHES_page",
],
},
"GITHUB_LIST_PULL_REQUESTS": {
"display_name": "List Pull Requests",
"action_fields": [
"GITHUB_LIST_PULL_REQUESTS_owner",
"GITHUB_LIST_PULL_REQUESTS_repo",
"GITHUB_LIST_PULL_REQUESTS_state",
"GITHUB_LIST_PULL_REQUESTS_head",
"GITHUB_LIST_PULL_REQUESTS_base",
"GITHUB_LIST_PULL_REQUESTS_sort",
"GITHUB_LIST_PULL_REQUESTS_direction",
"GITHUB_LIST_PULL_REQUESTS_per_page",
"GITHUB_LIST_PULL_REQUESTS_page",
],
},
}
_all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]}
_bool_variables = {
"GITHUB_CREATE_A_PULL_REQUEST_maintainer_can_modify",
"GITHUB_CREATE_A_PULL_REQUEST_draft",
"GITHUB_LIST_BRANCHES_protected",
}
inputs = [
*ComposioBaseComponent._base_inputs,
MessageTextInput(
name="GITHUB_CREATE_AN_ISSUE_owner",
display_name="Owner",
info="The account owner of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_CREATE_AN_ISSUE_repo",
display_name="Repo",
info="The name of the repository. The name is not case sensitive. ",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_CREATE_AN_ISSUE_title",
display_name="Title",
info="The title of the issue.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_CREATE_AN_ISSUE_body",
display_name="Body",
info="The contents of the issue.",
show=False,
),
MessageTextInput(
name="GITHUB_CREATE_AN_ISSUE_assignee",
display_name="Assignee",
info="Login for the user that this issue should be assigned to. _NOTE: Only users with push access can set the assignee for new issues. The assignee is silently dropped otherwise. **This field is deprecated.**_ ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_CREATE_AN_ISSUE_milestone",
display_name="Milestone",
info="Milestone",
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_CREATE_AN_ISSUE_labels",
display_name="Labels",
info="Labels to associate with this issue. _NOTE: Only users with push access can set labels for new issues. Labels are silently dropped otherwise._ ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_CREATE_AN_ISSUE_assignees",
display_name="Assignees",
info="Logins for Users to assign to this issue. _NOTE: Only users with push access can set assignees for new issues. Assignees are silently dropped otherwise._ ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_PULL_REQUESTS_owner",
display_name="Owner",
info="The account owner of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_LIST_PULL_REQUESTS_repo",
display_name="Repo",
info="The name of the repository. The name is not case sensitive. ",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_LIST_PULL_REQUESTS_state",
display_name="State",
info="Either `open`, `closed`, or `all` to filter by state.",
show=False,
value="open",
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_PULL_REQUESTS_head",
display_name="Head",
info="Filter pulls by head user or head organization and branch name in the format of `user:ref-name` or `organization:ref-name`. For example: `github:new-script-format` or `octocat:test-branch`. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_PULL_REQUESTS_base",
display_name="Base",
info="Filter pulls by base branch name. Example: `gh-pages`.",
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_PULL_REQUESTS_sort",
display_name="Sort",
info="What to sort results by. `popularity` will sort by the number of comments. `long-running` will sort by date created and will limit the results to pull requests that have been open for more than a month and have had activity within the past month. ", # noqa: E501
show=False,
value="created",
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_PULL_REQUESTS_direction",
display_name="Direction",
info="The direction of the sort. Default: `desc` when sort is `created` or sort is not specified, otherwise `asc`. ", # noqa: E501
show=False,
advanced=True,
),
IntInput(
name="GITHUB_LIST_PULL_REQUESTS_per_page",
display_name="Per Page",
info="The number of results per page (max 100)",
show=False,
value=1,
advanced=True,
),
IntInput(
name="GITHUB_LIST_PULL_REQUESTS_page",
display_name="Page",
info="The page number of the results to fetch",
show=False,
value=1,
advanced=True,
),
MessageTextInput(
name="GITHUB_CREATE_A_PULL_REQUEST_owner",
display_name="Owner",
info="The account owner of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_CREATE_A_PULL_REQUEST_repo",
display_name="Repo",
info="The name of the repository. The name is not case sensitive. ",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_CREATE_A_PULL_REQUEST_title",
display_name="Title",
info="The title of the new pull request. Required unless `issue` is specified.",
show=False,
),
MessageTextInput(
name="GITHUB_CREATE_A_PULL_REQUEST_head",
display_name="Head",
info="The name of the branch where your changes are implemented. For cross-repository pull requests in the same network, namespace `head` with a user like this: `username:branch`. ", # noqa: E501
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_CREATE_A_PULL_REQUEST_head_repo",
display_name="Head Repo",
info="The name of the repository where the changes in the pull request were made. This field is required for cross-repository pull requests if both repositories are owned by the same organization. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_CREATE_A_PULL_REQUEST_base",
display_name="Base",
info="The name of the branch you want the changes pulled into. This should be an existing branch on the current repository. You cannot submit a pull request to one repository that requests a merge to a base of another repository. ", # noqa: E501
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_CREATE_A_PULL_REQUEST_body",
display_name="Body",
info="The contents of the pull request.",
show=False,
),
BoolInput(
name="GITHUB_CREATE_A_PULL_REQUEST_maintainer_can_modify",
display_name="Maintainer Can Modify",
info="Indicates whether maintainers can modify the pull request",
show=False,
advanced=True,
),
BoolInput(
name="GITHUB_CREATE_A_PULL_REQUEST_draft",
display_name="Draft",
info="Indicates whether the pull request is a draft",
show=False,
advanced=True,
),
IntInput(
name="GITHUB_CREATE_A_PULL_REQUEST_issue",
display_name="Issue",
info="An issue in the repository to convert to a pull request. The issue title, body, and comments will become the title, body, and comments on the new pull request. Required unless `title` is specified. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_owner",
display_name="Owner",
info="The account owner of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_repo",
display_name="Repo",
info="The name of the repository. The name is not case sensitive. ",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_milestone",
display_name="Milestone",
info="If an `integer` is passed, it should refer to a milestone by its `number` field. If the string `*` is passed, issues with any milestone are accepted. If the string `none` is passed, issues without milestones are returned. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_state",
display_name="State",
info="Indicates the state of the issues to return.",
show=False,
value="open",
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_assignee",
display_name="Assignee",
info="Can be the name of a user. Pass in `none` for issues with no assigned user, and `*` for issues assigned to any user. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_creator",
display_name="Creator",
info="The user that created the issue.",
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_mentioned",
display_name="Mentioned",
info="A user that's mentioned in the issue.",
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_labels",
display_name="Labels",
info="A list of comma separated label names. Example: `bug,ui,@high`",
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_sort",
display_name="Sort",
info="What to sort results by",
show=False,
value="created",
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_direction",
display_name="Direction",
info="The direction to sort the results by",
show=False,
value="desc",
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_since",
display_name="Since",
info="Only show results that were last updated after the given time. This is a timestamp in ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601) format: `YYYY-MM-DDTHH:MM:SSZ`. ", # noqa: E501
show=False,
advanced=True,
),
IntInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_per_page",
display_name="Per Page",
info="The number of results per page (max 100)",
show=False,
value=1,
advanced=True,
),
IntInput(
name="GITHUB_LIST_REPOSITORY_ISSUES_page",
display_name="Page",
info="The page number of the results to fetch",
show=False,
value=1,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_BRANCHES_owner",
display_name="Owner",
info="The account owner of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_LIST_BRANCHES_repo",
display_name="Repo",
info="The name of the repository. The name is not case sensitive. ",
show=False,
required=True,
),
BoolInput(
name="GITHUB_LIST_BRANCHES_protected",
display_name="Protected",
info="Setting to `true` returns only protected branches. When set to `false`, only unprotected branches are returned. Omitting this parameter returns all branches", # noqa: E501
show=False,
),
IntInput(
name="GITHUB_LIST_BRANCHES_per_page",
display_name="Per Page",
info="The number of results per page (max 100)",
show=False,
value=30,
advanced=True,
),
IntInput(
name="GITHUB_LIST_BRANCHES_page",
display_name="Page",
info="The page number of the results to fetch",
show=False,
value=1,
advanced=True,
),
MessageTextInput(
name="GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER_owner",
display_name="Owner",
info="The account owner of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER_repo",
display_name="Repo",
info="The name of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_GET_A_PULL_REQUEST_owner",
display_name="Owner",
info="The account owner of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_GET_A_PULL_REQUEST_repo",
display_name="Repo",
info="The name of the repository. The name is not case sensitive. ",
show=False,
required=True,
),
IntInput(
name="GITHUB_GET_A_PULL_REQUEST_pull_number",
display_name="Pull Number",
info="The number that identifies the pull request.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_LIST_COMMITS_owner",
display_name="Owner",
info="The account owner of the repository. The name is not case sensitive.",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_LIST_COMMITS_repo",
display_name="Repo",
info="The name of the repository. The name is not case sensitive. ",
show=False,
required=True,
),
MessageTextInput(
name="GITHUB_LIST_COMMITS_sha",
display_name="SHA",
info="SHA or branch to start listing commits from. Default: the repository's default branch (usually `main`). ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_COMMITS_path",
display_name="Path",
info="Only commits containing this file path will be returned.",
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_COMMITS_author",
display_name="Author",
info="GitHub username or email address to use to filter by commit author.",
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_COMMITS_committer",
display_name="Committer",
info="GitHub username or email address to use to filter by commit committer.",
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_COMMITS_since",
display_name="Since",
info="Only show results that were last updated after the given time. This is a timestamp in ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601) format: `YYYY-MM-DDTHH:MM:SSZ`. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GITHUB_LIST_COMMITS_until",
display_name="Until",
info="Only commits before this date will be returned. This is a timestamp in ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601) format: `YYYY-MM-DDTHH:MM:SSZ`. ", # noqa: E501
show=False,
advanced=True,
),
IntInput(
name="GITHUB_LIST_COMMITS_per_page",
display_name="Per Page",
info="The number of results per page (max 100)",
show=False,
value=1,
advanced=True,
),
IntInput(
name="GITHUB_LIST_COMMITS_page",
display_name="Page",
info="The page number of the results to fetch",
show=False,
value=1,
advanced=True,
),
]
def execute_action(self):
"""Execute action and return response as Message."""
toolset = self._build_wrapper()
try:
self._build_action_maps()
# Get the display name from the action list
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else self.action
# Use the display_to_key_map to get the action key
action_key = self._display_to_key_map.get(display_name)
if not action_key:
msg = f"Invalid action: {display_name}"
raise ValueError(msg)
enum_name = getattr(Action, action_key)
params = {}
if action_key in self._actions_data:
for field in self._actions_data[action_key]["action_fields"]:
value = getattr(self, field)
if value is None or value == "":
continue
if (
field
in [
"GITHUB_CREATE_AN_ISSUE_labels",
"GITHUB_CREATE_AN_ISSUE_assignees",
"GITHUB_LIST_REPOSITORY_ISSUES_labels",
]
and value
):
value = [item.strip() for item in value.split(",")]
if field in self._bool_variables:
value = bool(value)
param_name = field.replace(action_key + "_", "")
params[param_name] = value
result = toolset.execute_action(
action=enum_name,
params=params,
)
if not result.get("successful"):
try:
message_str = result.get("error", {})
error_message = message_str.split("`")[1]
error_msg_json = json.loads(error_message)
except (IndexError, json.JSONDecodeError):
return {"error": str(message_str)}
return {
"code": error_msg_json.get("status"),
"message": error_msg_json.get("message"),
"documentation_url": error_msg_json.get("documentation_url"),
}
result_data = result.get("data", [])
if (
len(result_data) != 1
and not self._actions_data.get(action_key, {}).get("result_field")
and self._actions_data.get(action_key, {}).get("get_result_field")
):
msg = f"Expected a dict with a single key, got {len(result_data)} keys: {result_data.keys()}"
raise ValueError(msg)
if isinstance(result_data.get("details"), list):
return result_data.get("details")
return result_data # noqa: TRY300
except Exception as e:
logger.error(f"Error executing action: {e}")
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else str(self.action)
msg = f"Failed to execute {display_name}: {e!s}"
raise ValueError(msg) from e
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
return super().update_build_config(build_config, field_value, field_name)
def set_default_tools(self):
self._default_tools = {
self.sanitize_action_name("GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER").replace(" ", "-"),
self.sanitize_action_name("GITHUB_CREATE_A_PULL_REQUEST").replace(" ", "-"),
}
"""Set the default tools for GitHub component."""

View file

@ -1,406 +1,38 @@
import json
from typing import Any
from composio import Action
from langflow.base.composio.composio_base import ComposioBaseComponent
from langflow.inputs.inputs import (
BoolInput,
FileInput,
IntInput,
MessageTextInput,
)
from langflow.logging import logger
class ComposioGmailAPIComponent(ComposioBaseComponent):
"""Gmail API component for interacting with Gmail services."""
display_name: str = "Gmail"
name = "GmailAPI"
icon = "Google"
documentation: str = "https://docs.composio.dev"
app_name = "gmail"
# Gmail-specific actions
_actions_data: dict = {
"GMAIL_SEND_EMAIL": {
"display_name": "Send Email",
"action_fields": [
"recipient_email",
"subject",
"body",
"cc",
"bcc",
"is_html",
"gmail_user_id",
"attachment",
],
},
"GMAIL_FETCH_EMAILS": {
"display_name": "Fetch Emails",
"action_fields": [
"gmail_user_id",
"max_results",
"query",
"page_token",
"label_ids",
"include_spam_trash",
],
"get_result_field": True,
"result_field": "messages",
},
"GMAIL_GET_PROFILE": {
"display_name": "Get User Profile",
"action_fields": ["gmail_user_id"],
},
"GMAIL_FETCH_MESSAGE_BY_MESSAGE_ID": {
"display_name": "Get Email By ID",
"action_fields": ["message_id", "gmail_user_id", "format"],
"get_result_field": False,
},
"GMAIL_CREATE_EMAIL_DRAFT": {
"display_name": "Create Draft Email",
"action_fields": [
"recipient_email",
"subject",
"body",
"cc",
"bcc",
"is_html",
"attachment",
"gmail_user_id",
],
},
"GMAIL_FETCH_MESSAGE_BY_THREAD_ID": {
"display_name": "Get Message By Thread ID",
"action_fields": ["thread_id", "page_token", "gmail_user_id"],
"get_result_field": False,
},
"GMAIL_LIST_THREADS": {
"display_name": "List Email Threads",
"action_fields": ["max_results", "query", "gmail_user_id", "page_token"],
"get_result_field": True,
"result_field": "threads",
},
"GMAIL_REPLY_TO_THREAD": {
"display_name": "Reply To Thread",
"action_fields": ["thread_id", "message_body", "recipient_email", "gmail_user_id", "cc", "bcc", "is_html"],
},
"GMAIL_LIST_LABELS": {
"display_name": "List Email Labels",
"action_fields": ["gmail_user_id"],
"get_result_field": True,
"result_field": "labels",
},
"GMAIL_CREATE_LABEL": {
"display_name": "Create Email Label",
"action_fields": ["label_name", "label_list_visibility", "message_list_visibility", "gmail_user_id"],
},
"GMAIL_GET_PEOPLE": {
"display_name": "Get Contacts",
"action_fields": ["resource_name", "person_fields"],
"get_result_field": True,
"result_field": "people_data",
},
"GMAIL_REMOVE_LABEL": {
"display_name": "Delete Email Label",
"action_fields": ["label_id", "gmail_user_id"],
"get_result_field": False,
},
"GMAIL_GET_ATTACHMENT": {
"display_name": "Get Attachment",
"action_fields": ["message_id", "attachment_id", "file_name", "gmail_user_id"],
},
}
_all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]}
_bool_variables = {"is_html", "include_spam_trash"}
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.post_processors = {
"GMAIL_SEND_EMAIL": self._process_send_email_response,
"GMAIL_FETCH_EMAILS": self._process_fetch_emails_response,
}
# Combine base inputs with Gmail-specific inputs
inputs = [
*ComposioBaseComponent._base_inputs,
# Email composition fields
MessageTextInput(
name="recipient_email",
display_name="Recipient Email",
info="Email address of the recipient",
show=False,
required=True,
advanced=False,
),
MessageTextInput(
name="subject",
display_name="Subject",
info="Subject of the email",
show=False,
required=True,
advanced=False,
),
MessageTextInput(
name="body",
display_name="Body",
required=True,
info="Content of the email",
show=False,
advanced=False,
),
MessageTextInput(
name="cc",
display_name="CC",
info="Email addresses to CC (Carbon Copy) in the email, separated by commas",
show=False,
advanced=True,
),
MessageTextInput(
name="bcc",
display_name="BCC",
info="Email addresses to BCC (Blind Carbon Copy) in the email, separated by commas",
show=False,
advanced=True,
),
BoolInput(
name="is_html",
display_name="Is HTML",
info="Specify whether the email body contains HTML content (true/false)",
show=False,
value=False,
advanced=True,
),
# Email retrieval and management fields
MessageTextInput(
name="gmail_user_id",
display_name="User ID",
info="The user's email address or 'me' for the authenticated user",
show=False,
advanced=True,
),
IntInput(
name="max_results",
display_name="Max Results",
required=True,
info="Maximum number of emails to be returned",
show=False,
advanced=False,
),
MessageTextInput(
name="message_id",
display_name="Message ID",
info="The ID of the specific email message",
show=False,
required=True,
advanced=False,
),
MessageTextInput(
name="thread_id",
display_name="Thread ID",
info="The ID of the email thread",
show=False,
required=True,
advanced=False,
),
MessageTextInput(
name="query",
display_name="Query",
info="Search query to filter emails (e.g., 'from:someone@email.com' or 'subject:hello')",
show=False,
advanced=False,
),
MessageTextInput(
name="message_body",
display_name="Message Body",
info="The body content of the message to be sent",
show=False,
advanced=True,
),
# Label management fields
MessageTextInput(
name="label_name",
display_name="Label Name",
info="Name of the Gmail label to create, modify, or filter by",
show=False,
required=True,
advanced=False,
),
MessageTextInput(
name="label_id",
display_name="Label ID",
info="The ID of the Gmail label",
show=False,
advanced=False,
),
MessageTextInput(
name="label_ids",
display_name="Label Ids",
info="Comma-separated list of label IDs to filter messages",
show=False,
advanced=True,
),
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",
show=False,
advanced=True,
),
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",
show=False,
advanced=True,
),
# Pagination and filtering
MessageTextInput(
name="page_token",
display_name="Page Token",
info="Token for retrieving the next page of results",
show=False,
advanced=True,
),
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,
advanced=True,
),
MessageTextInput(
name="format",
display_name="Format",
info="The format to return the message in. Possible values: minimal, full, raw, metadata",
show=False,
advanced=True,
),
# Contact management fields
MessageTextInput(
name="resource_name",
display_name="Resource Name",
info="The resource name of the person to provide information about",
show=False,
advanced=True,
),
MessageTextInput(
name="person_fields",
display_name="Person fields",
info="Fields to return for the person. Multiple fields can be specified by separating them with commas",
show=False,
advanced=True,
),
# Attachment handling
MessageTextInput(
name="attachment_id",
display_name="Attachment ID",
info="Id of the attachment",
show=False,
required=True,
advanced=False,
),
MessageTextInput(
name="file_name",
display_name="File name",
info="File name of the attachment file",
show=False,
required=True,
advanced=False,
),
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,
),
]
def _process_send_email_response(self, raw_data):
"""Post-processor for GMAIL_SEND_EMAIL action."""
if isinstance(raw_data, dict):
response_data = raw_data.get("response_data", raw_data)
def execute_action(self):
"""Execute action and return response as Message."""
toolset = self._build_wrapper()
return {
"message_id": response_data.get("id"),
"thread_id": response_data.get("threadId"),
"label_ids": response_data.get("labelIds", []),
}
return raw_data
try:
self._build_action_maps()
# Get the display name from the action list
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else self.action
# Use the display_to_key_map to get the action key
action_key = self._display_to_key_map.get(display_name)
if not action_key:
msg = f"Invalid action: {display_name}"
raise ValueError(msg)
enum_name = getattr(Action, action_key)
params = {}
if action_key in self._actions_data:
for field in self._actions_data[action_key]["action_fields"]:
value = getattr(self, field)
if value is None or value == "":
continue
if field in ["cc", "bcc", "label_ids"] and value:
value = [item.strip() for item in value.split(",")]
if field in self._bool_variables:
value = bool(value)
params[field] = value
if params.get("gmail_user_id"):
params["user_id"] = params.pop("gmail_user_id")
result = toolset.execute_action(
action=enum_name,
params=params,
)
if not result.get("successful"):
message_str = result.get("data", {}).get("message", "{}")
try:
error_data = json.loads(message_str).get("error", {})
except json.JSONDecodeError:
error_data = {"error": "Failed to get exact error details"}
return {
"code": error_data.get("code"),
"message": error_data.get("message"),
"errors": error_data.get("errors", []),
"status": error_data.get("status"),
}
result_data = result.get("data", {})
actions_data = self._actions_data.get(action_key, {})
# If 'get_result_field' is True and 'result_field' is specified, extract the data
# using 'result_field'. Otherwise, fall back to the entire 'data' field in the response.
if actions_data.get("get_result_field") and actions_data.get("result_field"):
result_data = result_data.get(actions_data.get("result_field"), result.get("data", []))
if len(result_data) != 1 and not actions_data.get("result_field") and actions_data.get("get_result_field"):
msg = f"Expected a dict with a single key, got {len(result_data)} keys: {result_data.keys()}"
raise ValueError(msg)
return result_data # noqa: TRY300
except Exception as e:
logger.error(f"Error executing action: {e}")
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else str(self.action)
msg = f"Failed to execute {display_name}: {e!s}"
raise ValueError(msg) from e
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
return super().update_build_config(build_config, field_value, field_name)
def _process_fetch_emails_response(self, raw_data):
"""Post-processor for GMAIL_FETCH_EMAILS action."""
if isinstance(raw_data, dict):
messages = raw_data.get("messages", [])
if messages:
return messages
return raw_data
def set_default_tools(self):
self._default_tools = {
"GMAIL_SEND_EMAIL",
"GMAIL_FETCH_EMAILS",
}
"""Set the default tools for Gmail component."""

View file

@ -1,787 +1,11 @@
from typing import Any
from composio import Action
from langflow.base.composio.composio_base import ComposioBaseComponent
from langflow.inputs import (
BoolInput,
IntInput,
MessageTextInput,
)
from langflow.logging import logger
class ComposioGoogleCalendarAPIComponent(ComposioBaseComponent):
"""Google Calendar API component for interacting with Google Calendar services."""
display_name: str = "Google Calendar"
description: str = "Google Calendar API"
icon = "Googlecalendar"
documentation: str = "https://docs.composio.dev"
app_name = "googlecalendar"
_actions_data: dict = {
"GOOGLECALENDAR_UPDATE_EVENT": {
"display_name": "Update Google Event",
"action_fields": [
"GOOGLECALENDAR_UPDATE_EVENT_description",
"GOOGLECALENDAR_UPDATE_EVENT_eventType",
"GOOGLECALENDAR_UPDATE_EVENT_create_meeting_room",
"GOOGLECALENDAR_UPDATE_EVENT_guestsCanSeeOtherGuests",
"GOOGLECALENDAR_UPDATE_EVENT_guestsCanInviteOthers",
"GOOGLECALENDAR_UPDATE_EVENT_location",
"GOOGLECALENDAR_UPDATE_EVENT_summary",
"GOOGLECALENDAR_UPDATE_EVENT_transparency",
"GOOGLECALENDAR_UPDATE_EVENT_visibility",
"GOOGLECALENDAR_UPDATE_EVENT_timezone",
"GOOGLECALENDAR_UPDATE_EVENT_recurrence",
"GOOGLECALENDAR_UPDATE_EVENT_guests_can_modify",
"GOOGLECALENDAR_UPDATE_EVENT_attendees",
"GOOGLECALENDAR_UPDATE_EVENT_send_updates",
"GOOGLECALENDAR_UPDATE_EVENT_start_datetime",
"GOOGLECALENDAR_UPDATE_EVENT_event_duration_hour",
"GOOGLECALENDAR_UPDATE_EVENT_event_duration_minutes",
"GOOGLECALENDAR_UPDATE_EVENT_calendar_id",
"GOOGLECALENDAR_UPDATE_EVENT_event_id",
],
},
"GOOGLECALENDAR_REMOVE_ATTENDEE": {
"display_name": "Remove Attendee From Event",
"action_fields": [
"GOOGLECALENDAR_REMOVE_ATTENDEE_calendar_id",
"GOOGLECALENDAR_REMOVE_ATTENDEE_event_id",
"GOOGLECALENDAR_REMOVE_ATTENDEE_attendee_email",
],
},
"GOOGLECALENDAR_GET_CURRENT_DATE_TIME": {
"display_name": "Get Current Date And Time",
"action_fields": ["GOOGLECALENDAR_GET_CURRENT_DATE_TIME_timezone"],
},
"GOOGLECALENDAR_QUICK_ADD": {
"display_name": "Quick Add Event",
"action_fields": [
"GOOGLECALENDAR_QUICK_ADD_calendar_id",
"GOOGLECALENDAR_QUICK_ADD_text",
"GOOGLECALENDAR_QUICK_ADD_send_updates",
],
},
"GOOGLECALENDAR_LIST_CALENDARS": {
"display_name": "List Google Calendars",
"action_fields": [
"GOOGLECALENDAR_LIST_CALENDARS_max_results",
"GOOGLECALENDAR_LIST_CALENDARS_min_access_role",
"GOOGLECALENDAR_LIST_CALENDARS_page_token",
"GOOGLECALENDAR_LIST_CALENDARS_show_deleted",
"GOOGLECALENDAR_LIST_CALENDARS_show_hidden",
"GOOGLECALENDAR_LIST_CALENDARS_sync_token",
],
},
"GOOGLECALENDAR_FIND_EVENT": {
"display_name": "Find Event",
"action_fields": [
"GOOGLECALENDAR_FIND_EVENT_calendar_id",
"GOOGLECALENDAR_FIND_EVENT_query",
"GOOGLECALENDAR_FIND_EVENT_max_results",
"GOOGLECALENDAR_FIND_EVENT_order_by",
"GOOGLECALENDAR_FIND_EVENT_show_deleted",
"GOOGLECALENDAR_FIND_EVENT_single_events",
"GOOGLECALENDAR_FIND_EVENT_timeMax",
"GOOGLECALENDAR_FIND_EVENT_timeMin",
"GOOGLECALENDAR_FIND_EVENT_updated_min",
"GOOGLECALENDAR_FIND_EVENT_event_types",
"GOOGLECALENDAR_FIND_EVENT_page_token",
],
},
"GOOGLECALENDAR_CREATE_EVENT": {
"display_name": "Create Event",
"action_fields": [
"GOOGLECALENDAR_CREATE_EVENT_description",
"GOOGLECALENDAR_CREATE_EVENT_eventType",
"GOOGLECALENDAR_CREATE_EVENT_create_meeting_room",
"GOOGLECALENDAR_CREATE_EVENT_guestsCanSeeOtherGuests",
"GOOGLECALENDAR_CREATE_EVENT_guestsCanInviteOthers",
"GOOGLECALENDAR_CREATE_EVENT_location",
"GOOGLECALENDAR_CREATE_EVENT_summary",
"GOOGLECALENDAR_CREATE_EVENT_transparency",
"GOOGLECALENDAR_CREATE_EVENT_visibility",
"GOOGLECALENDAR_CREATE_EVENT_timezone",
"GOOGLECALENDAR_CREATE_EVENT_recurrence",
"GOOGLECALENDAR_CREATE_EVENT_guests_can_modify",
"GOOGLECALENDAR_CREATE_EVENT_attendees",
"GOOGLECALENDAR_CREATE_EVENT_send_updates",
"GOOGLECALENDAR_CREATE_EVENT_start_datetime",
"GOOGLECALENDAR_CREATE_EVENT_event_duration_hour",
"GOOGLECALENDAR_CREATE_EVENT_event_duration_minutes",
"GOOGLECALENDAR_CREATE_EVENT_calendar_id",
],
},
"GOOGLECALENDAR_FIND_FREE_SLOTS": {
"display_name": "Find Free Slots",
"action_fields": [
"GOOGLECALENDAR_FIND_FREE_SLOTS_time_min",
"GOOGLECALENDAR_FIND_FREE_SLOTS_time_max",
"GOOGLECALENDAR_FIND_FREE_SLOTS_timezone",
"GOOGLECALENDAR_FIND_FREE_SLOTS_group_expansion_max",
"GOOGLECALENDAR_FIND_FREE_SLOTS_calendar_expansion_max",
"GOOGLECALENDAR_FIND_FREE_SLOTS_items",
],
},
"GOOGLECALENDAR_PATCH_CALENDAR": {
"display_name": "Patch Calendar",
"action_fields": [
"GOOGLECALENDAR_PATCH_CALENDAR_calendar_id",
"GOOGLECALENDAR_PATCH_CALENDAR_description",
"GOOGLECALENDAR_PATCH_CALENDAR_location",
"GOOGLECALENDAR_PATCH_CALENDAR_summary",
"GOOGLECALENDAR_PATCH_CALENDAR_timezone",
],
},
"GOOGLECALENDAR_GET_CALENDAR": {
"display_name": "Fetch Google Calendar",
"action_fields": ["GOOGLECALENDAR_GET_CALENDAR_calendar_id"],
},
"GOOGLECALENDAR_DELETE_EVENT": {
"display_name": "Delete Event",
"action_fields": ["GOOGLECALENDAR_DELETE_EVENT_calendar_id", "GOOGLECALENDAR_DELETE_EVENT_event_id"],
},
"GOOGLECALENDAR_DUPLICATE_CALENDAR": {
"display_name": "Duplicate Calendar",
"action_fields": ["GOOGLECALENDAR_DUPLICATE_CALENDAR_summary"],
},
}
_list_variables = {
"GOOGLECALENDAR_FIND_EVENT_event_types",
"GOOGLECALENDAR_CREATE_EVENT_recurrence",
"GOOGLECALENDAR_CREATE_EVENT_attendees",
"GOOGLECALENDAR_FIND_FREE_SLOTS_items",
"GOOGLECALENDAR_UPDATE_EVENT_recurrence",
"GOOGLECALENDAR_UPDATE_EVENT_attendees",
}
_all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]}
_bool_variables = {
"GOOGLECALENDAR_LIST_CALENDARS_show_deleted",
"GOOGLECALENDAR_LIST_CALENDARS_show_hidden",
"GOOGLECALENDAR_FIND_EVENT_show_deleted",
"GOOGLECALENDAR_FIND_EVENT_single_events",
"GOOGLECALENDAR_CREATE_EVENT_create_meeting_room",
"GOOGLECALENDAR_CREATE_EVENT_guestsCanSeeOtherGuests",
"GOOGLECALENDAR_CREATE_EVENT_guestsCanInviteOthers",
"GOOGLECALENDAR_CREATE_EVENT_guests_can_modify",
"GOOGLECALENDAR_CREATE_EVENT_send_updates",
"GOOGLECALENDAR_UPDATE_EVENT_create_meeting_room",
"GOOGLECALENDAR_UPDATE_EVENT_guestsCanSeeOtherGuests",
"GOOGLECALENDAR_UPDATE_EVENT_guestsCanInviteOthers",
"GOOGLECALENDAR_UPDATE_EVENT_guests_can_modify",
"GOOGLECALENDAR_UPDATE_EVENT_send_updates",
}
inputs = [
*ComposioBaseComponent._base_inputs,
IntInput(
name="GOOGLECALENDAR_LIST_CALENDARS_max_results",
display_name="Max Results",
info="Maximum number of entries returned on one result page. The page size can never be larger than 250 entries.", # noqa: E501
show=False,
value=10,
),
MessageTextInput(
name="GOOGLECALENDAR_LIST_CALENDARS_min_access_role",
display_name="Min Access Role",
info="The minimum access role for the user in the returned entries. Accepted values are 'owner' & 'reader'",
show=False,
),
MessageTextInput(
name="GOOGLECALENDAR_LIST_CALENDARS_page_token",
display_name="Page Token",
info="Token specifying which result page to return.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_LIST_CALENDARS_show_deleted",
display_name="Show Deleted",
info="Whether to include deleted calendar list entries in the result.",
show=False,
value=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_LIST_CALENDARS_show_hidden",
display_name="Show Hidden",
info="Whether to show hidden entries.",
show=False,
value=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_LIST_CALENDARS_sync_token",
display_name="Sync Token",
info="Token obtained from the nextSyncToken field returned on the last page of results from the previous list request.", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_EVENT_calendar_id",
display_name="Calendar Id",
info="Identifier of the Google Calendar. Use 'primary' for the currently logged in user's primary calendar.", # noqa: E501
show=False,
value="primary",
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_EVENT_query",
display_name="Query",
info="Search term to find events that match these terms in the event's summary, description, location, attendee's displayName, attendee's email, organizer's displayName, organizer's email, etc if needed.", # noqa: E501
show=False,
),
IntInput(
name="GOOGLECALENDAR_FIND_EVENT_max_results",
display_name="Max Results",
info="Maximum number of events returned on one result page. The page size can never be larger than 2500 events. The default value is 10.", # noqa: E501
show=False,
value=10,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_EVENT_order_by",
display_name="Order By",
info="The order of the events returned in the result. Acceptable values are 'startTime' and 'updated'.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_FIND_EVENT_show_deleted",
display_name="Show Deleted",
info="Whether to include deleted events (with status equals 'cancelled') in the result.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_FIND_EVENT_single_events",
display_name="Single Events",
info="Whether to expand recurring events into instances and only return single one-off events and instances of recurring events, but not the underlying recurring events themselves.", # noqa: E501
show=False,
value=True,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_EVENT_timeMax",
display_name="Timemax",
info="Upper bound (exclusive) for an event's start time to filter by. Accepts multiple formats:, 1. ISO format with timezone (e.g., 2024-12-06T13:00:00Z), 2. Comma-separated format (e.g., 2024,12,06,13,00,00), 3. Simple datetime format (e.g., 2024-12-06 13:00:00)", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_EVENT_timeMin",
display_name="Timemin",
info="Lower bound (exclusive) for an event's end time to filter by. Accepts multiple formats:, 1. ISO format with timezone (e.g., 2024-12-06T13:00:00Z), 2. Comma-separated format (e.g., 2024,12,06,13,00,00), 3. Simple datetime format (e.g., 2024-12-06 13:00:00)", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_EVENT_updated_min",
display_name="Updated Min",
info="Lower bound for an event's last modification time to filter by. Accepts multiple formats:, 1. ISO format with timezone (e.g., 2024-12-06T13:00:00Z), 2. Comma-separated format (e.g., 2024,12,06,13,00,00), 3. Simple datetime format (e.g., 2024-12-06 13:00:00)", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_EVENT_event_types",
display_name="Event Types",
info="List of event types to return. Possible values are: default, outOfOffice, focusTime, workingLocation.", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_EVENT_page_token",
display_name="Page Token",
info="Token specifying which result page to return. Optional.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_DUPLICATE_CALENDAR_summary",
display_name="Summary/Title",
info="Title of the calendar to be duplicated.",
show=False,
value="",
),
MessageTextInput(
name="GOOGLECALENDAR_REMOVE_ATTENDEE_calendar_id",
display_name="Calendar Id",
info="ID of the Google Calendar",
show=False,
value="primary",
),
MessageTextInput(
name="GOOGLECALENDAR_REMOVE_ATTENDEE_event_id",
display_name="Event Id",
info="ID of the event",
show=False,
required=True,
),
MessageTextInput(
name="GOOGLECALENDAR_REMOVE_ATTENDEE_attendee_email",
display_name="Attendee Email",
info="Email address of the attendee to be removed",
show=False,
required=True,
),
MessageTextInput(
name="GOOGLECALENDAR_GET_CALENDAR_calendar_id",
display_name="Calendar Id",
info="The ID of the Google Calendar that needs to be fetched. Default is 'primary'.",
show=False,
value="primary",
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_description",
display_name="Description",
info="Description of the event. Can contain HTML. Optional.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_eventType",
display_name="Event Type",
info="Type of the event, immutable post-creation. Currently, only 'default'",
show=False,
value="default",
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_CREATE_EVENT_create_meeting_room",
display_name="Create Meeting Room",
info="If true, a Google Meet link is created and added to the event.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_CREATE_EVENT_guestsCanSeeOtherGuests",
display_name="Guests Can See Other Guests",
info="Whether attendees other than the organizer can see who the event's attendees are.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_CREATE_EVENT_guestsCanInviteOthers",
display_name="Guests Can Invite Others",
info="Whether attendees other than the organizer can invite others to the event.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_location",
display_name="Location",
info="Geographic location of the event as free-form text.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_summary",
display_name="Summary/Title",
info="Summary (title) of the event.",
show=False,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_transparency",
display_name="Event Transparency",
info="'opaque' (busy) or 'transparent' (available).",
show=False,
value="opaque",
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_visibility",
display_name="Event Visibility",
info="Event visibility: 'default', 'public', 'private', or 'confidential'.",
show=False,
value="default",
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_timezone",
display_name="Timezone",
info="IANA timezone name (e.g., 'America/New_York'). Required if datetime is naive. If datetime includes timezone info (Z or offset), this field is optional and defaults to UTC.", # noqa: E501
show=False,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_recurrence",
display_name="Recurrence",
info="List of RRULE, EXRULE, RDATE, EXDATE lines for recurring events.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_CREATE_EVENT_guests_can_modify",
display_name="Guests Can Modify",
info="If True, guests can modify the event.",
show=False,
value=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_attendees",
display_name="Attendees",
info="List of attendee emails (strings).",
show=False,
),
BoolInput(
name="GOOGLECALENDAR_CREATE_EVENT_send_updates",
display_name="Send Updates",
info="Defaults to True. Whether to send updates to the attendees.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_start_datetime",
display_name="Start Datetime",
info="Naive date/time (YYYY-MM-DDTHH:MM:SS) with NO offsets or Z. e.g. '2025-01-16T13:00:00'",
show=False,
required=True,
),
IntInput(
name="GOOGLECALENDAR_CREATE_EVENT_event_duration_hour",
display_name="Event Duration Hour",
info="Number of hours (0-24).",
show=False,
value=0,
advanced=True,
),
IntInput(
name="GOOGLECALENDAR_CREATE_EVENT_event_duration_minutes",
display_name="Event Duration Minutes",
info="Number of minutes (0-59).",
show=False,
value=30,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_CREATE_EVENT_calendar_id",
display_name="Calendar Id",
info="The ID of the Google Calendar. `primary` for interacting with the primary calendar.",
show=False,
value="primary",
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_DELETE_EVENT_calendar_id",
display_name="Calendar Id",
info="ID of the Google Calendar",
show=False,
value="primary",
),
MessageTextInput(
name="GOOGLECALENDAR_DELETE_EVENT_event_id",
display_name="Event Id",
info="ID of the event to be deleted",
show=False,
required=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_FREE_SLOTS_time_min",
display_name="Time Min",
info="The start datetime of the interval for the query. Supports multiple formats:, 1. ISO format with timezone (e.g., 2024-12-06T13:00:00Z), 2. Comma-separated format (e.g., 2024,12,06,13,00,00), 3. Simple datetime format (e.g., 2024-12-06 13:00:00)", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_FREE_SLOTS_time_max",
display_name="Time Max",
info="The end datetime of the interval for the query. Supports multiple formats:, 1. ISO format with timezone (e.g., 2024-12-06T13:00:00Z), 2. Comma-separated format (e.g., 2024,12,06,13,00,00), 3. Simple datetime format (e.g., 2024-12-06 13:00:00)", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_FREE_SLOTS_timezone",
display_name="Timezone",
info="Time zone used in the response. Optional. The default is UTC.",
show=False,
value="UTC",
advanced=True,
),
IntInput(
name="GOOGLECALENDAR_FIND_FREE_SLOTS_group_expansion_max",
display_name="Group Expansion Max",
info="Maximal number of calendar identifiers to be provided for a single group. Optional. An error is returned for a group with more members than this value. Maximum value is 100.", # noqa: E501
show=False,
value=100,
advanced=True,
),
IntInput(
name="GOOGLECALENDAR_FIND_FREE_SLOTS_calendar_expansion_max",
display_name="Calendar Expansion Max",
info="Maximal number of calendars for which FreeBusy information is to be provided. Optional. Maximum value is 50.", # noqa: E501
show=False,
value=50,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_FIND_FREE_SLOTS_items",
display_name="Items",
info="List of calendars ids for which to fetch",
show=False,
),
MessageTextInput(
name="GOOGLECALENDAR_QUICK_ADD_calendar_id",
display_name="Calendar Id",
info="Calendar identifier. To list calendars to retrieve calendar IDs use relevant tools. To access the primary calendar of the currently logged in user, use the 'primary' keyword.", # noqa: E501
show=False,
value="primary",
),
MessageTextInput(
name="GOOGLECALENDAR_QUICK_ADD_text",
display_name="Text",
info="The text describing the event to be created.",
show=False,
value="",
),
MessageTextInput(
name="GOOGLECALENDAR_QUICK_ADD_send_updates",
display_name="Send Updates",
info="Guests who should receive notifications about the creation of the new event. Accepted fields include 'all', 'none', 'externalOnly'", # noqa: E501
show=False,
value="none",
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_PATCH_CALENDAR_calendar_id",
display_name="Calendar Id",
info="The ID of the Google Calendar that needs to be updated.",
show=False,
required=True,
),
MessageTextInput(
name="GOOGLECALENDAR_PATCH_CALENDAR_description",
display_name="Description",
info="Description of the calendar. Optional.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_PATCH_CALENDAR_location",
display_name="Location",
info="Geographic location of the calendar as free-form text.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_PATCH_CALENDAR_summary",
display_name="Title/Summary",
info="Title of the calendar. This field is required and cannot be left blank as per the Google Calendar API requirements.", # noqa: E501
show=False,
required=True,
),
MessageTextInput(
name="GOOGLECALENDAR_PATCH_CALENDAR_timezone",
display_name="Timezone",
info="The time zone of the calendar. (Formatted as an IANA Time Zone Database name, e.g. 'Europe/Zurich').",
show=False,
advanced=True,
),
IntInput(
name="GOOGLECALENDAR_GET_CURRENT_DATE_TIME_timezone",
display_name="Timezone",
info="The timezone offset from UTC to retrieve current date and time, like for location of UTC+6, you give 6, for UTC -9, your give -9.", # noqa: E501
show=False,
value=0,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_description",
display_name="Description",
info="Description of the event. Can contain HTML. Optional.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_eventType",
display_name="EventType",
info="Type of the event, immutable post-creation. Currently, only 'default' and 'workingLocation' can be created.", # noqa: E501
show=False,
value="default",
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_UPDATE_EVENT_create_meeting_room",
display_name="Create Meeting Room",
info="If true, a Google Meet link is created and added to the event.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_UPDATE_EVENT_guestsCanSeeOtherGuests",
display_name="Guests Can See Other Guests",
info="Whether attendees other than the organizer can see who the event's attendees are.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_UPDATE_EVENT_guestsCanInviteOthers",
display_name="Guests Can Invite Others",
info="Whether attendees other than the organizer can invite others to the event.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_location",
display_name="Location",
info="Geographic location of the event as free-form text.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_summary",
display_name="Summary/Title",
info="Summary (title) of the event.",
show=False,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_transparency",
display_name="Event Transparency",
info="'opaque' (busy) or 'transparent' (available).",
show=False,
value="opaque",
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_visibility",
display_name="Event Visibility",
info="Event visibility: 'default', 'public', 'private', or 'confidential'.",
show=False,
value="default",
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_timezone",
display_name="Timezone",
info="IANA timezone name (e.g., 'America/New_York'). Required if datetime is naive. If datetime includes timezone info (Z or offset), this field is optional and defaults to UTC.", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_recurrence",
display_name="Recurrence",
info="List of RRULE, EXRULE, RDATE, EXDATE lines for recurring events.",
show=False,
advanced=True,
),
BoolInput(
name="GOOGLECALENDAR_UPDATE_EVENT_guests_can_modify",
display_name="Guests Can Modify",
info="If True, guests can modify the event.",
show=False,
value=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_attendees",
display_name="Attendees",
info="List of attendee emails (strings).",
show=False,
),
BoolInput(
name="GOOGLECALENDAR_UPDATE_EVENT_send_updates",
display_name="Send Updates",
info="Defaults to True. Whether to send updates to the attendees.",
show=False,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_start_datetime",
display_name="Start Datetime",
info="Naive date/time (YYYY-MM-DDTHH:MM:SS) with NO offsets or Z. e.g. '2025-01-16T13:00:00'",
show=False,
required=True,
),
IntInput(
name="GOOGLECALENDAR_UPDATE_EVENT_event_duration_hour",
display_name="Event Duration Hour",
info="Number of hours (0-24).",
show=False,
value=0,
advanced=True,
),
IntInput(
name="GOOGLECALENDAR_UPDATE_EVENT_event_duration_minutes",
display_name="Event Duration Minutes",
info="Number of minutes (0-59).",
show=False,
value=30,
advanced=True,
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_calendar_id",
display_name="Calendar Id",
info="ID of the Google Calendar",
show=False,
value="primary",
),
MessageTextInput(
name="GOOGLECALENDAR_UPDATE_EVENT_event_id",
display_name="Event Id",
info="ID of the event to be updated",
show=False,
required=True,
),
]
def execute_action(self):
"""Execute action and return response as Message."""
toolset = self._build_wrapper()
try:
self._build_action_maps()
# Get the display name from the action list
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else self.action
# Use the display_to_key_map to get the action key
action_key = self._display_to_key_map.get(display_name)
if not action_key:
msg = f"Invalid action: {display_name}"
raise ValueError(msg)
enum_name = getattr(Action, action_key)
params = {}
if action_key in self._actions_data:
for field in self._actions_data[action_key]["action_fields"]:
value = getattr(self, field)
if value is None or value == "":
continue
if field in self._list_variables and value:
value = [item.strip() for item in value.split(",")]
if field in self._bool_variables:
value = bool(value)
param_name = field.replace(action_key + "_", "")
params[param_name] = value
result = toolset.execute_action(
action=enum_name,
params=params,
)
if not result.get("successful"):
message_str = result.get("error", {})
return {"error": message_str}
result_data = result.get("data", [])
if (
len(result_data) != 1
and not self._actions_data.get(action_key, {}).get("result_field")
and self._actions_data.get(action_key, {}).get("get_result_field")
):
msg = f"Expected a dict with a single key, got {len(result_data)} keys: {result_data.keys()}"
raise ValueError(msg)
if action_key == "GOOGLECALENDAR_GET_CURRENT_DATE_TIME":
return result_data
return result_data[next(iter(result_data))]
except Exception as e:
logger.error(f"Error executing action: {e}")
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else str(self.action)
msg = f"Failed to execute {display_name}: {e!s}"
raise ValueError(msg) from e
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
return super().update_build_config(build_config, field_value, field_name)
def set_default_tools(self):
"""Set the default tools for Google Calendar component."""

View file

@ -0,0 +1,11 @@
from langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioGooglemeetAPIComponent(ComposioBaseComponent):
display_name: str = "Google Meet"
icon = "Googlemeet"
documentation: str = "https://docs.composio.dev"
app_name = "googlemeet"
def set_default_tools(self):
"""Set the default tools for Google Calendar component."""

View file

@ -0,0 +1,8 @@
from base.langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioGoogleTasksAPIComponent(ComposioBaseComponent):
display_name: str = "Google Tasks"
icon = "GoogleTasks"
documentation: str = "https://docs.composio.dev"
app_name = "googletasks"

View file

@ -0,0 +1,11 @@
from langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioLinearAPIComponent(ComposioBaseComponent):
display_name: str = "Linear"
icon = "Linear"
documentation: str = "https://docs.composio.dev"
app_name = "linear"
def set_default_tools(self):
"""Set the default tools for Linear component."""

View file

@ -1,765 +1,11 @@
import json
from typing import Any
from composio import Action
from langflow.base.composio.composio_base import ComposioBaseComponent
from langflow.inputs import BoolInput, FileInput, IntInput, MessageTextInput
from langflow.logging import logger
class ComposioOutlookAPIComponent(ComposioBaseComponent):
display_name: str = "Outlook"
description: str = "Outlook API"
icon = "Outlook"
documentation: str = "https://docs.composio.dev"
app_name = "outlook"
_actions_data: dict = {
"OUTLOOK_OUTLOOK_REPLY_EMAIL": {
"display_name": "Reply To Email",
"action_fields": [
"OUTLOOK_OUTLOOK_REPLY_EMAIL_user_id",
"OUTLOOK_OUTLOOK_REPLY_EMAIL_message_id",
"OUTLOOK_OUTLOOK_REPLY_EMAIL_comment",
"OUTLOOK_OUTLOOK_REPLY_EMAIL_cc_emails",
"OUTLOOK_OUTLOOK_REPLY_EMAIL_bcc_emails",
],
"get_result_field": False,
},
"OUTLOOK_OUTLOOK_GET_PROFILE": {
"display_name": "Get Profile",
"action_fields": ["OUTLOOK_OUTLOOK_GET_PROFILE_user_id"],
"get_result_field": True,
"result_field": "response_data",
},
"OUTLOOK_OUTLOOK_SEND_EMAIL": {
"display_name": "Send Email",
"action_fields": [
"OUTLOOK_OUTLOOK_SEND_EMAIL_user_id",
"OUTLOOK_OUTLOOK_SEND_EMAIL_subject",
"OUTLOOK_OUTLOOK_SEND_EMAIL_body",
"OUTLOOK_OUTLOOK_SEND_EMAIL_to_email",
"OUTLOOK_OUTLOOK_SEND_EMAIL_to_name",
"OUTLOOK_OUTLOOK_SEND_EMAIL_cc_emails",
"OUTLOOK_OUTLOOK_SEND_EMAIL_bcc_emails",
"OUTLOOK_OUTLOOK_SEND_EMAIL_is_html",
"OUTLOOK_OUTLOOK_SEND_EMAIL_save_to_sent_items",
"OUTLOOK_OUTLOOK_SEND_EMAIL_attachment",
],
"get_result_field": False,
},
"OUTLOOK_OUTLOOK_LIST_MESSAGES": {
"display_name": "List Messages",
"action_fields": [
"OUTLOOK_OUTLOOK_LIST_MESSAGES_user_id",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_folder",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_top",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_skip",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_is_read",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_importance",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_subject",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_gt",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_startswith",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_endswith",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_contains",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_ge",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_lt",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_le",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_from_address",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_has_attachments",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_body_preview_contains",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_sent_date_time_gt",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_sent_date_time_lt",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_categories",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_select",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_orderby",
],
"get_result_field": True,
"result_field": "value",
},
"OUTLOOK_OUTLOOK_LIST_EVENTS": {
"display_name": "List Events",
"action_fields": [
"OUTLOOK_OUTLOOK_LIST_EVENTS_user_id",
"OUTLOOK_OUTLOOK_LIST_EVENTS_top",
"OUTLOOK_OUTLOOK_LIST_EVENTS_skip",
"OUTLOOK_OUTLOOK_LIST_EVENTS_filter",
"OUTLOOK_OUTLOOK_LIST_EVENTS_select",
"OUTLOOK_OUTLOOK_LIST_EVENTS_orderby",
"OUTLOOK_OUTLOOK_LIST_EVENTS_timezone",
],
"get_result_field": True,
"result_field": "value",
},
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT": {
"display_name": "Create Calendar Event",
"action_fields": [
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_user_id",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_subject",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_body",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_is_html",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_start_datetime",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_end_datetime",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_time_zone",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_is_online_meeting",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_online_meeting_provider",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_attendees_info",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_location",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_show_as",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_categories",
],
"get_result_field": True,
"result_field": "response_data",
},
"OUTLOOK_OUTLOOK_GET_EVENT": {
"display_name": "Get Calendar Event",
"action_fields": ["OUTLOOK_OUTLOOK_GET_EVENT_user_id", "OUTLOOK_OUTLOOK_GET_EVENT_event_id"],
"get_result_field": True,
"result_field": "response_data",
},
"OUTLOOK_OUTLOOK_CREATE_DRAFT": {
"display_name": "Create Email Draft",
"action_fields": [
"OUTLOOK_OUTLOOK_CREATE_DRAFT_subject",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_body",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_to_recipients",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_cc_recipients",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_bcc_recipients",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_is_html",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_attachment",
],
"get_result_field": True,
"result_field": "response_data",
},
}
_all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]}
_bool_variables = {
"OUTLOOK_OUTLOOK_SEND_EMAIL_is_html",
"OUTLOOK_OUTLOOK_SEND_EMAIL_save_to_sent_items",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_is_html",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_is_html",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_is_online_meeting",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_is_read",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_has_attachments",
}
_list_variables = {
"OUTLOOK_OUTLOOK_LIST_EVENTS_select",
"OUTLOOK_OUTLOOK_LIST_EVENTS_orderby",
"OUTLOOK_OUTLOOK_SEND_EMAIL_cc_emails",
"OUTLOOK_OUTLOOK_SEND_EMAIL_bcc_emails",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_to_recipients",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_cc_recipients",
"OUTLOOK_OUTLOOK_CREATE_DRAFT_bcc_recipients",
"OUTLOOK_OUTLOOK_REPLY_EMAIL_cc_emails",
"OUTLOOK_OUTLOOK_REPLY_EMAIL_bcc_emails",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_attendees_info",
"OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_categories",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_categories",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_select",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_orderby",
}
inputs = [
*ComposioBaseComponent._base_inputs,
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_EVENTS_user_id",
display_name="User Id",
info="The target user's email address or 'me' for the authenticated user.",
show=False,
value="me",
advanced=True,
),
IntInput(
name="OUTLOOK_OUTLOOK_LIST_EVENTS_top",
display_name="Max Results",
info="The maximum number of events to return per request.",
show=False,
value=10,
),
IntInput(
name="OUTLOOK_OUTLOOK_LIST_EVENTS_skip",
display_name="Skip",
info="The number of events to skip before starting to collect results.",
show=False,
value=0,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_EVENTS_filter",
display_name="Filter",
info="OData query string to filter results. Example: start/dateTime ge '2024-01-01T00:00:00'",
show=False,
value="",
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_EVENTS_select",
display_name="Select",
info="List of properties to include in the response comma separated.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_EVENTS_orderby",
display_name="Orderby",
info="Properties to sort results by comma separated.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_EVENTS_timezone",
display_name="Timezone",
info="The timezone for event start and end times in the response.",
show=False,
value="UTC",
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_user_id",
display_name="User Id",
info="The user's email address or 'me' for the authenticated user.",
show=False,
value="me",
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_subject",
display_name="Subject",
info="Subject of the email",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_body",
display_name="Body",
info="Body content of the email. Can be plain text or HTML based on is_html flag.",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_to_email",
display_name="Recipient Email",
info="Recipient email address",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_to_name",
display_name="To Name",
info="Recipient display name",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_cc_emails",
display_name="CC",
info="List of CC recipient email addresses comma separated",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_bcc_emails",
display_name="BCC",
info="List of BCC recipient email addresses comma separated",
show=False,
advanced=True,
),
BoolInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_is_html",
display_name="Is HTML",
info="Set to True if the body content is HTML formatted",
show=False,
value=False,
advanced=True,
),
BoolInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_save_to_sent_items",
display_name="Save To Sent Items",
info="Whether to save the sent email to Sent Items folder.",
show=False,
value=True,
advanced=True,
),
FileInput(
name="OUTLOOK_OUTLOOK_SEND_EMAIL_attachment",
display_name="Attachment",
file_types=[
"csv",
"txt",
"doc",
"docx",
"xls",
"xlsx",
"pdf",
"png",
"jpg",
"jpeg",
"gif",
"zip",
"rar",
"ppt",
"pptx",
],
info="Add an attachment",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CREATE_DRAFT_subject",
display_name="Subject",
info="Subject of the email",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CREATE_DRAFT_body",
display_name="Body",
info="Body content of the email. Can be plain text or HTML based on is_html flag",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CREATE_DRAFT_to_recipients",
display_name="Recipient Email",
info="List of recipient email addresses comma separated",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CREATE_DRAFT_cc_recipients",
display_name="Cc Recipients",
info="List of CC recipient email addresses",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CREATE_DRAFT_bcc_recipients",
display_name="BCC",
info="List of BCC recipient email addresses comma separated",
show=False,
advanced=True,
),
BoolInput(
name="OUTLOOK_OUTLOOK_CREATE_DRAFT_is_html",
display_name="Is HTML",
info="Set to True if the body content is HTML formatted",
show=False,
value=False,
advanced=True,
),
FileInput(
name="OUTLOOK_OUTLOOK_CREATE_DRAFT_attachment",
display_name="Attachment",
file_types=[
"csv",
"txt",
"doc",
"docx",
"xls",
"xlsx",
"pdf",
"png",
"jpg",
"jpeg",
"gif",
"zip",
"rar",
"ppt",
"pptx",
],
info="Add an attachment",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_GET_PROFILE_user_id",
display_name="User Id",
info="The user's email address or 'me' for the authenticated user.",
show=False,
value="me",
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_REPLY_EMAIL_user_id",
display_name="User Id",
info="The user's email address or 'me' for the authenticated user.",
show=False,
value="me",
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_REPLY_EMAIL_message_id",
display_name="Message Id",
info="The ID of the message to reply to. Can be obtained from OUTLOOK_LIST_MESSAGES action.",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_REPLY_EMAIL_comment",
display_name="Comment",
info="Comment to include in the reply. Must be plain text.",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_REPLY_EMAIL_cc_emails",
display_name="CC",
info="List of CC recipient email addresses comma separated",
show=False,
value=[],
is_list=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_REPLY_EMAIL_bcc_emails",
display_name="BCC",
info="List of BCC recipient email addresses comma separated",
show=False,
value=[],
is_list=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_user_id",
display_name="User Id",
info="The user's email address or 'me' for the authenticated user.",
show=False,
value="me",
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_subject",
display_name="Subject",
info="Subject of the event. Example: 'Team Meeting'.",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_body",
display_name="Body",
info="Body content of the event. Can be plain text or HTML.",
show=False,
required=True,
),
BoolInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_is_html",
display_name="Is Html",
info="Set to True if the body content should be interpreted as HTML.",
show=False,
value=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_start_datetime",
display_name="Start Datetime",
info="Start date/time (ISO 8601). Example: '2025-01-03T10:00:00Z'.",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_end_datetime",
display_name="End Datetime",
info="End date/time (ISO 8601). Example: '2025-01-03T11:00:00Z'.",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_time_zone",
display_name="Time Zone",
info="Time zone (e.g., 'UTC' or 'America/Los_Angeles').",
show=False,
required=True,
),
BoolInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_is_online_meeting",
display_name="Is Online Meeting",
info="Set to True to make this an online meeting and generate a Teams URL.",
show=False,
value=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_online_meeting_provider",
display_name="Online Meeting Provider",
info="The online meeting service provider. Currently only supports 'teamsForBusiness'.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_attendees_info",
display_name="Attendees",
info="A list of attendee information. Only email is required for each attendee., Example: [{ 'email': 'team@example.com', 'name': 'Team', 'type': 'required' }, { 'email': 'other@example.com', 'type': 'optional' }, { 'email': 'other2@example.com' }]", # noqa: E501
show=False,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_location",
display_name="Location",
info="Location of the event (e.g., 'Conference Room').",
show=False,
value="",
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_show_as",
display_name="Show As",
info="Status of the event: 'free', 'tentative', 'busy', or 'oof'.",
show=False,
value="busy",
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_CALENDAR_CREATE_EVENT_categories",
display_name="Categories",
info="List of categories associated with the event comma separated.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_GET_EVENT_user_id",
display_name="User Id",
info="The user's email address or 'me' for the authenticated user.",
show=False,
value="me",
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_GET_EVENT_event_id",
display_name="Event Id",
info="The ID of the calendar event to retrieve.",
show=False,
required=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_user_id",
display_name="User Id",
info="The target user's email address or 'me' for the authenticated user. For delegated access scenarios, this should be the email of the shared mailbox or delegated user.", # noqa: E501
show=False,
value="me",
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_folder",
display_name="Folder",
info="",
show=False,
value="inbox",
advanced=True,
),
IntInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_top",
display_name="Max Results",
info="The maximum number of messages to return per request. Must be a positive integer between 1 and 1000.",
show=False,
value=10,
),
IntInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_skip",
display_name="Skip",
info="The number of messages to skip before starting to collect results. Use for paginated responses.",
show=False,
value=0,
advanced=True,
),
BoolInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_is_read",
display_name="Is Read",
info="Filter messages by read status. If set to False, only unread messages will be returned.",
show=False,
value=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_importance",
display_name="Importance",
info="Filter messages by importance. For example, 'high', 'normal', or 'low'.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_subject",
display_name="Subject",
info="Filter messages by subject (exact match).",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_gt",
display_name="Received Date Time Gt",
info="Filter messages with a receivedDateTime greater than the specified value. Example: '2023-01-01T00:00:00Z'.", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_startswith",
display_name="Subject Startswith",
info="Filter messages where the subject starts with the specified string.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_endswith",
display_name="Subject Endswith",
info="Filter messages where the subject ends with the specified string.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_contains",
display_name="Subject Contains",
info="Filter messages where the subject contains the specified substring.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_ge",
display_name="Received Date Time Ge",
info="Filter messages with a receivedDateTime greater than or equal to the specified value.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_lt",
display_name="Received Date Time Lt",
info="Filter messages with a receivedDateTime less than the specified value.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_le",
display_name="Received Date Time Le",
info="Filter messages with a receivedDateTime less than or equal to the specified value.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_from_address",
display_name="From Address",
info="Filter messages by the sender's email address. Uses equality check on from/emailAddress/address.",
show=False,
advanced=True,
),
BoolInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_has_attachments",
display_name="Has Attachments",
info="Filter messages by whether they have attachments.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_body_preview_contains",
display_name="Body Preview Contains",
info="Filter messages where the bodyPreview contains the specified substring.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_sent_date_time_gt",
display_name="Sent Date Time Gt",
info="Filter messages with a sentDateTime greater than the specified value.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_sent_date_time_lt",
display_name="Sent Date Time Lt",
info="Filter messages with a sentDateTime less than the specified value.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_categories",
display_name="Categories",
info="Filter messages by categories. Matches if the message contains any of the specified categories.",
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_select",
display_name="Select",
info="A list of properties to include in the response comma separated. Common properties: 'subject', 'from', 'toRecipients', 'receivedDateTime'.", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="OUTLOOK_OUTLOOK_LIST_MESSAGES_orderby",
display_name="Orderby",
info="Specify properties to sort results by. For example, 'receivedDateTime desc' for newest messages first.", # noqa: E501
show=False,
advanced=True,
),
]
def execute_action(self):
"""Execute action and return response as Message."""
toolset = self._build_wrapper()
try:
self._build_action_maps()
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else self.action
action_key = self._display_to_key_map.get(display_name)
if not action_key:
msg = f"Invalid action: {display_name}"
raise ValueError(msg)
enum_name = getattr(Action, action_key)
params = {}
if action_key in self._actions_data:
for field in self._actions_data[action_key]["action_fields"]:
value = getattr(self, field)
if value is None or value == "":
continue
if field in self._list_variables and value:
value = [item.strip() for item in value.split(",")]
if field in self._bool_variables:
value = bool(value)
param_name = field.replace(action_key + "_", "")
params[param_name] = value
result = toolset.execute_action(
action=enum_name,
params=params,
)
if not result.get("successful"):
error_data = result.get("data", {})
error_message = error_data.get("message", str(result.get("error", "Unknown Error")))
if isinstance(error_message, str):
try:
error_obj = json.loads(error_message).get("error", {})
error_obj["status_code"] = error_data.get("status_code", 400)
return error_obj # noqa: TRY300
except json.JSONDecodeError:
return {"error": error_message, "status_code": error_data.get("status_code", 400)}
return error_message
result_data = result.get("data", {})
actions_data = self._actions_data.get(action_key, {})
if actions_data.get("get_result_field") and actions_data.get("result_field"):
response_data = result_data.get("response_data", {})
if response_data and actions_data.get("result_field") in response_data:
result_data = response_data.get(actions_data.get("result_field"), result.get("data", []))
else:
result_data = result_data.get(actions_data.get("result_field"), result.get("data", []))
if len(result_data) != 1 and not actions_data.get("result_field") and actions_data.get("get_result_field"):
msg = f"Expected a dict with a single key, got {len(result_data)} keys: {result_data.keys()}"
raise ValueError(msg)
return result_data # noqa: TRY300
except Exception as e:
logger.error(f"Error executing action: {e}")
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else str(self.action)
msg = f"Failed to execute {display_name}: {e!s}"
raise ValueError(msg) from e
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
return super().update_build_config(build_config, field_value, field_name)
def set_default_tools(self):
self._default_tools = {
self.sanitize_action_name("OUTLOOK_OUTLOOK_SEND_EMAIL").replace(" ", "-"),
self.sanitize_action_name("OUTLOOK_OUTLOOK_LIST_MESSAGES").replace(" ", "-"),
}
"""Set the default tools for Gmail component."""

View file

@ -0,0 +1,11 @@
from langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioRedditAPIComponent(ComposioBaseComponent):
display_name: str = "Reddit"
icon = "Reddit"
documentation: str = "https://docs.composio.dev"
app_name = "reddit"
def set_default_tools(self):
"""Set the default tools for Reddit component."""

View file

@ -1,586 +1,11 @@
from typing import Any
from composio import Action
from langflow.base.composio.composio_base import ComposioBaseComponent
from langflow.inputs import (
BoolInput,
IntInput,
MessageTextInput,
)
from langflow.logging import logger
class ComposioSlackAPIComponent(ComposioBaseComponent):
display_name: str = "Slack"
description: str = "Slack API"
icon = "Slack"
documentation: str = "https://docs.composio.dev"
app_name = "slack"
_actions_data: dict = {
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION": {
"display_name": "List Users",
"action_fields": [
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_limit",
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_cursor",
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_include_locale",
],
},
"SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS": {
"display_name": "List Channels",
"action_fields": [
"SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_exclude_archived",
"SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_types",
"SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_limit",
"SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_cursor",
],
},
"SLACK_UPDATES_A_SLACK_MESSAGE": {
"display_name": "Update Slack Chat Message",
"action_fields": [
"SLACK_UPDATES_A_SLACK_MESSAGE_as_user",
"SLACK_UPDATES_A_SLACK_MESSAGE_attachments",
"SLACK_UPDATES_A_SLACK_MESSAGE_blocks",
"SLACK_UPDATES_A_SLACK_MESSAGE_channel",
"SLACK_UPDATES_A_SLACK_MESSAGE_link_names",
"SLACK_UPDATES_A_SLACK_MESSAGE_parse",
"SLACK_UPDATES_A_SLACK_MESSAGE_text",
"SLACK_UPDATES_A_SLACK_MESSAGE_ts",
],
},
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL": {
"display_name": "Post Message To Channel",
"action_fields": [
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_as_user",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_attachments",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_blocks",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_channel",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_icon_emoji",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_icon_url",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_link_names",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_mrkdwn",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_parse",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_reply_broadcast",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_text",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_thread_ts",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_unfurl_links",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_unfurl_media",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_username",
],
},
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY": {
"display_name": "Search Messages Endpoint",
"action_fields": [
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_count",
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_highlight",
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_page",
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_query",
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_sort",
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_sort_dir",
],
},
"SLACK_FETCH_CONVERSATION_HISTORY": {
"display_name": "Retrieve conversation history",
"action_fields": [
"SLACK_FETCH_CONVERSATION_HISTORY_channel",
"SLACK_FETCH_CONVERSATION_HISTORY_latest",
"SLACK_FETCH_CONVERSATION_HISTORY_oldest",
"SLACK_FETCH_CONVERSATION_HISTORY_inclusive",
"SLACK_FETCH_CONVERSATION_HISTORY_limit",
"SLACK_FETCH_CONVERSATION_HISTORY_cursor",
],
},
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME": {
"display_name": "Schedule Message In Chat",
"action_fields": [
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_as_user",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_attachments",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_blocks",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_channel",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_link_names",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_parse",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_post_at",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_reply_broadcast",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_text",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_thread_ts",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_unfurl_links",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_unfurl_media",
],
},
"SLACK_CREATE_A_REMINDER": {
"display_name": "Add Reminder For User",
"action_fields": [
"SLACK_CREATE_A_REMINDER_text",
"SLACK_CREATE_A_REMINDER_time",
"SLACK_CREATE_A_REMINDER_user",
],
},
}
_all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]}
_bool_variables = {
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_include_locale",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_as_user",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_link_names",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_mrkdwn",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_reply_broadcast",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_unfurl_links",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_unfurl_media",
"SLACK_FETCH_CONVERSATION_HISTORY_inclusive",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_as_user",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_link_names",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_reply_broadcast",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_unfurl_links",
"SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_unfurl_media",
"SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_exclude_archived",
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_highlight",
}
inputs = [
*ComposioBaseComponent._base_inputs,
IntInput(
name="SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_limit",
display_name="Limit",
info="The maximum number of items to return. Fewer than the requested number of items may be returned, even if the end of the users list hasn't been reached. Providing no `limit` value will result in Slack attempting to deliver you the entire result set. If the collection is too large you may experience `limit_required` or HTTP 500 errors. ", # noqa: E501
show=False,
value=1,
),
MessageTextInput(
name="SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_cursor",
display_name="Cursor",
info="Paginate through collections of data by setting the `cursor` parameter to a `next_cursor` attribute returned by a previous request's `response_metadata`. Default value fetches the first `page` of the collection", # noqa: E501
show=False,
advanced=True,
),
BoolInput(
name="SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_include_locale",
display_name="Include Locale",
info="Set this to `true` to receive the locale for users. Defaults to `false`",
show=False,
),
BoolInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_as_user",
display_name="As User",
info="Pass true to post the message as the authed user, instead of as a bot. Defaults to false",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_attachments",
display_name="Attachments",
info="A JSON-based array of structured attachments, presented as a URL-encoded string. ",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_blocks",
display_name="Blocks",
info="A JSON-based array of structured blocks, presented as a URL-encoded string. ",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_channel",
display_name="Channel",
info="Channel, private group, or IM channel to send message to. Can be an encoded ID, or a name ",
show=False,
required=True,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_icon_emoji",
display_name="Icon Emoji",
info="Emoji to use as the icon for this message. Overrides `icon_url`. Must be used in conjunction with `as_user` set to `false`, otherwise ignored", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_icon_url",
display_name="Icon Url",
info="URL to an image to use as the icon for this message. Must be used in conjunction with `as_user` set to false, otherwise ignored", # noqa: E501
show=False,
advanced=True,
),
BoolInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_link_names",
display_name="Link Names",
info="Find and link channel names and usernames.",
show=False,
advanced=True,
),
BoolInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_mrkdwn",
display_name="Mrkdwn",
info="Disable Slack markup parsing by setting to `false`. Enabled by default.",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_parse",
display_name="Parse",
info="Change how messages are treated. Defaults to `none` ",
show=False,
advanced=True,
),
BoolInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_reply_broadcast",
display_name="Reply Broadcast",
info="Used in conjunction with `thread_ts` and indicates whether reply should be made visible to everyone in the channel or conversation. Defaults to `false`. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_text",
display_name="Text",
info="How this field works and whether it is required depends on other fields you use in your API call",
show=False,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_thread_ts",
display_name="Thread Ts",
info="Provide another message's `ts` value to make this message a reply. Avoid using a reply's `ts` value; use its parent instead. ", # noqa: E501
show=False,
),
BoolInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_unfurl_links",
display_name="Unfurl Links",
info="Pass true to enable unfurling of primarily text-based content.",
show=False,
advanced=True,
),
BoolInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_unfurl_media",
display_name="Unfurl Media",
info="Pass false to disable unfurling of media content.",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_username",
display_name="Username",
info="Set your bot's user name. Must be used in conjunction with `as_user` set to false, otherwise ignored",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_UPDATES_A_SLACK_MESSAGE_as_user",
display_name="As User",
info="Pass true to update the message as the authed user",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_UPDATES_A_SLACK_MESSAGE_attachments",
display_name="Attachments",
info="A JSON-based array of structured attachments, presented as a URL-encoded string. This field is required when not presenting `text`. If you don't include this field, the message's previous `attachments` will be retained. To remove previous `attachments`, include an empty array for this field. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_UPDATES_A_SLACK_MESSAGE_blocks",
display_name="Blocks",
info="A JSON-based array of structured blocks, presented as a URL-encoded string. If you don't include this field, the message's previous `blocks` will be retained. To remove previous `blocks`, include an empty array for this field. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_UPDATES_A_SLACK_MESSAGE_channel",
display_name="Channel ID",
info="Channel ID containing the message to be updated.",
show=False,
required=True,
),
MessageTextInput(
name="SLACK_UPDATES_A_SLACK_MESSAGE_link_names",
display_name="Link Names",
info="Find and link channel names and usernames. Defaults to `none`. If you do not specify a value for this field, the original value set for the message will be overwritten with the default, `none`. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_UPDATES_A_SLACK_MESSAGE_parse",
display_name="Parse",
info="Change how messages are treated. Defaults to `client`, unlike `chat.postMessage`. Accepts either `none` or `full`. If you do not specify a value for this field, the original value set for the message will be overwritten with the default, `client`. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_UPDATES_A_SLACK_MESSAGE_text",
display_name="Text",
info="New text for the message, using the default formatting rules. It's not required when presenting `blocks` or `attachments`. ", # noqa: E501
show=False,
),
MessageTextInput(
name="SLACK_UPDATES_A_SLACK_MESSAGE_ts",
display_name="Ts",
info="Timestamp of the message to be updated.",
show=False,
required=True,
),
MessageTextInput(
name="SLACK_FETCH_CONVERSATION_HISTORY_channel",
display_name="Channel ID",
info="Channel ID to fetch history for.",
show=False,
),
IntInput(
name="SLACK_FETCH_CONVERSATION_HISTORY_latest",
display_name="Latest",
info="End of time range of messages to include in results.",
show=False,
advanced=True,
),
IntInput(
name="SLACK_FETCH_CONVERSATION_HISTORY_oldest",
display_name="Oldest",
info="Start of time range of messages to include in results.",
show=False,
advanced=True,
),
BoolInput(
name="SLACK_FETCH_CONVERSATION_HISTORY_inclusive",
display_name="Inclusive",
info="Include messages with latest or oldest timestamp in results only when either timestamp is specified. ", # noqa: E501
show=False,
advanced=True,
),
IntInput(
name="SLACK_FETCH_CONVERSATION_HISTORY_limit",
display_name="Limit",
info="The maximum number of items to return. Fewer than the requested number of items may be returned, even if the end of the users list hasn't been reached. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_FETCH_CONVERSATION_HISTORY_cursor",
display_name="Cursor",
info="Paginate through collections of data by setting the `cursor` parameter to a `next_cursor` attribute returned by a previous request's `response_metadata`. Default value fetches the first 'page' of the collection. ", # noqa: E501
show=False,
advanced=True,
),
BoolInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_as_user",
display_name="As User",
info="Pass true to post the message as the authed user, instead of as a bot. Defaults to false",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_attachments",
display_name="Attachments",
info="A JSON-based array of structured attachments, presented as a URL-encoded string. ",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_blocks",
display_name="Blocks",
info="A JSON-based array of structured blocks, presented as a URL-encoded string. ",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_channel",
display_name="Channel",
info="Channel, private group, or DM channel to send message to. Can be an encoded ID, or a name",
show=False,
required=True,
),
BoolInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_link_names",
display_name="Link Names",
info="Find and link channel names and usernames.",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_parse",
display_name="Parse",
info="Change how messages are treated. Defaults to `none`",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_post_at",
display_name="Post At",
info="Unix EPOCH timestamp of time in future to send the message.",
show=False,
),
BoolInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_reply_broadcast",
display_name="Reply Broadcast",
info="Used in conjunction with `thread_ts` and indicates whether reply should be made visible to everyone in the channel or conversation. Defaults to `false`. ", # noqa: E501
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_text",
display_name="Text",
info="How this field works and whether it is required depends on other fields you use in your API call",
show=False,
),
IntInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_thread_ts",
display_name="Thread Ts",
info="Provide another message's `ts` value to make this message a reply. Avoid using a reply's `ts` value; use its parent instead. ", # noqa: E501
show=False,
advanced=True,
),
BoolInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_unfurl_links",
display_name="Unfurl Links",
info="Pass true to enable unfurling of primarily text-based content.",
show=False,
advanced=True,
),
BoolInput(
name="SLACK_SCHEDULES_A_MESSAGE_TO_A_CHANNEL_AT_A_SPECIFIED_TIME_unfurl_media",
display_name="Unfurl Media",
info="Pass false to disable unfurling of media content.",
show=False,
advanced=True,
),
BoolInput(
name="SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_exclude_archived",
display_name="Exclude Archived",
info="Set to `true` to exclude archived channels from the list",
show=False,
),
MessageTextInput(
name="SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_types",
display_name="Types",
info="Mix and match channel types by providing a comma-separated list of any combination of `public_channel`, `private_channel`, `mpim`, `im` ", # noqa: E501
show=False,
),
IntInput(
name="SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_limit",
display_name="Limit",
info="The maximum number of items to return. Fewer than the requested number of items may be returned, even if the end of the list hasn't been reached. Must be an integer no larger than 1000. ", # noqa: E501
show=False,
value=1,
),
MessageTextInput(
name="SLACK_LIST_ALL_SLACK_TEAM_CHANNELS_WITH_VARIOUS_FILTERS_cursor",
display_name="Cursor",
info="Paginate through collections of data by setting the `cursor` parameter to a `next_cursor` attribute returned by a previous request's `response_metadata`. Default value fetches the first 'page' of the collection", # noqa: E501
show=False,
advanced=True,
),
IntInput(
name="SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_count",
display_name="Count",
info="Pass the number of results you want per 'page'. Maximum of `100`.",
show=False,
value=1,
advanced=True,
),
BoolInput(
name="SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_highlight",
display_name="Highlight",
info="Pass a value of `true` to enable query highlight markers",
show=False,
advanced=True,
),
IntInput(
name="SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_page",
display_name="Page",
info="Page",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_query",
display_name="Query",
info="Search query.",
show=False,
required=True,
),
MessageTextInput(
name="SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_sort",
display_name="Sort",
info="Return matches sorted by either `score` or `timestamp`.",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY_sort_dir",
display_name="Sort Dir",
info="Change sort direction to ascending (`asc`) or descending (`desc`).",
show=False,
advanced=True,
),
MessageTextInput(
name="SLACK_CREATE_A_REMINDER_text",
display_name="Text",
info="The content of the reminder",
show=False,
required=True,
),
MessageTextInput(
name="SLACK_CREATE_A_REMINDER_time",
display_name="Time",
info="When this reminder should happen: the Unix timestamp (up to five years from now), the number of seconds until the reminder (if within 24 hours), or a natural language description (Ex. 'in 15 minutes,' or 'every Thursday') ", # noqa: E501
show=False,
required=True,
),
MessageTextInput(
name="SLACK_CREATE_A_REMINDER_user",
display_name="User",
info="The user who will receive the reminder. If no user is specified, the reminder will go to user who created it. ", # noqa: E501
show=False,
),
]
def execute_action(self):
"""Execute action and return response as Message."""
toolset = self._build_wrapper()
try:
self._build_action_maps()
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else self.action
action_key = self._display_to_key_map.get(display_name)
if not action_key:
msg = f"Invalid action: {display_name}"
raise ValueError(msg)
enum_name = getattr(Action, action_key)
params = {}
if action_key in self._actions_data:
for field in self._actions_data[action_key]["action_fields"]:
value = getattr(self, field)
if value is None or value == "":
continue
if field in self._bool_variables:
value = bool(value)
param_name = field.replace(action_key + "_", "")
if param_name == "as_user":
value = True
params[param_name] = value
result = toolset.execute_action(
action=enum_name,
params=params,
)
if not result.get("successful"):
return {"error": result.get("error", "No response")}
return result.get("data", [])
except Exception as e:
logger.error(f"Error executing action: {e}")
display_name = self.action[0]["name"] if isinstance(self.action, list) and self.action else str(self.action)
msg = f"Failed to execute {display_name}: {e!s}"
raise ValueError(msg) from e
def update_build_config(self, build_config: dict, field_value: Any, field_name: str | None = None) -> dict:
return super().update_build_config(build_config, field_value, field_name)
def set_default_tools(self):
self._default_tools = {
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL",
"SLACK_SEARCH_FOR_MESSAGES_WITH_QUERY",
}
"""Set the default tools for Slack component."""

View file

@ -0,0 +1,11 @@
from langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioSlackbotAPIComponent(ComposioBaseComponent):
display_name: str = "Slackbot"
icon = "Slack"
documentation: str = "https://docs.composio.dev"
app_name = "slackbot"
def set_default_tools(self):
"""Set the default tools for Slackbot component."""

View file

@ -0,0 +1,11 @@
from langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioSupabaseAPIComponent(ComposioBaseComponent):
display_name: str = "Supabase"
icon = "Supabase"
documentation: str = "https://docs.composio.dev"
app_name = "supabase"
def set_default_tools(self):
"""Set the default tools for Supabase component."""

View file

@ -0,0 +1,11 @@
from langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioTodoistAPIComponent(ComposioBaseComponent):
display_name: str = "Todoist"
icon = "Todoist"
documentation: str = "https://docs.composio.dev"
app_name = "todoist"
def set_default_tools(self):
"""Set the default tools for Todoist component."""

View file

@ -0,0 +1,11 @@
from langflow.base.composio.composio_base import ComposioBaseComponent
class ComposioYoutubeAPIComponent(ComposioBaseComponent):
display_name: str = "Youtube"
icon = "Youtube"
documentation: str = "https://docs.composio.dev"
app_name = "youtube"
def set_default_tools(self):
"""Set the default tools for Youtube component."""

View file

@ -1,132 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from langflow.base.composio.composio_base import ComposioBaseComponent
from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient
class MockComposioToolSet:
def __init__(self, api_key=None):
self.api_key = api_key
self.client = MagicMock()
def get_tools(self, *_):
return []
def execute_action(self, *_, **__):
return {"successful": True, "data": {"result": "mocked response"}}
class TestComposioBase(ComponentTestBaseWithoutClient):
@pytest.fixture
def component_class(self):
class TestComponent(ComposioBaseComponent):
def execute_action(self):
return []
return TestComponent
@pytest.fixture(autouse=True)
def mock_composio_toolset(self):
with patch("langflow.base.composio.composio_base.ComposioToolSet", MockComposioToolSet):
yield
@pytest.fixture
def default_kwargs(self):
return {
"api_key": "",
"entity_id": "default",
"action": None,
}
@pytest.fixture
def file_names_mapping(self):
# Component not yet released, mark all versions as non-existent
return [
{"version": "1.0.17", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.18", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.19", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.0", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.1", "module": "composio", "file_name": DID_NOT_EXIST},
]
def test_build_wrapper_no_api_key(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
with pytest.raises(ValueError, match="Please provide a valid Composio API Key in the component settings"):
component._build_wrapper()
def test_build_wrapper_with_api_key(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
component.api_key = "test_key"
wrapper = component._build_wrapper()
assert isinstance(wrapper, MockComposioToolSet)
assert wrapper.api_key == "test_key"
def test_build_action_maps(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
# Test with empty actions data
component._actions_data = {}
component._build_action_maps()
assert component._display_to_key_map == {}
assert component._key_to_display_map == {}
assert component._sanitized_names == {}
# Test with sample actions data
component._actions_data = {
"ACTION_1": {"display_name": "Action One"},
"ACTION_2": {"display_name": "Action Two"},
}
component._build_action_maps()
assert component._display_to_key_map == {
"Action One": "ACTION_1",
"Action Two": "ACTION_2",
}
assert component._key_to_display_map == {
"ACTION_1": "Action One",
"ACTION_2": "Action Two",
}
def test_get_action_fields(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
component._actions_data = {
"ACTION_1": {"action_fields": ["field1", "field2"]},
"ACTION_2": {"action_fields": ["field3"]},
}
# Test with valid action key
fields = component._get_action_fields("ACTION_1")
assert fields == {"field1", "field2"}
# Test with non-existent action key
fields = component._get_action_fields("NON_EXISTENT")
assert fields == set()
# Test with None action key
fields = component._get_action_fields(None)
assert fields == set()
def test_show_hide_fields(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
component._all_fields = {"field1", "field2"}
component._bool_variables = {"field2"}
component._actions_data = {
"ACTION_1": {"display_name": "Action One", "action_fields": ["field1"]},
}
build_config = {
"field1": {"show": False, "value": "old_value"},
"field2": {"show": False, "value": True},
}
# Test with no field value
component.show_hide_fields(build_config, None)
assert not build_config["field1"]["show"]
assert not build_config["field2"]["show"]
assert build_config["field1"]["value"] == ""
assert build_config["field2"]["value"] is False
# Test with valid action
component.show_hide_fields(build_config, [{"name": "Action One"}])
assert build_config["field1"]["show"] # Should be shown since it's in ACTION_1's fields
assert not build_config["field2"]["show"] # Should remain hidden

View file

@ -1,226 +0,0 @@
from unittest.mock import patch
import pytest
from composio import Action
from langflow.components.composio.github_composio import ComposioGitHubAPIComponent
from langflow.schema.dataframe import DataFrame
from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient
from .test_base import MockComposioToolSet
class MockAction:
GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER = "GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER"
GITHUB_LIST_BRANCHES = "GITHUB_LIST_BRANCHES"
GITHUB_LIST_REPOSITORY_ISSUES = "GITHUB_LIST_REPOSITORY_ISSUES"
class TestGitHubComponent(ComponentTestBaseWithoutClient):
@pytest.fixture(autouse=True)
def mock_composio_toolset(self):
with patch("langflow.base.composio.composio_base.ComposioToolSet", MockComposioToolSet):
yield
@pytest.fixture
def component_class(self):
return ComposioGitHubAPIComponent
@pytest.fixture
def default_kwargs(self):
return {
"api_key": "",
"entity_id": "default",
"action": None,
}
@pytest.fixture
def file_names_mapping(self):
# Component not yet released, mark all versions as non-existent
return [
{"version": "1.0.17", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.18", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.19", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.0", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.1", "module": "composio", "file_name": DID_NOT_EXIST},
]
def test_init(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
assert component.display_name == "GitHub"
assert component.app_name == "github"
assert "GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER" in component._actions_data
assert "GITHUB_LIST_BRANCHES" in component._actions_data
def test_execute_action_star_a_repo(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
monkeypatch.setattr(
Action,
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER",
MockAction.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER,
)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Star A Repository"}]
component.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER_owner = "langflow-ai"
component.GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER_repo = "langflow"
# For this specific test, customize the _actions_data to not use get_result_field
component._actions_data = {
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER": {
"display_name": "Star A Repository",
"action_fields": [
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER_owner",
"GITHUB_STAR_A_REPOSITORY_FOR_THE_AUTHENTICATED_USER_repo",
],
},
}
# Execute action
result = component.execute_action()
assert result == {"result": "mocked response"}
def test_execute_action_list_branches(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
monkeypatch.setattr(Action, "GITHUB_LIST_BRANCHES", MockAction.GITHUB_LIST_BRANCHES)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "List Branches"}]
component.GITHUB_LIST_BRANCHES_owner = "langflow-ai"
component.GITHUB_LIST_BRANCHES_repo = "langflow"
# For this specific test, customize the _actions_data to not use get_result_field
component._actions_data = {
"GITHUB_LIST_BRANCHES": {
"display_name": "List Branches",
"action_fields": [
"GITHUB_LIST_BRANCHES_owner",
"GITHUB_LIST_BRANCHES_repo",
"GITHUB_LIST_BRANCHES_protected",
"GITHUB_LIST_BRANCHES_per_page",
"GITHUB_LIST_BRANCHES_page",
],
},
}
# Execute action
result = component.execute_action()
assert result == {"result": "mocked response"}
def test_execute_action_list_repo_issues(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
monkeypatch.setattr(Action, "GITHUB_LIST_REPOSITORY_ISSUES", MockAction.GITHUB_LIST_REPOSITORY_ISSUES)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "List Repository Issues"}]
component.GITHUB_LIST_REPOSITORY_ISSUES_owner = "langflow-ai"
component.GITHUB_LIST_REPOSITORY_ISSUES_repo = "langflow"
# For this specific test, customize the _actions_data to not use get_result_field
component._actions_data = {
"GITHUB_LIST_REPOSITORY_ISSUES": {
"display_name": "List Repository Issues",
"action_fields": [
"GITHUB_LIST_REPOSITORY_ISSUES_owner",
"GITHUB_LIST_REPOSITORY_ISSUES_repo",
"GITHUB_LIST_REPOSITORY_ISSUES_milestone",
"GITHUB_LIST_REPOSITORY_ISSUES_state",
"GITHUB_LIST_REPOSITORY_ISSUES_assignee",
"GITHUB_LIST_REPOSITORY_ISSUES_creator",
"GITHUB_LIST_REPOSITORY_ISSUES_mentioned",
"GITHUB_LIST_REPOSITORY_ISSUES_labels",
"GITHUB_LIST_REPOSITORY_ISSUES_sort",
"GITHUB_LIST_REPOSITORY_ISSUES_direction",
"GITHUB_LIST_REPOSITORY_ISSUES_since",
"GITHUB_LIST_REPOSITORY_ISSUES_per_page",
"GITHUB_LIST_REPOSITORY_ISSUES_page",
],
},
}
# Execute action
result = component.execute_action()
assert result == {"result": "mocked response"}
def test_execute_action_invalid_action(self, component_class, default_kwargs):
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Invalid Action"}]
# Execute action should raise ValueError
with pytest.raises(ValueError, match="Invalid action: Invalid Action"):
component.execute_action()
def test_as_dataframe(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
monkeypatch.setattr(Action, "GITHUB_LIST_REPOSITORY_ISSUES", MockAction.GITHUB_LIST_REPOSITORY_ISSUES)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "List Repository Issues"}]
component.max_results = 10
# Create mock email data that would be returned by execute_action
mock_issues = [
{
"url": "url1",
"repository_url": "repository_url1",
"id": "id1",
"title": "test issue",
"state": "open",
},
{
"url": "url2",
"repository_url": "repository_url2",
"id": "id2",
"title": "test issue",
"state": "open",
},
]
# Mock the execute_action method to return our mock data
with patch.object(component, "execute_action", return_value=mock_issues):
# Test as_dataframe method
result = component.as_dataframe()
# Verify the result is a DataFrame
assert isinstance(result, DataFrame)
# Verify the DataFrame is not empty
assert not result.empty
# Check for expected content in the DataFrame string representation
data_str = str(result)
assert "test issue" in data_str
def test_update_build_config(self, component_class, default_kwargs):
# Test that the GitHub component properly inherits and uses the base component's
# update_build_config method
component = component_class(**default_kwargs)
build_config = {
"auth_link": {"value": "", "auth_tooltip": ""},
"action": {
"options": [],
"helper_text": "",
"helper_text_metadata": {},
},
}
# Test with empty API key
result = component.update_build_config(build_config, "", "api_key")
assert result["auth_link"]["value"] == ""
assert "Please provide a valid Composio API Key" in result["auth_link"]["auth_tooltip"]
assert result["action"]["options"] == []
# Test with valid API key
component.api_key = "test_key"
result = component.update_build_config(build_config, "test_key", "api_key")
assert len(result["action"]["options"]) > 0 # Should have GitHub actions

View file

@ -1,218 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from composio import Action
from langflow.components.composio.gmail_composio import ComposioGmailAPIComponent
from langflow.schema.dataframe import DataFrame
from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient
from .test_base import MockComposioToolSet
class MockAction:
GMAIL_SEND_EMAIL = "GMAIL_SEND_EMAIL"
GMAIL_FETCH_EMAILS = "GMAIL_FETCH_EMAILS"
GMAIL_GET_PROFILE = "GMAIL_GET_PROFILE"
class TestGmailComponent(ComponentTestBaseWithoutClient):
@pytest.fixture(autouse=True)
def mock_composio_toolset(self):
with patch("langflow.base.composio.composio_base.ComposioToolSet", MockComposioToolSet):
yield
@pytest.fixture
def component_class(self):
return ComposioGmailAPIComponent
@pytest.fixture
def default_kwargs(self):
return {
"api_key": "",
"entity_id": "default",
"action": None,
"recipient_email": "",
"subject": "",
"body": "",
"is_html": False,
"max_results": 10,
"query": "",
}
@pytest.fixture
def file_names_mapping(self):
# Component not yet released, mark all versions as non-existent
return [
{"version": "1.0.17", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.18", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.19", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.0", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.1", "module": "composio", "file_name": DID_NOT_EXIST},
]
def test_init(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
assert component.display_name == "Gmail"
assert component.name == "GmailAPI"
assert component.app_name == "gmail"
assert "GMAIL_SEND_EMAIL" in component._actions_data
assert "GMAIL_FETCH_EMAILS" in component._actions_data
def test_execute_action_send_email(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
monkeypatch.setattr(Action, "GMAIL_SEND_EMAIL", MockAction.GMAIL_SEND_EMAIL)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Send Email"}]
component.recipient_email = "test@example.com"
component.subject = "Test Subject"
component.body = "Test Body"
component.is_html = False
# For this specific test, customize the _actions_data to not use get_result_field
component._actions_data = {
"GMAIL_SEND_EMAIL": {
"display_name": "Send Email",
"action_fields": ["recipient_email", "subject", "body", "is_html"],
"get_result_field": False,
}
}
# Execute action
result = component.execute_action()
assert result == {"result": "mocked response"}
def test_execute_action_fetch_emails(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
monkeypatch.setattr(Action, "GMAIL_FETCH_EMAILS", MockAction.GMAIL_FETCH_EMAILS)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Fetch Emails"}]
component.max_results = 10
component.query = "from:test@example.com"
# For this specific test, we need to customize the action_data to handle results field
component._actions_data = {
"GMAIL_FETCH_EMAILS": {
"display_name": "Fetch Emails",
"action_fields": ["max_results", "query"],
"result_field": "messages",
"get_result_field": True,
}
}
# Create a mock for the toolset with specific structure for this test
mock_toolset = MagicMock()
mock_toolset.execute_action.return_value = {"successful": True, "data": {"messages": "mocked response"}}
# Patch the _build_wrapper method
with patch.object(component, "_build_wrapper", return_value=mock_toolset):
result = component.execute_action()
# Based on the component's actual behavior, it returns the result_field directly
assert result == "mocked response"
def test_execute_action_get_profile(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
monkeypatch.setattr(Action, "GMAIL_GET_PROFILE", MockAction.GMAIL_GET_PROFILE)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Get User Profile"}]
# For this specific test, customize the _actions_data to not use get_result_field
component._actions_data = {
"GMAIL_GET_PROFILE": {
"display_name": "Get User Profile",
"action_fields": ["gmail_user_id"],
"get_result_field": False,
}
}
# Execute action
result = component.execute_action()
assert result == {"result": "mocked response"}
def test_execute_action_invalid_action(self, component_class, default_kwargs):
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Invalid Action"}]
# Execute action should raise ValueError
with pytest.raises(ValueError, match="Invalid action: Invalid Action"):
component.execute_action()
def test_as_dataframe(self, component_class, default_kwargs, monkeypatch):
# Mock Action enum
monkeypatch.setattr(Action, "GMAIL_FETCH_EMAILS", MockAction.GMAIL_FETCH_EMAILS)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Fetch Emails"}]
component.max_results = 10
# Create mock email data that would be returned by execute_action
mock_emails = [
{
"id": "1",
"threadId": "thread1",
"subject": "Test Email 1",
"from": "sender1@example.com",
"date": "2023-01-01",
"snippet": "This is a test email",
},
{
"id": "2",
"threadId": "thread2",
"subject": "Test Email 2",
"from": "sender2@example.com",
"date": "2023-01-02",
"snippet": "This is another test email",
},
]
# Mock the execute_action method to return our mock data
with patch.object(component, "execute_action", return_value=mock_emails):
# Test as_dataframe method
result = component.as_dataframe()
# Verify the result is a DataFrame
assert isinstance(result, DataFrame)
# Verify the DataFrame is not empty
assert not result.empty
# Check for expected content in the DataFrame string representation
data_str = str(result)
assert "test email" in data_str
def test_update_build_config(self, component_class, default_kwargs):
# Test that the Gmail component properly inherits and uses the base component's
# update_build_config method
component = component_class(**default_kwargs)
build_config = {
"auth_link": {"value": "", "auth_tooltip": ""},
"action": {
"options": [],
"helper_text": "",
"helper_text_metadata": {},
},
}
# Test with empty API key
result = component.update_build_config(build_config, "", "api_key")
assert result["auth_link"]["value"] == ""
assert "Please provide a valid Composio API Key" in result["auth_link"]["auth_tooltip"]
assert result["action"]["options"] == []
# Test with valid API key
component.api_key = "test_key"
result = component.update_build_config(build_config, "test_key", "api_key")
assert len(result["action"]["options"]) > 0 # Should have Gmail actions

View file

@ -1,182 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from composio import Action
from langflow.components.composio.googlecalendar_composio import ComposioGoogleCalendarAPIComponent
from langflow.schema.dataframe import DataFrame
from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient
from .test_base import MockComposioToolSet
class MockAction:
GOOGLECALENDAR_CREATE_EVENT = "GOOGLECALENDAR_CREATE_EVENT"
GOOGLECALENDAR_LIST_CALENDARS = "GOOGLECALENDAR_LIST_CALENDARS"
class TestGoogleCalendarComponent(ComponentTestBaseWithoutClient):
@pytest.fixture(autouse=True)
def mock_composio_toolset(self):
with patch("langflow.base.composio.composio_base.ComposioToolSet", MockComposioToolSet):
yield
@pytest.fixture
def component_class(self):
return ComposioGoogleCalendarAPIComponent
@pytest.fixture
def default_kwargs(self):
return {
"api_key": "",
"entity_id": "default",
"action": None,
}
@pytest.fixture
def file_names_mapping(self):
return [
{"version": "1.0.17", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.18", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.19", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.0", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.1", "module": "composio", "file_name": DID_NOT_EXIST},
]
def test_init(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
assert component.display_name == "Google Calendar"
assert component.app_name == "googlecalendar"
assert "GOOGLECALENDAR_CREATE_EVENT" in component._actions_data
assert "GOOGLECALENDAR_LIST_CALENDARS" in component._actions_data
def test_execute_action_create_event(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(Action, "GOOGLECALENDAR_CREATE_EVENT", MockAction.GOOGLECALENDAR_CREATE_EVENT)
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Create Event"}]
component.GOOGLECALENDAR_CREATE_EVENT_attendees = "test@example.com"
component.GOOGLECALENDAR_CREATE_EVENT_start_datetime = "2025-01-16T15:00:00"
component.GOOGLECALENDAR_CREATE_EVENT_summary = "test title"
component._actions_data = {
"GOOGLECALENDAR_CREATE_EVENT": {
"display_name": "Create Event",
"action_fields": [
"GOOGLECALENDAR_CREATE_EVENT_description",
"GOOGLECALENDAR_CREATE_EVENT_eventType",
"GOOGLECALENDAR_CREATE_EVENT_create_meeting_room",
"GOOGLECALENDAR_CREATE_EVENT_guestsCanSeeOtherGuests",
"GOOGLECALENDAR_CREATE_EVENT_guestsCanInviteOthers",
"GOOGLECALENDAR_CREATE_EVENT_location",
"GOOGLECALENDAR_CREATE_EVENT_summary",
"GOOGLECALENDAR_CREATE_EVENT_transparency",
"GOOGLECALENDAR_CREATE_EVENT_visibility",
"GOOGLECALENDAR_CREATE_EVENT_timezone",
"GOOGLECALENDAR_CREATE_EVENT_recurrence",
"GOOGLECALENDAR_CREATE_EVENT_guests_can_modify",
"GOOGLECALENDAR_CREATE_EVENT_attendees",
"GOOGLECALENDAR_CREATE_EVENT_send_updates",
"GOOGLECALENDAR_CREATE_EVENT_start_datetime",
"GOOGLECALENDAR_CREATE_EVENT_event_duration_hour",
"GOOGLECALENDAR_CREATE_EVENT_event_duration_minutes",
"GOOGLECALENDAR_CREATE_EVENT_calendar_id",
],
}
}
result = component.execute_action()
assert result == "mocked response"
def test_execute_action_list_calendars(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(Action, "GOOGLECALENDAR_LIST_CALENDARS", MockAction.GOOGLECALENDAR_LIST_CALENDARS)
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "List Google Calendars"}]
component.GOOGLECALENDAR_LIST_CALENDARS_max_results = 1
component._actions_data = {
"GOOGLECALENDAR_LIST_CALENDARS": {
"display_name": "List Google Calendars",
"action_fields": [
"GOOGLECALENDAR_LIST_CALENDARS_max_results",
"GOOGLECALENDAR_LIST_CALENDARS_min_access_role",
"GOOGLECALENDAR_LIST_CALENDARS_page_token",
"GOOGLECALENDAR_LIST_CALENDARS_show_deleted",
"GOOGLECALENDAR_LIST_CALENDARS_show_hidden",
"GOOGLECALENDAR_LIST_CALENDARS_sync_token",
],
}
}
mock_toolset = MagicMock()
mock_toolset.execute_action.return_value = {"successful": True, "data": {"messages": "mocked response"}}
with patch.object(component, "_build_wrapper", return_value=mock_toolset):
result = component.execute_action()
assert result == "mocked response"
def test_execute_action_invalid_action(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Invalid Action"}]
with pytest.raises(ValueError, match="Invalid action: Invalid Action"):
component.execute_action()
def test_as_dataframe(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(Action, "GOOGLECALENDAR_LIST_CALENDARS", MockAction.GOOGLECALENDAR_LIST_CALENDARS)
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "List Google Calendars"}]
component.GOOGLECALENDAR_LIST_CALENDARS_max_results = 10
mock_emails = [
{
"kind": "test kind 1",
"etag": "1",
"id": "1",
"summary": "test summary 1",
"description": "test description 1",
},
{
"kind": "test kind 2",
"etag": "2",
"id": "2",
"summary": "test summary 2",
"description": "test description 2",
},
]
with patch.object(component, "execute_action", return_value=mock_emails):
result = component.as_dataframe()
assert isinstance(result, DataFrame)
assert not result.empty
data_str = str(result)
assert "test summary 2" in data_str
def test_update_build_config(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
build_config = {
"auth_link": {"value": "", "auth_tooltip": ""},
"action": {
"options": [],
"helper_text": "",
"helper_text_metadata": {},
},
}
result = component.update_build_config(build_config, "", "api_key")
assert result["auth_link"]["value"] == ""
assert "Please provide a valid Composio API Key" in result["auth_link"]["auth_tooltip"]
assert result["action"]["options"] == []
component.api_key = "test_key"
result = component.update_build_config(build_config, "test_key", "api_key")
assert len(result["action"]["options"]) > 0

View file

@ -1,184 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from composio import Action
from langflow.components.composio.outlook_composio import ComposioOutlookAPIComponent
from langflow.schema.dataframe import DataFrame
from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient
from .test_base import MockComposioToolSet
class MockAction:
OUTLOOK_OUTLOOK_SEND_EMAIL = "OUTLOOK_OUTLOOK_SEND_EMAIL"
OUTLOOK_OUTLOOK_LIST_MESSAGES = "OUTLOOK_OUTLOOK_LIST_MESSAGES"
class TestOutlookComponent(ComponentTestBaseWithoutClient):
@pytest.fixture(autouse=True)
def mock_composio_toolset(self):
with patch("langflow.base.composio.composio_base.ComposioToolSet", MockComposioToolSet):
yield
@pytest.fixture
def component_class(self):
return ComposioOutlookAPIComponent
@pytest.fixture
def default_kwargs(self):
return {
"api_key": "",
"entity_id": "default",
"action": None,
}
@pytest.fixture
def file_names_mapping(self):
return [
{"version": "1.0.17", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.18", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.19", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.0", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.1", "module": "composio", "file_name": DID_NOT_EXIST},
]
def test_init(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
assert component.display_name == "Outlook"
assert component.app_name == "outlook"
assert "OUTLOOK_OUTLOOK_SEND_EMAIL" in component._actions_data
assert "OUTLOOK_OUTLOOK_LIST_MESSAGES" in component._actions_data
def test_execute_action_send_email(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(Action, "OUTLOOK_OUTLOOK_SEND_EMAIL", MockAction.OUTLOOK_OUTLOOK_SEND_EMAIL)
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Send Email"}]
component.OUTLOOK_OUTLOOK_SEND_EMAIL_subject = "Test Subject"
component.OUTLOOK_OUTLOOK_SEND_EMAIL_body = "Test Body"
component.OUTLOOK_OUTLOOK_SEND_EMAIL_to_email = "test@example.com"
component._actions_data = {
"OUTLOOK_OUTLOOK_SEND_EMAIL": {
"display_name": "Send Email",
"action_fields": [
"OUTLOOK_OUTLOOK_SEND_EMAIL_user_id",
"OUTLOOK_OUTLOOK_SEND_EMAIL_subject",
"OUTLOOK_OUTLOOK_SEND_EMAIL_body",
"OUTLOOK_OUTLOOK_SEND_EMAIL_to_email",
"OUTLOOK_OUTLOOK_SEND_EMAIL_to_name",
"OUTLOOK_OUTLOOK_SEND_EMAIL_cc_emails",
"OUTLOOK_OUTLOOK_SEND_EMAIL_bcc_emails",
"OUTLOOK_OUTLOOK_SEND_EMAIL_is_html",
"OUTLOOK_OUTLOOK_SEND_EMAIL_save_to_sent_items",
"OUTLOOK_OUTLOOK_SEND_EMAIL_attachment",
],
},
}
result = component.execute_action()
assert result == {"result": "mocked response"}
def test_execute_action_fetch_emails(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(Action, "OUTLOOK_OUTLOOK_LIST_MESSAGES", MockAction.OUTLOOK_OUTLOOK_LIST_MESSAGES)
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "List Messages"}]
component.OUTLOOK_OUTLOOK_LIST_MESSAGES_folder = "Inbox"
component.OUTLOOK_OUTLOOK_LIST_MESSAGES_top = 10
component._actions_data = {
"OUTLOOK_OUTLOOK_LIST_MESSAGES": {
"display_name": "List Messages",
"action_fields": [
"OUTLOOK_OUTLOOK_LIST_MESSAGES_user_id",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_folder",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_top",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_skip",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_is_read",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_importance",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_subject",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_gt",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_startswith",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_endswith",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_subject_contains",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_ge",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_lt",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_received_date_time_le",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_from_address",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_has_attachments",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_body_preview_contains",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_sent_date_time_gt",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_sent_date_time_lt",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_categories",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_select",
"OUTLOOK_OUTLOOK_LIST_MESSAGES_orderby",
],
"get_result_field": True,
"result_field": "value",
},
}
mock_toolset = MagicMock()
mock_toolset.execute_action.return_value = {
"successful": True,
"data": {"response_data": {"value": [{"subject": "Test Email", "from": "test@example.com"}]}},
}
with patch.object(component, "_build_wrapper", return_value=mock_toolset):
result = component.execute_action()
assert result == [{"subject": "Test Email", "from": "test@example.com"}]
def test_execute_action_invalid_action(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Invalid Action"}]
with pytest.raises(ValueError, match="Invalid action: Invalid Action"):
component.execute_action()
def test_as_dataframe(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(Action, "OUTLOOK_OUTLOOK_SEND_EMAIL", MockAction.OUTLOOK_OUTLOOK_SEND_EMAIL)
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Send Email"}]
component.OUTLOOK_OUTLOOK_SEND_EMAIL_subject = "Test Subject"
component.OUTLOOK_OUTLOOK_SEND_EMAIL_body = "Test Body"
component.OUTLOOK_OUTLOOK_SEND_EMAIL_to_email = "test@example.com"
mock_emails = [
{
"message": "Email sent successfully.",
}
]
with patch.object(component, "execute_action", return_value=mock_emails):
result = component.as_dataframe()
assert isinstance(result, DataFrame)
assert not result.empty
data_str = str(result)
assert "Email sent successfully." in data_str
def test_update_build_config(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
build_config = {
"auth_link": {"value": "", "auth_tooltip": ""},
"action": {
"options": [],
"helper_text": "",
"helper_text_metadata": {},
},
}
result = component.update_build_config(build_config, "", "api_key")
assert result["auth_link"]["value"] == ""
assert "Please provide a valid Composio API Key" in result["auth_link"]["auth_tooltip"]
assert result["action"]["options"] == []
component.api_key = "test_key"
result = component.update_build_config(build_config, "test_key", "api_key")
assert len(result["action"]["options"]) > 0

View file

@ -1,176 +0,0 @@
from unittest.mock import MagicMock, patch
import pytest
from composio import Action
from langflow.components.composio.slack_composio import ComposioSlackAPIComponent
from langflow.schema.dataframe import DataFrame
from tests.base import DID_NOT_EXIST, ComponentTestBaseWithoutClient
from .test_base import MockComposioToolSet
class MockAction:
SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL = "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL"
SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION = "SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION"
class TestSlackComponent(ComponentTestBaseWithoutClient):
@pytest.fixture(autouse=True)
def mock_composio_toolset(self):
with patch("langflow.base.composio.composio_base.ComposioToolSet", MockComposioToolSet):
yield
@pytest.fixture
def component_class(self):
return ComposioSlackAPIComponent
@pytest.fixture
def default_kwargs(self):
return {
"api_key": "",
"entity_id": "default",
"action": None,
}
@pytest.fixture
def file_names_mapping(self):
return [
{"version": "1.0.17", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.18", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.0.19", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.0", "module": "composio", "file_name": DID_NOT_EXIST},
{"version": "1.1.1", "module": "composio", "file_name": DID_NOT_EXIST},
]
def test_init(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
assert component.display_name == "Slack"
assert component.app_name == "slack"
assert "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL" in component._actions_data
assert "SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION" in component._actions_data
def test_execute_action_send_message_to_channel(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(
Action, "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL", MockAction.SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL
)
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Post Message To Channel"}]
component.SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_channel = "random"
component.SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_text = "Test Body"
component._actions_data = {
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL": {
"display_name": "Post Message To Channel",
"action_fields": [
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_as_user",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_attachments",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_blocks",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_channel",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_icon_emoji",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_icon_url",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_link_names",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_mrkdwn",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_parse",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_reply_broadcast",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_text",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_thread_ts",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_unfurl_links",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_unfurl_media",
"SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_username",
],
},
}
result = component.execute_action()
assert result == {"result": "mocked response"}
def test_execute_action_list_all_slack_team_users(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(
Action,
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION",
MockAction.SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION,
)
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "List Users"}]
component.SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_limit = 1
component._actions_data = {
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION": {
"display_name": "List Users",
"action_fields": [
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_limit",
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_cursor",
"SLACK_LIST_ALL_SLACK_TEAM_USERS_WITH_PAGINATION_include_locale",
],
},
}
mock_toolset = MagicMock()
mock_toolset.execute_action.return_value = {"successful": True, "data": {"messages": "mocked response"}}
with patch.object(component, "_build_wrapper", return_value=mock_toolset):
result = component.execute_action()
assert result == {"messages": "mocked response"}
def test_execute_action_invalid_action(self, component_class, default_kwargs):
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Invalid Action"}]
# Execute action should raise ValueError
with pytest.raises(ValueError, match="Invalid action: Invalid Action"):
component.execute_action()
def test_as_dataframe(self, component_class, default_kwargs, monkeypatch):
monkeypatch.setattr(
Action, "SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL", MockAction.SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL
)
# Setup component
component = component_class(**default_kwargs)
component.api_key = "test_key"
component.action = [{"name": "Post Message To Channel"}]
component.SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_channel = "random"
component.SLACK_SENDS_A_MESSAGE_TO_A_SLACK_CHANNEL_text = "Test Body"
mock_slack_messages = [
{"channel": "channel1", "user": "user1", "text": "text message 1", "ts": "ts1", "team": "team1"},
{"channel": "channel2", "user": "user2", "text": "text message 2", "ts": "ts2", "team": "team2"},
]
with patch.object(component, "execute_action", return_value=mock_slack_messages):
result = component.as_dataframe()
assert isinstance(result, DataFrame)
assert not result.empty
data_str = str(result)
assert "text message 1" in data_str
assert "text message 2" in data_str
def test_update_build_config(self, component_class, default_kwargs):
component = component_class(**default_kwargs)
build_config = {
"auth_link": {"value": "", "auth_tooltip": ""},
"action": {
"options": [],
"helper_text": "",
"helper_text_metadata": {},
},
}
result = component.update_build_config(build_config, "", "api_key")
assert result["auth_link"]["value"] == ""
assert "Please provide a valid Composio API Key" in result["auth_link"]["auth_tooltip"]
assert result["action"]["options"] == []
component.api_key = "test_key"
result = component.update_build_config(build_config, "test_key", "api_key")
assert len(result["action"]["options"]) > 0

View file

@ -70,6 +70,7 @@ export default function NodeStatus({
);
const connectionLink = nodeAuth?.value;
const apiKeyValue = (data.node?.template as any)?.api_key?.value ?? "";
const isAuthenticated = nodeAuth?.value === "validated";
const pollingInterval = useRef<NodeJS.Timeout | null>(null);
@ -104,33 +105,61 @@ export default function NodeStatus({
// Start polling when connection is initiated
const startPolling = () => {
customOpenNewTab(connectionLink);
stopPolling();
setIsPolling(true);
pollingInterval.current = setInterval(() => {
mutateTemplate(
{ validate: data.node?.template?.auth?.value || "" },
data.id,
data.node,
(newNode) => {
setNode(nodeId, (old) => ({
...old,
data: { ...old.data, node: newNode },
}));
},
postTemplateValue,
setErrorData,
nodeAuth?.name ?? "auth_link",
() => {},
data.node.tool_mode,
);
}, POLLING_INTERVAL);
mutateTemplate(
{ validate: "" },
data.id,
data.node,
(newNode) => {
setNode(nodeId, (old) => ({
...old,
data: { ...old.data, node: newNode },
}));
pollingTimeout.current = setTimeout(() => {
stopPolling();
}, POLLING_TIMEOUT);
const updatedAuth = Object.values(newNode?.template ?? {}).find(
(value: any) => value?.type === "auth",
) as any;
const oauthUrl = updatedAuth?.value;
if (
oauthUrl &&
typeof oauthUrl === "string" &&
oauthUrl.startsWith("http")
) {
customOpenNewTab(oauthUrl);
}
},
postTemplateValue,
setErrorData,
nodeAuth?.name ?? "auth_link",
() => {
pollingInterval.current = setInterval(() => {
mutateTemplate(
{ validate: data.node?.template?.auth?.value || "" },
data.id,
data.node,
(newNode) => {
setNode(nodeId, (old) => ({
...old,
data: { ...old.data, node: newNode },
}));
},
postTemplateValue,
setErrorData,
nodeAuth?.name ?? "auth_link",
() => {},
data.node.tool_mode,
);
}, POLLING_INTERVAL);
pollingTimeout.current = setTimeout(() => {
stopPolling();
}, POLLING_TIMEOUT);
},
data.node.tool_mode,
);
};
useEffect(() => {
@ -316,7 +345,9 @@ export default function NodeStatus({
: isAuthenticated && !isPolling
? "border-accent-emerald-foreground hover:border-accent-amber-foreground"
: "",
connectionLink === "" && "cursor-not-allowed opacity-50",
connectionLink === "" &&
(!apiKeyValue || apiKeyValue === "COMPOSIO_API_KEY") &&
"cursor-not-allowed opacity-50",
);
};
@ -394,7 +425,11 @@ export default function NodeStatus({
<div>
<Button
unstyled
disabled={connectionLink === "" || connectionLink === "error"}
disabled={
(connectionLink === "" &&
(!apiKeyValue || apiKeyValue === "COMPOSIO_API_KEY")) ||
connectionLink === "error"
}
className={getConnectionButtonClasses(
connectionLink,
isAuthenticated,

View file

@ -0,0 +1,21 @@
const GoogleTasks = (props) => (
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 755.78 560.77"
height="23"
width="23"
{...props}
>
<polygon
fill="#0066DA"
points="505.79,113.39 469.59,133.29 451.59,167.59 469.59,209.59 503.59,246.39 540.29,225.89 562.89,186.99 540.29,138.79"
/>
<path
fill="#2684FC"
d="M365.49,253.69l86.2-86.2c25.4,19.3,44,46.9,51.9,78.8l-118.3,118.4c-10.6,10.6-27.9,10.6-38.5,0l-79.7-79.7c-8.5-8.5-8.5-22.3,0-30.8l34-34c8.5-8.5,22.3-8.5,30.8,0L365.49,253.69z M601.19,117.99l-34.6-34.6c-8.5-8.5-22.3-8.5-30.8,0l-30,30c24,20,43.5,45.1,57.1,73.6l38.3-38.3C609.69,140.29,609.69,126.39,601.19,117.99z M507.69,280.39c0,78.2-63.4,141.6-141.6,141.6s-141.6-63.4-141.6-141.6s63.3-141.6,141.6-141.6c32.2,0,61.8,10.7,85.6,28.8l54.2-54.2c-37.8-31.8-86.5-50.9-139.8-50.9c-120.4,0.1-217.9,97.6-217.9,217.9s97.5,217.8,217.8,217.8s217.9-97.5,217.9-217.8c0-33.4-7.6-65.1-21-93.4l-59.3,59.4C506.19,257.29,507.69,268.69,507.69,280.39z"
/>
</svg>
);
export default GoogleTasks;

View file

@ -0,0 +1 @@
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 755.78 560.77" style="enable-background:new 0 0 755.78 560.77;" xml:space="preserve" height="17" width="23"><style type="text/css"> .st0{fill:#0066DA;} .st1{fill:#2684FC;} .st2{fill:none;}</style><g> <polygon class="st0" points="505.79,113.39 469.59,133.29 451.59,167.59 469.59,209.59 503.59,246.39 540.29,225.89 562.89,186.99 540.29,138.79 " /> <path class="st1" d="M365.49,253.69l86.2-86.2c25.4,19.3,44,46.9,51.9,78.8l-118.3,118.4c-10.6,10.6-27.9,10.6-38.5,0l-79.7-79.7 c-8.5-8.5-8.5-22.3,0-30.8l34-34c8.5-8.5,22.3-8.5,30.8,0L365.49,253.69z M601.19,117.99l-34.6-34.6c-8.5-8.5-22.3-8.5-30.8,0 l-30,30c24,20,43.5,45.1,57.1,73.6l38.3-38.3C609.69,140.29,609.69,126.39,601.19,117.99z M507.69,280.39 c0,78.2-63.4,141.6-141.6,141.6s-141.6-63.4-141.6-141.6s63.3-141.6,141.6-141.6c32.2,0,61.8,10.7,85.6,28.8l54.2-54.2 c-37.8-31.8-86.5-50.9-139.8-50.9c-120.4,0.1-217.9,97.6-217.9,217.9s97.5,217.8,217.8,217.8s217.9-97.5,217.9-217.8 c0-33.4-7.6-65.1-21-93.4l-59.3,59.4C506.19,257.29,507.69,268.69,507.69,280.39z" /></g><rect x="127.89" y="30.39" class="st2" width="500" height="500" /></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

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

View file

@ -0,0 +1,30 @@
const Icon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width="23px"
height="23px"
>
<path
fill="#48b564"
d="M35.76,26.36h0.01c0,0-3.77,5.53-6.94,9.64c-2.74,3.55-3.54,6.59-3.77,8.06 C24.97,44.6,24.53,45,24,45s-0.97-0.4-1.06-0.94c-0.23-1.47-1.03-4.51-3.77-8.06c-0.42-0.55-0.85-1.12-1.28-1.7L28.24,22l8.33-9.88 C37.49,14.05,38,16.21,38,18.5C38,21.4,37.17,24.09,35.76,26.36z"
/>
<path
fill="#fcc60e"
d="M28.24,22L17.89,34.3c-2.82-3.78-5.66-7.94-5.66-7.94h0.01c-0.3-0.48-0.57-0.97-0.8-1.48L19.76,15 c-0.79,0.95-1.26,2.17-1.26,3.5c0,3.04,2.46,5.5,5.5,5.5C25.71,24,27.24,23.22,28.24,22z"
/>
<path
fill="#2c85eb"
d="M28.4,4.74l-8.57,10.18L13.27,9.2C15.83,6.02,19.69,4,24,4C25.54,4,27.02,4.26,28.4,4.74z"
/>
<path
fill="#ed5748"
d="M19.83,14.92L19.76,15l-8.32,9.88C10.52,22.95,10,20.79,10,18.5c0-3.54,1.23-6.79,3.27-9.3 L19.83,14.92z"
/>
<path
fill="#5695f6"
d="M28.24,22c0.79-0.95,1.26-2.17,1.26-3.5c0-3.04-2.46-5.5-5.5-5.5c-1.71,0-3.24,0.78-4.24,2L28.4,4.74 c3.59,1.22,6.53,3.91,8.17,7.38L28.24,22z"
/>
</svg>
);
export default Icon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="23px" height="23px"><path fill="#48b564" d="M35.76,26.36h0.01c0,0-3.77,5.53-6.94,9.64c-2.74,3.55-3.54,6.59-3.77,8.06 C24.97,44.6,24.53,45,24,45s-0.97-0.4-1.06-0.94c-0.23-1.47-1.03-4.51-3.77-8.06c-0.42-0.55-0.85-1.12-1.28-1.7L28.24,22l8.33-9.88 C37.49,14.05,38,16.21,38,18.5C38,21.4,37.17,24.09,35.76,26.36z"/><path fill="#fcc60e" d="M28.24,22L17.89,34.3c-2.82-3.78-5.66-7.94-5.66-7.94h0.01c-0.3-0.48-0.57-0.97-0.8-1.48L19.76,15 c-0.79,0.95-1.26,2.17-1.26,3.5c0,3.04,2.46,5.5,5.5,5.5C25.71,24,27.24,23.22,28.24,22z"/><path fill="#2c85eb" d="M28.4,4.74l-8.57,10.18L13.27,9.2C15.83,6.02,19.69,4,24,4C25.54,4,27.02,4.26,28.4,4.74z"/><path fill="#ed5748" d="M19.83,14.92L19.76,15l-8.32,9.88C10.52,22.95,10,20.79,10,18.5c0-3.54,1.23-6.79,3.27-9.3 L19.83,14.92z"/><path fill="#5695f6" d="M28.24,22c0.79-0.95,1.26-2.17,1.26-3.5c0-3.04-2.46-5.5-5.5-5.5c-1.71,0-3.24,0.78-4.24,2L28.4,4.74 c3.59,1.22,6.53,3.91,8.17,7.38L28.24,22z"/></svg>

After

Width:  |  Height:  |  Size: 996 B

View file

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

View file

@ -0,0 +1,34 @@
const GooglemeetIcon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width="23px"
height="23px"
>
<rect
width="16"
height="16"
x="12"
y="16"
fill="#fff"
transform="rotate(-90 20 24)"
/>
<polygon fill="#1e88e5" points="3,17 3,31 8,32 13,31 13,17 8,16" />
<path
fill="#4caf50"
d="M37,24v14c0,1.657-1.343,3-3,3H13l-1-5l1-5h14v-7l5-1L37,24z"
/>
<path
fill="#fbc02d"
d="M37,10v14H27v-7H13l-1-5l1-5h21C35.657,7,37,8.343,37,10z"
/>
<path fill="#1565c0" d="M13,31v10H6c-1.657,0-3-1.343-3-3v-7H13z" />
<polygon fill="#e53935" points="13,7 13,17 3,17" />
<polygon fill="#2e7d32" points="38,24 37,32.45 27,24 37,15.55" />
<path
fill="#4caf50"
d="M46,10.11v27.78c0,0.84-0.98,1.31-1.63,0.78L37,32.45v-16.9l7.37-6.22C45.02,8.8,46,9.27,46,10.11z"
/>
</svg>
);
export default GooglemeetIcon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="23px" height="23px"><rect width="16" height="16" x="12" y="16" fill="#fff" transform="rotate(-90 20 24)"/><polygon fill="#1e88e5" points="3,17 3,31 8,32 13,31 13,17 8,16"/><path fill="#4caf50" d="M37,24v14c0,1.657-1.343,3-3,3H13l-1-5l1-5h14v-7l5-1L37,24z"/><path fill="#fbc02d" d="M37,10v14H27v-7H13l-1-5l1-5h21C35.657,7,37,8.343,37,10z"/><path fill="#1565c0" d="M13,31v10H6c-1.657,0-3-1.343-3-3v-7H13z"/><polygon fill="#e53935" points="13,7 13,17 3,17"/><polygon fill="#2e7d32" points="38,24 37,32.45 27,24 37,15.55"/><path fill="#4caf50" d="M46,10.11v27.78c0,0.84-0.98,1.31-1.63,0.78L37,32.45v-16.9l7.37-6.22C45.02,8.8,46,9.27,46,10.11z"/></svg>

After

Width:  |  Height:  |  Size: 715 B

View file

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

View file

@ -0,0 +1,20 @@
const Icon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width="23px"
height="23px"
>
<path
fill="#43a047"
d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"
/>
<path fill="#c8e6c9" d="M40 13L30 13 30 3z" />
<path fill="#2e7d32" d="M30 13L40 23 40 13z" />
<path
fill="#e8f5e9"
d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"
/>
</svg>
);
export default Icon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="23px" height="23px"><path fill="#43a047" d="M37,45H11c-1.657,0-3-1.343-3-3V6c0-1.657,1.343-3,3-3h19l10,10v29C40,43.657,38.657,45,37,45z"/><path fill="#c8e6c9" d="M40 13L30 13 30 3z"/><path fill="#2e7d32" d="M30 13L40 23 40 13z"/><path fill="#e8f5e9" d="M31,23H17h-2v2v2v2v2v2v2v2h18v-2v-2v-2v-2v-2v-2v-2H31z M17,25h4v2h-4V25z M17,29h4v2h-4V29z M17,33h4v2h-4V33z M31,35h-8v-2h8V35z M31,31h-8v-2h8V31z M31,27h-8v-2h8V27z"/></svg>

After

Width:  |  Height:  |  Size: 495 B

View file

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

View file

@ -106,6 +106,18 @@ export const lazyIconsMapping = {
import("@/icons/GoogleDrive").then((mod) => ({
default: mod.GoogleDriveIcon,
})),
Googlemeet: () =>
import("@/icons/googlemeet").then((mod) => ({
default: mod.GooglemeetIcon,
})),
GoogleTasks: () =>
import("@/icons/GoogleTasks").then((mod) => ({
default: mod.GoogleTasksIcon,
})),
Googlesheets: () =>
import("@/icons/googlesheets").then((mod) => ({
default: mod.GooglesheetsIcon,
})),
GoogleGenerativeAI: () =>
import("@/icons/GoogleGenerativeAI").then((mod) => ({
default: mod.GoogleGenerativeAIIcon,
@ -122,6 +134,18 @@ export const lazyIconsMapping = {
import("@/icons/GradientSparkles").then((mod) => ({
default: mod.GradientInfinity,
})),
Googlemaps: () =>
import("@/icons/googlemaps").then((mod) => ({
default: mod.GooglemapsIcon,
})),
Todoist: () =>
import("@/icons/todoist").then((mod) => ({
default: mod.TodoistIcon,
})),
Zoom: () =>
import("@/icons/zoom").then((mod) => ({
default: mod.ZoomIcon,
})),
GradientUngroup: () =>
import("@/icons/GradientSparkles").then((mod) => ({
default: mod.GradientUngroup,
@ -158,6 +182,8 @@ export const lazyIconsMapping = {
import("@/icons/JigsawStack").then((mod) => ({
default: mod.JigsawStackIcon,
})),
Linear: () =>
import("@/icons/linear").then((mod) => ({ default: mod.LinearIcon })),
LangChain: () =>
import("@/icons/LangChain").then((mod) => ({ default: mod.LangChainIcon })),
Langwatch: () =>
@ -221,6 +247,8 @@ export const lazyIconsMapping = {
})),
Redis: () =>
import("@/icons/Redis").then((mod) => ({ default: mod.RedisIcon })),
Reddit: () =>
import("@/icons/reddit").then((mod) => ({ default: mod.RedditIcon })),
SambaNova: () =>
import("@/icons/SambaNova").then((mod) => ({ default: mod.SambaNovaIcon })),
ScrapeGraph: () =>

View file

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

View file

@ -0,0 +1,20 @@
const Icon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="23px"
height="23px"
viewBox="0 0 23 23"
version="1.1"
fill="currentColor"
{...props}
>
<g id="surface1">
<path d="M 2.910156 12.402344 C 3.105469 14.300781 3.933594 16.144531 5.386719 17.597656 C 6.839844 19.050781 8.683594 19.878906 10.582031 20.078125 Z M 2.910156 12.402344 " />
<path d="M 2.875 11.015625 L 11.972656 20.113281 C 12.742188 20.066406 13.511719 19.921875 14.25 19.671875 L 3.3125 8.734375 C 3.066406 9.476562 2.917969 10.242188 2.875 11.015625 Z M 2.875 11.015625 " />
<path d="M 3.707031 7.773438 L 15.214844 19.28125 C 15.8125 18.996094 16.382812 18.636719 16.914062 18.203125 L 4.78125 6.074219 C 4.351562 6.605469 3.992188 7.175781 3.707031 7.773438 Z M 3.707031 7.773438 " />
<path d="M 5.425781 5.363281 C 8.796875 2.03125 14.230469 2.046875 17.585938 5.402344 C 20.941406 8.757812 20.953125 14.1875 17.625 17.558594 Z M 5.425781 5.363281 " />
</g>
</svg>
);
export default Icon;

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="23px" height="23px" viewBox="0 0 23 23" version="1.1">
<g id="surface1">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 2.910156 12.402344 C 3.105469 14.300781 3.933594 16.144531 5.386719 17.597656 C 6.839844 19.050781 8.683594 19.878906 10.582031 20.078125 Z M 2.910156 12.402344 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 2.875 11.015625 L 11.972656 20.113281 C 12.742188 20.066406 13.511719 19.921875 14.25 19.671875 L 3.3125 8.734375 C 3.066406 9.476562 2.917969 10.242188 2.875 11.015625 Z M 2.875 11.015625 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 3.707031 7.773438 L 15.214844 19.28125 C 15.8125 18.996094 16.382812 18.636719 16.914062 18.203125 L 4.78125 6.074219 C 4.351562 6.605469 3.992188 7.175781 3.707031 7.773438 Z M 3.707031 7.773438 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,0%,0%);fill-opacity:1;" d="M 5.425781 5.363281 C 8.796875 2.03125 14.230469 2.046875 17.585938 5.402344 C 20.941406 8.757812 20.953125 14.1875 17.625 17.558594 Z M 5.425781 5.363281 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

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

View file

@ -0,0 +1,13 @@
const RedditIconSVG = (props) => (
<svg viewBox="0 0 16 16" width="23px" height="23px" {...props}>
<path
fill="#fe3155"
d="M8,0c4.418,0,8,3.582,8,8s-3.582,8-8,8s-8-3.582-8-8S3.582,0,8,0z"
/>
<path
fill="#fff"
d="M12.124,6.875c-0.285,0.009-0.558,0.127-0.766,0.324c-0.911-0.62-1.98-0.96-3.08-0.984l0.52-2.496 l1.711,0.36c0.047,0.44,0.442,0.758,0.882,0.711c0.438-0.049,0.756-0.442,0.709-0.882c-0.047-0.44-0.442-0.758-0.88-0.709 c-0.253,0.025-0.48,0.173-0.605,0.391l-1.96-0.391C8.522,3.167,8.387,3.251,8.358,3.387c0,0.002,0,0.002,0,0.004L7.767,6.167 c-1.113,0.016-2.198,0.36-3.12,0.984c-0.469-0.442-1.209-0.42-1.651,0.051c-0.442,0.471-0.42,1.209,0.053,1.651 c0.091,0.085,0.196,0.16,0.313,0.211c-0.007,0.118-0.007,0.235,0,0.353c0,1.791,2.089,3.249,4.664,3.249 c2.576,0,4.664-1.456,4.664-3.249c0.007-0.118,0.007-0.234,0-0.353c0.402-0.2,0.655-0.615,0.645-1.064 C13.311,7.356,12.771,6.853,12.124,6.875z M5.335,8.8c0-0.442,0.358-0.8,0.802-0.8c0.442,0,0.8,0.358,0.8,0.8s-0.358,0.8-0.8,0.8 S5.335,9.242,5.335,8.8z M9.971,11C9.616,11.328,8.988,11.616,8,11.616S6.384,11.328,6.029,11c-0.073-0.067-0.062-0.227,0.031-0.304 c0.078-0.065,0.193-0.065,0.275,0c0.48,0.353,1.07,0.48,1.665,0.48s1.185-0.127,1.665-0.48c0.082-0.065,0.196-0.065,0.275,0 C10.033,10.773,10.044,10.933,9.971,11z M9.842,9.6C9.4,9.6,9.04,9.242,9.04,8.8C9.042,8.358,9.4,8,9.842,8 c0.442,0,0.8,0.358,0.8,0.8S10.284,9.6,9.842,9.6z"
/>
</svg>
);
export default RedditIconSVG;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16px" height="16px" baseProfile="basic"><path fill="#fe3155" d="M8,0c4.418,0,8,3.582,8,8s-3.582,8-8,8s-8-3.582-8-8S3.582,0,8,0z"/><path fill="#fff" d="M12.124,6.875c-0.285,0.009-0.558,0.127-0.766,0.324c-0.911-0.62-1.98-0.96-3.08-0.984l0.52-2.496 l1.711,0.36c0.047,0.44,0.442,0.758,0.882,0.711c0.438-0.049,0.756-0.442,0.709-0.882c-0.047-0.44-0.442-0.758-0.88-0.709 c-0.253,0.025-0.48,0.173-0.605,0.391l-1.96-0.391C8.522,3.167,8.387,3.251,8.358,3.387c0,0.002,0,0.002,0,0.004L7.767,6.167 c-1.113,0.016-2.198,0.36-3.12,0.984c-0.469-0.442-1.209-0.42-1.651,0.051c-0.442,0.471-0.42,1.209,0.053,1.651 c0.091,0.085,0.196,0.16,0.313,0.211c-0.007,0.118-0.007,0.235,0,0.353c0,1.791,2.089,3.249,4.664,3.249 c2.576,0,4.664-1.456,4.664-3.249c0.007-0.118,0.007-0.234,0-0.353c0.402-0.2,0.655-0.615,0.645-1.064 C13.311,7.356,12.771,6.853,12.124,6.875z M5.335,8.8c0-0.442,0.358-0.8,0.802-0.8c0.442,0,0.8,0.358,0.8,0.8s-0.358,0.8-0.8,0.8 S5.335,9.242,5.335,8.8z M9.971,11C9.616,11.328,8.988,11.616,8,11.616S6.384,11.328,6.029,11c-0.073-0.067-0.062-0.227,0.031-0.304 c0.078-0.065,0.193-0.065,0.275,0c0.48,0.353,1.07,0.48,1.665,0.48s1.185-0.127,1.665-0.48c0.082-0.065,0.196-0.065,0.275,0 C10.033,10.773,10.044,10.933,9.971,11z M9.842,9.6C9.4,9.6,9.04,9.242,9.04,8.8C9.042,8.358,9.4,8,9.842,8 c0.442,0,0.8,0.358,0.8,0.8S10.284,9.6,9.842,9.6z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

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

View file

@ -0,0 +1,15 @@
const Icon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width="23px"
height="23px"
baseProfile="basic"
>
<path
fill="#f44336"
d="M37,6H11c-2.761,0-5,2.239-5,5v5.29l6.406,3.7c0.309,0.178,0.689,0.179,0.999,0.001l12.747-7.329 c0.152-0.087,0.338-0.089,0.492-0.004l1.666,0.923c0.34,0.189,0.344,0.677,0.007,0.871L13.4,23.024 c-0.309,0.178-0.689,0.177-0.998-0.001L6,19.329v3.045l6.406,3.7c0.309,0.178,0.689,0.179,0.999,0.001l12.747-7.329 c0.152-0.087,0.338-0.089,0.492-0.004l1.666,0.923c0.34,0.189,0.344,0.677,0.007,0.871L13.4,29.108 c-0.309,0.178-0.689,0.177-0.998-0.001L6,25.413v3.045l6.406,3.7c0.309,0.178,0.689,0.179,0.999,0.001l12.747-7.329 c0.152-0.087,0.338-0.089,0.492-0.004l1.666,0.923c0.34,0.189,0.344,0.677,0.007,0.871L13.4,35.192 c-0.309,0.178-0.689,0.177-0.998-0.001L6,31.497V37c0,2.761,2.239,5,5,5h26c2.761,0,5-2.239,5-5V11C42,8.239,39.761,6,37,6z"
/>
</svg>
);
export default Icon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="23px" height="23px" baseProfile="basic"><path fill="#f44336" d="M37,6H11c-2.761,0-5,2.239-5,5v5.29l6.406,3.7c0.309,0.178,0.689,0.179,0.999,0.001l12.747-7.329 c0.152-0.087,0.338-0.089,0.492-0.004l1.666,0.923c0.34,0.189,0.344,0.677,0.007,0.871L13.4,23.024 c-0.309,0.178-0.689,0.177-0.998-0.001L6,19.329v3.045l6.406,3.7c0.309,0.178,0.689,0.179,0.999,0.001l12.747-7.329 c0.152-0.087,0.338-0.089,0.492-0.004l1.666,0.923c0.34,0.189,0.344,0.677,0.007,0.871L13.4,29.108 c-0.309,0.178-0.689,0.177-0.998-0.001L6,25.413v3.045l6.406,3.7c0.309,0.178,0.689,0.179,0.999,0.001l12.747-7.329 c0.152-0.087,0.338-0.089,0.492-0.004l1.666,0.923c0.34,0.189,0.344,0.677,0.007,0.871L13.4,35.192 c-0.309,0.178-0.689,0.177-0.998-0.001L6,31.497V37c0,2.761,2.239,5,5,5h26c2.761,0,5-2.239,5-5V11C42,8.239,39.761,6,37,6z"/></svg>

After

Width:  |  Height:  |  Size: 866 B

View file

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

View file

@ -0,0 +1,16 @@
const Icon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 48 48"
width="23px"
height="23px"
>
<circle cx="24" cy="24" r="20" fill="#2196f3" />
<path
fill="#fff"
d="M29,31H14c-1.657,0-3-1.343-3-3V17h15c1.657,0,3,1.343,3,3V31z"
/>
<polygon fill="#fff" points="37,31 31,27 31,21 37,17" />
</svg>
);
export default Icon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="23px" height="23px"><circle cx="24" cy="24" r="20" fill="#2196f3"/><path fill="#fff" d="M29,31H14c-1.657,0-3-1.343-3-3V17h15c1.657,0,3,1.343,3,3V31z"/><polygon fill="#fff" points="37,31 31,27 31,21 37,17"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View file

@ -232,6 +232,13 @@ export const SIDEBAR_CATEGORIES = [
export const SIDEBAR_BUNDLES = [
{ display_name: "AI/ML API", name: "aiml", icon: "AIML" },
{ display_name: "AgentQL", name: "agentql", icon: "AgentQL" },
{
display_name: "Language Models",
name: "languagemodels",
icon: "BrainCircuit",
},
{ display_name: "Embeddings", name: "embeddings", icon: "Binary" },
{ display_name: "Memories", name: "memories", icon: "Cpu" },
{ display_name: "Amazon", name: "amazon", icon: "Amazon" },
{ display_name: "Anthropic", name: "anthropic", icon: "Anthropic" },
{ display_name: "Apify", name: "apify", icon: "Apify" },
@ -255,15 +262,9 @@ export const SIDEBAR_BUNDLES = [
{ display_name: "Exa", name: "exa", icon: "Exa" },
{ display_name: "Firecrawl", name: "firecrawl", icon: "FirecrawlCrawlApi" },
{ display_name: "Git", name: "git", icon: "GitLoader" },
{ display_name: "GitHub", name: "github", icon: "Github" },
{ display_name: "Glean", name: "glean", icon: "Glean" },
{ display_name: "Gmail", name: "gmail", icon: "Gmail" },
{ display_name: "Google", name: "google", icon: "Google" },
{
display_name: "Googlecalendar",
name: "googlecalendar",
icon: "Googlecalendar",
},
{ display_name: "Groq", name: "groq", icon: "Groq" },
{
display_name: "Home Assistant",
@ -290,7 +291,6 @@ export const SIDEBAR_BUNDLES = [
{ display_name: "Ollama", name: "ollama", icon: "Ollama" },
{ display_name: "OpenAI", name: "openai", icon: "OpenAI" },
{ display_name: "OpenRouter", name: "openrouter", icon: "OpenRouter" },
{ display_name: "Outlook", name: "outlook", icon: "Outlook" },
{ display_name: "Perplexity", name: "perplexity", icon: "Perplexity" },
{ display_name: "Redis", name: "redis", icon: "Redis" },
{ display_name: "SambaNova", name: "sambanova", icon: "SambaNova" },
@ -372,7 +372,12 @@ export const nodeIconToDisplayIconMap: Record<string, string> = {
ChatOutput: "MessagesSquare",
//Integration Icons
Outlook: "Outlook",
AIML: "AIML",
Linear: "Linear",
Reddit: "Reddit",
Googlemaps: "Googlemaps",
Todoist: "Todoist",
Zoom: "Zoom",
AIML: "AI/ML",
AgentQL: "AgentQL",
LanguageModels: "BrainCircuit",
EmbeddingModels: "Binary",
@ -409,8 +414,11 @@ export const nodeIconToDisplayIconMap: Record<string, string> = {
FirecrawlScrapeApi: "Firecrawl",
GitbookLoader: "GitBook",
GoogleGenerativeAI: "GoogleGenerativeAI",
Googlesheets: "Googlesheets",
GoogleSearchAPI: "Google",
GoogleSearchAPIWrapper: "Google",
Googlemeet: "Googlemeet",
GoogleTasks: "GoogleTasks",
GoogleSearchResults: "Google",
GoogleSearchRun: "Google",
GoogleSerperAPI: "Google",

103
uv.lock generated
View file

@ -1316,47 +1316,49 @@ wheels = [
]
[[package]]
name = "composio-core"
version = "0.7.15"
name = "composio"
version = "0.8.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "click" },
{ name = "fastapi" },
{ name = "importlib-metadata" },
{ name = "inflection" },
{ name = "jsonref" },
{ name = "jsonschema" },
{ name = "paramiko" },
{ name = "composio-client" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "pyperclip" },
{ name = "pysher" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "rich" },
{ name = "semver" },
{ name = "sentry-sdk" },
{ name = "uvicorn" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3c/9d/3b3b8ca7269da92e602601afc2b32656f6ac94d42754292c5f9f669a575a/composio_core-0.7.15.tar.gz", hash = "sha256:6797c46f404b9c265cd05c07bd28b4104f425024ef683ac24092087e9590033d", size = 330325, upload-time = "2025-04-07T21:52:51.921Z" }
sdist = { url = "https://files.pythonhosted.org/packages/46/31/c9486e483832199f957c6728b2479ffd8946af1b7f69297aca5c0c8b1b1a/composio-0.8.5.tar.gz", hash = "sha256:7e5a5c2cfc3d13083c35e0279cd28e9df30acaae21632cb0eb7f948e6500a2a6", size = 38268, upload-time = "2025-07-24T11:44:35.777Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/b4/928bd4cd6d5fa6f8c4d651cf11cfb99a257ad3c4988e084bc99a794520c6/composio_core-0.7.15-py3-none-any.whl", hash = "sha256:3b4c19fcf8c084ed8b873b8e24ea55084495a046eb971af994e6aae6e1bfcea7", size = 493768, upload-time = "2025-04-07T21:52:50.085Z" },
{ url = "https://files.pythonhosted.org/packages/a3/7f/b5bc19d2246e3171227cfe6f33a63f5e3670d09ff535d7a9c7a94658c1fb/composio-0.8.5-py3-none-any.whl", hash = "sha256:b673554a0276c5394d639382edb7300a8c0506a1452316535eb7cc17daff0528", size = 47275, upload-time = "2025-07-24T11:44:22.666Z" },
]
[[package]]
name = "composio-client"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/21/a2/71e97c8f9c0c4fb0a1734417f629c83717c5f973b0cd8ccb95b803dd9224/composio_client-1.6.0.tar.gz", hash = "sha256:2adaf9bdaf449384d1d32f9361af20009c0675c68381cd9e9a4234f08969ab23", size = 169235, upload-time = "2025-07-21T06:07:18.72Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/2c/19f4ff86f0b3c5fb442e70185ead4feae6237abecfb35f109c9ccc2a6d5e/composio_client-1.6.0-py3-none-any.whl", hash = "sha256:e32055eeae8b49e7b94329808604f849e74bc557a453d3e37b37fe5cb627930b", size = 199273, upload-time = "2025-07-21T06:07:17.316Z" },
]
[[package]]
name = "composio-langchain"
version = "0.7.15"
version = "0.8.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "composio-core" },
{ name = "composio" },
{ name = "langchain" },
{ name = "langchain-openai" },
{ name = "langchainhub" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/7a/e127ea858ceda35341ee399480d2d5705bd4a2dbbabadf8f0bc35d05c48d/composio_langchain-0.7.15.tar.gz", hash = "sha256:cb75c460289ecdf9590caf7ddc0d7888b0a6622ca4f800c9358abe90c25d055e", size = 4477, upload-time = "2025-04-07T21:50:19.224Z" }
sdist = { url = "https://files.pythonhosted.org/packages/48/fe/43cac59464b70794ab56d9e79d2a6325e067208beedf8f647ef100af4a71/composio_langchain-0.8.5.tar.gz", hash = "sha256:2ed844fd9cec9f2fce1e7f4a7a8e3cde9b1921277a6835d21ab6e0d05e4aa847", size = 3072, upload-time = "2025-07-24T11:44:41.2Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/67/324cb0d8ce29c6689d41751b8e29c2600ac89028fd2e69440526929cb422/composio_langchain-0.7.15-py3-none-any.whl", hash = "sha256:a71b5371ad6c3ee4d4289c7a994fad1424e24c29a38e820b6b2ed259056abb65", size = 4897, upload-time = "2025-04-07T21:50:01.291Z" },
{ url = "https://files.pythonhosted.org/packages/92/fe/9463c00a48a740e13af0ac82af6091a6ab536d19d0bae5f00c1a6154bc47/composio_langchain-0.8.5-py3-none-any.whl", hash = "sha256:b7e983944aa352b0b763086ae57ac93ce3a7c9d845295c60378e3f196c13cf4e", size = 3351, upload-time = "2025-07-24T11:44:31.291Z" },
]
[[package]]
@ -3878,15 +3880,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
]
[[package]]
name = "inflection"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/7e/691d061b7329bc8d54edbf0ec22fbfb2afe61facb681f9aaa9bff7a27d04/inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417", size = 15091, upload-time = "2020-08-22T08:16:29.139Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
@ -4841,7 +4834,7 @@ dependencies = [
{ name = "certifi" },
{ name = "chromadb" },
{ name = "cleanlab-tlm" },
{ name = "composio-core" },
{ name = "composio" },
{ name = "composio-langchain" },
{ name = "datasets" },
{ name = "docling-core" },
@ -5037,8 +5030,8 @@ requires-dist = [
{ name = "chromadb", specifier = "==0.5.23" },
{ name = "cleanlab-tlm", specifier = ">=1.1.2" },
{ name = "clickhouse-connect", marker = "extra == 'clickhouse-connect'", specifier = "==0.7.19" },
{ name = "composio-core", specifier = "==0.7.15" },
{ name = "composio-langchain", specifier = "==0.7.15" },
{ name = "composio", specifier = "==0.8.5" },
{ name = "composio-langchain", specifier = "==0.8.5" },
{ name = "couchbase", marker = "extra == 'couchbase'", specifier = ">=4.2.1" },
{ name = "ctransformers", marker = "extra == 'local'", specifier = ">=0.2.10" },
{ name = "datasets", specifier = ">2.14.7" },
@ -7545,20 +7538,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/2f/804f58f0b856ab3bf21617cccf5b39206e6c4c94c2cd227bde125ea6105f/parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b", size = 20475, upload-time = "2023-03-27T02:01:09.31Z" },
]
[[package]]
name = "paramiko"
version = "3.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bcrypt" },
{ name = "cryptography" },
{ name = "pynacl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/15/ad6ce226e8138315f2451c2aeea985bf35ee910afb477bae7477dc3a8f3b/paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822", size = 1566110, upload-time = "2025-02-04T02:37:59.783Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298, upload-time = "2025-02-04T02:37:57.672Z" },
]
[[package]]
name = "parso"
version = "0.8.4"
@ -8572,26 +8551,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/2a/7c24a6144eaa06d18ed52822ea2b0f119fd9267cd1abbb75dae4d89a3803/pymongo-4.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:45ee87a4e12337353242bc758accc7fb47a2f2d9ecc0382a61e64c8f01e86708", size = 976873, upload-time = "2024-10-01T23:07:19.721Z" },
]
[[package]]
name = "pynacl"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" },
{ url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" },
{ url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" },
{ url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" },
{ url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" },
{ url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" },
{ url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" },
]
[[package]]
name = "pyparsing"
version = "3.2.3"