diff --git a/src/backend/base/langflow/components/composio/__init__.py b/src/backend/base/langflow/components/composio/__init__.py
index 61596ba63..a7d3af130 100644
--- a/src/backend/base/langflow/components/composio/__init__.py
+++ b/src/backend/base/langflow/components/composio/__init__.py
@@ -1,10 +1,12 @@
from .composio_api import ComposioAPIComponent
+from .github_composio import ComposioGitHubAPIComponent
from .gmail_composio import ComposioGmailAPIComponent
from .googlecalendar_composio import ComposioGoogleCalendarAPIComponent
from .slack_composio import ComposioSlackAPIComponent
__all__ = [
"ComposioAPIComponent",
+ "ComposioGitHubAPIComponent",
"ComposioGmailAPIComponent",
"ComposioGoogleCalendarAPIComponent",
"ComposioSlackAPIComponent",
diff --git a/src/backend/base/langflow/components/composio/github_composio.py b/src/backend/base/langflow/components/composio/github_composio.py
new file mode 100644
index 000000000..bf5fdfa29
--- /dev/null
+++ b/src/backend/base/langflow/components/composio/github_composio.py
@@ -0,0 +1,649 @@
+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(" ", "-"),
+ }
diff --git a/src/backend/tests/unit/components/bundles/composio/test_github.py b/src/backend/tests/unit/components/bundles/composio/test_github.py
new file mode 100644
index 000000000..0d61f687d
--- /dev/null
+++ b/src/backend/tests/unit/components/bundles/composio/test_github.py
@@ -0,0 +1,226 @@
+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
diff --git a/src/frontend/src/icons/github/github.jsx b/src/frontend/src/icons/github/github.jsx
new file mode 100644
index 000000000..c3cb817db
--- /dev/null
+++ b/src/frontend/src/icons/github/github.jsx
@@ -0,0 +1,12 @@
+const Icon = (props) => (
+
+);
+export default Icon;
diff --git a/src/frontend/src/icons/github/github.svg b/src/frontend/src/icons/github/github.svg
new file mode 100644
index 000000000..46c6c8326
--- /dev/null
+++ b/src/frontend/src/icons/github/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/frontend/src/icons/github/index.tsx b/src/frontend/src/icons/github/index.tsx
new file mode 100644
index 000000000..9d5c56ad4
--- /dev/null
+++ b/src/frontend/src/icons/github/index.tsx
@@ -0,0 +1,9 @@
+import React, { forwardRef } from "react";
+import GithubIconSVG from "./github";
+
+export const GithubIcon = forwardRef<
+ SVGSVGElement,
+ React.PropsWithChildren<{}>
+>((props, ref) => {
+ return ;
+});
diff --git a/src/frontend/src/utils/styleUtils.ts b/src/frontend/src/utils/styleUtils.ts
index 343bc4f8f..8c9b624a7 100644
--- a/src/frontend/src/utils/styleUtils.ts
+++ b/src/frontend/src/utils/styleUtils.ts
@@ -231,6 +231,7 @@ export const SIDEBAR_CATEGORIES = [
export const SIDEBAR_BUNDLES = [
{ display_name: "Amazon", name: "amazon", icon: "Amazon" },
{ display_name: "Gmail", name: "gmail", icon: "Gmail" },
+ { display_name: "GitHub", name: "github", icon: "Github" },
{
display_name: "Googlecalendar",
name: "googlecalendar",