diff --git a/docs/docs/integrations/tools/github.ipynb b/docs/docs/integrations/tools/github.ipynb index cca81b1777e..3c5f8fa01eb 100644 --- a/docs/docs/integrations/tools/github.ipynb +++ b/docs/docs/integrations/tools/github.ipynb @@ -200,6 +200,37 @@ "8. **Delete File**- deletes a file from the repository." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Include release tools\n", + "\n", + "By default, the toolkit does not include release-related tools. You can include them by setting `include_release_tools=True` when initializing the toolkit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "toolkit = GitHubToolkit.from_github_api_wrapper(github, include_release_tools=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Settings `include_release_tools=True` will include the following tools:\n", + "\n", + "* **Get Latest Release**- fetches the latest release from the repository.\n", + "\n", + "* **Get Releases**- fetches the latest 5 releases from the repository.\n", + "\n", + "* **Get Release**- fetches a specific release from the repository by tag name, e.g. `v1.0.0`.\n" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -321,7 +352,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.13.1" } }, "nbformat": 4, diff --git a/libs/community/langchain_community/agent_toolkits/github/toolkit.py b/libs/community/langchain_community/agent_toolkits/github/toolkit.py index b98785bbef8..7bc9bca0425 100644 --- a/libs/community/langchain_community/agent_toolkits/github/toolkit.py +++ b/libs/community/langchain_community/agent_toolkits/github/toolkit.py @@ -16,7 +16,10 @@ from langchain_community.tools.github.prompt import ( GET_FILES_FROM_DIRECTORY_PROMPT, GET_ISSUE_PROMPT, GET_ISSUES_PROMPT, + GET_LATEST_RELEASE_PROMPT, GET_PR_PROMPT, + GET_RELEASE_PROMPT, + GET_RELEASES_PROMPT, LIST_BRANCHES_IN_REPO_PROMPT, LIST_PRS_PROMPT, LIST_PULL_REQUEST_FILES, @@ -152,6 +155,15 @@ class SearchIssuesAndPRs(BaseModel): ) +class TagName(BaseModel): + """Schema for operations that require a tag name as input.""" + + tag_name: str = Field( + ..., + description="The tag name of the release, e.g. `v1.0.0`.", + ) + + class GitHubToolkit(BaseToolkit): """GitHub Toolkit. @@ -218,6 +230,25 @@ class GitHubToolkit(BaseToolkit): Search code Create review request + Include release tools: + By default, the toolkit does not include release-related tools. + You can include them by setting ``include_release_tools=True`` when + initializing the toolkit: + + .. code-block:: python + + toolkit = GitHubToolkit.from_github_api_wrapper( + github, include_release_tools=True + ) + + Setting ``include_release_tools=True`` will include the following tools: + + .. code-block:: none + + Get latest release + Get releases + Get release + Use within an agent: .. code-block:: python @@ -268,12 +299,14 @@ class GitHubToolkit(BaseToolkit): @classmethod def from_github_api_wrapper( - cls, github_api_wrapper: GitHubAPIWrapper + cls, github_api_wrapper: GitHubAPIWrapper, include_release_tools: bool = False ) -> "GitHubToolkit": """Create a GitHubToolkit from a GitHubAPIWrapper. Args: github_api_wrapper: GitHubAPIWrapper. The GitHub API wrapper. + include_release_tools: bool. Whether to include release-related tools. + Defaults to False. Returns: GitHubToolkit. The GitHub toolkit. @@ -406,6 +439,29 @@ class GitHubToolkit(BaseToolkit): "args_schema": CreateReviewRequest, }, ] + + release_operations: List[Dict] = [ + { + "mode": "get_latest_release", + "name": "Get latest release", + "description": GET_LATEST_RELEASE_PROMPT, + "args_schema": NoInput, + }, + { + "mode": "get_releases", + "name": "Get releases", + "description": GET_RELEASES_PROMPT, + "args_schema": NoInput, + }, + { + "mode": "get_release", + "name": "Get release", + "description": GET_RELEASE_PROMPT, + "args_schema": TagName, + }, + ] + + operations = operations + (release_operations if include_release_tools else []) tools = [ GitHubAction( name=action["name"], diff --git a/libs/community/langchain_community/tools/github/prompt.py b/libs/community/langchain_community/tools/github/prompt.py index 3d66713e02b..c75d750407a 100644 --- a/libs/community/langchain_community/tools/github/prompt.py +++ b/libs/community/langchain_community/tools/github/prompt.py @@ -98,3 +98,12 @@ This tool will create a new branch in the repository. **VERY IMPORTANT**: You mu GET_FILES_FROM_DIRECTORY_PROMPT = """ This tool will fetch a list of all files in a specified directory. **VERY IMPORTANT**: You must specify the path of the directory as a string input parameter.""" + +GET_LATEST_RELEASE_PROMPT = """ +This tool will fetch the latest release of the repository. No input parameters are required.""" + +GET_RELEASES_PROMPT = """ +This tool will fetch the latest 5 releases of the repository. No input parameters are required.""" + +GET_RELEASE_PROMPT = """ +This tool will fetch a specific release of the repository. **VERY IMPORTANT**: You must specify the tag name of the release as a string input parameter.""" diff --git a/libs/community/langchain_community/utilities/github.py b/libs/community/langchain_community/utilities/github.py index 2674b749de8..c50a1a77c43 100644 --- a/libs/community/langchain_community/utilities/github.py +++ b/libs/community/langchain_community/utilities/github.py @@ -813,6 +813,56 @@ class GitHubAPIWrapper(BaseModel): except Exception as e: return f"Failed to create a review request with error {e}" + def get_latest_release(self) -> str: + """ + Fetches the latest release of the repository. + + Returns: + str: The latest release + """ + release = self.github_repo_instance.get_latest_release() + return ( + f"Latest title: {release.title} " + f"tag: {release.tag_name} " + f"body: {release.body}" + ) + + def get_releases(self) -> str: + """ + Fetches all releases of the repository. + + Returns: + str: The releases + """ + releases = self.github_repo_instance.get_releases() + max_results = min(5, releases.totalCount) + results = [f"Top {max_results} results:"] + for release in releases[:max_results]: + results.append( + f"Title: {release.title}, " + f"Tag: {release.tag_name}, " + f"Body: {release.body}" + ) + + return "\n".join(results) + + def get_release(self, tag_name: str) -> str: + """ + Fetches a specific release of the repository. + + Parameters: + tag_name(str): The tag name of the release + + Returns: + str: The release + """ + release = self.github_repo_instance.get_release(tag_name) + return ( + f"Release: {release.title} " + f"tag: {release.tag_name} " + f"body: {release.body}" + ) + def run(self, mode: str, query: str) -> str: if mode == "get_issue": return json.dumps(self.get_issue(int(query))) @@ -854,5 +904,11 @@ class GitHubAPIWrapper(BaseModel): return self.search_code(query) elif mode == "create_review_request": return self.create_review_request(query) + elif mode == "get_latest_release": + return self.get_latest_release() + elif mode == "get_releases": + return self.get_releases() + elif mode == "get_release": + return self.get_release(query) else: raise ValueError("Invalid mode" + mode) diff --git a/libs/community/tests/integration_tests/utilities/test_github.py b/libs/community/tests/integration_tests/utilities/test_github.py index 973599f273d..538112fa0a8 100644 --- a/libs/community/tests/integration_tests/utilities/test_github.py +++ b/libs/community/tests/integration_tests/utilities/test_github.py @@ -22,6 +22,18 @@ def test_get_open_issues(api_client: GitHubAPIWrapper) -> None: assert len(issues) != 0 +def test_get_latest_release(api_client: GitHubAPIWrapper) -> None: + """Basic test to fetch latest release""" + release = api_client.get_latest_release() + assert release is not None + + +def test_get_releases(api_client: GitHubAPIWrapper) -> None: + """Basic test to fetch releases""" + releases = api_client.get_releases() + assert releases is not None + + def test_search_issues_and_prs(api_client: GitHubAPIWrapper) -> None: """Basic test to search issues and PRs""" results = api_client.search_issues_and_prs("is:pr is:merged") diff --git a/libs/community/tests/unit_tests/agent_toolkits/test_github_toolkit.py b/libs/community/tests/unit_tests/agent_toolkits/test_github_toolkit.py new file mode 100644 index 00000000000..8f4d894fe8b --- /dev/null +++ b/libs/community/tests/unit_tests/agent_toolkits/test_github_toolkit.py @@ -0,0 +1,26 @@ +from unittest.mock import MagicMock + +from langchain_community.agent_toolkits.github.toolkit import GitHubToolkit +from langchain_community.utilities.github import GitHubAPIWrapper + + +def test_github_toolkit() -> None: + # Create a mock GitHub wrapper with required attributes + mock_github = MagicMock(spec=GitHubAPIWrapper) + mock_github.github_repository = "fake/repo" + mock_github.github_app_id = "fake_id" + mock_github.github_app_private_key = "fake_key" + mock_github.active_branch = "main" + mock_github.github_base_branch = "main" + + # Test without release tools + toolkit = GitHubToolkit.from_github_api_wrapper(mock_github) + tools = toolkit.get_tools() + assert len(tools) == 21 # Base number of tools + + # Test with release tools + toolkit_with_releases = GitHubToolkit.from_github_api_wrapper( + mock_github, include_release_tools=True + ) + tools_with_releases = toolkit_with_releases.get_tools() + assert len(tools_with_releases) == 24 # Base tools + 3 release tools