diff --git a/src/backend/base/langflow/components/composio/__init__.py b/src/backend/base/langflow/components/composio/__init__.py
index cdfe0dd2d9bc..bee7f51f6d87 100644
--- a/src/backend/base/langflow/components/composio/__init__.py
+++ b/src/backend/base/langflow/components/composio/__init__.py
@@ -2,6 +2,7 @@
from .github_composio import ComposioGitHubAPIComponent
from .gmail_composio import ComposioGmailAPIComponent
from .googlecalendar_composio import ComposioGoogleCalendarAPIComponent
+from .reddit_composio import ComposioRedditAPIComponent
from .slack_composio import ComposioSlackAPIComponent
from .youtube_composio import ComposioYoutubeAPIComponent
@@ -10,6 +11,7 @@
"ComposioGitHubAPIComponent",
"ComposioGmailAPIComponent",
"ComposioGoogleCalendarAPIComponent",
+ "ComposioRedditAPIComponent",
"ComposioSlackAPIComponent",
"ComposioYoutubeAPIComponent",
]
diff --git a/src/backend/base/langflow/components/composio/reddit_composio.py b/src/backend/base/langflow/components/composio/reddit_composio.py
new file mode 100644
index 000000000000..70dfb6108369
--- /dev/null
+++ b/src/backend/base/langflow/components/composio/reddit_composio.py
@@ -0,0 +1,374 @@
+from typing import Any
+import json
+
+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 ComposioRedditAPIComponent(ComposioBaseComponent):
+ display_name: str = "Reddit"
+ description: str = "Reddit API"
+ icon = "Reddit"
+ documentation: str = "https://docs.composio.dev"
+ app_name = "reddit"
+
+ _actions_data: dict = {
+ "REDDIT_CREATE_REDDIT_POST": {
+ "display_name": "Create Reddit Post",
+ "action_fields": [
+ "REDDIT_CREATE_REDDIT_POST_flair_id",
+ "REDDIT_CREATE_REDDIT_POST_kind",
+ "REDDIT_CREATE_REDDIT_POST_subreddit",
+ "REDDIT_CREATE_REDDIT_POST_text",
+ "REDDIT_CREATE_REDDIT_POST_title",
+ "REDDIT_CREATE_REDDIT_POST_url",
+ ],
+ "get_result_field": True,
+ "result_field": "items",
+ },
+ "REDDIT_DELETE_REDDIT_COMMENT": {
+ "display_name": "Delete Reddit Comment",
+ "action_fields": ["REDDIT_DELETE_REDDIT_COMMENT_id"],
+ "get_result_field": False,
+ },
+ "REDDIT_DELETE_REDDIT_POST": {
+ "display_name": "Delete Reddit Post",
+ "action_fields": ["REDDIT_DELETE_REDDIT_POST_id"],
+ "get_result_field": False,
+ },
+ "REDDIT_EDIT_REDDIT_COMMENT_OR_POST": {
+ "display_name": "Edit Reddit Comment Or Post",
+ "action_fields": [
+ "REDDIT_EDIT_REDDIT_COMMENT_OR_POST_text",
+ "REDDIT_EDIT_REDDIT_COMMENT_OR_POST_thing_id",
+ ],
+ "get_result_field": False,
+ },
+ "REDDIT_GET_USER_FLAIR": {
+ "display_name": "Get User Flair",
+ "action_fields": ["REDDIT_GET_USER_FLAIR_subreddit"],
+ "get_result_field": True,
+ "result_field": "flair_list",
+ },
+ "REDDIT_POST_REDDIT_COMMENT": {
+ "display_name": "Post Reddit Comment",
+ "action_fields": [
+ "REDDIT_POST_REDDIT_COMMENT_text",
+ "REDDIT_POST_REDDIT_COMMENT_thing_id",
+ ],
+ "get_result_field": False,
+ },
+ "REDDIT_RETRIEVE_POST_COMMENTS": {
+ "display_name": "Retrieve Post Comments",
+ "action_fields": ["REDDIT_RETRIEVE_POST_COMMENTS_article"],
+ "get_result_field": True,
+ "result_field": "comments",
+ },
+ "REDDIT_RETRIEVE_REDDIT_POST": {
+ "display_name": "Retrieve Reddit Post",
+ "action_fields": [
+ "REDDIT_RETRIEVE_REDDIT_POST_size",
+ "REDDIT_RETRIEVE_REDDIT_POST_subreddit",
+ ],
+ "get_result_field": True,
+ "result_field": "data",
+ },
+ "REDDIT_RETRIEVE_SPECIFIC_COMMENT": {
+ "display_name": "Retrieve Specific Comment",
+ "action_fields": ["REDDIT_RETRIEVE_SPECIFIC_COMMENT_id"],
+ "get_result_field": True,
+ "result_field": "things",
+ },
+ "REDDIT_SEARCH_ACROSS_SUBREDDITS": {
+ "display_name": "Search Across Subreddits",
+ "action_fields": [
+ "REDDIT_SEARCH_ACROSS_SUBREDDITS_limit",
+ "REDDIT_SEARCH_ACROSS_SUBREDDITS_restrict_sr",
+ "REDDIT_SEARCH_ACROSS_SUBREDDITS_search_query",
+ "REDDIT_SEARCH_ACROSS_SUBREDDITS_sort",
+ ],
+ "get_result_field": True,
+ "result_field": "data",
+ },
+ }
+
+ _all_fields = {
+ field
+ for action_data in _actions_data.values()
+ for field in action_data["action_fields"]
+ }
+
+ _bool_variables = {
+ "REDDIT_SEARCH_ACROSS_SUBREDDITS_restrict_sr",
+ }
+
+ inputs = [
+ *ComposioBaseComponent._base_inputs,
+ MessageTextInput(
+ name="REDDIT_CREATE_REDDIT_POST_flair_id",
+ display_name="Flair Id",
+ info="The ID of the flair to apply to the post. Use the 'REDDIT_GET_USER_FLAIR' action to find available flair IDs for the specified subreddit.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_CREATE_REDDIT_POST_kind",
+ display_name="Kind",
+ info="The type of the post. Use 'self' for a text-based post or 'link' for a post that links to an external URL.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_CREATE_REDDIT_POST_subreddit",
+ display_name="Subreddit",
+ info="The name of the subreddit (without the 'r/' prefix) where the post will be submitted.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_CREATE_REDDIT_POST_text",
+ display_name="Text",
+ info="The markdown-formatted text content for a 'self' post. Required if `kind` is 'self'.",
+ show=False,
+ ),
+ MessageTextInput(
+ name="REDDIT_CREATE_REDDIT_POST_title",
+ display_name="Title",
+ info="The title of the post. Must be 300 characters or less.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_CREATE_REDDIT_POST_url",
+ display_name="Url",
+ info="The URL for a 'link' post. Required if `kind` is 'link'.",
+ show=False,
+ ),
+ MessageTextInput(
+ name="REDDIT_DELETE_REDDIT_COMMENT_id",
+ display_name="Id",
+ info="The full 'thing ID' (fullname, e.g., 't1_c0s4w1c') of the comment to delete; typically starts with 't1_'.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_DELETE_REDDIT_POST_id",
+ display_name="Id",
+ info="The full name (fullname) of the Reddit post to be deleted. This ID must start with 't3_' followed by the post's unique base36 identifier.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_EDIT_REDDIT_COMMENT_OR_POST_text",
+ display_name="Text",
+ info="The new raw markdown text for the body of the comment or self-post.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_EDIT_REDDIT_COMMENT_OR_POST_thing_id",
+ display_name="Thing Id",
+ info="The full name (fullname) of the comment or self-post to edit. This is a combination of a prefix (e.g., 't1_' for comment, 't3_' for post) and the item's ID.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_GET_USER_FLAIR_subreddit",
+ display_name="Subreddit",
+ info="Name of the subreddit (e.g., 'pics', 'gaming') for which to retrieve available link flairs.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_POST_REDDIT_COMMENT_text",
+ display_name="Text",
+ info="The raw Markdown text of the comment to be submitted.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_POST_REDDIT_COMMENT_thing_id",
+ display_name="Thing Id",
+ info="The ID of the parent post (link) or comment, prefixed with 't3_' for a post (e.g., 't3_10omtdx') or 't1_' for a comment (e.g., 't1_h2g9w8l').",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_RETRIEVE_POST_COMMENTS_article",
+ display_name="Article",
+ info="Base_36 ID of the Reddit post (e.g., 'q5u7q5'), typically found in the post's URL and not including the 't3_' prefix.",
+ show=False,
+ required=True,
+ ),
+ IntInput(
+ name="REDDIT_RETRIEVE_REDDIT_POST_size",
+ display_name="Size",
+ info="The maximum number of posts to return. Default is 5. Set to 0 to retrieve all available posts (or the maximum allowed by the Reddit API for a single request, typically up to 100 for this type of listing).",
+ show=False,
+ value=5,
+ ),
+ MessageTextInput(
+ name="REDDIT_RETRIEVE_REDDIT_POST_subreddit",
+ display_name="Subreddit",
+ info="The name of the subreddit from which to retrieve posts (e.g., 'popular', 'pics'). Do not include 'r/'.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_RETRIEVE_SPECIFIC_COMMENT_id",
+ display_name="Id",
+ info="Fullname of the comment or post to retrieve (e.g., 't1_c123456', 't3_x56789').",
+ show=False,
+ required=True,
+ ),
+ IntInput(
+ name="REDDIT_SEARCH_ACROSS_SUBREDDITS_limit",
+ display_name="Limit",
+ info="The maximum number of search results to return. Default is 5. Maximum allowed value is 100.",
+ show=False,
+ value=5,
+ ),
+ BoolInput(
+ name="REDDIT_SEARCH_ACROSS_SUBREDDITS_restrict_sr",
+ display_name="Restrict Sr",
+ info="If True (default), confines the search to posts and comments within subreddits. If False, the search scope is broader and may include matching subreddit names or other Reddit entities.",
+ show=False,
+ value=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_SEARCH_ACROSS_SUBREDDITS_search_query",
+ display_name="Search Query",
+ info="The search query string used to find content across subreddits.",
+ show=False,
+ required=True,
+ ),
+ MessageTextInput(
+ name="REDDIT_SEARCH_ACROSS_SUBREDDITS_sort",
+ display_name="Sort",
+ info="The criterion for sorting search results. 'relevance' (default) sorts by relevance to the query. 'new' sorts by newest first. 'top' sorts by highest score (typically all-time). 'comments' sorts by the number of comments.",
+ show=False,
+ value="relevance",
+ ),
+ ]
+
+ def _find_key_recursively(self, data, key):
+ """Recursively search for a key in nested dicts/lists and return its value if found."""
+ if isinstance(data, dict):
+ if key in data:
+ return data[key]
+ for v in data.values():
+ found = self._find_key_recursively(v, key)
+ if found is not None:
+ return found
+ elif isinstance(data, list):
+ for item in data:
+ found = self._find_key_recursively(item, key)
+ if found is not None:
+ return found
+ return None
+
+ def _convert_pandas_to_python(self, obj):
+ """Recursively convert pandas objects to plain Python objects."""
+ if hasattr(obj, 'to_dict'): # pandas DataFrame
+ return obj.to_dict('records')
+ elif hasattr(obj, 'tolist'): # pandas Series
+ return obj.tolist()
+ elif isinstance(obj, dict):
+ return {k: self._convert_pandas_to_python(v) for k, v in obj.items()}
+ elif isinstance(obj, list):
+ return [self._convert_pandas_to_python(item) for item in obj]
+ else:
+ return obj
+
+ 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
+
+ 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 = result.get("data", {}).get("message", {})
+
+ error_info = {"error": result.get("error", "No response")}
+ if isinstance(message, str):
+ try:
+ parsed_message = json.loads(message)
+ if isinstance(parsed_message, dict) and "error" in parsed_message:
+ error_data = parsed_message["error"]
+ error_info = {
+ "error": {
+ "code": error_data.get("code", "Unknown"),
+ "message": error_data.get("message", "No error message"),
+ }
+ }
+ except (json.JSONDecodeError, KeyError) as e:
+ logger.error(f"Failed to parse error message as JSON: {e}")
+ error_info = {"error": str(message)}
+ elif isinstance(message, dict) and "error" in message:
+ error_data = message["error"]
+ error_info = {
+ "error": {
+ "code": error_data.get("code", "Unknown"),
+ "message": error_data.get("message", "No error message"),
+ }
+ }
+
+ return error_info
+
+ result_data = result.get("data", [])
+ action_data = self._actions_data.get(action_key, {})
+ if action_data.get("get_result_field"):
+ result_field = action_data.get("result_field")
+ if result_field:
+ found = self._find_key_recursively(result_data, result_field)
+ if found is not None:
+ return self._convert_pandas_to_python(found)
+ return self._convert_pandas_to_python(result_data)
+ if result_data and isinstance(result_data, dict):
+ converted_data = self._convert_pandas_to_python(result_data)
+ return [converted_data[next(iter(converted_data))]]
+ return self._convert_pandas_to_python(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):
+ self._default_tools = {
+ self.sanitize_action_name("REDDIT_CREATE_REDDIT_POST").replace(" ", "-"),
+ self.sanitize_action_name("REDDIT_RETRIEVE_REDDIT_POST").replace(" ", "-"),
+ }
diff --git a/src/backend/base/langflow/components/composio/youtube_composio.py b/src/backend/base/langflow/components/composio/youtube_composio.py
index 25e369bc9fed..35a76ceb5963 100644
--- a/src/backend/base/langflow/components/composio/youtube_composio.py
+++ b/src/backend/base/langflow/components/composio/youtube_composio.py
@@ -341,7 +341,7 @@ def execute_action(self):
return found
return result_data
if result_data and isinstance(result_data, dict):
- return result_data[next(iter(result_data))]
+ return [result_data[next(iter(result_data))]]
return result_data # noqa: TRY300
except Exception as e:
logger.error(f"Error executing action: {e}")
diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json
index 983c23906bf4..8458c9435b17 100644
--- a/src/frontend/package-lock.json
+++ b/src/frontend/package-lock.json
@@ -708,7 +708,6 @@
},
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0",
- "extraneous": true,
"inBundle": true,
"license": "MIT",
"engines": {
diff --git a/src/frontend/src/icons/lazyIconImports.ts b/src/frontend/src/icons/lazyIconImports.ts
index 78ddf87e2b22..7756c7e5fc2a 100644
--- a/src/frontend/src/icons/lazyIconImports.ts
+++ b/src/frontend/src/icons/lazyIconImports.ts
@@ -56,6 +56,8 @@ export const lazyIconsMapping = {
import("@/icons/Cohere").then((mod) => ({ default: mod.CohereIcon })),
Composio: () =>
import("@/icons/Composio").then((mod) => ({ default: mod.ComposioIcon })),
+ Reddit: () =>
+ import("@/icons/reddit").then((mod) => ({ default: mod.RedditIcon })),
Confluence: () =>
import("@/icons/Confluence").then((mod) => ({
default: mod.ConfluenceIcon,
diff --git a/src/frontend/src/icons/reddit/index.tsx b/src/frontend/src/icons/reddit/index.tsx
new file mode 100644
index 000000000000..2fbef9906f46
--- /dev/null
+++ b/src/frontend/src/icons/reddit/index.tsx
@@ -0,0 +1,9 @@
+import React, { forwardRef } from "react";
+import RedditIconSVG from "./reddit";
+
+export const RedditIcon = forwardRef<
+ SVGSVGElement,
+ React.PropsWithChildren<{}>
+>((props, ref) => {
+ return ;
+});
\ No newline at end of file
diff --git a/src/frontend/src/icons/reddit/reddit.jsx b/src/frontend/src/icons/reddit/reddit.jsx
new file mode 100644
index 000000000000..b3570bdb2ed2
--- /dev/null
+++ b/src/frontend/src/icons/reddit/reddit.jsx
@@ -0,0 +1,12 @@
+const RedditIconSVG = (props) => (
+
+ );
+ export default RedditIconSVG;
\ No newline at end of file
diff --git a/src/frontend/src/icons/reddit/reddit.svg b/src/frontend/src/icons/reddit/reddit.svg
new file mode 100644
index 000000000000..d64e3d1ba5dd
--- /dev/null
+++ b/src/frontend/src/icons/reddit/reddit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts
index 8c9b624a7be7..941b3fa6a83e 100644
--- a/src/frontend/src/utils/styleUtils.ts
+++ b/src/frontend/src/utils/styleUtils.ts
@@ -1,3 +1,4 @@
+import { RedditIcon } from "../icons/reddit";
import { BotMessageSquareIcon } from "@/icons/BotMessageSquare";
import { GradientSave } from "@/icons/GradientSparkles";
import { fontAwesomeIcons, isFontAwesomeIcon } from "@/icons/fontAwesomeIcons";
@@ -229,6 +230,7 @@ export const SIDEBAR_CATEGORIES = [
];
export const SIDEBAR_BUNDLES = [
+ { display_name: "Reddit", name: "reddit", icon: "Reddit" },
{ display_name: "Amazon", name: "amazon", icon: "Amazon" },
{ display_name: "Gmail", name: "gmail", icon: "Gmail" },
{ display_name: "GitHub", name: "github", icon: "Github" },
@@ -330,6 +332,7 @@ export const nodeIconToDisplayIconMap: Record = {
ChatInput: "MessagesSquare",
ChatOutput: "MessagesSquare",
//Integration Icons
+ Reddit: "Reddit",
AIML: "AI/ML",
AgentQL: "AgentQL",
AirbyteJSONLoader: "Airbyte",