-
Notifications
You must be signed in to change notification settings - Fork 7.1k
feat: add Composio YouTube component #8227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add Composio YouTube component #8227
Conversation
Hi! I'm I would like to apply some automated changes to this pull request, but it looks like I don't have the necessary permissions to do so. To get this pull request into a mergeable state, please do one of the following two things:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughA new YouTube API integration component, Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ComposioYoutubeAPIComponent
participant ComposioToolSet
participant YouTubeAPI
User->>ComposioYoutubeAPIComponent: Selects action and provides inputs
ComposioYoutubeAPIComponent->>ComposioToolSet: Maps action and prepares parameters
ComposioToolSet->>YouTubeAPI: Executes YouTube API action
YouTubeAPI-->>ComposioToolSet: Returns API response
ComposioToolSet-->>ComposioYoutubeAPIComponent: Delivers response
ComposioYoutubeAPIComponent->>ComposioYoutubeAPIComponent: Processes response, handles errors, extracts data
ComposioYoutubeAPIComponent-->>User: Returns result or error information
Suggested labels
✨ Finishing Touches🧪 Generate Unit Tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/backend/tests/unit/components/bundles/composio/test_youtube.py (1)
54-74
: Consider adding assertion for API parameters.The test verifies the result but doesn't check if the correct parameters were passed to the toolset. Consider adding an assertion to verify that
channel_handle
was correctly passed.You could enhance the test by capturing and verifying the parameters:
# After line 72, add: # Verify the correct parameters were passed mock_toolset = component._build_wrapper() mock_toolset.execute_action.assert_called_once() call_kwargs = mock_toolset.execute_action.call_args.kwargs assert call_kwargs['params'].get('channel_handle') == 'test_handle'src/backend/base/langflow/components/composio/youtube_composio.py (1)
315-392
: Consider refactoring the complex execute_action method.The method has high cyclomatic complexity with deeply nested error handling logic. Consider extracting the error parsing logic into a separate method for better maintainability.
Extract the error handling logic into a separate method:
def _parse_error_response(self, result: dict) -> dict: """Parse error response from API result.""" 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_infoThen simplify the main method by calling
return self._parse_error_response(result)
on line 374.🧰 Tools
🪛 Pylint (3.3.7)
[refactor] 315-315: Too many local variables (20/15)
(R0914)
[refactor] 315-315: Too many branches (15/12)
(R0912)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/backend/base/langflow/components/composio/__init__.py
(1 hunks)src/backend/base/langflow/components/composio/youtube_composio.py
(1 hunks)src/backend/tests/unit/components/bundles/composio/test_youtube.py
(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/backend/base/langflow/components/composio/__init__.py (1)
src/backend/base/langflow/components/composio/youtube_composio.py (1)
ComposioYoutubeAPIComponent
(14-401)
🪛 Pylint (3.3.7)
src/backend/tests/unit/components/bundles/composio/test_youtube.py
[error] 5-5: No name 'components' in module 'langflow'
(E0611)
[error] 6-6: No name 'schema' in module 'langflow'
(E0611)
[refactor] 13-13: Too few public methods (0/2)
(R0903)
src/backend/base/langflow/components/composio/youtube_composio.py
[refactor] 315-315: Too many local variables (20/15)
(R0914)
[refactor] 315-315: Too many branches (15/12)
(R0912)
⏰ Context from checks skipped due to timeout of 90000ms (4)
- GitHub Check: Optimize new Python code in this PR
- GitHub Check: Update Starter Projects
- GitHub Check: Run Ruff Check and Format
- GitHub Check: Ruff Style Check (3.13)
🔇 Additional comments (3)
src/backend/base/langflow/components/composio/__init__.py (1)
6-6
: LGTM! Import and export follow established patterns.The addition of
ComposioYoutubeAPIComponent
to both the import statement and__all__
list is consistent with the existing pattern for other Composio components.Also applies to: 14-14
src/backend/tests/unit/components/bundles/composio/test_youtube.py (1)
13-16
: Test mock class is appropriately minimal.The static analysis warning about too few public methods can be safely ignored. This is a test mock class that only needs to define the action constants.
🧰 Tools
🪛 Pylint (3.3.7)
[refactor] 13-13: Too few public methods (0/2)
(R0903)
src/backend/base/langflow/components/composio/youtube_composio.py (1)
299-313
: Efficient recursive key search implementation.The
_find_key_recursively
method is well-implemented with proper handling of both dictionaries and lists. Good use of early returns to optimize performance.
src/backend/base/langflow/components/composio/youtube_composio.py
Outdated
Show resolved
Hide resolved
src/backend/base/langflow/components/composio/youtube_composio.py
Outdated
Show resolved
Hide resolved
Hey @edwinjosechittilappilly, this is due to YouTube API rate limits. We've requested an increase to our rate limits from YouTube. Alternatively, user can use their own OAuth app while creating the integration (this needs to be created on Composio dashboard) and it will work fine. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/backend/base/langflow/components/composio/youtube_composio.py (1)
260-274
: Add type hints for better code clarity.The recursive search logic is correct and useful for navigating YouTube API response structures. Consider adding type hints for better maintainability.
- def _find_key_recursively(self, data, key): + def _find_key_recursively(self, data: Any, key: str) -> Any: """Recursively search for a key in nested dicts/lists and return its value if found."""
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/backend/base/langflow/components/composio/__init__.py
(2 hunks)src/backend/base/langflow/components/composio/youtube_composio.py
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/backend/base/langflow/components/composio/init.py
🧰 Additional context used
🪛 Pylint (3.3.7)
src/backend/base/langflow/components/composio/youtube_composio.py
[refactor] 276-276: Too many local variables (20/15)
(R0914)
[refactor] 276-276: Too many branches (14/12)
(R0912)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: Update Starter Projects
🔇 Additional comments (3)
src/backend/base/langflow/components/composio/youtube_composio.py (3)
14-97
: Well-structured component design.The class follows established patterns with comprehensive actions data and dynamic field computation. The data-driven approach ensures consistency between actions and their associated fields.
99-258
: Comprehensive and well-documented input definitions.The input fields provide thorough coverage of YouTube API parameters with consistent naming, appropriate types, and helpful documentation. The default values are sensible and the field organization follows the established pattern.
352-359
: Appropriate delegation and sensible defaults.The configuration methods properly delegate to the parent class and set reasonable default tools for YouTube functionality.
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 found | ||
return result_data | ||
if result_data and isinstance(result_data, dict): | ||
return result_data[next(iter(result_data))] | ||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Refactor to reduce complexity and address edge cases.
The method has excessive complexity (20+ variables, 14+ branches) and contains several potential edge cases:
- Line 282: If
self.action
is an empty list,self.action[0]
will raise IndexError - Line 344: Accessing
next(iter(result_data))
without checking if dict is empty will raise StopIteration - Line 297: Parameter name extraction assumes specific naming pattern
Consider refactoring into smaller methods:
+ def _extract_action_key(self, action) -> str:
+ """Extract and validate action key from action input."""
+ if isinstance(action, list):
+ if not action:
+ raise ValueError("Action list cannot be empty")
+ display_name = action[0]["name"]
+ else:
+ display_name = action
+
+ action_key = self._display_to_key_map.get(display_name)
+ if not action_key:
+ raise ValueError(f"Invalid action: {display_name}")
+ return action_key
+
+ def _build_action_params(self, action_key: str) -> dict:
+ """Build parameters for the specified action."""
+ 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 not None and value != "":
+ param_name = field.replace(f"{action_key}_", "")
+ params[param_name] = value
+ return params
+
+ def _process_error_response(self, result: dict) -> dict:
+ """Process and structure error responses."""
+ # Extract existing error handling logic here
+ pass
Then simplify the main method:
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)
+ action_key = self._extract_action_key(self.action)
+ params = self._build_action_params(action_key)
enum_name = getattr(Action, action_key)
- # ... existing parameter building logic
result = toolset.execute_action(action=enum_name, params=params)
if not result.get("successful"):
- # ... existing error handling
+ return self._process_error_response(result)
# ... rest of success handling
Also fix the potential edge case:
- if result_data and isinstance(result_data, dict):
- return result_data[next(iter(result_data))]
+ if result_data and isinstance(result_data, dict) and result_data:
+ return result_data[next(iter(result_data))]
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
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 found | |
return result_data | |
if result_data and isinstance(result_data, dict): | |
return result_data[next(iter(result_data))] | |
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 _extract_action_key(self, action) -> str: | |
"""Extract and validate action key from action input.""" | |
if isinstance(action, list): | |
if not action: | |
raise ValueError("Action list cannot be empty") | |
display_name = action[0]["name"] | |
else: | |
display_name = action | |
action_key = self._display_to_key_map.get(display_name) | |
if not action_key: | |
raise ValueError(f"Invalid action: {display_name}") | |
return action_key | |
def _build_action_params(self, action_key: str) -> dict: | |
"""Build parameters for the specified action.""" | |
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 not None and value != "": | |
param_name = field.replace(f"{action_key}_", "") | |
params[param_name] = value | |
return params | |
def _process_error_response(self, result: dict) -> dict: | |
"""Process and structure error responses.""" | |
# TODO: move existing error-handling logic here | |
pass | |
def execute_action(self): | |
"""Execute action and return response as Message.""" | |
toolset = self._build_wrapper() | |
try: | |
self._build_action_maps() | |
action_key = self._extract_action_key(self.action) | |
params = self._build_action_params(action_key) | |
enum_name = getattr(Action, action_key) | |
result = toolset.execute_action(action=enum_name, params=params) | |
if not result.get("successful"): | |
return self._process_error_response(result) | |
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 found | |
return result_data | |
if result_data and isinstance(result_data, dict) and 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}") | |
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 |
🧰 Tools
🪛 Pylint (3.3.7)
[refactor] 276-276: Too many local variables (20/15)
(R0914)
[refactor] 276-276: Too many branches (14/12)
(R0912)
494a47e
to
05ab666
Compare
05ab666
to
f2cb2e9
Compare
This pull request introduces a new YouTube component to the
langflow
project, including backend support & unit tests. The most important changes are:Backend support:
src/backend/base/langflow/components/composio/__init__.py
: Added import and registration forComposioYoutubeAPIComponent
.Unit tests:
src/backend/tests/unit/components/bundles/composio/test_youtube.py
: Added comprehensive unit tests forComposioYoutubeAPIComponent
, including tests for initialization, action execution, data conversion, and configuration updates.Summary by CodeRabbit
New Features
Tests