feat: Add Composio GitHub component (#7640)

* chore: action params naming

* chore: remove comments

* chore: replaced MessageTextInput field with StrInput

* feat: add google calendar component

* feat: replaced loops with hardcoded display-name action-enum mapping to improve performance

* chore: format

* fix: add type ignore for action_key in getattr call

* feat: add google sheets component

* fix: format google calendar utils

* feat: add google meet Component

* chore: minor improvement

* chore: format & lint

* fix: google meet component

* feat: add GitHub component

* fix: format

* fix: lint

* fix: typo

* feat: add Slack Component

* fix: format

* fix: rest bool value to None

* chore: disabled slack tools temporarily

* fix: add condition to set list variables to None in when action is changed

* chore: capitalise display names

* fix: update list issues field to MessateTextInput

* fix: format/lint in slack component

* fix: google calendar logo

* fix: revert setting bool field to None

* feat: composio-core & composio-core version bump to 0.7.10

* fix: minor bugs

* feat: add accepted values to AccessType field in google meet component

* feat: add accepted values for entry point access field in Google meet component

* fix: Google Calendar display names

* feat: replace list with nested list for batch update field in Google sheets

* fix: display name in Google sheets

* fix: format

* fix: titlecase display name in google meet component

* feat: set advaced to true for advanced fields

* feat: add condition to skip empty list fields in execute_action

* chore: improve display names GitHub Component

* fix: slack component display names & minor enhancements

* feat: update condition to skip empty fields while executing action

* feat: fix google calendar field description

* feat: update googlemeet component to use new inputs & composio base class

* chore: update googlemeet component filename

* feat: update github component to use new inputs & composio base class

* feat: update google calendar to use new inputs & composio base class

* feat: update google sheets component to use new inputs & Composio base class

* feat: update slack component to use new inputs & Composio base class

* fix: format

* chore: cleanup un-used code

* chore: format

* feat: add missing fields & actions

* chore: fix typo

* feat: rm other components

* feat: improve error message format & revert composio libs bump

* chore: revert uv.lock file

* update tests

* fix: remove duplicate action field in GMAIL_FETCH_EMAILS

* fix: remove unused code

* fix: add ignore statement

* feat: add github Component

* feat: delete Gmail component relates files to keep PR clean

* add gmail component & test file to keep PR clean

* clean pr

* clean PR

* fix: lint/format

* fix: typo in testcase file

* fix: remove component name

* fix: replace separator in field names

* fix: add app_name

* fix: minor bugs & improved response format

* chore: empty commit

* chore: improve input field info

* fix: format/lint

* fix: Composio GitHub component unit tests

---------

Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
Abhishek Patil 2025-05-27 23:21:02 +05:30 committed by GitHub
commit 61b89b000a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 900 additions and 0 deletions

View file

@ -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",

View file

@ -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(" ", "-"),
}

View file

@ -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

View file

@ -0,0 +1,12 @@
const Icon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 30 30"
width="23px"
height="23px"
>
{" "}
<path d="M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z" />
</svg>
);
export default Icon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" width="23px" height="23px"> <path d="M15,3C8.373,3,3,8.373,3,15c0,5.623,3.872,10.328,9.092,11.63C12.036,26.468,12,26.28,12,26.047v-2.051 c-0.487,0-1.303,0-1.508,0c-0.821,0-1.551-0.353-1.905-1.009c-0.393-0.729-0.461-1.844-1.435-2.526 c-0.289-0.227-0.069-0.486,0.264-0.451c0.615,0.174,1.125,0.596,1.605,1.222c0.478,0.627,0.703,0.769,1.596,0.769 c0.433,0,1.081-0.025,1.691-0.121c0.328-0.833,0.895-1.6,1.588-1.962c-3.996-0.411-5.903-2.399-5.903-5.098 c0-1.162,0.495-2.286,1.336-3.233C9.053,10.647,8.706,8.73,9.435,8c1.798,0,2.885,1.166,3.146,1.481C13.477,9.174,14.461,9,15.495,9 c1.036,0,2.024,0.174,2.922,0.483C18.675,9.17,19.763,8,21.565,8c0.732,0.731,0.381,2.656,0.102,3.594 c0.836,0.945,1.328,2.066,1.328,3.226c0,2.697-1.904,4.684-5.894,5.097C18.199,20.49,19,22.1,19,23.313v2.734 c0,0.104-0.023,0.179-0.035,0.268C23.641,24.676,27,20.236,27,15C27,8.373,21.627,3,15,3z"/></svg>

After

Width:  |  Height:  |  Size: 939 B

View file

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

View file

@ -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",