From 136d2f5a80310b208ea1180592f22336a53619d5 Mon Sep 17 00:00:00 2001 From: Uday-sidagana Date: Thu, 5 Jun 2025 11:27:38 +0530 Subject: [PATCH 01/11] feat: add Youtube component --- .../components/composio/youtube_composio.py | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/components/composio/youtube_composio.py b/src/backend/base/langflow/components/composio/youtube_composio.py index 734446aa1414..90bd807c047c 100644 --- a/src/backend/base/langflow/components/composio/youtube_composio.py +++ b/src/backend/base/langflow/components/composio/youtube_composio.py @@ -1,6 +1,7 @@ from typing import Any from composio import Action +import json from langflow.base.composio.composio_base import ComposioBaseComponent from langflow.inputs import ( @@ -21,10 +22,14 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_GET_CHANNEL_ID_BY_HANDLE": { "display_name": "Get Channel ID by Handle", "action_fields": ["YOUTUBE_GET_CHANNEL_ID_BY_HANDLE_channel_handle"], + "get_result_field": True, + "result_field": "items", }, "YOUTUBE_LIST_CAPTION_TRACK": { "display_name": "List Caption Track", "action_fields": ["YOUTUBE_LIST_CAPTION_TRACK_part", "YOUTUBE_LIST_CAPTION_TRACK_videoId"], + "get_result_field": True, + "result_field": "items", }, "YOUTUBE_LIST_CHANNEL_VIDEOS": { "display_name": "List Channel Videos", @@ -34,6 +39,8 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_LIST_CHANNEL_VIDEOS_pageToken", "YOUTUBE_LIST_CHANNEL_VIDEOS_part", ], + "get_result_field": True, + "result_field": "items", }, "YOUTUBE_LIST_USER_PLAYLISTS": { "display_name": "List User Playlists", @@ -42,6 +49,8 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_LIST_USER_PLAYLISTS_pageToken", "YOUTUBE_LIST_USER_PLAYLISTS_part", ], + "get_result_field": True, + "result_field": "items", }, "YOUTUBE_LIST_USER_SUBSCRIPTIONS": { "display_name": "List User Subscriptions", @@ -50,10 +59,14 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_LIST_USER_SUBSCRIPTIONS_pageToken", "YOUTUBE_LIST_USER_SUBSCRIPTIONS_part", ], + "get_result_field": True, + "result_field": "items", }, "YOUTUBE_LOAD_CAPTIONS": { "display_name": "Load Captions", "action_fields": ["YOUTUBE_LOAD_CAPTIONS_id", "YOUTUBE_LOAD_CAPTIONS_tfmt"], + "get_result_field": True, + "result_field": "data", }, "YOUTUBE_SEARCH_YOU_TUBE": { "display_name": "Search YouTube", @@ -64,10 +77,14 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_SEARCH_YOU_TUBE_q", "YOUTUBE_SEARCH_YOU_TUBE_type", ], + "get_result_field": True, + "result_field": "response_data", #Next_page_token }, "YOUTUBE_SUBSCRIBE_CHANNEL": { "display_name": "Subscribe Channel", "action_fields": ["YOUTUBE_SUBSCRIBE_CHANNEL_channelId"], + "get_result_field": True, + "result_field": "snippet", }, "YOUTUBE_UPDATE_THUMBNAIL": { "display_name": "Update Thumbnail", @@ -81,12 +98,14 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_UPLOAD_VIDEO_privacyStatus", "YOUTUBE_UPLOAD_VIDEO_tags", "YOUTUBE_UPLOAD_VIDEO_title", - "YOUTUBE_UPLOAD_VIDEO_videoFilePath", + "YOUTUBE_UPLOAD_VIDEO_videoFilePath", ], }, "YOUTUBE_VIDEO_DETAILS": { "display_name": "Video Details", "action_fields": ["YOUTUBE_VIDEO_DETAILS_id", "YOUTUBE_VIDEO_DETAILS_part"], + "get_result_field": True, + "result_field": "items", }, } @@ -348,6 +367,22 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): ), ] + 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 execute_action(self): """Execute action and return response as Message.""" toolset = self._build_wrapper() @@ -384,6 +419,16 @@ def execute_action(self): return {"error": result.get("error", "No response")} result_data = result.get("data", []) + # Handle result_field if get_result_field is True + 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 found + return result_data + # Default behavior if result_data and isinstance(result_data, dict): return result_data[next(iter(result_data))] return result_data # noqa: TRY300 @@ -400,4 +445,4 @@ def set_default_tools(self): self._default_tools = { self.sanitize_action_name("YOUTUBE_SEARCH_YOU_TUBE").replace(" ", "-"), self.sanitize_action_name("YOUTUBE_VIDEO_DETAILS").replace(" ", "-"), - } + } \ No newline at end of file From 5228a73184f14037124c21cf8b68f405868b34bf Mon Sep 17 00:00:00 2001 From: abhishekpatil4 Date: Fri, 6 Jun 2025 01:46:56 +0530 Subject: [PATCH 02/11] fix: update result_field --- .../langflow/components/composio/youtube_composio.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/backend/base/langflow/components/composio/youtube_composio.py b/src/backend/base/langflow/components/composio/youtube_composio.py index 90bd807c047c..01da46535e60 100644 --- a/src/backend/base/langflow/components/composio/youtube_composio.py +++ b/src/backend/base/langflow/components/composio/youtube_composio.py @@ -1,7 +1,6 @@ from typing import Any from composio import Action -import json from langflow.base.composio.composio_base import ComposioBaseComponent from langflow.inputs import ( @@ -50,7 +49,7 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_LIST_USER_PLAYLISTS_part", ], "get_result_field": True, - "result_field": "items", + "result_field": "response_data", }, "YOUTUBE_LIST_USER_SUBSCRIPTIONS": { "display_name": "List User Subscriptions", @@ -78,7 +77,7 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_SEARCH_YOU_TUBE_type", ], "get_result_field": True, - "result_field": "response_data", #Next_page_token + "result_field": "response_data", # Next_page_token }, "YOUTUBE_SUBSCRIBE_CHANNEL": { "display_name": "Subscribe Channel", @@ -98,7 +97,7 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "YOUTUBE_UPLOAD_VIDEO_privacyStatus", "YOUTUBE_UPLOAD_VIDEO_tags", "YOUTUBE_UPLOAD_VIDEO_title", - "YOUTUBE_UPLOAD_VIDEO_videoFilePath", + "YOUTUBE_UPLOAD_VIDEO_videoFilePath", ], }, "YOUTUBE_VIDEO_DETAILS": { @@ -445,4 +444,4 @@ def set_default_tools(self): self._default_tools = { self.sanitize_action_name("YOUTUBE_SEARCH_YOU_TUBE").replace(" ", "-"), self.sanitize_action_name("YOUTUBE_VIDEO_DETAILS").replace(" ", "-"), - } \ No newline at end of file + } From 4510093553fe5c5fa74d97f3597b7c97148dd423 Mon Sep 17 00:00:00 2001 From: abhishekpatil4 Date: Fri, 6 Jun 2025 02:15:11 +0530 Subject: [PATCH 03/11] fix: error message to show all details --- .../components/composio/youtube_composio.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/backend/base/langflow/components/composio/youtube_composio.py b/src/backend/base/langflow/components/composio/youtube_composio.py index 01da46535e60..a5b544b67b07 100644 --- a/src/backend/base/langflow/components/composio/youtube_composio.py +++ b/src/backend/base/langflow/components/composio/youtube_composio.py @@ -1,3 +1,4 @@ +import json from typing import Any from composio import Action @@ -415,7 +416,33 @@ def execute_action(self): params=params, ) if not result.get("successful"): - return {"error": result.get("error", "No response")} + 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", []) # Handle result_field if get_result_field is True From 26aa3740e6f6d47e6ccbe2ab6cbdf43f273811ad Mon Sep 17 00:00:00 2001 From: abhishekpatil4 Date: Fri, 6 Jun 2025 02:24:19 +0530 Subject: [PATCH 04/11] chore: rm actions --- .../components/composio/youtube_composio.py | 73 +------------------ 1 file changed, 1 insertion(+), 72 deletions(-) diff --git a/src/backend/base/langflow/components/composio/youtube_composio.py b/src/backend/base/langflow/components/composio/youtube_composio.py index a5b544b67b07..8160c5b4669a 100644 --- a/src/backend/base/langflow/components/composio/youtube_composio.py +++ b/src/backend/base/langflow/components/composio/youtube_composio.py @@ -86,21 +86,6 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): "get_result_field": True, "result_field": "snippet", }, - "YOUTUBE_UPDATE_THUMBNAIL": { - "display_name": "Update Thumbnail", - "action_fields": ["YOUTUBE_UPDATE_THUMBNAIL_thumbnailUrl", "YOUTUBE_UPDATE_THUMBNAIL_videoId"], - }, - "YOUTUBE_UPLOAD_VIDEO": { - "display_name": "Upload Video", - "action_fields": [ - "YOUTUBE_UPLOAD_VIDEO_categoryId", - "YOUTUBE_UPLOAD_VIDEO_description", - "YOUTUBE_UPLOAD_VIDEO_privacyStatus", - "YOUTUBE_UPLOAD_VIDEO_tags", - "YOUTUBE_UPLOAD_VIDEO_title", - "YOUTUBE_UPLOAD_VIDEO_videoFilePath", - ], - }, "YOUTUBE_VIDEO_DETAILS": { "display_name": "Video Details", "action_fields": ["YOUTUBE_VIDEO_DETAILS_id", "YOUTUBE_VIDEO_DETAILS_part"], @@ -109,7 +94,7 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): }, } - _list_variables = {"YOUTUBE_UPDATE_VIDEO_tags", "YOUTUBE_UPLOAD_VIDEO_tags"} + _list_variables = {"YOUTUBE_UPDATE_VIDEO_tags"} _all_fields = {field for action_data in _actions_data.values() for field in action_data["action_fields"]} @@ -258,20 +243,6 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): show=False, required=True, ), - MessageTextInput( - name="YOUTUBE_UPDATE_THUMBNAIL_thumbnailUrl", - display_name="Thumbnail URL", - info="URL of the new thumbnail image", - show=False, - required=True, - ), - MessageTextInput( - name="YOUTUBE_UPDATE_THUMBNAIL_videoId", - display_name="Video ID", - info="YouTube video ID for which the thumbnail should be updated", - show=False, - required=True, - ), MessageTextInput( name="YOUTUBE_UPDATE_VIDEO_categoryId", display_name="Category ID", @@ -309,48 +280,6 @@ class ComposioYoutubeAPIComponent(ComposioBaseComponent): show=False, required=True, ), - MessageTextInput( - name="YOUTUBE_UPLOAD_VIDEO_categoryId", - display_name="Category ID", - info="YouTube category ID of the video", - show=False, - required=True, - ), - MessageTextInput( - name="YOUTUBE_UPLOAD_VIDEO_description", - display_name="Description", - info="The description of the video", - show=False, - required=True, - ), - MessageTextInput( - name="YOUTUBE_UPLOAD_VIDEO_privacyStatus", - display_name="Privacy Status", - info="The privacy status of the video. Valid values are 'public', 'private', and 'unlisted'", - show=False, - required=True, - ), - MessageTextInput( - name="YOUTUBE_UPLOAD_VIDEO_tags", - display_name="Tags", - info="List of tags associated with the video", - show=False, - required=True, - ), - MessageTextInput( - name="YOUTUBE_UPLOAD_VIDEO_title", - display_name="Title", - info="The title of the video", - show=False, - required=True, - ), - MessageTextInput( - name="YOUTUBE_UPLOAD_VIDEO_videoFilePath", - display_name="Video File Path", - info="File path of the video to be uploaded", - show=False, - required=True, - ), MessageTextInput( name="YOUTUBE_VIDEO_DETAILS_id", display_name="ID", From 402df6c67c047833634fc8fa3de4e07b8ded34d0 Mon Sep 17 00:00:00 2001 From: Uday-sidagana Date: Fri, 6 Jun 2025 03:54:46 +0530 Subject: [PATCH 05/11] fix: errors handled --- .../base/langflow/components/composio/youtube_composio.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/base/langflow/components/composio/youtube_composio.py b/src/backend/base/langflow/components/composio/youtube_composio.py index 8160c5b4669a..5b6e17750275 100644 --- a/src/backend/base/langflow/components/composio/youtube_composio.py +++ b/src/backend/base/langflow/components/composio/youtube_composio.py @@ -380,6 +380,9 @@ def execute_action(self): result_field = action_data.get("result_field") if result_field: found = self._find_key_recursively(result_data, result_field) + # If found is empty (None, empty list, or empty dict), return the entire data dict + if found is None or found == [] or found == {}: + return result_data if found is not None: return found return result_data From 9b47de38175c8337d33e0ab9ecd7714b2b365070 Mon Sep 17 00:00:00 2001 From: Uday-sidagana Date: Fri, 6 Jun 2025 03:57:39 +0530 Subject: [PATCH 06/11] fix: style fix for format --- .../base/langflow/components/composio/youtube_composio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/base/langflow/components/composio/youtube_composio.py b/src/backend/base/langflow/components/composio/youtube_composio.py index 5b6e17750275..18bfa793313e 100644 --- a/src/backend/base/langflow/components/composio/youtube_composio.py +++ b/src/backend/base/langflow/components/composio/youtube_composio.py @@ -381,7 +381,7 @@ def execute_action(self): if result_field: found = self._find_key_recursively(result_data, result_field) # If found is empty (None, empty list, or empty dict), return the entire data dict - if found is None or found == [] or found == {}: + if found is None or found in ([], {}): return result_data if found is not None: return found From 67ec450de73d11255f9843b4c3a7d611ac73949f Mon Sep 17 00:00:00 2001 From: Uday-sidagana Date: Tue, 10 Jun 2025 10:52:24 +0530 Subject: [PATCH 07/11] fix: error data frame --- .../components/composio/youtube_composio.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/components/composio/youtube_composio.py b/src/backend/base/langflow/components/composio/youtube_composio.py index 18bfa793313e..f52bbcf88579 100644 --- a/src/backend/base/langflow/components/composio/youtube_composio.py +++ b/src/backend/base/langflow/components/composio/youtube_composio.py @@ -371,7 +371,10 @@ def execute_action(self): } } - return error_info + # Flatten error for table rendering + if "error" in error_info and isinstance(error_info["error"], dict): + return [error_info["error"]] + return [error_info] result_data = result.get("data", []) # Handle result_field if get_result_field is True @@ -382,13 +385,19 @@ def execute_action(self): found = self._find_key_recursively(result_data, result_field) # If found is empty (None, empty list, or empty dict), return the entire data dict if found is None or found in ([], {}): + if isinstance(result_data, dict): + return [result_data] return result_data if found is not None: + if isinstance(found, dict): + return [found] return found + if isinstance(result_data, dict): + return [result_data] return result_data # Default behavior 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}") From 05ab6660a6e9d7b95341e78d96b393e6241661aa Mon Sep 17 00:00:00 2001 From: abhishekpatil4 Date: Sun, 15 Jun 2025 13:04:52 +0530 Subject: [PATCH 08/11] SP --- .../langflow/components/composio/__init__.py | 2 + .../components/composio/reddit_composio.py | 312 ++++++++++++++++++ src/frontend/package-lock.json | 1 - src/frontend/src/icons/lazyIconImports.ts | 2 + src/frontend/src/icons/reddit/index.tsx | 9 + src/frontend/src/icons/reddit/reddit.jsx | 9 + src/frontend/src/icons/reddit/reddit.svg | 6 + src/frontend/src/utils/styleUtils.ts | 3 + 8 files changed, 343 insertions(+), 1 deletion(-) create mode 100644 src/backend/base/langflow/components/composio/reddit_composio.py create mode 100644 src/frontend/src/icons/reddit/index.tsx create mode 100644 src/frontend/src/icons/reddit/reddit.jsx create mode 100644 src/frontend/src/icons/reddit/reddit.svg 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..91ac02e716fd --- /dev/null +++ b/src/backend/base/langflow/components/composio/reddit_composio.py @@ -0,0 +1,312 @@ +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 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", + ], + }, + "REDDIT_DELETE_REDDIT_COMMENT": { + "display_name": "Delete Reddit Comment", + "action_fields": ["REDDIT_DELETE_REDDIT_COMMENT_id"], + }, + "REDDIT_DELETE_REDDIT_POST": { + "display_name": "Delete Reddit Post", + "action_fields": ["REDDIT_DELETE_REDDIT_POST_id"], + }, + "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", + ], + }, + "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", + ], + }, + "REDDIT_RETRIEVE_POST_COMMENTS": { + "display_name": "Retrieve Post Comments", + "action_fields": ["REDDIT_RETRIEVE_POST_COMMENTS_article"], + }, + "REDDIT_RETRIEVE_REDDIT_POST": { + "display_name": "Retrieve Reddit Post", + "action_fields": [ + "REDDIT_RETRIEVE_REDDIT_POST_size", + "REDDIT_RETRIEVE_REDDIT_POST_subreddit", + ], + }, + "REDDIT_RETRIEVE_SPECIFIC_COMMENT": { + "display_name": "Retrieve Specific Comment", + "action_fields": ["REDDIT_RETRIEVE_SPECIFIC_COMMENT_id"], + }, + "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", + ], + }, + } + + _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 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 + "_", "") + + 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")} + + 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"): + 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"): # noqa: E501 + msg = f"Expected a dict with a single key, got {len(result_data)} keys: {result_data.keys()}" + raise ValueError(msg) + return 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/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..7c54cf1f3111 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..f50eac45cb4a --- /dev/null +++ b/src/frontend/src/icons/reddit/reddit.jsx @@ -0,0 +1,9 @@ +const Icon = (props) => ( + + + + + + +); +export default Icon; diff --git a/src/frontend/src/icons/reddit/reddit.svg b/src/frontend/src/icons/reddit/reddit.svg new file mode 100644 index 000000000000..89c48f6bb325 --- /dev/null +++ b/src/frontend/src/icons/reddit/reddit.svg @@ -0,0 +1,6 @@ + + + + + + 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", From 5873ea0802244b4cc9654e73f7e1fcd69eb524d0 Mon Sep 17 00:00:00 2001 From: Uday-sidagana Date: Sun, 15 Jun 2025 20:50:12 +0530 Subject: [PATCH 09/11] feat: update Reddit icon components and lazy imports --- src/frontend/src/icons/lazyIconImports.ts | 2 +- src/frontend/src/icons/reddit/reddit.jsx | 21 ++++++++++++--------- src/frontend/src/icons/reddit/reddit.svg | 7 +------ 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/frontend/src/icons/lazyIconImports.ts b/src/frontend/src/icons/lazyIconImports.ts index 7c54cf1f3111..7756c7e5fc2a 100644 --- a/src/frontend/src/icons/lazyIconImports.ts +++ b/src/frontend/src/icons/lazyIconImports.ts @@ -57,7 +57,7 @@ export const lazyIconsMapping = { Composio: () => import("@/icons/Composio").then((mod) => ({ default: mod.ComposioIcon })), Reddit: () => - import("@/icons/Reddit").then((mod) => ({ default: mod.RedditIcon })), + 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/reddit.jsx b/src/frontend/src/icons/reddit/reddit.jsx index f50eac45cb4a..b3570bdb2ed2 100644 --- a/src/frontend/src/icons/reddit/reddit.jsx +++ b/src/frontend/src/icons/reddit/reddit.jsx @@ -1,9 +1,12 @@ -const Icon = (props) => ( - - - - - - -); -export default Icon; +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 index 89c48f6bb325..d64e3d1ba5dd 100644 --- a/src/frontend/src/icons/reddit/reddit.svg +++ b/src/frontend/src/icons/reddit/reddit.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file From 06ad4b7a0e919102882b70c08dcd2bb3da0b4043 Mon Sep 17 00:00:00 2001 From: Uday-sidagana Date: Sun, 15 Jun 2025 23:43:39 +0530 Subject: [PATCH 10/11] fix: I need changes --- .../components/composio/reddit_composio.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/backend/base/langflow/components/composio/reddit_composio.py b/src/backend/base/langflow/components/composio/reddit_composio.py index 91ac02e716fd..cafe68607cec 100644 --- a/src/backend/base/langflow/components/composio/reddit_composio.py +++ b/src/backend/base/langflow/components/composio/reddit_composio.py @@ -10,7 +10,6 @@ ) from langflow.logging import logger - class ComposioRedditAPIComponent(ComposioBaseComponent): display_name: str = "Reddit" description: str = "Reddit API" @@ -29,14 +28,18 @@ class ComposioRedditAPIComponent(ComposioBaseComponent): "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", @@ -44,6 +47,7 @@ class ComposioRedditAPIComponent(ComposioBaseComponent): "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", @@ -57,10 +61,13 @@ class ComposioRedditAPIComponent(ComposioBaseComponent): "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", @@ -68,10 +75,14 @@ class ComposioRedditAPIComponent(ComposioBaseComponent): "REDDIT_RETRIEVE_REDDIT_POST_size", "REDDIT_RETRIEVE_REDDIT_POST_subreddit", ], + "get_result_field": True, + "result_field": "posts_list", }, "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", @@ -81,6 +92,8 @@ class ComposioRedditAPIComponent(ComposioBaseComponent): "REDDIT_SEARCH_ACROSS_SUBREDDITS_search_query", "REDDIT_SEARCH_ACROSS_SUBREDDITS_sort", ], + "get_result_field": True, + "result_field": "search_results", }, } @@ -249,11 +262,9 @@ def execute_action(self): try: self._build_action_maps() - display_name = ( - self.action[0]["name"] - if isinstance(self.action, list) and self.action - else self.action - ) + # 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}" @@ -272,7 +283,6 @@ def execute_action(self): value = bool(value) param_name = field.replace(action_key + "_", "") - params[param_name] = value result = toolset.execute_action( @@ -284,6 +294,8 @@ def execute_action(self): 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"): # noqa: E501 From 1631f9967f4e2d61d61c1113e2128bfe65f1b2b5 Mon Sep 17 00:00:00 2001 From: Uday-sidagana Date: Mon, 16 Jun 2025 02:13:00 +0530 Subject: [PATCH 11/11] fix: errors and df --- .../components/composio/reddit_composio.py | 96 ++++++++++++++----- 1 file changed, 73 insertions(+), 23 deletions(-) diff --git a/src/backend/base/langflow/components/composio/reddit_composio.py b/src/backend/base/langflow/components/composio/reddit_composio.py index cafe68607cec..70dfb6108369 100644 --- a/src/backend/base/langflow/components/composio/reddit_composio.py +++ b/src/backend/base/langflow/components/composio/reddit_composio.py @@ -1,4 +1,5 @@ from typing import Any +import json from composio import Action @@ -76,7 +77,7 @@ class ComposioRedditAPIComponent(ComposioBaseComponent): "REDDIT_RETRIEVE_REDDIT_POST_subreddit", ], "get_result_field": True, - "result_field": "posts_list", + "result_field": "data", }, "REDDIT_RETRIEVE_SPECIFIC_COMMENT": { "display_name": "Retrieve Specific Comment", @@ -93,7 +94,7 @@ class ComposioRedditAPIComponent(ComposioBaseComponent): "REDDIT_SEARCH_ACROSS_SUBREDDITS_sort", ], "get_result_field": True, - "result_field": "search_results", + "result_field": "data", }, } @@ -256,15 +257,42 @@ class ComposioRedditAPIComponent(ComposioBaseComponent): ), ] + 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() - # 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}" @@ -279,9 +307,6 @@ def execute_action(self): if value is None or value == "": continue - if field in self._bool_variables: - value = bool(value) - param_name = field.replace(action_key + "_", "") params[param_name] = value @@ -290,25 +315,50 @@ def execute_action(self): params=params, ) if not result.get("successful"): - return {"error": result.get("error", "No response")} + message = result.get("data", {}).get("message", {}) - 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"): # noqa: E501 - msg = f"Expected a dict with a single key, got {len(result_data)} keys: {result_data.keys()}" - raise ValueError(msg) - return result_data + 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) - ) + 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