From dde9f6c746c4cfe98918667f2e5644b3c29f9e82 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 16 May 2024 11:02:24 +0100 Subject: [PATCH 01/47] WIP: GitHub integration --- camel/loaders/__init__.py | 2 + camel/loaders/github_loader.py | 73 +++++++++++++++++++++++++++ camel/prompts/github.py | 45 +++++++++++++++++ camel/prompts/task_prompt_template.py | 2 + camel/types/enums.py | 1 + examples/github/github_agent.py | 65 ++++++++++++++++++++++++ pyproject.toml | 1 + 7 files changed, 189 insertions(+) create mode 100644 camel/loaders/github_loader.py create mode 100644 camel/prompts/github.py create mode 100644 examples/github/github_agent.py diff --git a/camel/loaders/__init__.py b/camel/loaders/__init__.py index ee476ee28..cfaab9658 100644 --- a/camel/loaders/__init__.py +++ b/camel/loaders/__init__.py @@ -13,10 +13,12 @@ # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== from .base_io import File, read_file +from .github_loader import GitHubLoader from .unstructured_io import UnstructuredIO __all__ = [ 'File', 'read_file', 'UnstructuredIO', + 'GitHubLoader', ] diff --git a/camel/loaders/github_loader.py b/camel/loaders/github_loader.py new file mode 100644 index 000000000..0b114e21c --- /dev/null +++ b/camel/loaders/github_loader.py @@ -0,0 +1,73 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from github import Auth, Github + + +class GitHubLoaderIssue: + def __init__(self, title, body, file_name, file_content): + self.title = title + self.body = body + self.file_name = file_name + self.file_content = file_content + + +class GitHubLoader: + def __init__(self, repo_name, access_token): + self.github = Github(auth=Auth.Token(access_token)) + self.repo = self.github.get_repo(repo_name) + + def retrieve_issue_list(self): + issues = self.repo.get_issues(state='open') + return [ + GitHubLoaderIssue( + issue.title, + issue.body, + issue.labels[0].name, + self.retrieve_file_content(issue.labels[0].name), + ) + for issue in issues + if not issue.pull_request + ] + + def retrieve_latest_issue(self): + issues = self.retrieve_issue_list() + return issues[0] + + def retrieve_file_content(self, file_path): + file_content = self.repo.get_contents(file_path) + return file_content.decoded_content.decode() + + def create_pull_request( + self, file_path, new_content, pr_title, commit_message + ): + branch_name = f"github-agent-update-{file_path.replace('/', '-')}" + sb = self.repo.get_branch(self.repo.default_branch) + self.repo.create_git_ref( + ref=f"refs/heads/{branch_name}", sha=sb.commit.sha + ) + file = self.repo.get_contents(file_path) + self.repo.update_file( + file.path, commit_message, new_content, file.sha, branch=branch_name + ) + pr = self.repo.create_pull( + title=pr_title, + body=commit_message, + head=branch_name, + base=self.repo.default_branch, + ) + return pr + + def commit_file_content(self, file_path, new_content, commit_message): + file = self.repo.get_contents(file_path) + self.repo.update_file(file.path, commit_message, new_content, file.sha) diff --git a/camel/prompts/github.py b/camel/prompts/github.py new file mode 100644 index 000000000..a4fbef87a --- /dev/null +++ b/camel/prompts/github.py @@ -0,0 +1,45 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from typing import Any + +from camel.prompts.base import TextPrompt, TextPromptDict + + +class GitHubPromptTemplateDict(TextPromptDict): + r"""A dictionary containing :obj:`TextPrompt` used in the `GitHub` + task. + + Attributes: + SOLVE_ISSUE (TextPrompt): A prompt to solve an issue + in code based on the original source code and the + issue description. + """ + + SOLVE_ISSUE = TextPrompt( + """You need to solve this issue: {issue_description} +The source code for which this issue needs to be solved is: +{source_code} + +Your output has to be a full corrected version of the source code. +Please do not include anything else in the response, including any +kind of formatting markdown.""" + ) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.update( + { + "solve_issue": self.SOLVE_ISSUE, + } + ) diff --git a/camel/prompts/task_prompt_template.py b/camel/prompts/task_prompt_template.py index e61769264..123af750a 100644 --- a/camel/prompts/task_prompt_template.py +++ b/camel/prompts/task_prompt_template.py @@ -21,6 +21,7 @@ from camel.prompts.evaluation import ( EvaluationPromptTemplateDict, ) +from camel.prompts.github import GitHubPromptTemplateDict from camel.prompts.misalignment import MisalignmentPromptTemplateDict from camel.prompts.object_recognition import ObjectRecognitionPromptTemplateDict from camel.prompts.role_description_prompt_template import ( @@ -49,6 +50,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: { TaskType.AI_SOCIETY: AISocietyPromptTemplateDict(), TaskType.CODE: CodePromptTemplateDict(), + TaskType.GITHUB: GitHubPromptTemplateDict(), TaskType.MISALIGNMENT: MisalignmentPromptTemplateDict(), TaskType.TRANSLATION: TranslationPromptTemplateDict(), TaskType.EVALUATION: EvaluationPromptTemplateDict(), diff --git a/camel/types/enums.py b/camel/types/enums.py index 1a67693b0..b3b50c6fc 100644 --- a/camel/types/enums.py +++ b/camel/types/enums.py @@ -184,6 +184,7 @@ def output_dim(self) -> int: class TaskType(Enum): AI_SOCIETY = "ai_society" CODE = "code" + GITHUB = "github" MISALIGNMENT = "misalignment" TRANSLATION = "translation" EVALUATION = "evaluation" diff --git a/examples/github/github_agent.py b/examples/github/github_agent.py new file mode 100644 index 000000000..56d3d1db0 --- /dev/null +++ b/examples/github/github_agent.py @@ -0,0 +1,65 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import os + +from camel.agents import ChatAgent +from camel.loaders import GitHubLoader +from camel.messages import BaseMessage +from camel.prompts import PromptTemplateGenerator +from camel.types import TaskType + + +def solve_latest_issue( + loader: GitHubLoader, + model=None, +) -> None: + latest_issue = loader.retrieve_latest_issue() + prompt_template = PromptTemplateGenerator().get_prompt_from_key( + TaskType.GITHUB, 'solve_issue' + ) + prompt = prompt_template.format( + issue_description=latest_issue.body, + source_code=latest_issue.file_content, + ) + print(f"Prompt: {prompt}") + assistant_sys_msg = BaseMessage.make_assistant_message( + role_name="Software Engineer", + content="You are an experienced software engineer who specializes on data structures and algorithms tasks.", + ) + agent = ChatAgent(assistant_sys_msg, model_type=model) + agent.reset() + + user_msg = BaseMessage.make_user_message(role_name="User", content=prompt) + assistant_response = agent.step(user_msg) + + if len(assistant_response.msgs) > 0: + print(f"Assistant response: {assistant_response.msg.content}") + print("Committing...") + loader.create_pull_request( + latest_issue.file_name, + assistant_response.msg.content, + f"[GitHub Agent] Solved issue : {latest_issue.title}", + "I hope it works", + ) + + +def main(model=None) -> None: + repo_name = "eigent-ai/lambda-working-repo" + access_token = os.getenv("GITHUB_ACCESS_TOKEN") + loader = GitHubLoader(repo_name, access_token) + solve_latest_issue(loader, model) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 251ee8d87..de5e6a28d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,7 @@ cohere = { version = "^4.56", optional = true } # test pytest = { version = "^7", optional = true} mock = { version = "^5", optional = true} +pygithub = "^2.3.0" [tool.poetry.extras] From c1e84e78a117f565b82b74c2ca073131445aa1de Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 16 May 2024 11:08:39 +0100 Subject: [PATCH 02/47] Prettify console logs --- examples/github/github_agent.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/github/github_agent.py b/examples/github/github_agent.py index 56d3d1db0..6d694af7f 100644 --- a/examples/github/github_agent.py +++ b/examples/github/github_agent.py @@ -13,11 +13,14 @@ # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== import os +from colorama import Fore + from camel.agents import ChatAgent from camel.loaders import GitHubLoader from camel.messages import BaseMessage from camel.prompts import PromptTemplateGenerator from camel.types import TaskType +from camel.utils import print_text_animated def solve_latest_issue( @@ -32,7 +35,7 @@ def solve_latest_issue( issue_description=latest_issue.body, source_code=latest_issue.file_content, ) - print(f"Prompt: {prompt}") + print(Fore.YELLOW + f"Final prompt:\n{prompt}\n") assistant_sys_msg = BaseMessage.make_assistant_message( role_name="Software Engineer", content="You are an experienced software engineer who specializes on data structures and algorithms tasks.", @@ -44,8 +47,10 @@ def solve_latest_issue( assistant_response = agent.step(user_msg) if len(assistant_response.msgs) > 0: - print(f"Assistant response: {assistant_response.msg.content}") - print("Committing...") + print( + Fore.GREEN + f"Agent response:\n{assistant_response.msg.content}\n" + ) + print_text_animated(Fore.BLUE + "Comitting.........\n") loader.create_pull_request( latest_issue.file_name, assistant_response.msg.content, From ddec21771962d2fac721ff0d59178dcd2f438177 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 16 May 2024 12:34:05 +0100 Subject: [PATCH 03/47] Add github function to support created a pull request --- camel/functions/__init__.py | 2 + camel/functions/github_functions.py | 77 +++++++++++++++++++++++++++++ camel/loaders/github_loader.py | 8 ++- camel/prompts/github.py | 11 +++-- examples/github/github_agent.py | 33 +++++++++---- 5 files changed, 115 insertions(+), 16 deletions(-) create mode 100644 camel/functions/github_functions.py diff --git a/camel/functions/__init__.py b/camel/functions/__init__.py index 1f63cdfb8..58f2c3255 100644 --- a/camel/functions/__init__.py +++ b/camel/functions/__init__.py @@ -18,6 +18,7 @@ get_openai_tool_schema, ) +from .github_functions import GITHUB_FUNCS from .google_maps_function import MAP_FUNCS from .math_functions import MATH_FUNCS from .open_api_function import OPENAPI_FUNCS @@ -31,6 +32,7 @@ 'OpenAIFunction', 'get_openai_function_schema', 'get_openai_tool_schema', + 'GITHUB_FUNCS', 'MAP_FUNCS', 'MATH_FUNCS', 'OPENAPI_FUNCS', diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py new file mode 100644 index 000000000..72484820b --- /dev/null +++ b/camel/functions/github_functions.py @@ -0,0 +1,77 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import os +from typing import List + +from camel.functions import OpenAIFunction +from camel.loaders import GitHubLoader + + +def get_github_access_token() -> str: + r"""Retrieve the GitHub access token from environment variables. + + Returns: + str: A string containing the GitHub access token. + + Raises: + ValueError: If the API key or secret is not found in the environment + variables. + """ + # Get `GITHUB_ACCESS_TOKEN` here: + # https://github.com/settings/tokens + GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN") + + if not GITHUB_ACCESS_TOKEN: + raise ValueError( + "GITHUB_ACCESS_TOKEN not found in environment variables. Get it " + "here: `https://github.com/settings/tokens`." + ) + return GITHUB_ACCESS_TOKEN + + +def create_pull_request( + repo_name, file_path, new_content, issue_title, issue_number +): + r"""Creates a pull request. + + This function creates a pull request in specified repository, which updates a + file in the specific path with new content. The pull request description + contains information about the issue title and number. + + Args: + repo_name (str): The name of the repository in which to create the pull request. + file_path (str): The path of the file to be updated in the repository. + new_content (str): The specified new content of the specified file. + issue_title (str): The title of the issue that is solved by this pull request. + issue_number (str): The number of the issue that is solved by this pull request. + + Returns: + str: A formatted report of the whether the pull request was created + successfully or not. + """ + loader = GitHubLoader(repo_name, get_github_access_token()) + loader.create_pull_request( + file_path, + new_content, + f"[GitHub Agent] Solved issue: {issue_title}", + f"Fixes #{issue_number}", + ) + + return "Pull request created successfully." + + +GITHUB_FUNCS: List[OpenAIFunction] = [ + OpenAIFunction(func) # type: ignore[arg-type] + for func in [create_pull_request] +] diff --git a/camel/loaders/github_loader.py b/camel/loaders/github_loader.py index 0b114e21c..869e5804c 100644 --- a/camel/loaders/github_loader.py +++ b/camel/loaders/github_loader.py @@ -15,14 +15,17 @@ class GitHubLoaderIssue: - def __init__(self, title, body, file_name, file_content): + def __init__(self, title, body, number, file_path, file_content): self.title = title self.body = body - self.file_name = file_name + self.number = number + self.file_path = file_path self.file_content = file_content class GitHubLoader: + r"""A class for loading data from GitHub repositories.""" + def __init__(self, repo_name, access_token): self.github = Github(auth=Auth.Token(access_token)) self.repo = self.github.get_repo(repo_name) @@ -33,6 +36,7 @@ def retrieve_issue_list(self): GitHubLoaderIssue( issue.title, issue.body, + issue.number, issue.labels[0].name, self.retrieve_file_content(issue.labels[0].name), ) diff --git a/camel/prompts/github.py b/camel/prompts/github.py index a4fbef87a..8713f651c 100644 --- a/camel/prompts/github.py +++ b/camel/prompts/github.py @@ -27,13 +27,16 @@ class GitHubPromptTemplateDict(TextPromptDict): """ SOLVE_ISSUE = TextPrompt( - """You need to solve this issue: {issue_description} + """You need to solve the issue with title: {issue_title} The source code for which this issue needs to be solved is: {source_code} +The issue number is: {issue_number}. -Your output has to be a full corrected version of the source code. -Please do not include anything else in the response, including any -kind of formatting markdown.""" +You will need to use the provided github function to create a pull request +that updates the file on path {file_path} in the repository {repo_name}. +The new_content property of the function should be the corrected source code. +Return response of this function as the output of this task. +""" ) def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/examples/github/github_agent.py b/examples/github/github_agent.py index 6d694af7f..9ecdc5576 100644 --- a/examples/github/github_agent.py +++ b/examples/github/github_agent.py @@ -16,6 +16,8 @@ from colorama import Fore from camel.agents import ChatAgent +from camel.configs import FunctionCallingConfig +from camel.functions import GITHUB_FUNCS from camel.loaders import GitHubLoader from camel.messages import BaseMessage from camel.prompts import PromptTemplateGenerator @@ -25,6 +27,7 @@ def solve_latest_issue( loader: GitHubLoader, + repo_name, model=None, ) -> None: latest_issue = loader.retrieve_latest_issue() @@ -32,38 +35,48 @@ def solve_latest_issue( TaskType.GITHUB, 'solve_issue' ) prompt = prompt_template.format( + issue_title=latest_issue.title, + issue_number=latest_issue.number, issue_description=latest_issue.body, + file_path=latest_issue.file_path, source_code=latest_issue.file_content, + repo_name=repo_name, ) print(Fore.YELLOW + f"Final prompt:\n{prompt}\n") + + function_list = [ + *GITHUB_FUNCS, + ] assistant_sys_msg = BaseMessage.make_assistant_message( role_name="Software Engineer", content="You are an experienced software engineer who specializes on data structures and algorithms tasks.", ) - agent = ChatAgent(assistant_sys_msg, model_type=model) + assistant_model_config = FunctionCallingConfig.from_openai_function_list( + function_list=function_list, + kwargs=dict(temperature=0.0), + ) + agent = ChatAgent( + assistant_sys_msg, + model_type=model, + model_config=assistant_model_config, + function_list=function_list, + ) agent.reset() user_msg = BaseMessage.make_user_message(role_name="User", content=prompt) assistant_response = agent.step(user_msg) if len(assistant_response.msgs) > 0: - print( + print_text_animated( Fore.GREEN + f"Agent response:\n{assistant_response.msg.content}\n" ) - print_text_animated(Fore.BLUE + "Comitting.........\n") - loader.create_pull_request( - latest_issue.file_name, - assistant_response.msg.content, - f"[GitHub Agent] Solved issue : {latest_issue.title}", - "I hope it works", - ) def main(model=None) -> None: repo_name = "eigent-ai/lambda-working-repo" access_token = os.getenv("GITHUB_ACCESS_TOKEN") loader = GitHubLoader(repo_name, access_token) - solve_latest_issue(loader, model) + solve_latest_issue(loader, repo_name, model) if __name__ == "__main__": From 5bf879a3f237cbbb341b1e88dc2b065818511947 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 16 May 2024 12:49:39 +0100 Subject: [PATCH 04/47] Use function call to retrieve an issue from GitHub --- camel/functions/github_functions.py | 29 ++++++++++++++++++++++++++++- camel/loaders/github_loader.py | 7 +++++-- camel/prompts/github.py | 14 ++++++++------ examples/github/github_agent.py | 18 ++++-------------- 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index 72484820b..f7421210d 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -40,6 +40,33 @@ def get_github_access_token() -> str: return GITHUB_ACCESS_TOKEN +def retrieve_issue(repo_name, issue_number): + r"""Retrieves an issue from a GitHub repository. + + This function retrieves an issue from a specified repository using the + issue number. + + Args: + repo_name (str): The name of the repository from which to retrieve the issue. + issue_number (str): The number of the issue to retrieve. + + Returns: + str: A formatted report of the retrieved issue. + """ + loader = GitHubLoader(repo_name, get_github_access_token()) + issue = loader.retrieve_issue(issue_number) + + if issue: + return ( + f"Title: {issue.title}\n" + f"Body: {issue.body}\n" + f"Number: {issue.number}\n" + f"File Path: {issue.file_path}\n" + f"File Content: {issue.file_content}" + ) + return "Issue not found." + + def create_pull_request( repo_name, file_path, new_content, issue_title, issue_number ): @@ -73,5 +100,5 @@ def create_pull_request( GITHUB_FUNCS: List[OpenAIFunction] = [ OpenAIFunction(func) # type: ignore[arg-type] - for func in [create_pull_request] + for func in [retrieve_issue, create_pull_request] ] diff --git a/camel/loaders/github_loader.py b/camel/loaders/github_loader.py index 869e5804c..c4c3017a6 100644 --- a/camel/loaders/github_loader.py +++ b/camel/loaders/github_loader.py @@ -44,9 +44,12 @@ def retrieve_issue_list(self): if not issue.pull_request ] - def retrieve_latest_issue(self): + def retrieve_issue(self, issue_number): issues = self.retrieve_issue_list() - return issues[0] + for issue in issues: + if issue.number == issue_number: + return issue + return None def retrieve_file_content(self, file_path): file_content = self.repo.get_contents(file_path) diff --git a/camel/prompts/github.py b/camel/prompts/github.py index 8713f651c..80fc348b0 100644 --- a/camel/prompts/github.py +++ b/camel/prompts/github.py @@ -27,13 +27,15 @@ class GitHubPromptTemplateDict(TextPromptDict): """ SOLVE_ISSUE = TextPrompt( - """You need to solve the issue with title: {issue_title} -The source code for which this issue needs to be solved is: -{source_code} -The issue number is: {issue_number}. + """You need to solve the issue with number: {issue_number} +For this you will have to use the provided github function to retrieve +that issue. You will get all the necessary parameters to later create a +pull request. -You will need to use the provided github function to create a pull request -that updates the file on path {file_path} in the repository {repo_name}. +When you have the issue, please follow the instruction and make the necessary +changes to the source code provided. Once you have made the changes, you will +need to use another provided github function to create a pull request +that updates the file on the provided file path in the repository {repo_name}. The new_content property of the function should be the corrected source code. Return response of this function as the output of this task. """ diff --git a/examples/github/github_agent.py b/examples/github/github_agent.py index 9ecdc5576..8a0ad1f09 100644 --- a/examples/github/github_agent.py +++ b/examples/github/github_agent.py @@ -11,36 +11,28 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -import os - from colorama import Fore from camel.agents import ChatAgent from camel.configs import FunctionCallingConfig from camel.functions import GITHUB_FUNCS -from camel.loaders import GitHubLoader from camel.messages import BaseMessage from camel.prompts import PromptTemplateGenerator from camel.types import TaskType from camel.utils import print_text_animated -def solve_latest_issue( - loader: GitHubLoader, +def solve_issue( repo_name, + issue_number, model=None, ) -> None: - latest_issue = loader.retrieve_latest_issue() prompt_template = PromptTemplateGenerator().get_prompt_from_key( TaskType.GITHUB, 'solve_issue' ) prompt = prompt_template.format( - issue_title=latest_issue.title, - issue_number=latest_issue.number, - issue_description=latest_issue.body, - file_path=latest_issue.file_path, - source_code=latest_issue.file_content, repo_name=repo_name, + issue_number=issue_number, ) print(Fore.YELLOW + f"Final prompt:\n{prompt}\n") @@ -74,9 +66,7 @@ def solve_latest_issue( def main(model=None) -> None: repo_name = "eigent-ai/lambda-working-repo" - access_token = os.getenv("GITHUB_ACCESS_TOKEN") - loader = GitHubLoader(repo_name, access_token) - solve_latest_issue(loader, repo_name, model) + solve_issue(repo_name=repo_name, issue_number=1, model=model) if __name__ == "__main__": From d8b5e295a9a1314ddd826369ac7c2d048ce799db Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 16 May 2024 15:32:08 +0100 Subject: [PATCH 05/47] unit test for retrieve_issue --- camel/functions/github_functions.py | 34 ++++++++++------ test/functions/test_github_functions.py | 54 +++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 12 deletions(-) create mode 100644 test/functions/test_github_functions.py diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index f7421210d..113fec680 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -40,7 +40,20 @@ def get_github_access_token() -> str: return GITHUB_ACCESS_TOKEN -def retrieve_issue(repo_name, issue_number): +def get_github_loader(access_token: str, repo_name: str) -> GitHubLoader: + r"""Create a GitHub loader object. + + Args: + access_token (str): The GitHub access token. + repo_name (str): The name of the repository. + + Returns: + GitHubLoader: A GitHub loader object. + """ + return GitHubLoader(repo_name, access_token) + + +def retrieve_issue(loader, issue_number): r"""Retrieves an issue from a GitHub repository. This function retrieves an issue from a specified repository using the @@ -53,7 +66,6 @@ def retrieve_issue(repo_name, issue_number): Returns: str: A formatted report of the retrieved issue. """ - loader = GitHubLoader(repo_name, get_github_access_token()) issue = loader.retrieve_issue(issue_number) if issue: @@ -68,7 +80,7 @@ def retrieve_issue(repo_name, issue_number): def create_pull_request( - repo_name, file_path, new_content, issue_title, issue_number + loader, file_path, new_content, issue_title, issue_number ): r"""Creates a pull request. @@ -87,15 +99,13 @@ def create_pull_request( str: A formatted report of the whether the pull request was created successfully or not. """ - loader = GitHubLoader(repo_name, get_github_access_token()) - loader.create_pull_request( - file_path, - new_content, - f"[GitHub Agent] Solved issue: {issue_title}", - f"Fixes #{issue_number}", - ) - - return "Pull request created successfully." + title = f"[GitHub Agent] Solved issue: {issue_title}" + body = f"Fixes #{issue_number}" + pr = loader.create_pull_request(file_path, new_content, title, body) + + if pr: + return f"Title: {title}\n" f"Body: {body}\n" + return "Failed to create the pull request." GITHUB_FUNCS: List[OpenAIFunction] = [ diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py new file mode 100644 index 000000000..5afd39ca3 --- /dev/null +++ b/test/functions/test_github_functions.py @@ -0,0 +1,54 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from unittest.mock import patch + +from camel.functions.github_functions import ( + get_github_access_token, + retrieve_issue, +) +from camel.loaders.github_loader import GitHubLoader, GitHubLoaderIssue + +mock_issue = GitHubLoaderIssue( + title='Time complexity for product_of_array_except_self.py', + body='Improve the time complexity for the product_of_array_except_self.py file', + number=1, + file_path='product_of_array_except_self.py', + file_content='def product_of_array_except_self(nums): ...', +) + + +@patch( + 'camel.loaders.github_loader.GitHubLoader.retrieve_issue', + return_value=mock_issue, +) +@patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) +@patch.object(GitHubLoader, 'retrieve_issue', return_value=mock_issue) +def test_retrieve_issue(mock_retrieve_issue, mock_get_github_access_token): + expected_response = ( + "Title: Time complexity for product_of_array_except_self.py\n" + "Body: Improve the time complexity for the product_of_array_except_self.py file\n" + "Number: 1\n" + "File Path: product_of_array_except_self.py\n" + "File Content: def product_of_array_except_self(nums): ..." + ) + + loader = GitHubLoader('test/repo', get_github_access_token()) + + assert retrieve_issue(loader, 1) == expected_response + + +if __name__ == '__main__': + import pytest + + pytest.main() From e239b8fcccf67c1f8d75cd303e9f523878c8dce1 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 16 May 2024 15:41:09 +0100 Subject: [PATCH 06/47] add test_create_pull_request --- test/functions/test_github_functions.py | 33 +++++++++++++++++++++---- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 5afd39ca3..8de31de83 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -14,6 +14,7 @@ from unittest.mock import patch from camel.functions.github_functions import ( + create_pull_request, get_github_access_token, retrieve_issue, ) @@ -28,13 +29,9 @@ ) -@patch( - 'camel.loaders.github_loader.GitHubLoader.retrieve_issue', - return_value=mock_issue, -) @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) @patch.object(GitHubLoader, 'retrieve_issue', return_value=mock_issue) -def test_retrieve_issue(mock_retrieve_issue, mock_get_github_access_token): +def test_retrieve_issue(mock_retrieve_issue): expected_response = ( "Title: Time complexity for product_of_array_except_self.py\n" "Body: Improve the time complexity for the product_of_array_except_self.py file\n" @@ -48,6 +45,32 @@ def test_retrieve_issue(mock_retrieve_issue, mock_get_github_access_token): assert retrieve_issue(loader, 1) == expected_response +@patch( + 'camel.functions.github_functions.get_github_access_token', + return_value="1", +) +@patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) +@patch.object(GitHubLoader, 'create_pull_request', return_value="pr") +def test_create_pull_request( + mock_get_github_access_token, mock_create_pull_request +): + expected_response = ( + "Title: [GitHub Agent] Solved issue: Time complexity for product_of_array_except_self.py\n" + "Body: Fixes #1\n" + ) + loader = GitHubLoader('test/repo', get_github_access_token()) + + pr = create_pull_request( + loader, + mock_issue.file_path, + mock_issue.file_content, + mock_issue.title, + mock_issue.number, + ) + + assert pr == expected_response + + if __name__ == '__main__': import pytest From 60671ef48b26b0230be369cd6b4451252f6db91e Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 16 May 2024 15:54:42 +0100 Subject: [PATCH 07/47] minor fixes --- camel/functions/github_functions.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index 113fec680..7861968c9 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -53,7 +53,7 @@ def get_github_loader(access_token: str, repo_name: str) -> GitHubLoader: return GitHubLoader(repo_name, access_token) -def retrieve_issue(loader, issue_number): +def retrieve_issue(repo_name, issue_number): r"""Retrieves an issue from a GitHub repository. This function retrieves an issue from a specified repository using the @@ -66,6 +66,7 @@ def retrieve_issue(loader, issue_number): Returns: str: A formatted report of the retrieved issue. """ + loader = get_github_loader(get_github_access_token(), repo_name) issue = loader.retrieve_issue(issue_number) if issue: @@ -80,7 +81,7 @@ def retrieve_issue(loader, issue_number): def create_pull_request( - loader, file_path, new_content, issue_title, issue_number + repo_name, file_path, new_content, issue_title, issue_number ): r"""Creates a pull request. @@ -99,6 +100,7 @@ def create_pull_request( str: A formatted report of the whether the pull request was created successfully or not. """ + loader = get_github_loader(get_github_access_token(), repo_name) title = f"[GitHub Agent] Solved issue: {issue_title}" body = f"Fixes #{issue_number}" pr = loader.create_pull_request(file_path, new_content, title, body) @@ -110,5 +112,10 @@ def create_pull_request( GITHUB_FUNCS: List[OpenAIFunction] = [ OpenAIFunction(func) # type: ignore[arg-type] - for func in [retrieve_issue, create_pull_request] + for func in [ + get_github_access_token, + get_github_loader, + retrieve_issue, + create_pull_request, + ] ] From 5c8cb3d0d012732706fdcd26d764328e6f7ac2c6 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 16 May 2024 16:07:41 +0100 Subject: [PATCH 08/47] Move github example to function calls --- examples/{github => function_call}/github_agent.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{github => function_call}/github_agent.py (100%) diff --git a/examples/github/github_agent.py b/examples/function_call/github_agent.py similarity index 100% rename from examples/github/github_agent.py rename to examples/function_call/github_agent.py From 11257458442ad5b8957aa02737942935b88bebc7 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 12:17:27 +0100 Subject: [PATCH 09/47] Update functions api to be testable --- camel/functions/github_functions.py | 16 ++++++++++++++-- test/functions/test_github_functions.py | 5 ++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index 7861968c9..e92217dd6 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -54,6 +54,11 @@ def get_github_loader(access_token: str, repo_name: str) -> GitHubLoader: def retrieve_issue(repo_name, issue_number): + loader = get_github_loader(get_github_access_token(), repo_name) + retrieve_issue_with_loader(loader, issue_number) + + +def retrieve_issue_with_loader(loader, issue_number): r"""Retrieves an issue from a GitHub repository. This function retrieves an issue from a specified repository using the @@ -66,7 +71,6 @@ def retrieve_issue(repo_name, issue_number): Returns: str: A formatted report of the retrieved issue. """ - loader = get_github_loader(get_github_access_token(), repo_name) issue = loader.retrieve_issue(issue_number) if issue: @@ -82,6 +86,15 @@ def retrieve_issue(repo_name, issue_number): def create_pull_request( repo_name, file_path, new_content, issue_title, issue_number +): + loader = get_github_loader(get_github_access_token(), repo_name) + create_pull_request_with_loader( + loader, file_path, new_content, issue_title, issue_number + ) + + +def create_pull_request_with_loader( + loader, file_path, new_content, issue_title, issue_number ): r"""Creates a pull request. @@ -100,7 +113,6 @@ def create_pull_request( str: A formatted report of the whether the pull request was created successfully or not. """ - loader = get_github_loader(get_github_access_token(), repo_name) title = f"[GitHub Agent] Solved issue: {issue_title}" body = f"Fixes #{issue_number}" pr = loader.create_pull_request(file_path, new_content, title, body) diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 8de31de83..493a8c22b 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -15,7 +15,6 @@ from camel.functions.github_functions import ( create_pull_request, - get_github_access_token, retrieve_issue, ) from camel.loaders.github_loader import GitHubLoader, GitHubLoaderIssue @@ -40,7 +39,7 @@ def test_retrieve_issue(mock_retrieve_issue): "File Content: def product_of_array_except_self(nums): ..." ) - loader = GitHubLoader('test/repo', get_github_access_token()) + loader = GitHubLoader('test/repo', '1') assert retrieve_issue(loader, 1) == expected_response @@ -58,8 +57,8 @@ def test_create_pull_request( "Title: [GitHub Agent] Solved issue: Time complexity for product_of_array_except_self.py\n" "Body: Fixes #1\n" ) - loader = GitHubLoader('test/repo', get_github_access_token()) + loader = GitHubLoader('test/repo', '1') pr = create_pull_request( loader, mock_issue.file_path, From df9c38e24a8789cb11c3e0eb4119939a34a35e5e Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 12:46:27 +0100 Subject: [PATCH 10/47] fix functions --- camel/functions/github_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index e92217dd6..ec3ba3103 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -55,7 +55,7 @@ def get_github_loader(access_token: str, repo_name: str) -> GitHubLoader: def retrieve_issue(repo_name, issue_number): loader = get_github_loader(get_github_access_token(), repo_name) - retrieve_issue_with_loader(loader, issue_number) + return retrieve_issue_with_loader(loader, issue_number) def retrieve_issue_with_loader(loader, issue_number): @@ -88,7 +88,7 @@ def create_pull_request( repo_name, file_path, new_content, issue_title, issue_number ): loader = get_github_loader(get_github_access_token(), repo_name) - create_pull_request_with_loader( + return create_pull_request_with_loader( loader, file_path, new_content, issue_title, issue_number ) From ffa82ede7bb69e7e5ba81a618d432798223aa651 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 14:04:16 +0100 Subject: [PATCH 11/47] update tests --- test/functions/test_github_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 493a8c22b..7889d9ae1 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -14,8 +14,8 @@ from unittest.mock import patch from camel.functions.github_functions import ( - create_pull_request, - retrieve_issue, + create_pull_request_with_loader, + retrieve_issue_with_loader, ) from camel.loaders.github_loader import GitHubLoader, GitHubLoaderIssue @@ -41,7 +41,7 @@ def test_retrieve_issue(mock_retrieve_issue): loader = GitHubLoader('test/repo', '1') - assert retrieve_issue(loader, 1) == expected_response + assert retrieve_issue_with_loader(loader, 1) == expected_response @patch( @@ -59,7 +59,7 @@ def test_create_pull_request( ) loader = GitHubLoader('test/repo', '1') - pr = create_pull_request( + pr = create_pull_request_with_loader( loader, mock_issue.file_path, mock_issue.file_content, From 84a87c3737b71913cddb393ae599ed0413e9d4b0 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 14:24:47 +0100 Subject: [PATCH 12/47] update comments --- camel/functions/github_functions.py | 33 +++++++++++++++++++++++++++++ pyproject.toml | 2 -- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index ec3ba3103..7e9b2d045 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -54,6 +54,19 @@ def get_github_loader(access_token: str, repo_name: str) -> GitHubLoader: def retrieve_issue(repo_name, issue_number): + r"""Retrieves an issue from a GitHub repository. + + This function retrieves an issue from a specified repository using the + issue number. It takes the repo name to be easily accessible from an + agent + + Args: + repo_name (str): The name of the repository from which to retrieve the issue. + issue_number (str): The number of the issue to retrieve. + + Returns: + str: A formatted report of the retrieved issue. + """ loader = get_github_loader(get_github_access_token(), repo_name) return retrieve_issue_with_loader(loader, issue_number) @@ -87,6 +100,26 @@ def retrieve_issue_with_loader(loader, issue_number): def create_pull_request( repo_name, file_path, new_content, issue_title, issue_number ): + r"""Creates a pull request. + + This function creates a pull request in specified repository, which updates a + file in the specific path with new content. The pull request description + contains information about the issue title and number. + + It takes the repo name to be easily accessible from an + agent. + + Args: + repo_name (str): The name of the repository in which to create the pull request. + file_path (str): The path of the file to be updated in the repository. + new_content (str): The specified new content of the specified file. + issue_title (str): The title of the issue that is solved by this pull request. + issue_number (str): The number of the issue that is solved by this pull request. + + Returns: + str: A formatted report of the whether the pull request was created + successfully or not. + """ loader = get_github_loader(get_github_access_token(), repo_name) return create_pull_request_with_loader( loader, file_path, new_content, issue_title, issue_number diff --git a/pyproject.toml b/pyproject.toml index de5e6a28d..e8aef46f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,8 +82,6 @@ cohere = { version = "^4.56", optional = true } # test pytest = { version = "^7", optional = true} mock = { version = "^5", optional = true} -pygithub = "^2.3.0" - [tool.poetry.extras] test = ["pytest", "mock"] From 9877a9781b7cac7b9ea8e16cda7dfb8c3de3f009 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 14:40:26 +0100 Subject: [PATCH 13/47] add type hinting --- camel/functions/github_functions.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index 7e9b2d045..bd01e846f 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -34,7 +34,7 @@ def get_github_access_token() -> str: if not GITHUB_ACCESS_TOKEN: raise ValueError( - "GITHUB_ACCESS_TOKEN not found in environment variables. Get it " + "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it " "here: `https://github.com/settings/tokens`." ) return GITHUB_ACCESS_TOKEN @@ -53,7 +53,7 @@ def get_github_loader(access_token: str, repo_name: str) -> GitHubLoader: return GitHubLoader(repo_name, access_token) -def retrieve_issue(repo_name, issue_number): +def retrieve_issue(repo_name: str, issue_number: int): r"""Retrieves an issue from a GitHub repository. This function retrieves an issue from a specified repository using the @@ -62,7 +62,7 @@ def retrieve_issue(repo_name, issue_number): Args: repo_name (str): The name of the repository from which to retrieve the issue. - issue_number (str): The number of the issue to retrieve. + issue_number (int): The number of the issue to retrieve. Returns: str: A formatted report of the retrieved issue. @@ -71,7 +71,7 @@ def retrieve_issue(repo_name, issue_number): return retrieve_issue_with_loader(loader, issue_number) -def retrieve_issue_with_loader(loader, issue_number): +def retrieve_issue_with_loader(loader: GitHubLoader, issue_number: int): r"""Retrieves an issue from a GitHub repository. This function retrieves an issue from a specified repository using the @@ -79,7 +79,7 @@ def retrieve_issue_with_loader(loader, issue_number): Args: repo_name (str): The name of the repository from which to retrieve the issue. - issue_number (str): The number of the issue to retrieve. + issue_number (int): The number of the issue to retrieve. Returns: str: A formatted report of the retrieved issue. @@ -98,7 +98,11 @@ def retrieve_issue_with_loader(loader, issue_number): def create_pull_request( - repo_name, file_path, new_content, issue_title, issue_number + repo_name: str, + file_path: str, + new_content: str, + issue_title: str, + issue_number: int, ): r"""Creates a pull request. @@ -114,7 +118,7 @@ def create_pull_request( file_path (str): The path of the file to be updated in the repository. new_content (str): The specified new content of the specified file. issue_title (str): The title of the issue that is solved by this pull request. - issue_number (str): The number of the issue that is solved by this pull request. + issue_number (int): The number of the issue that is solved by this pull request. Returns: str: A formatted report of the whether the pull request was created @@ -127,7 +131,11 @@ def create_pull_request( def create_pull_request_with_loader( - loader, file_path, new_content, issue_title, issue_number + loader: GitHubLoader, + file_path: str, + new_content: str, + issue_title: str, + issue_number: int, ): r"""Creates a pull request. From 65a89d28571924f53ec199309f35d1b4d18bcb37 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 14:42:37 +0100 Subject: [PATCH 14/47] update tests --- examples/function_call/github_agent.py | 3 ++- test/functions/test_github_functions.py | 6 ------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/function_call/github_agent.py b/examples/function_call/github_agent.py index 8a0ad1f09..be9226eeb 100644 --- a/examples/function_call/github_agent.py +++ b/examples/function_call/github_agent.py @@ -41,7 +41,8 @@ def solve_issue( ] assistant_sys_msg = BaseMessage.make_assistant_message( role_name="Software Engineer", - content="You are an experienced software engineer who specializes on data structures and algorithms tasks.", + content="""You are an experienced software engineer who + specializes on data structures and algorithms tasks.""", ) assistant_model_config = FunctionCallingConfig.from_openai_function_list( function_list=function_list, diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 7889d9ae1..8a380d864 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -68,9 +68,3 @@ def test_create_pull_request( ) assert pr == expected_response - - -if __name__ == '__main__': - import pytest - - pytest.main() From c37e7ff7a5e896c4bcb96abd343d110cf2bcb2c0 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 16:01:55 +0100 Subject: [PATCH 15/47] update pyproject --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index e8aef46f5..489b00067 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,6 +168,7 @@ all = [ "neo4j", # retrievers "rank-bm25", + "pygithub", ] [tool.poetry.group.dev] From e129853eb003f247b67cae69f2f6098b63988bab Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 16:04:29 +0100 Subject: [PATCH 16/47] Update pyproject.toml --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 489b00067..6a2ee3bcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,7 +145,6 @@ all = [ # tools "beautifulsoup4", "docx2txt", - "pygithub", "PyMuPDF", "wikipedia", "wolframalpha", From 4b4ee83b99099bbc137bfc04b749ec606f37a338 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Mon, 20 May 2024 16:12:56 +0100 Subject: [PATCH 17/47] update test repo path --- examples/function_call/github_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/function_call/github_agent.py b/examples/function_call/github_agent.py index be9226eeb..34390e5f0 100644 --- a/examples/function_call/github_agent.py +++ b/examples/function_call/github_agent.py @@ -66,7 +66,7 @@ def solve_issue( def main(model=None) -> None: - repo_name = "eigent-ai/lambda-working-repo" + repo_name = "camel-ai/test-github-agent" solve_issue(repo_name=repo_name, issue_number=1, model=model) From f5578f7cf59940a2cdb7de4bc2e51f6b11a0ef6d Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Tue, 21 May 2024 11:07:51 +0100 Subject: [PATCH 18/47] improve unit tests for github functions --- camel/functions/github_functions.py | 68 +------------------------ test/functions/test_github_functions.py | 21 +++++--- 2 files changed, 15 insertions(+), 74 deletions(-) diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index bd01e846f..38455fa89 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -40,40 +40,9 @@ def get_github_access_token() -> str: return GITHUB_ACCESS_TOKEN -def get_github_loader(access_token: str, repo_name: str) -> GitHubLoader: - r"""Create a GitHub loader object. - - Args: - access_token (str): The GitHub access token. - repo_name (str): The name of the repository. - - Returns: - GitHubLoader: A GitHub loader object. - """ - return GitHubLoader(repo_name, access_token) - - def retrieve_issue(repo_name: str, issue_number: int): r"""Retrieves an issue from a GitHub repository. - This function retrieves an issue from a specified repository using the - issue number. It takes the repo name to be easily accessible from an - agent - - Args: - repo_name (str): The name of the repository from which to retrieve the issue. - issue_number (int): The number of the issue to retrieve. - - Returns: - str: A formatted report of the retrieved issue. - """ - loader = get_github_loader(get_github_access_token(), repo_name) - return retrieve_issue_with_loader(loader, issue_number) - - -def retrieve_issue_with_loader(loader: GitHubLoader, issue_number: int): - r"""Retrieves an issue from a GitHub repository. - This function retrieves an issue from a specified repository using the issue number. @@ -84,6 +53,7 @@ def retrieve_issue_with_loader(loader: GitHubLoader, issue_number: int): Returns: str: A formatted report of the retrieved issue. """ + loader = GitHubLoader(repo_name, get_github_access_token()) issue = loader.retrieve_issue(issue_number) if issue: @@ -110,39 +80,6 @@ def create_pull_request( file in the specific path with new content. The pull request description contains information about the issue title and number. - It takes the repo name to be easily accessible from an - agent. - - Args: - repo_name (str): The name of the repository in which to create the pull request. - file_path (str): The path of the file to be updated in the repository. - new_content (str): The specified new content of the specified file. - issue_title (str): The title of the issue that is solved by this pull request. - issue_number (int): The number of the issue that is solved by this pull request. - - Returns: - str: A formatted report of the whether the pull request was created - successfully or not. - """ - loader = get_github_loader(get_github_access_token(), repo_name) - return create_pull_request_with_loader( - loader, file_path, new_content, issue_title, issue_number - ) - - -def create_pull_request_with_loader( - loader: GitHubLoader, - file_path: str, - new_content: str, - issue_title: str, - issue_number: int, -): - r"""Creates a pull request. - - This function creates a pull request in specified repository, which updates a - file in the specific path with new content. The pull request description - contains information about the issue title and number. - Args: repo_name (str): The name of the repository in which to create the pull request. file_path (str): The path of the file to be updated in the repository. @@ -154,6 +91,7 @@ def create_pull_request_with_loader( str: A formatted report of the whether the pull request was created successfully or not. """ + loader = GitHubLoader(repo_name, get_github_access_token()) title = f"[GitHub Agent] Solved issue: {issue_title}" body = f"Fixes #{issue_number}" pr = loader.create_pull_request(file_path, new_content, title, body) @@ -166,8 +104,6 @@ def create_pull_request_with_loader( GITHUB_FUNCS: List[OpenAIFunction] = [ OpenAIFunction(func) # type: ignore[arg-type] for func in [ - get_github_access_token, - get_github_loader, retrieve_issue, create_pull_request, ] diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 8a380d864..1632ba3b6 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -14,8 +14,9 @@ from unittest.mock import patch from camel.functions.github_functions import ( - create_pull_request_with_loader, - retrieve_issue_with_loader, + create_pull_request, + get_github_access_token, + retrieve_issue, ) from camel.loaders.github_loader import GitHubLoader, GitHubLoaderIssue @@ -28,6 +29,13 @@ ) +def test_get_github_access_token(monkeypatch): + monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') + expected_access_token = 'TOKEN' + access_token = get_github_access_token() + assert access_token == expected_access_token + + @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) @patch.object(GitHubLoader, 'retrieve_issue', return_value=mock_issue) def test_retrieve_issue(mock_retrieve_issue): @@ -39,9 +47,7 @@ def test_retrieve_issue(mock_retrieve_issue): "File Content: def product_of_array_except_self(nums): ..." ) - loader = GitHubLoader('test/repo', '1') - - assert retrieve_issue_with_loader(loader, 1) == expected_response + assert retrieve_issue('test/repo', 1) == expected_response @patch( @@ -58,9 +64,8 @@ def test_create_pull_request( "Body: Fixes #1\n" ) - loader = GitHubLoader('test/repo', '1') - pr = create_pull_request_with_loader( - loader, + pr = create_pull_request( + 'test/repo', mock_issue.file_path, mock_issue.file_content, mock_issue.title, From dc513717506a8eee80c3fda1657f590c282f4121 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Tue, 21 May 2024 12:08:36 +0100 Subject: [PATCH 19/47] add more tests --- camel/functions/github_functions.py | 8 ++- camel/loaders/github_loader.py | 22 ++++++-- test/functions/test_github_functions.py | 61 +++++++++++++++++++++ test/loaders/test_github_loader.py | 71 +++++++++++++++++++++++++ 4 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 test/loaders/test_github_loader.py diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py index 38455fa89..4e618efff 100644 --- a/camel/functions/github_functions.py +++ b/camel/functions/github_functions.py @@ -14,6 +14,8 @@ import os from typing import List +from github import Auth, Github + from camel.functions import OpenAIFunction from camel.loaders import GitHubLoader @@ -53,7 +55,8 @@ def retrieve_issue(repo_name: str, issue_number: int): Returns: str: A formatted report of the retrieved issue. """ - loader = GitHubLoader(repo_name, get_github_access_token()) + github = Github(auth=Auth.Token(get_github_access_token())) + loader = GitHubLoader(github, repo_name) issue = loader.retrieve_issue(issue_number) if issue: @@ -91,7 +94,8 @@ def create_pull_request( str: A formatted report of the whether the pull request was created successfully or not. """ - loader = GitHubLoader(repo_name, get_github_access_token()) + github = Github(auth=Auth.Token(get_github_access_token())) + loader = GitHubLoader(github, repo_name) title = f"[GitHub Agent] Solved issue: {issue_title}" body = f"Fixes #{issue_number}" pr = loader.create_pull_request(file_path, new_content, title, body) diff --git a/camel/loaders/github_loader.py b/camel/loaders/github_loader.py index c4c3017a6..dcac99945 100644 --- a/camel/loaders/github_loader.py +++ b/camel/loaders/github_loader.py @@ -11,7 +11,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from github import Auth, Github class GitHubLoaderIssue: @@ -22,12 +21,29 @@ def __init__(self, title, body, number, file_path, file_content): self.file_path = file_path self.file_content = file_content + def __eq__(self, other): + if isinstance(other, GitHubLoaderIssue): + return ( + self.number == other.number + and self.title == other.title + and self.body == other.body + and self.file_path == other.file_path + and self.file_content == other.file_content + ) + return False + + def __repr__(self): + return ( + f"GitHubLoaderIssue(number={self.number}, title={self.title}, " + f"body={self.body}, file_path={self.file_path}, file_content={self.file_content})" + ) + class GitHubLoader: r"""A class for loading data from GitHub repositories.""" - def __init__(self, repo_name, access_token): - self.github = Github(auth=Auth.Token(access_token)) + def __init__(self, github, repo_name): + self.github = github self.repo = self.github.get_repo(repo_name) def retrieve_issue_list(self): diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 1632ba3b6..e0ddf3342 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -13,6 +13,8 @@ # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== from unittest.mock import patch +import pytest + from camel.functions.github_functions import ( create_pull_request, get_github_access_token, @@ -28,6 +30,8 @@ file_content='def product_of_array_except_self(nums): ...', ) +# get_github_access_token + def test_get_github_access_token(monkeypatch): monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') @@ -36,6 +40,17 @@ def test_get_github_access_token(monkeypatch): assert access_token == expected_access_token +def test_get_github_access_token_no_token(monkeypatch): + monkeypatch.delenv('GITHUB_ACCESS_TOKEN', raising=False) + with pytest.raises(ValueError) as error: + get_github_access_token() + assert ( + str(error.value) + == "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it here: `https://github.com/settings/tokens`." + ) + + +# retrieve_issue @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) @patch.object(GitHubLoader, 'retrieve_issue', return_value=mock_issue) def test_retrieve_issue(mock_retrieve_issue): @@ -50,6 +65,16 @@ def test_retrieve_issue(mock_retrieve_issue): assert retrieve_issue('test/repo', 1) == expected_response +@patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) +@patch.object(GitHubLoader, 'retrieve_issue', return_value=None) +def test_retrieve_issue_not_found(mock_retrieve_issue): + expected_response = "Issue not found." + assert retrieve_issue('test/repo', 1) == expected_response + + +# create_pull_request + + @patch( 'camel.functions.github_functions.get_github_access_token', return_value="1", @@ -73,3 +98,39 @@ def test_create_pull_request( ) assert pr == expected_response + + +@patch( + 'camel.functions.github_functions.get_github_access_token', + return_value=None, +) +def test_create_pull_request_no_access_token(mock_get_github_access_token): + with pytest.raises(AssertionError) as error: + create_pull_request( + 'test/repo', + mock_issue.file_path, + mock_issue.file_content, + mock_issue.title, + mock_issue.number, + ) + assert str(error.value) == "" # from Auth.Token initializer + + +@patch( + 'camel.functions.github_functions.get_github_access_token', + return_value="1", +) +@patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) +@patch.object(GitHubLoader, 'create_pull_request', return_value=None) +def test_create_pull_request_failed( + mock_get_github_access_token, mock_create_pull_request +): + expected_response = "Failed to create the pull request." + pr = create_pull_request( + 'test/repo', + mock_issue.file_path, + mock_issue.file_content, + mock_issue.title, + mock_issue.number, + ) + assert pr == expected_response diff --git a/test/loaders/test_github_loader.py b/test/loaders/test_github_loader.py new file mode 100644 index 000000000..a3a2607cd --- /dev/null +++ b/test/loaders/test_github_loader.py @@ -0,0 +1,71 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== + +from unittest.mock import MagicMock + +from camel.loaders.github_loader import GitHubLoader, GitHubLoaderIssue + + +def test_init(): + # Create a mock GitHub instance + mock_github = MagicMock() + + # Call the constructor of the GitHubLoader class + github_loader = GitHubLoader(mock_github, "repo_name") + + # Assert that the repo attribute is set to the return value of the get_repo method + assert github_loader.repo == mock_github.get_repo.return_value + + # Assert that the get_repo method was called with the correct argument + mock_github.get_repo.assert_called_once_with("repo_name") + + +def test_retrieve_issue_list(): + # Create a mock GitHub instance + mock_github = MagicMock() + + # Create a mock GitHubLoader instance + mock_github_loader = GitHubLoader(mock_github, "repo_name") + + # Create a mock issue object + mock_issue = MagicMock() + mock_issue.number = 1 + mock_issue.title = "Test Issue" + mock_issue.body = "This is a test issue" + mock_issue.pull_request = False + + mock_label = MagicMock() + mock_label.name = "path/to/file" + mock_issue.labels = [mock_label] + + # Mock the get_issues method of the mock_repo instance to return a list containing the mock issue object + mock_github_loader.repo.get_issues.return_value = [mock_issue] + mock_github_loader.retrieve_file_content = MagicMock( + return_value="This is the content of the file" + ) + + # Call the retrieve_issue_list method + issue_list = mock_github_loader.retrieve_issue_list() + + # Assert the returned issue list + expected_issue = GitHubLoaderIssue( + "Test Issue", + "This is a test issue", + 1, + "path/to/file", + "This is the content of the file", + ) + assert issue_list == [ + expected_issue + ], f"Expected {expected_issue}, but got {issue_list}" From 24aefa63bae2a56d2985d55ace901b515d57cf70 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Tue, 21 May 2024 12:14:46 +0100 Subject: [PATCH 20/47] add more tests --- test/loaders/test_github_loader.py | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/loaders/test_github_loader.py b/test/loaders/test_github_loader.py index a3a2607cd..f25f11f05 100644 --- a/test/loaders/test_github_loader.py +++ b/test/loaders/test_github_loader.py @@ -69,3 +69,42 @@ def test_retrieve_issue_list(): assert issue_list == [ expected_issue ], f"Expected {expected_issue}, but got {issue_list}" + + +def test_retrieve_issue(): + # Create a mock GitHub instance + mock_github = MagicMock() + + # Create a mock GitHubLoader instance + mock_github_loader = GitHubLoader(mock_github, "repo_name") + + # Create a mock issue object + mock_issue = MagicMock() + mock_issue.number = 1 + mock_issue.title = "Test Issue" + mock_issue.body = "This is a test issue" + mock_issue.pull_request = False + + mock_label = MagicMock() + mock_label.name = "path/to/file" + mock_issue.labels = [mock_label] + + # Mock the get_issues method of the mock_repo instance to return a list containing the mock issue object + mock_github_loader.repo.get_issues.return_value = [mock_issue] + mock_github_loader.retrieve_file_content = MagicMock( + return_value="This is the content of the file" + ) + + # Call the retrieve_issue method + issue = mock_github_loader.retrieve_issue(1) + # Assert the returned issue + expected_issue = GitHubLoaderIssue( + "Test Issue", + "This is a test issue", + 1, + "path/to/file", + "This is the content of the file", + ) + assert ( + issue == expected_issue + ), f"Expected {expected_issue}, but got {issue}" From 5c7c797bf8ed5f1c6acc594e600c8bc5c07c4a9b Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Tue, 21 May 2024 12:32:19 +0100 Subject: [PATCH 21/47] add more tests --- test/loaders/test_github_loader.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/loaders/test_github_loader.py b/test/loaders/test_github_loader.py index f25f11f05..4dfc817a8 100644 --- a/test/loaders/test_github_loader.py +++ b/test/loaders/test_github_loader.py @@ -31,7 +31,10 @@ def test_init(): mock_github.get_repo.assert_called_once_with("repo_name") -def test_retrieve_issue_list(): +def test_retrieve_issue_list(monkeypatch): + # Set a dummy access token in the environment variables + monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') + # Create a mock GitHub instance mock_github = MagicMock() @@ -71,7 +74,10 @@ def test_retrieve_issue_list(): ], f"Expected {expected_issue}, but got {issue_list}" -def test_retrieve_issue(): +def test_retrieve_issue(monkeypatch): + # Set a dummy access token in the environment variables + monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') + # Create a mock GitHub instance mock_github = MagicMock() From 9aeb3c3e6aa1b6a0f5cf2a759169d1e31d0e80bf Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Tue, 21 May 2024 13:35:34 +0100 Subject: [PATCH 22/47] improved tests for functions --- test/functions/test_github_functions.py | 194 ++++++++++++------------ 1 file changed, 98 insertions(+), 96 deletions(-) diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index e0ddf3342..49b60648d 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -13,14 +13,12 @@ # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== from unittest.mock import patch -import pytest +from github import Auth, Github from camel.functions.github_functions import ( create_pull_request, - get_github_access_token, - retrieve_issue, ) -from camel.loaders.github_loader import GitHubLoader, GitHubLoaderIssue +from camel.loaders.github_loader import GitHubLoaderIssue mock_issue = GitHubLoaderIssue( title='Time complexity for product_of_array_except_self.py', @@ -33,104 +31,108 @@ # get_github_access_token -def test_get_github_access_token(monkeypatch): +# def test_get_github_access_token(monkeypatch): +# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') +# expected_access_token = 'TOKEN' +# access_token = get_github_access_token() +# assert access_token == expected_access_token + + +# def test_get_github_access_token_no_token(monkeypatch): +# monkeypatch.delenv('GITHUB_ACCESS_TOKEN', raising=False) +# with pytest.raises(ValueError) as error: +# get_github_access_token() +# assert ( +# str(error.value) +# == "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it here: `https://github.com/settings/tokens`." +# ) + + +# # retrieve_issue +# @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) +# @patch.object(GitHubLoader, 'retrieve_issue', return_value=mock_issue) +# def test_retrieve_issue(monkeypatch, mock_retrieve_issue): +# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') +# expected_response = ( +# "Title: Time complexity for product_of_array_except_self.py\n" +# "Body: Improve the time complexity for the product_of_array_except_self.py file\n" +# "Number: 1\n" +# "File Path: product_of_array_except_self.py\n" +# "File Content: def product_of_array_except_self(nums): ..." +# ) + +# assert retrieve_issue('test/repo', 1) == expected_response + + +# @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) +# @patch.object(GitHubLoader, 'retrieve_issue', return_value=None) +# def test_retrieve_issue_not_found(monkeypatch, mock_retrieve_issue): +# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') +# expected_response = "Issue not found." +# assert retrieve_issue('test/repo', 1) == expected_response + + +# # create_pull_request + + +# @patch( +# 'camel.functions.github_functions.get_github_access_token', +# return_value="1", +# ) +# @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) +# @patch.object(GitHubLoader, 'create_pull_request', return_value="pr") +# def test_create_pull_request( +# monkeypatch, mock_get_github_access_token, mock_create_pull_request +# ): +# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') +# expected_response = ( +# "Title: [GitHub Agent] Solved issue: Time complexity for product_of_array_except_self.py\n" +# "Body: Fixes #1\n" +# ) + +# pr = create_pull_request( +# 'test/repo', +# mock_issue.file_path, +# mock_issue.file_content, +# mock_issue.title, +# mock_issue.number, +# ) + +# assert pr == expected_response + + +# @patch( +# 'camel.functions.github_functions.get_github_access_token', +# return_value=None, +# ) +# def test_create_pull_request_no_access_token(monkeypatch, mock_get_github_access_token): +# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') +# with pytest.raises(AssertionError) as error: +# create_pull_request( +# 'test/repo', +# mock_issue.file_path, +# mock_issue.file_content, +# mock_issue.title, +# mock_issue.number, +# ) +# assert str(error.value) == "" # from Auth.Token initializer + + +@patch.object(Github, 'get_repo', return_value="test/repo") +@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) +def test_create_pull_request_failed(monkeypatch): monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') - expected_access_token = 'TOKEN' - access_token = get_github_access_token() - assert access_token == expected_access_token - - -def test_get_github_access_token_no_token(monkeypatch): - monkeypatch.delenv('GITHUB_ACCESS_TOKEN', raising=False) - with pytest.raises(ValueError) as error: - get_github_access_token() - assert ( - str(error.value) - == "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it here: `https://github.com/settings/tokens`." - ) - - -# retrieve_issue -@patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) -@patch.object(GitHubLoader, 'retrieve_issue', return_value=mock_issue) -def test_retrieve_issue(mock_retrieve_issue): - expected_response = ( - "Title: Time complexity for product_of_array_except_self.py\n" - "Body: Improve the time complexity for the product_of_array_except_self.py file\n" - "Number: 1\n" - "File Path: product_of_array_except_self.py\n" - "File Content: def product_of_array_except_self(nums): ..." - ) - - assert retrieve_issue('test/repo', 1) == expected_response - - -@patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) -@patch.object(GitHubLoader, 'retrieve_issue', return_value=None) -def test_retrieve_issue_not_found(mock_retrieve_issue): - expected_response = "Issue not found." - assert retrieve_issue('test/repo', 1) == expected_response - - -# create_pull_request + expected_response = "Failed to create the pull request." + patch_path = 'camel.loaders.github_loader.GitHubLoader.create_pull_request' + with patch(patch_path) as mock_github_loader_create_pull_request: + mock_github_loader_create_pull_request.return_value = None -@patch( - 'camel.functions.github_functions.get_github_access_token', - return_value="1", -) -@patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) -@patch.object(GitHubLoader, 'create_pull_request', return_value="pr") -def test_create_pull_request( - mock_get_github_access_token, mock_create_pull_request -): - expected_response = ( - "Title: [GitHub Agent] Solved issue: Time complexity for product_of_array_except_self.py\n" - "Body: Fixes #1\n" - ) - - pr = create_pull_request( - 'test/repo', - mock_issue.file_path, - mock_issue.file_content, - mock_issue.title, - mock_issue.number, - ) - - assert pr == expected_response - - -@patch( - 'camel.functions.github_functions.get_github_access_token', - return_value=None, -) -def test_create_pull_request_no_access_token(mock_get_github_access_token): - with pytest.raises(AssertionError) as error: - create_pull_request( + pr = create_pull_request( 'test/repo', mock_issue.file_path, mock_issue.file_content, mock_issue.title, mock_issue.number, ) - assert str(error.value) == "" # from Auth.Token initializer - - -@patch( - 'camel.functions.github_functions.get_github_access_token', - return_value="1", -) -@patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) -@patch.object(GitHubLoader, 'create_pull_request', return_value=None) -def test_create_pull_request_failed( - mock_get_github_access_token, mock_create_pull_request -): - expected_response = "Failed to create the pull request." - pr = create_pull_request( - 'test/repo', - mock_issue.file_path, - mock_issue.file_content, - mock_issue.title, - mock_issue.number, - ) - assert pr == expected_response + assert pr == expected_response From ad2907411edbcccfb3d4e32be0d62f87f78f1759 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Tue, 21 May 2024 14:15:48 +0100 Subject: [PATCH 23/47] fix remaining tests --- test/functions/test_github_functions.py | 162 +++++++++++------------- 1 file changed, 77 insertions(+), 85 deletions(-) diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 49b60648d..102730623 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -13,10 +13,13 @@ # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== from unittest.mock import patch +import pytest from github import Auth, Github from camel.functions.github_functions import ( create_pull_request, + get_github_access_token, + retrieve_issue, ) from camel.loaders.github_loader import GitHubLoaderIssue @@ -31,91 +34,80 @@ # get_github_access_token -# def test_get_github_access_token(monkeypatch): -# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') -# expected_access_token = 'TOKEN' -# access_token = get_github_access_token() -# assert access_token == expected_access_token - - -# def test_get_github_access_token_no_token(monkeypatch): -# monkeypatch.delenv('GITHUB_ACCESS_TOKEN', raising=False) -# with pytest.raises(ValueError) as error: -# get_github_access_token() -# assert ( -# str(error.value) -# == "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it here: `https://github.com/settings/tokens`." -# ) - - -# # retrieve_issue -# @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) -# @patch.object(GitHubLoader, 'retrieve_issue', return_value=mock_issue) -# def test_retrieve_issue(monkeypatch, mock_retrieve_issue): -# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') -# expected_response = ( -# "Title: Time complexity for product_of_array_except_self.py\n" -# "Body: Improve the time complexity for the product_of_array_except_self.py file\n" -# "Number: 1\n" -# "File Path: product_of_array_except_self.py\n" -# "File Content: def product_of_array_except_self(nums): ..." -# ) - -# assert retrieve_issue('test/repo', 1) == expected_response - - -# @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) -# @patch.object(GitHubLoader, 'retrieve_issue', return_value=None) -# def test_retrieve_issue_not_found(monkeypatch, mock_retrieve_issue): -# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') -# expected_response = "Issue not found." -# assert retrieve_issue('test/repo', 1) == expected_response - - -# # create_pull_request - - -# @patch( -# 'camel.functions.github_functions.get_github_access_token', -# return_value="1", -# ) -# @patch.object(GitHubLoader, '__init__', lambda self, *args, **kwargs: None) -# @patch.object(GitHubLoader, 'create_pull_request', return_value="pr") -# def test_create_pull_request( -# monkeypatch, mock_get_github_access_token, mock_create_pull_request -# ): -# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') -# expected_response = ( -# "Title: [GitHub Agent] Solved issue: Time complexity for product_of_array_except_self.py\n" -# "Body: Fixes #1\n" -# ) - -# pr = create_pull_request( -# 'test/repo', -# mock_issue.file_path, -# mock_issue.file_content, -# mock_issue.title, -# mock_issue.number, -# ) - -# assert pr == expected_response - - -# @patch( -# 'camel.functions.github_functions.get_github_access_token', -# return_value=None, -# ) -# def test_create_pull_request_no_access_token(monkeypatch, mock_get_github_access_token): -# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') -# with pytest.raises(AssertionError) as error: -# create_pull_request( -# 'test/repo', -# mock_issue.file_path, -# mock_issue.file_content, -# mock_issue.title, -# mock_issue.number, -# ) -# assert str(error.value) == "" # from Auth.Token initializer +def test_get_github_access_token(monkeypatch): + monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') + expected_access_token = 'TOKEN' + access_token = get_github_access_token() + assert access_token == expected_access_token + + +def test_get_github_access_token_no_token(monkeypatch): + monkeypatch.delenv('GITHUB_ACCESS_TOKEN', raising=False) + with pytest.raises(ValueError) as error: + get_github_access_token() + assert ( + str(error.value) + == "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it here: `https://github.com/settings/tokens`." + ) + + +# retrieve_issue + + +@patch.object(Github, 'get_repo', return_value="test/repo") +@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) +def test_retrieve_issue(monkeypatch): + monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') + expected_response = ( + "Title: Time complexity for product_of_array_except_self.py\n" + "Body: Improve the time complexity for the product_of_array_except_self.py file\n" + "Number: 1\n" + "File Path: product_of_array_except_self.py\n" + "File Content: def product_of_array_except_self(nums): ..." + ) + + patch_path = 'camel.loaders.github_loader.GitHubLoader.retrieve_issue_list' + with patch(patch_path) as mock_github_retrieve_issue: + mock_github_retrieve_issue.return_value = [mock_issue] + assert retrieve_issue('test/repo', 1) == expected_response + + +@patch.object(Github, 'get_repo', return_value="test/repo") +@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) +def test_retrieve_issue_not_found(monkeypatch): + monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') + expected_response = "Issue not found." + + patch_path = 'camel.loaders.github_loader.GitHubLoader.retrieve_issue' + with patch(patch_path) as mock_github_retrieve_issue: + mock_github_retrieve_issue.return_value = None + assert retrieve_issue('test/repo', 1) == expected_response + + +# create_pull_request + + +@patch.object(Github, 'get_repo', return_value="test/repo") +@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) +def test_create_pull_request(monkeypatch): + monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') + expected_response = ( + "Title: [GitHub Agent] Solved issue: Time complexity for product_of_array_except_self.py\n" + "Body: Fixes #1\n" + ) + + patch_path = 'camel.loaders.github_loader.GitHubLoader.create_pull_request' + with patch(patch_path) as mock_github_loader_create_pull_request: + mock_github_loader_create_pull_request.return_value = "some response" + + pr = create_pull_request( + 'test/repo', + mock_issue.file_path, + mock_issue.file_content, + mock_issue.title, + mock_issue.number, + ) + assert pr == expected_response @patch.object(Github, 'get_repo', return_value="test/repo") From a612d13754ef04423f7d684ad4f74a276a94ff64 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Tue, 21 May 2024 15:17:21 +0100 Subject: [PATCH 24/47] patching access token in tests --- test/functions/test_github_functions.py | 47 ++++++++++++++++--------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 102730623..0f4a47f8a 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -13,12 +13,10 @@ # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== from unittest.mock import patch -import pytest from github import Auth, Github from camel.functions.github_functions import ( create_pull_request, - get_github_access_token, retrieve_issue, ) from camel.loaders.github_loader import GitHubLoaderIssue @@ -34,26 +32,29 @@ # get_github_access_token -def test_get_github_access_token(monkeypatch): - monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') - expected_access_token = 'TOKEN' - access_token = get_github_access_token() - assert access_token == expected_access_token - - -def test_get_github_access_token_no_token(monkeypatch): - monkeypatch.delenv('GITHUB_ACCESS_TOKEN', raising=False) - with pytest.raises(ValueError) as error: - get_github_access_token() - assert ( - str(error.value) - == "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it here: `https://github.com/settings/tokens`." - ) +# def test_get_github_access_token(monkeypatch): +# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') +# expected_access_token = 'TOKEN' +# access_token = get_github_access_token() +# assert access_token == expected_access_token + +# def test_get_github_access_token_no_token(monkeypatch): +# monkeypatch.delenv('GITHUB_ACCESS_TOKEN', raising=False) +# with pytest.raises(ValueError) as error: +# get_github_access_token() +# assert ( +# str(error.value) +# == "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it here: `https://github.com/settings/tokens`." +# ) # retrieve_issue +@patch( + 'camel.functions.github_functions.get_github_access_token', + return_value="1", +) @patch.object(Github, 'get_repo', return_value="test/repo") @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) def test_retrieve_issue(monkeypatch): @@ -72,6 +73,10 @@ def test_retrieve_issue(monkeypatch): assert retrieve_issue('test/repo', 1) == expected_response +@patch( + 'camel.functions.github_functions.get_github_access_token', + return_value="1", +) @patch.object(Github, 'get_repo', return_value="test/repo") @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) def test_retrieve_issue_not_found(monkeypatch): @@ -87,6 +92,10 @@ def test_retrieve_issue_not_found(monkeypatch): # create_pull_request +@patch( + 'camel.functions.github_functions.get_github_access_token', + return_value="1", +) @patch.object(Github, 'get_repo', return_value="test/repo") @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) def test_create_pull_request(monkeypatch): @@ -110,6 +119,10 @@ def test_create_pull_request(monkeypatch): assert pr == expected_response +@patch( + 'camel.functions.github_functions.get_github_access_token', + return_value="1", +) @patch.object(Github, 'get_repo', return_value="test/repo") @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) def test_create_pull_request_failed(monkeypatch): From 4ab14f454709568246fae4dbeacaa0f77a0db9a8 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Tue, 21 May 2024 15:57:17 +0100 Subject: [PATCH 25/47] patching access token in tests --- test/functions/test_github_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py index 0f4a47f8a..b6ec1b601 100644 --- a/test/functions/test_github_functions.py +++ b/test/functions/test_github_functions.py @@ -57,7 +57,7 @@ ) @patch.object(Github, 'get_repo', return_value="test/repo") @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_retrieve_issue(monkeypatch): +def test_retrieve_issue(monkeypatch, mock_get_github_access_token): monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') expected_response = ( "Title: Time complexity for product_of_array_except_self.py\n" @@ -79,7 +79,7 @@ def test_retrieve_issue(monkeypatch): ) @patch.object(Github, 'get_repo', return_value="test/repo") @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_retrieve_issue_not_found(monkeypatch): +def test_retrieve_issue_not_found(monkeypatch, mock_get_github_access_token): monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') expected_response = "Issue not found." @@ -98,7 +98,7 @@ def test_retrieve_issue_not_found(monkeypatch): ) @patch.object(Github, 'get_repo', return_value="test/repo") @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_create_pull_request(monkeypatch): +def test_create_pull_request(monkeypatch, mock_get_github_access_token): monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') expected_response = ( "Title: [GitHub Agent] Solved issue: Time complexity for product_of_array_except_self.py\n" @@ -125,7 +125,7 @@ def test_create_pull_request(monkeypatch): ) @patch.object(Github, 'get_repo', return_value="test/repo") @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_create_pull_request_failed(monkeypatch): +def test_create_pull_request_failed(monkeypatch, mock_get_github_access_token): monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') expected_response = "Failed to create the pull request." From 0dd85da6bccf9327cca453428b4bf01019a7c53d Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Fri, 24 May 2024 17:08:27 +0100 Subject: [PATCH 26/47] integrate github toolkit --- examples/function_call/github_agent.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/function_call/github_agent.py b/examples/function_call/github_agent.py index 34390e5f0..1d31f7f35 100644 --- a/examples/function_call/github_agent.py +++ b/examples/function_call/github_agent.py @@ -15,9 +15,9 @@ from camel.agents import ChatAgent from camel.configs import FunctionCallingConfig -from camel.functions import GITHUB_FUNCS from camel.messages import BaseMessage from camel.prompts import PromptTemplateGenerator +from camel.toolkits import GithubToolkit from camel.types import TaskType from camel.utils import print_text_animated @@ -36,23 +36,21 @@ def solve_issue( ) print(Fore.YELLOW + f"Final prompt:\n{prompt}\n") - function_list = [ - *GITHUB_FUNCS, - ] + toolkit = GithubToolkit(repo_name=repo_name) assistant_sys_msg = BaseMessage.make_assistant_message( role_name="Software Engineer", content="""You are an experienced software engineer who specializes on data structures and algorithms tasks.""", ) assistant_model_config = FunctionCallingConfig.from_openai_function_list( - function_list=function_list, + function_list=toolkit.get_tools(), kwargs=dict(temperature=0.0), ) agent = ChatAgent( assistant_sys_msg, model_type=model, model_config=assistant_model_config, - function_list=function_list, + function_list=toolkit.get_tools(), ) agent.reset() From 3abded9c43bd8b905ed1c3d6b919f7e310520ed2 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sat, 25 May 2024 16:25:15 +0200 Subject: [PATCH 27/47] Introduce GithubToolkit --- pyproject.toml | 2 +- test/functions/test_github_functions.py | 143 ------------------------ test/loaders/test_github_loader.py | 116 ------------------- 3 files changed, 1 insertion(+), 260 deletions(-) delete mode 100644 test/functions/test_github_functions.py delete mode 100644 test/loaders/test_github_loader.py diff --git a/pyproject.toml b/pyproject.toml index 6a2ee3bcc..e8aef46f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,6 +145,7 @@ all = [ # tools "beautifulsoup4", "docx2txt", + "pygithub", "PyMuPDF", "wikipedia", "wolframalpha", @@ -167,7 +168,6 @@ all = [ "neo4j", # retrievers "rank-bm25", - "pygithub", ] [tool.poetry.group.dev] diff --git a/test/functions/test_github_functions.py b/test/functions/test_github_functions.py deleted file mode 100644 index b6ec1b601..000000000 --- a/test/functions/test_github_functions.py +++ /dev/null @@ -1,143 +0,0 @@ -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -# Licensed under the Apache License, Version 2.0 (the “License”); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an “AS IS” BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from unittest.mock import patch - -from github import Auth, Github - -from camel.functions.github_functions import ( - create_pull_request, - retrieve_issue, -) -from camel.loaders.github_loader import GitHubLoaderIssue - -mock_issue = GitHubLoaderIssue( - title='Time complexity for product_of_array_except_self.py', - body='Improve the time complexity for the product_of_array_except_self.py file', - number=1, - file_path='product_of_array_except_self.py', - file_content='def product_of_array_except_self(nums): ...', -) - -# get_github_access_token - - -# def test_get_github_access_token(monkeypatch): -# monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') -# expected_access_token = 'TOKEN' -# access_token = get_github_access_token() -# assert access_token == expected_access_token - -# def test_get_github_access_token_no_token(monkeypatch): -# monkeypatch.delenv('GITHUB_ACCESS_TOKEN', raising=False) -# with pytest.raises(ValueError) as error: -# get_github_access_token() -# assert ( -# str(error.value) -# == "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it here: `https://github.com/settings/tokens`." -# ) - - -# retrieve_issue - - -@patch( - 'camel.functions.github_functions.get_github_access_token', - return_value="1", -) -@patch.object(Github, 'get_repo', return_value="test/repo") -@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_retrieve_issue(monkeypatch, mock_get_github_access_token): - monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') - expected_response = ( - "Title: Time complexity for product_of_array_except_self.py\n" - "Body: Improve the time complexity for the product_of_array_except_self.py file\n" - "Number: 1\n" - "File Path: product_of_array_except_self.py\n" - "File Content: def product_of_array_except_self(nums): ..." - ) - - patch_path = 'camel.loaders.github_loader.GitHubLoader.retrieve_issue_list' - with patch(patch_path) as mock_github_retrieve_issue: - mock_github_retrieve_issue.return_value = [mock_issue] - assert retrieve_issue('test/repo', 1) == expected_response - - -@patch( - 'camel.functions.github_functions.get_github_access_token', - return_value="1", -) -@patch.object(Github, 'get_repo', return_value="test/repo") -@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_retrieve_issue_not_found(monkeypatch, mock_get_github_access_token): - monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') - expected_response = "Issue not found." - - patch_path = 'camel.loaders.github_loader.GitHubLoader.retrieve_issue' - with patch(patch_path) as mock_github_retrieve_issue: - mock_github_retrieve_issue.return_value = None - assert retrieve_issue('test/repo', 1) == expected_response - - -# create_pull_request - - -@patch( - 'camel.functions.github_functions.get_github_access_token', - return_value="1", -) -@patch.object(Github, 'get_repo', return_value="test/repo") -@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_create_pull_request(monkeypatch, mock_get_github_access_token): - monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') - expected_response = ( - "Title: [GitHub Agent] Solved issue: Time complexity for product_of_array_except_self.py\n" - "Body: Fixes #1\n" - ) - - patch_path = 'camel.loaders.github_loader.GitHubLoader.create_pull_request' - with patch(patch_path) as mock_github_loader_create_pull_request: - mock_github_loader_create_pull_request.return_value = "some response" - - pr = create_pull_request( - 'test/repo', - mock_issue.file_path, - mock_issue.file_content, - mock_issue.title, - mock_issue.number, - ) - assert pr == expected_response - - -@patch( - 'camel.functions.github_functions.get_github_access_token', - return_value="1", -) -@patch.object(Github, 'get_repo', return_value="test/repo") -@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_create_pull_request_failed(monkeypatch, mock_get_github_access_token): - monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') - expected_response = "Failed to create the pull request." - - patch_path = 'camel.loaders.github_loader.GitHubLoader.create_pull_request' - with patch(patch_path) as mock_github_loader_create_pull_request: - mock_github_loader_create_pull_request.return_value = None - - pr = create_pull_request( - 'test/repo', - mock_issue.file_path, - mock_issue.file_content, - mock_issue.title, - mock_issue.number, - ) - assert pr == expected_response diff --git a/test/loaders/test_github_loader.py b/test/loaders/test_github_loader.py deleted file mode 100644 index 4dfc817a8..000000000 --- a/test/loaders/test_github_loader.py +++ /dev/null @@ -1,116 +0,0 @@ -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -# Licensed under the Apache License, Version 2.0 (the “License”); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an “AS IS” BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== - -from unittest.mock import MagicMock - -from camel.loaders.github_loader import GitHubLoader, GitHubLoaderIssue - - -def test_init(): - # Create a mock GitHub instance - mock_github = MagicMock() - - # Call the constructor of the GitHubLoader class - github_loader = GitHubLoader(mock_github, "repo_name") - - # Assert that the repo attribute is set to the return value of the get_repo method - assert github_loader.repo == mock_github.get_repo.return_value - - # Assert that the get_repo method was called with the correct argument - mock_github.get_repo.assert_called_once_with("repo_name") - - -def test_retrieve_issue_list(monkeypatch): - # Set a dummy access token in the environment variables - monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') - - # Create a mock GitHub instance - mock_github = MagicMock() - - # Create a mock GitHubLoader instance - mock_github_loader = GitHubLoader(mock_github, "repo_name") - - # Create a mock issue object - mock_issue = MagicMock() - mock_issue.number = 1 - mock_issue.title = "Test Issue" - mock_issue.body = "This is a test issue" - mock_issue.pull_request = False - - mock_label = MagicMock() - mock_label.name = "path/to/file" - mock_issue.labels = [mock_label] - - # Mock the get_issues method of the mock_repo instance to return a list containing the mock issue object - mock_github_loader.repo.get_issues.return_value = [mock_issue] - mock_github_loader.retrieve_file_content = MagicMock( - return_value="This is the content of the file" - ) - - # Call the retrieve_issue_list method - issue_list = mock_github_loader.retrieve_issue_list() - - # Assert the returned issue list - expected_issue = GitHubLoaderIssue( - "Test Issue", - "This is a test issue", - 1, - "path/to/file", - "This is the content of the file", - ) - assert issue_list == [ - expected_issue - ], f"Expected {expected_issue}, but got {issue_list}" - - -def test_retrieve_issue(monkeypatch): - # Set a dummy access token in the environment variables - monkeypatch.setenv('GITHUB_ACCESS_TOKEN', 'TOKEN') - - # Create a mock GitHub instance - mock_github = MagicMock() - - # Create a mock GitHubLoader instance - mock_github_loader = GitHubLoader(mock_github, "repo_name") - - # Create a mock issue object - mock_issue = MagicMock() - mock_issue.number = 1 - mock_issue.title = "Test Issue" - mock_issue.body = "This is a test issue" - mock_issue.pull_request = False - - mock_label = MagicMock() - mock_label.name = "path/to/file" - mock_issue.labels = [mock_label] - - # Mock the get_issues method of the mock_repo instance to return a list containing the mock issue object - mock_github_loader.repo.get_issues.return_value = [mock_issue] - mock_github_loader.retrieve_file_content = MagicMock( - return_value="This is the content of the file" - ) - - # Call the retrieve_issue method - issue = mock_github_loader.retrieve_issue(1) - # Assert the returned issue - expected_issue = GitHubLoaderIssue( - "Test Issue", - "This is a test issue", - 1, - "path/to/file", - "This is the content of the file", - ) - assert ( - issue == expected_issue - ), f"Expected {expected_issue}, but got {issue}" From e4cda0736ffed8387af1c95a27a86607facbae83 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sat, 25 May 2024 19:44:22 +0200 Subject: [PATCH 28/47] remove github as prompt template --- camel/prompts/github.py | 50 ------------------ camel/prompts/task_prompt_template.py | 2 - camel/types/enums.py | 1 - examples/function_call/github_agent.py | 72 -------------------------- 4 files changed, 125 deletions(-) delete mode 100644 camel/prompts/github.py delete mode 100644 examples/function_call/github_agent.py diff --git a/camel/prompts/github.py b/camel/prompts/github.py deleted file mode 100644 index 80fc348b0..000000000 --- a/camel/prompts/github.py +++ /dev/null @@ -1,50 +0,0 @@ -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -# Licensed under the Apache License, Version 2.0 (the “License”); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an “AS IS” BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from typing import Any - -from camel.prompts.base import TextPrompt, TextPromptDict - - -class GitHubPromptTemplateDict(TextPromptDict): - r"""A dictionary containing :obj:`TextPrompt` used in the `GitHub` - task. - - Attributes: - SOLVE_ISSUE (TextPrompt): A prompt to solve an issue - in code based on the original source code and the - issue description. - """ - - SOLVE_ISSUE = TextPrompt( - """You need to solve the issue with number: {issue_number} -For this you will have to use the provided github function to retrieve -that issue. You will get all the necessary parameters to later create a -pull request. - -When you have the issue, please follow the instruction and make the necessary -changes to the source code provided. Once you have made the changes, you will -need to use another provided github function to create a pull request -that updates the file on the provided file path in the repository {repo_name}. -The new_content property of the function should be the corrected source code. -Return response of this function as the output of this task. -""" - ) - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self.update( - { - "solve_issue": self.SOLVE_ISSUE, - } - ) diff --git a/camel/prompts/task_prompt_template.py b/camel/prompts/task_prompt_template.py index 123af750a..e61769264 100644 --- a/camel/prompts/task_prompt_template.py +++ b/camel/prompts/task_prompt_template.py @@ -21,7 +21,6 @@ from camel.prompts.evaluation import ( EvaluationPromptTemplateDict, ) -from camel.prompts.github import GitHubPromptTemplateDict from camel.prompts.misalignment import MisalignmentPromptTemplateDict from camel.prompts.object_recognition import ObjectRecognitionPromptTemplateDict from camel.prompts.role_description_prompt_template import ( @@ -50,7 +49,6 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: { TaskType.AI_SOCIETY: AISocietyPromptTemplateDict(), TaskType.CODE: CodePromptTemplateDict(), - TaskType.GITHUB: GitHubPromptTemplateDict(), TaskType.MISALIGNMENT: MisalignmentPromptTemplateDict(), TaskType.TRANSLATION: TranslationPromptTemplateDict(), TaskType.EVALUATION: EvaluationPromptTemplateDict(), diff --git a/camel/types/enums.py b/camel/types/enums.py index b3b50c6fc..1a67693b0 100644 --- a/camel/types/enums.py +++ b/camel/types/enums.py @@ -184,7 +184,6 @@ def output_dim(self) -> int: class TaskType(Enum): AI_SOCIETY = "ai_society" CODE = "code" - GITHUB = "github" MISALIGNMENT = "misalignment" TRANSLATION = "translation" EVALUATION = "evaluation" diff --git a/examples/function_call/github_agent.py b/examples/function_call/github_agent.py deleted file mode 100644 index 1d31f7f35..000000000 --- a/examples/function_call/github_agent.py +++ /dev/null @@ -1,72 +0,0 @@ -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -# Licensed under the Apache License, Version 2.0 (the “License”); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an “AS IS” BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from colorama import Fore - -from camel.agents import ChatAgent -from camel.configs import FunctionCallingConfig -from camel.messages import BaseMessage -from camel.prompts import PromptTemplateGenerator -from camel.toolkits import GithubToolkit -from camel.types import TaskType -from camel.utils import print_text_animated - - -def solve_issue( - repo_name, - issue_number, - model=None, -) -> None: - prompt_template = PromptTemplateGenerator().get_prompt_from_key( - TaskType.GITHUB, 'solve_issue' - ) - prompt = prompt_template.format( - repo_name=repo_name, - issue_number=issue_number, - ) - print(Fore.YELLOW + f"Final prompt:\n{prompt}\n") - - toolkit = GithubToolkit(repo_name=repo_name) - assistant_sys_msg = BaseMessage.make_assistant_message( - role_name="Software Engineer", - content="""You are an experienced software engineer who - specializes on data structures and algorithms tasks.""", - ) - assistant_model_config = FunctionCallingConfig.from_openai_function_list( - function_list=toolkit.get_tools(), - kwargs=dict(temperature=0.0), - ) - agent = ChatAgent( - assistant_sys_msg, - model_type=model, - model_config=assistant_model_config, - function_list=toolkit.get_tools(), - ) - agent.reset() - - user_msg = BaseMessage.make_user_message(role_name="User", content=prompt) - assistant_response = agent.step(user_msg) - - if len(assistant_response.msgs) > 0: - print_text_animated( - Fore.GREEN + f"Agent response:\n{assistant_response.msg.content}\n" - ) - - -def main(model=None) -> None: - repo_name = "camel-ai/test-github-agent" - solve_issue(repo_name=repo_name, issue_number=1, model=model) - - -if __name__ == "__main__": - main() From c02cec498632b2579322e9e05838e7afe5e8366a Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sat, 25 May 2024 19:48:15 +0200 Subject: [PATCH 29/47] remove unused files --- camel/functions/__init__.py | 2 - camel/functions/github_functions.py | 114 ---------------------------- camel/loaders/__init__.py | 2 - camel/loaders/github_loader.py | 96 ----------------------- 4 files changed, 214 deletions(-) delete mode 100644 camel/functions/github_functions.py delete mode 100644 camel/loaders/github_loader.py diff --git a/camel/functions/__init__.py b/camel/functions/__init__.py index 58f2c3255..1f63cdfb8 100644 --- a/camel/functions/__init__.py +++ b/camel/functions/__init__.py @@ -18,7 +18,6 @@ get_openai_tool_schema, ) -from .github_functions import GITHUB_FUNCS from .google_maps_function import MAP_FUNCS from .math_functions import MATH_FUNCS from .open_api_function import OPENAPI_FUNCS @@ -32,7 +31,6 @@ 'OpenAIFunction', 'get_openai_function_schema', 'get_openai_tool_schema', - 'GITHUB_FUNCS', 'MAP_FUNCS', 'MATH_FUNCS', 'OPENAPI_FUNCS', diff --git a/camel/functions/github_functions.py b/camel/functions/github_functions.py deleted file mode 100644 index 4e618efff..000000000 --- a/camel/functions/github_functions.py +++ /dev/null @@ -1,114 +0,0 @@ -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -# Licensed under the Apache License, Version 2.0 (the “License”); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an “AS IS” BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -import os -from typing import List - -from github import Auth, Github - -from camel.functions import OpenAIFunction -from camel.loaders import GitHubLoader - - -def get_github_access_token() -> str: - r"""Retrieve the GitHub access token from environment variables. - - Returns: - str: A string containing the GitHub access token. - - Raises: - ValueError: If the API key or secret is not found in the environment - variables. - """ - # Get `GITHUB_ACCESS_TOKEN` here: - # https://github.com/settings/tokens - GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN") - - if not GITHUB_ACCESS_TOKEN: - raise ValueError( - "`GITHUB_ACCESS_TOKEN` not found in environment variables. Get it " - "here: `https://github.com/settings/tokens`." - ) - return GITHUB_ACCESS_TOKEN - - -def retrieve_issue(repo_name: str, issue_number: int): - r"""Retrieves an issue from a GitHub repository. - - This function retrieves an issue from a specified repository using the - issue number. - - Args: - repo_name (str): The name of the repository from which to retrieve the issue. - issue_number (int): The number of the issue to retrieve. - - Returns: - str: A formatted report of the retrieved issue. - """ - github = Github(auth=Auth.Token(get_github_access_token())) - loader = GitHubLoader(github, repo_name) - issue = loader.retrieve_issue(issue_number) - - if issue: - return ( - f"Title: {issue.title}\n" - f"Body: {issue.body}\n" - f"Number: {issue.number}\n" - f"File Path: {issue.file_path}\n" - f"File Content: {issue.file_content}" - ) - return "Issue not found." - - -def create_pull_request( - repo_name: str, - file_path: str, - new_content: str, - issue_title: str, - issue_number: int, -): - r"""Creates a pull request. - - This function creates a pull request in specified repository, which updates a - file in the specific path with new content. The pull request description - contains information about the issue title and number. - - Args: - repo_name (str): The name of the repository in which to create the pull request. - file_path (str): The path of the file to be updated in the repository. - new_content (str): The specified new content of the specified file. - issue_title (str): The title of the issue that is solved by this pull request. - issue_number (str): The number of the issue that is solved by this pull request. - - Returns: - str: A formatted report of the whether the pull request was created - successfully or not. - """ - github = Github(auth=Auth.Token(get_github_access_token())) - loader = GitHubLoader(github, repo_name) - title = f"[GitHub Agent] Solved issue: {issue_title}" - body = f"Fixes #{issue_number}" - pr = loader.create_pull_request(file_path, new_content, title, body) - - if pr: - return f"Title: {title}\n" f"Body: {body}\n" - return "Failed to create the pull request." - - -GITHUB_FUNCS: List[OpenAIFunction] = [ - OpenAIFunction(func) # type: ignore[arg-type] - for func in [ - retrieve_issue, - create_pull_request, - ] -] diff --git a/camel/loaders/__init__.py b/camel/loaders/__init__.py index cfaab9658..ee476ee28 100644 --- a/camel/loaders/__init__.py +++ b/camel/loaders/__init__.py @@ -13,12 +13,10 @@ # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== from .base_io import File, read_file -from .github_loader import GitHubLoader from .unstructured_io import UnstructuredIO __all__ = [ 'File', 'read_file', 'UnstructuredIO', - 'GitHubLoader', ] diff --git a/camel/loaders/github_loader.py b/camel/loaders/github_loader.py deleted file mode 100644 index dcac99945..000000000 --- a/camel/loaders/github_loader.py +++ /dev/null @@ -1,96 +0,0 @@ -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -# Licensed under the Apache License, Version 2.0 (the “License”); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an “AS IS” BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== - - -class GitHubLoaderIssue: - def __init__(self, title, body, number, file_path, file_content): - self.title = title - self.body = body - self.number = number - self.file_path = file_path - self.file_content = file_content - - def __eq__(self, other): - if isinstance(other, GitHubLoaderIssue): - return ( - self.number == other.number - and self.title == other.title - and self.body == other.body - and self.file_path == other.file_path - and self.file_content == other.file_content - ) - return False - - def __repr__(self): - return ( - f"GitHubLoaderIssue(number={self.number}, title={self.title}, " - f"body={self.body}, file_path={self.file_path}, file_content={self.file_content})" - ) - - -class GitHubLoader: - r"""A class for loading data from GitHub repositories.""" - - def __init__(self, github, repo_name): - self.github = github - self.repo = self.github.get_repo(repo_name) - - def retrieve_issue_list(self): - issues = self.repo.get_issues(state='open') - return [ - GitHubLoaderIssue( - issue.title, - issue.body, - issue.number, - issue.labels[0].name, - self.retrieve_file_content(issue.labels[0].name), - ) - for issue in issues - if not issue.pull_request - ] - - def retrieve_issue(self, issue_number): - issues = self.retrieve_issue_list() - for issue in issues: - if issue.number == issue_number: - return issue - return None - - def retrieve_file_content(self, file_path): - file_content = self.repo.get_contents(file_path) - return file_content.decoded_content.decode() - - def create_pull_request( - self, file_path, new_content, pr_title, commit_message - ): - branch_name = f"github-agent-update-{file_path.replace('/', '-')}" - sb = self.repo.get_branch(self.repo.default_branch) - self.repo.create_git_ref( - ref=f"refs/heads/{branch_name}", sha=sb.commit.sha - ) - file = self.repo.get_contents(file_path) - self.repo.update_file( - file.path, commit_message, new_content, file.sha, branch=branch_name - ) - pr = self.repo.create_pull( - title=pr_title, - body=commit_message, - head=branch_name, - base=self.repo.default_branch, - ) - return pr - - def commit_file_content(self, file_path, new_content, commit_message): - file = self.repo.get_contents(file_path) - self.repo.update_file(file.path, commit_message, new_content, file.sha) From 48a4b6d01d9e247e5aaab765666006a8a7bd4334 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sat, 25 May 2024 23:14:52 +0200 Subject: [PATCH 30/47] Add a function to retrieve merged PRs --- camel/toolkits/github_toolkit.py | 98 +++++++++++++++++++++++ examples/function_call/github_examples.py | 50 +++++++++++- 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 8f7e80388..72ac1a4b9 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -15,6 +15,7 @@ import os from dataclasses import dataclass from typing import List, Optional +from datetime import datetime, timedelta from camel.functions import OpenAIFunction @@ -71,6 +72,72 @@ def summary(self) -> str: ) +@dataclass +class GithubPullRequestDiff: + """ + Represents a single diff of a pull request on Github. + """ + + def __init__(self, filename: str, patch: str): + """ + Initialize a GithubPullRequestDiff object. + + Args: + filename (str): The name of the file that was changed. + patch (str): The diff patch for the file. + """ + self.filename = filename + self.patch = patch + + def summary(self): + """ + Returns a summary of the diff. + """ + return f"Filename: {self.filename}\nPatch: {self.patch}" + + +@dataclass +class GithubPullRequest: + """ + Represents a pull request on Github. + + Attributes: + title (str): The title of the pull request. + body (str): The body/content of the pull request. + file_path (str): The path of the file associated with the pull request. + file_content (str): The content of the file associated with the pull request. + """ + + def __init__( + self, + title: str, + body: str, + diffs: list[GithubPullRequestDiff], + ): + """ + Initialize a GithubPullRequest object. + + Args: + title (str): The title of the GitHub pull request. + body (str): The body/content of the GitHub pull request. + diffs (list[GithubPullRequestDiff]): A list of diffs for the pull request. + """ + self.title = title + self.body = body + self.diffs = diffs + + def summary(self): + """ + Returns a summary of the pull request. + """ + diff_summaries = '\n'.join(diff.summary() for diff in self.diffs) + return ( + f"Title: {self.title}\n" + f"Body: {self.body}\n" + f"Diffs: {diff_summaries}\n" + ) + + class GithubToolkit(BaseToolkit): r"""A class representing a toolkit for interacting with GitHub repositories. @@ -116,6 +183,7 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.retrieve_issue_list), OpenAIFunction(self.retrieve_issue), OpenAIFunction(self.create_pull_request), + OpenAIFunction(self.retrieve_merged_pull_requests), ] def get_github_access_token(self) -> str: @@ -176,6 +244,36 @@ def retrieve_issue(self, issue_number: int) -> Optional[str]: return issue.summary() return None + def retrieve_merged_pull_requests(self, days: int) -> list: + """ + Retrieves a summary of merged pull requests from the repository. + The summary will be provided for the last 7 days. + + Returns: + list: A list of merged pull request summaries. + """ + pull_requests = self.repo.get_pulls(state='closed') + merged_prs = [] + earliest_date: datetime = datetime.utcnow() - timedelta(days=14) + + for pr in pull_requests: + if ( + pr.merged + and pr.merged_at is not None + and pr.merged_at.timestamp() > earliest_date.timestamp() + ): + pr_details = GithubPullRequest(pr.title, pr.body, []) + + # Get files changed in the PR + files = pr.get_files() + + for file in files: + diff = GithubPullRequestDiff(file.filename, file.patch) + pr_details.diffs.append(diff) + + merged_prs.append(pr_details.summary()) + return merged_prs + def create_pull_request( self, file_path: str, diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index c49c046d4..4e4349e13 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -17,9 +17,55 @@ from camel.configs import ChatGPTConfig from camel.messages import BaseMessage from camel.toolkits import GithubToolkit +from camel.types import ModelType from camel.utils import print_text_animated +def write_weekly_pr_summary(repo_name, model=None): + prompt = """ + You need to write a summary of the pull requests that were merged in the last week. + You can use the provided github function retrieve_merged_pull_requests to retrieve the list of pull requests that were merged in the last week. + The function will return a list of pull requests with the following properties: title, body, and diffs. + Diffs is a list of dictionaries with the following properties: filename, diff. + You will have to look closely at each diff to understand the changes that were made in each pull request. + Output a twitter post that describes recent changes in the project and thanks the contributors. + + Here is an example of a summary for one pull request: + 📢 We've improved function calling in the 🐪 CAMEL-AI framework! + This update enhances the handling of various docstring styles and supports enum types, ensuring more accurate and reliable function calls. + Thanks to our contributor Jiahui Zhang for making this possible. + """ + print(Fore.YELLOW + f"Final prompt:\n{prompt}\n") + + toolkit = GithubToolkit(repo_name=repo_name) + assistant_sys_msg = BaseMessage.make_assistant_message( + role_name="Marketing Manager", + content=f""" + You are an experienced marketing manager responsible for posting weekly updates about the status + of an open source project {repo_name} on the project's blog. + """, + ) + assistant_model_config = FunctionCallingConfig.from_openai_function_list( + function_list=toolkit.get_tools(), + kwargs=dict(temperature=0.0), + ) + agent = ChatAgent( + assistant_sys_msg, + model_type=model, + model_config=assistant_model_config, + function_list=toolkit.get_tools(), + ) + agent.reset() + + user_msg = BaseMessage.make_user_message(role_name="User", content=prompt) + assistant_response = agent.step(user_msg) + + if len(assistant_response.msgs) > 0: + print_text_animated( + Fore.GREEN + f"Agent response:\n{assistant_response.msg.content}\n" + ) + + def solve_issue( repo_name, issue_number, @@ -68,8 +114,8 @@ def solve_issue( def main(model=None) -> None: - repo_name = "camel-ai/test-github-agent" - solve_issue(repo_name=repo_name, issue_number=1, model=model) + repo_name = "camel-ai/camel" + write_weekly_pr_summary(repo_name=repo_name, model=ModelType.GPT_4O) if __name__ == "__main__": From 42058211be51c487963d9b2e8af8fdbb13740535 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 00:35:10 +0200 Subject: [PATCH 31/47] Add a function to retrieve merged PRs --- camel/toolkits/github_toolkit.py | 14 ++++---- test/toolkits/test_github_toolkit.py | 52 +++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 72ac1a4b9..11812b46e 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -14,8 +14,8 @@ import os from dataclasses import dataclass -from typing import List, Optional from datetime import datetime, timedelta +from typing import List, Optional from camel.functions import OpenAIFunction @@ -244,17 +244,19 @@ def retrieve_issue(self, issue_number: int) -> Optional[str]: return issue.summary() return None - def retrieve_merged_pull_requests(self, days: int) -> list: - """ - Retrieves a summary of merged pull requests from the repository. - The summary will be provided for the last 7 days. + def retrieve_merged_pull_requests(self, days: int) -> List: + r"""Retrieves a summary of merged pull requests from the repository. + The summary will be provided for the last specified number of days. + + Args: + days (int): The number of days to retrieve merged pull requests for. Returns: list: A list of merged pull request summaries. """ pull_requests = self.repo.get_pulls(state='closed') merged_prs = [] - earliest_date: datetime = datetime.utcnow() - timedelta(days=14) + earliest_date: datetime = datetime.utcnow() - timedelta(days=days) for pr in pull_requests: if ( diff --git a/test/toolkits/test_github_toolkit.py b/test/toolkits/test_github_toolkit.py index 91ef76329..9e800d781 100644 --- a/test/toolkits/test_github_toolkit.py +++ b/test/toolkits/test_github_toolkit.py @@ -12,12 +12,18 @@ # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from datetime import datetime from unittest.mock import MagicMock, patch from github import Auth, Github from github.ContentFile import ContentFile -from camel.toolkits.github_toolkit import GithubIssue, GithubToolkit +from camel.toolkits.github_toolkit import ( + GithubIssue, + GithubPullRequest, + GithubPullRequestDiff, + GithubToolkit, +) @patch.object(Github, '__init__', lambda self, *args, **kwargs: None) @@ -162,6 +168,50 @@ def test_create_pull_request(monkeypatch): ), f"Expected {expected_response}, but got {pr}" +@patch.object(Github, 'get_repo', return_value=MagicMock()) +@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) +def test_retrieve_merged_pull_requests(monkeypatch): + # Call the constructor of the GithubToolkit class + github_toolkit = GithubToolkit("repo_name", "token") + + # Create a mock file + mock_file = MagicMock() + mock_file.filename = "path/to/file" + mock_file.diff = "This is the diff of the file" + + # Create a mock pull request + mock_pull_request = MagicMock() + mock_pull_request.title = "Test PR" + mock_pull_request.body = "This is a test issue" + mock_pull_request.merged_at = datetime.utcnow() + + # Create a mock file + mock_file = MagicMock() + mock_file.filename = "path/to/file" + mock_file.patch = "This is the diff of the file" + + # Mock the get_files method of the mock_pull_request instance to return a list containing the mock file object + mock_pull_request.get_files.return_value = [mock_file] + + # Mock the get_issues method of the mock repo instance to return a list containing the mock issue object + github_toolkit.repo.get_pulls.return_value = [mock_pull_request] + + pull_requests = github_toolkit.retrieve_merged_pull_requests(days=7) + # Assert the returned issue list + expected_pull_request = GithubPullRequest( + title="Test PR", + body="This is a test issue", + diffs=[ + GithubPullRequestDiff( + filename="path/to/file", patch="This is the diff of the file" + ) + ], + ) + assert pull_requests == [ + expected_pull_request.summary() + ], f"Expected {expected_pull_request.summary()}, but got {pull_requests}" + + def test_github_issue(): # Create a GithubIssue object issue = GithubIssue( From 73f5d51b790cdad6d79ad2d645966dd23be73ba3 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 00:43:19 +0200 Subject: [PATCH 32/47] support multiple types of PRs --- camel/toolkits/github_toolkit.py | 14 +++++++++++--- examples/function_call/github_examples.py | 3 ++- test/toolkits/test_github_toolkit.py | 4 ++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 11812b46e..7702640d8 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -183,7 +183,7 @@ def get_tools(self) -> List[OpenAIFunction]: OpenAIFunction(self.retrieve_issue_list), OpenAIFunction(self.retrieve_issue), OpenAIFunction(self.create_pull_request), - OpenAIFunction(self.retrieve_merged_pull_requests), + OpenAIFunction(self.retrieve_pull_requests), ] def get_github_access_token(self) -> str: @@ -244,17 +244,25 @@ def retrieve_issue(self, issue_number: int) -> Optional[str]: return issue.summary() return None - def retrieve_merged_pull_requests(self, days: int) -> List: + def retrieve_pull_requests( + self, days: int, state: Optional[str] = None + ) -> List: r"""Retrieves a summary of merged pull requests from the repository. The summary will be provided for the last specified number of days. Args: days (int): The number of days to retrieve merged pull requests for. + state (int): A specific state of PRs to retrieve (open/closed). Returns: list: A list of merged pull request summaries. """ - pull_requests = self.repo.get_pulls(state='closed') + + if state is None: + pull_requests = self.repo.get_pulls() + else: + pull_requests = self.repo.get_pulls(state=state) + merged_prs = [] earliest_date: datetime = datetime.utcnow() - timedelta(days=days) diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index 4e4349e13..1cbb575ab 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -24,7 +24,8 @@ def write_weekly_pr_summary(repo_name, model=None): prompt = """ You need to write a summary of the pull requests that were merged in the last week. - You can use the provided github function retrieve_merged_pull_requests to retrieve the list of pull requests that were merged in the last week. + You can use the provided github function retrieve_pull_requests to retrieve the list of pull requests that were merged in the last week. + You have to pass the number of days as the first parameter to retrieve_pull_requests and state='closed' as the second parameter. The function will return a list of pull requests with the following properties: title, body, and diffs. Diffs is a list of dictionaries with the following properties: filename, diff. You will have to look closely at each diff to understand the changes that were made in each pull request. diff --git a/test/toolkits/test_github_toolkit.py b/test/toolkits/test_github_toolkit.py index 9e800d781..796871927 100644 --- a/test/toolkits/test_github_toolkit.py +++ b/test/toolkits/test_github_toolkit.py @@ -170,7 +170,7 @@ def test_create_pull_request(monkeypatch): @patch.object(Github, 'get_repo', return_value=MagicMock()) @patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_retrieve_merged_pull_requests(monkeypatch): +def test_retrieve_pull_requests(monkeypatch): # Call the constructor of the GithubToolkit class github_toolkit = GithubToolkit("repo_name", "token") @@ -196,7 +196,7 @@ def test_retrieve_merged_pull_requests(monkeypatch): # Mock the get_issues method of the mock repo instance to return a list containing the mock issue object github_toolkit.repo.get_pulls.return_value = [mock_pull_request] - pull_requests = github_toolkit.retrieve_merged_pull_requests(days=7) + pull_requests = github_toolkit.retrieve_pull_requests(days=7) # Assert the returned issue list expected_pull_request = GithubPullRequest( title="Test PR", From 29320d49ba840c3c3a5165fc405cd047ae4b1179 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 00:45:08 +0200 Subject: [PATCH 33/47] update docstream --- camel/toolkits/github_toolkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 7702640d8..f5739d87d 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -252,7 +252,7 @@ def retrieve_pull_requests( Args: days (int): The number of days to retrieve merged pull requests for. - state (int): A specific state of PRs to retrieve (open/closed). + state (Optional[str]): A specific state of PRs to retrieve (open/closed). Returns: list: A list of merged pull request summaries. From c7d592cea71dd54827fa370e0dd2580b56eab4e7 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 00:46:43 +0200 Subject: [PATCH 34/47] update docstream --- camel/toolkits/github_toolkit.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index f5739d87d..98f5f355d 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -74,13 +74,10 @@ def summary(self) -> str: @dataclass class GithubPullRequestDiff: - """ - Represents a single diff of a pull request on Github. - """ + r"""Represents a single diff of a pull request on Github.""" - def __init__(self, filename: str, patch: str): - """ - Initialize a GithubPullRequestDiff object. + def __init__(self, filename: str, patch: str) -> None: + r"""Initialize a GithubPullRequestDiff object. Args: filename (str): The name of the file that was changed. @@ -89,17 +86,14 @@ def __init__(self, filename: str, patch: str): self.filename = filename self.patch = patch - def summary(self): - """ - Returns a summary of the diff. - """ + def summary(self) -> str: + r"""Returns a summary of the diff.""" return f"Filename: {self.filename}\nPatch: {self.patch}" @dataclass class GithubPullRequest: - """ - Represents a pull request on Github. + r"""Represents a pull request on Github. Attributes: title (str): The title of the pull request. @@ -113,9 +107,8 @@ def __init__( title: str, body: str, diffs: list[GithubPullRequestDiff], - ): - """ - Initialize a GithubPullRequest object. + ) -> None: + r"""Initialize a GithubPullRequest object. Args: title (str): The title of the GitHub pull request. @@ -126,10 +119,8 @@ def __init__( self.body = body self.diffs = diffs - def summary(self): - """ - Returns a summary of the pull request. - """ + def summary(self) -> str: + r"""Returns a summary of the pull request.""" diff_summaries = '\n'.join(diff.summary() for diff in self.diffs) return ( f"Title: {self.title}\n" From dc23185fe756856fd62caaa1fb085a90d4acfb58 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 00:48:25 +0200 Subject: [PATCH 35/47] syntax updates --- examples/function_call/github_examples.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index 1cbb575ab..c0792c604 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -17,7 +17,6 @@ from camel.configs import ChatGPTConfig from camel.messages import BaseMessage from camel.toolkits import GithubToolkit -from camel.types import ModelType from camel.utils import print_text_animated @@ -116,7 +115,7 @@ def solve_issue( def main(model=None) -> None: repo_name = "camel-ai/camel" - write_weekly_pr_summary(repo_name=repo_name, model=ModelType.GPT_4O) + write_weekly_pr_summary(repo_name=repo_name, model=model) if __name__ == "__main__": From 47793d0c253a5be70aa38696c21642feb01d758a Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 00:53:11 +0200 Subject: [PATCH 36/47] syntax updates --- camel/toolkits/github_toolkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 98f5f355d..c00734b83 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -87,7 +87,7 @@ def __init__(self, filename: str, patch: str) -> None: self.patch = patch def summary(self) -> str: - r"""Returns a summary of the diff.""" + r"""Returns a summary of this diff.""" return f"Filename: {self.filename}\nPatch: {self.patch}" From 0073d17024200d76b76e5124337386e2694685be Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 00:56:52 +0200 Subject: [PATCH 37/47] remove test temporary --- camel/toolkits/github_toolkit.py | 2 +- test/toolkits/test_github_toolkit.py | 78 ---------------------------- 2 files changed, 1 insertion(+), 79 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index c00734b83..1cc8bdcd3 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -159,7 +159,7 @@ def __init__( except ImportError: raise ImportError( "Please install `github` first. You can install it by running " - "`pip install wikipedia`." + "`pip install pygithub`." ) self.github = Github(auth=Auth.Token(access_token)) self.repo = self.github.get_repo(repo_name) diff --git a/test/toolkits/test_github_toolkit.py b/test/toolkits/test_github_toolkit.py index 796871927..c68f816e7 100644 --- a/test/toolkits/test_github_toolkit.py +++ b/test/toolkits/test_github_toolkit.py @@ -12,7 +12,6 @@ # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== -from datetime import datetime from unittest.mock import MagicMock, patch from github import Auth, Github @@ -20,8 +19,6 @@ from camel.toolkits.github_toolkit import ( GithubIssue, - GithubPullRequest, - GithubPullRequestDiff, GithubToolkit, ) @@ -166,78 +163,3 @@ def test_create_pull_request(monkeypatch): assert ( pr == expected_response ), f"Expected {expected_response}, but got {pr}" - - -@patch.object(Github, 'get_repo', return_value=MagicMock()) -@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) -def test_retrieve_pull_requests(monkeypatch): - # Call the constructor of the GithubToolkit class - github_toolkit = GithubToolkit("repo_name", "token") - - # Create a mock file - mock_file = MagicMock() - mock_file.filename = "path/to/file" - mock_file.diff = "This is the diff of the file" - - # Create a mock pull request - mock_pull_request = MagicMock() - mock_pull_request.title = "Test PR" - mock_pull_request.body = "This is a test issue" - mock_pull_request.merged_at = datetime.utcnow() - - # Create a mock file - mock_file = MagicMock() - mock_file.filename = "path/to/file" - mock_file.patch = "This is the diff of the file" - - # Mock the get_files method of the mock_pull_request instance to return a list containing the mock file object - mock_pull_request.get_files.return_value = [mock_file] - - # Mock the get_issues method of the mock repo instance to return a list containing the mock issue object - github_toolkit.repo.get_pulls.return_value = [mock_pull_request] - - pull_requests = github_toolkit.retrieve_pull_requests(days=7) - # Assert the returned issue list - expected_pull_request = GithubPullRequest( - title="Test PR", - body="This is a test issue", - diffs=[ - GithubPullRequestDiff( - filename="path/to/file", patch="This is the diff of the file" - ) - ], - ) - assert pull_requests == [ - expected_pull_request.summary() - ], f"Expected {expected_pull_request.summary()}, but got {pull_requests}" - - -def test_github_issue(): - # Create a GithubIssue object - issue = GithubIssue( - title="Test Issue", - body="This is a test issue", - number=1, - file_path="path/to/file", - file_content="This is the content of the file", - ) - - # Assert the attributes of the GithubIssue object - assert issue.title == "Test Issue" - assert issue.body == "This is a test issue" - assert issue.number == 1 - assert issue.file_path == "path/to/file" - assert issue.file_content == "This is the content of the file" - - # Test the summary method - summary = issue.summary() - expected_summary = ( - f"Title: {issue.title}\n" - f"Body: {issue.body}\n" - f"Number: {issue.number}\n" - f"File Path: {issue.file_path}\n" - f"File Content: {issue.file_content}" - ) - assert ( - summary == expected_summary - ), f"Expected {expected_summary}, but got {summary}" From dde63262d69ab8d7ec54acc907c6bad0c4291f4d Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 00:59:18 +0200 Subject: [PATCH 38/47] fix docstream --- camel/toolkits/github_toolkit.py | 2 +- test/toolkits/test_github_toolkit.py | 78 ++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 1cc8bdcd3..ecc472395 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -243,7 +243,7 @@ def retrieve_pull_requests( Args: days (int): The number of days to retrieve merged pull requests for. - state (Optional[str]): A specific state of PRs to retrieve (open/closed). + state (str, optional): A specific state of PRs to retrieve (open/closed). Returns: list: A list of merged pull request summaries. diff --git a/test/toolkits/test_github_toolkit.py b/test/toolkits/test_github_toolkit.py index c68f816e7..796871927 100644 --- a/test/toolkits/test_github_toolkit.py +++ b/test/toolkits/test_github_toolkit.py @@ -12,6 +12,7 @@ # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from datetime import datetime from unittest.mock import MagicMock, patch from github import Auth, Github @@ -19,6 +20,8 @@ from camel.toolkits.github_toolkit import ( GithubIssue, + GithubPullRequest, + GithubPullRequestDiff, GithubToolkit, ) @@ -163,3 +166,78 @@ def test_create_pull_request(monkeypatch): assert ( pr == expected_response ), f"Expected {expected_response}, but got {pr}" + + +@patch.object(Github, 'get_repo', return_value=MagicMock()) +@patch.object(Auth.Token, '__init__', lambda self, *args, **kwargs: None) +def test_retrieve_pull_requests(monkeypatch): + # Call the constructor of the GithubToolkit class + github_toolkit = GithubToolkit("repo_name", "token") + + # Create a mock file + mock_file = MagicMock() + mock_file.filename = "path/to/file" + mock_file.diff = "This is the diff of the file" + + # Create a mock pull request + mock_pull_request = MagicMock() + mock_pull_request.title = "Test PR" + mock_pull_request.body = "This is a test issue" + mock_pull_request.merged_at = datetime.utcnow() + + # Create a mock file + mock_file = MagicMock() + mock_file.filename = "path/to/file" + mock_file.patch = "This is the diff of the file" + + # Mock the get_files method of the mock_pull_request instance to return a list containing the mock file object + mock_pull_request.get_files.return_value = [mock_file] + + # Mock the get_issues method of the mock repo instance to return a list containing the mock issue object + github_toolkit.repo.get_pulls.return_value = [mock_pull_request] + + pull_requests = github_toolkit.retrieve_pull_requests(days=7) + # Assert the returned issue list + expected_pull_request = GithubPullRequest( + title="Test PR", + body="This is a test issue", + diffs=[ + GithubPullRequestDiff( + filename="path/to/file", patch="This is the diff of the file" + ) + ], + ) + assert pull_requests == [ + expected_pull_request.summary() + ], f"Expected {expected_pull_request.summary()}, but got {pull_requests}" + + +def test_github_issue(): + # Create a GithubIssue object + issue = GithubIssue( + title="Test Issue", + body="This is a test issue", + number=1, + file_path="path/to/file", + file_content="This is the content of the file", + ) + + # Assert the attributes of the GithubIssue object + assert issue.title == "Test Issue" + assert issue.body == "This is a test issue" + assert issue.number == 1 + assert issue.file_path == "path/to/file" + assert issue.file_content == "This is the content of the file" + + # Test the summary method + summary = issue.summary() + expected_summary = ( + f"Title: {issue.title}\n" + f"Body: {issue.body}\n" + f"Number: {issue.number}\n" + f"File Path: {issue.file_path}\n" + f"File Content: {issue.file_content}" + ) + assert ( + summary == expected_summary + ), f"Expected {expected_summary}, but got {summary}" From c6fd2c1ada716e9359915a2068bbd864a5076fcb Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Sun, 26 May 2024 01:03:15 +0200 Subject: [PATCH 39/47] fix syntax error --- camel/toolkits/github_toolkit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index ecc472395..32914929c 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -106,14 +106,14 @@ def __init__( self, title: str, body: str, - diffs: list[GithubPullRequestDiff], + diffs: List[GithubPullRequestDiff], ) -> None: r"""Initialize a GithubPullRequest object. Args: title (str): The title of the GitHub pull request. body (str): The body/content of the GitHub pull request. - diffs (list[GithubPullRequestDiff]): A list of diffs for the pull request. + diffs (List[GithubPullRequestDiff]): A list of diffs for the pull request. """ self.title = title self.body = body From 646629602566f605fceb4f9a49bda8d8ebe74368 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Wed, 29 May 2024 16:55:08 +0100 Subject: [PATCH 40/47] fix code review --- camel/toolkits/github_toolkit.py | 58 ++++++++--------------- examples/function_call/github_examples.py | 13 +++-- test/toolkits/test_github_toolkit.py | 16 ++++--- 3 files changed, 40 insertions(+), 47 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 32914929c..8ea91c922 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -57,8 +57,8 @@ def __init__( self.file_path = file_path self.file_content = file_content - def summary(self) -> str: - r"""Returns a summary of the issue. + def __str__(self) -> str: + r"""Returns a string representation of the issue. Returns: str: A string containing the title, body, number, file path, and file content of the issue. @@ -74,20 +74,19 @@ def summary(self) -> str: @dataclass class GithubPullRequestDiff: - r"""Represents a single diff of a pull request on Github.""" + r"""Represents a single diff of a pull request on Github. - def __init__(self, filename: str, patch: str) -> None: - r"""Initialize a GithubPullRequestDiff object. + Attributes: + filename (str): The name of the file that was changed. + patch (str): The diff patch for the file. + """ - Args: - filename (str): The name of the file that was changed. - patch (str): The diff patch for the file. - """ + def __init__(self, filename: str, patch: str) -> None: self.filename = filename self.patch = patch - def summary(self) -> str: - r"""Returns a summary of this diff.""" + def __str__(self) -> str: + r"""Returns a string representation of this diff.""" return f"Filename: {self.filename}\nPatch: {self.patch}" @@ -96,10 +95,9 @@ class GithubPullRequest: r"""Represents a pull request on Github. Attributes: - title (str): The title of the pull request. - body (str): The body/content of the pull request. - file_path (str): The path of the file associated with the pull request. - file_content (str): The content of the file associated with the pull request. + title (str): The title of the GitHub pull request. + body (str): The body/content of the GitHub pull request. + diffs (List[GithubPullRequestDiff]): A list of diffs for the pull request. """ def __init__( @@ -108,20 +106,13 @@ def __init__( body: str, diffs: List[GithubPullRequestDiff], ) -> None: - r"""Initialize a GithubPullRequest object. - - Args: - title (str): The title of the GitHub pull request. - body (str): The body/content of the GitHub pull request. - diffs (List[GithubPullRequestDiff]): A list of diffs for the pull request. - """ self.title = title self.body = body self.diffs = diffs - def summary(self) -> str: - r"""Returns a summary of the pull request.""" - diff_summaries = '\n'.join(diff.summary() for diff in self.diffs) + def __str__(self) -> str: + r"""Returns a string representation of the pull request.""" + diff_summaries = '\n'.join(str(diff) for diff in self.diffs) return ( f"Title: {self.title}\n" f"Body: {self.body}\n" @@ -232,28 +223,21 @@ def retrieve_issue(self, issue_number: int) -> Optional[str]: issues = self.retrieve_issue_list() for issue in issues: if issue.number == issue_number: - return issue.summary() + return str(issue) return None - def retrieve_pull_requests( - self, days: int, state: Optional[str] = None - ) -> List: + def retrieve_pull_requests(self, days: int, state: str) -> List: r"""Retrieves a summary of merged pull requests from the repository. The summary will be provided for the last specified number of days. Args: days (int): The number of days to retrieve merged pull requests for. - state (str, optional): A specific state of PRs to retrieve (open/closed). + state (str): A specific state of PRs to retrieve. Can be open or closed. Returns: list: A list of merged pull request summaries. """ - - if state is None: - pull_requests = self.repo.get_pulls() - else: - pull_requests = self.repo.get_pulls(state=state) - + pull_requests = self.repo.get_pulls(state=state) merged_prs = [] earliest_date: datetime = datetime.utcnow() - timedelta(days=days) @@ -272,7 +256,7 @@ def retrieve_pull_requests( diff = GithubPullRequestDiff(file.filename, file.patch) pr_details.diffs.append(diff) - merged_prs.append(pr_details.summary()) + merged_prs.append(str(pr_details)) return merged_prs def create_pull_request( diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index c0792c604..ec603e622 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -11,10 +11,13 @@ # See the License for the specific language governing permissions and # limitations under the License. # =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import argparse + from colorama import Fore from camel.agents import ChatAgent from camel.configs import ChatGPTConfig +from camel.functions import OpenAIFunction from camel.messages import BaseMessage from camel.toolkits import GithubToolkit from camel.utils import print_text_animated @@ -46,14 +49,14 @@ def write_weekly_pr_summary(repo_name, model=None): """, ) assistant_model_config = FunctionCallingConfig.from_openai_function_list( - function_list=toolkit.get_tools(), + function_list=[OpenAIFunction(toolkit.retrieve_pull_requests)], kwargs=dict(temperature=0.0), ) agent = ChatAgent( assistant_sys_msg, model_type=model, model_config=assistant_model_config, - function_list=toolkit.get_tools(), + function_list=[OpenAIFunction(toolkit.retrieve_pull_requests)], ) agent.reset() @@ -114,7 +117,11 @@ def solve_issue( def main(model=None) -> None: - repo_name = "camel-ai/camel" + parser = argparse.ArgumentParser(description='Enter repo name.') + parser.add_argument('repo_name', type=str, help='Name of the repository') + args = parser.parse_args() + + repo_name = args.repo_name write_weekly_pr_summary(repo_name=repo_name, model=model) diff --git a/test/toolkits/test_github_toolkit.py b/test/toolkits/test_github_toolkit.py index 796871927..a0728c043 100644 --- a/test/toolkits/test_github_toolkit.py +++ b/test/toolkits/test_github_toolkit.py @@ -124,9 +124,9 @@ def test_retrieve_issue(monkeypatch): file_path="path/to/file", file_content="This is the content of the file", ) - assert ( - issue == expected_issue.summary() - ), f"Expected {expected_issue.summary()}, but got {issue}" + assert issue == str( + expected_issue + ), f"Expected {expected_issue}, but got {issue}" @patch.object(Github, 'get_repo', return_value=MagicMock()) @@ -196,7 +196,9 @@ def test_retrieve_pull_requests(monkeypatch): # Mock the get_issues method of the mock repo instance to return a list containing the mock issue object github_toolkit.repo.get_pulls.return_value = [mock_pull_request] - pull_requests = github_toolkit.retrieve_pull_requests(days=7) + pull_requests = github_toolkit.retrieve_pull_requests( + days=7, state='closed' + ) # Assert the returned issue list expected_pull_request = GithubPullRequest( title="Test PR", @@ -208,8 +210,8 @@ def test_retrieve_pull_requests(monkeypatch): ], ) assert pull_requests == [ - expected_pull_request.summary() - ], f"Expected {expected_pull_request.summary()}, but got {pull_requests}" + str(expected_pull_request) + ], f"Expected {expected_pull_request}, but got {pull_requests}" def test_github_issue(): @@ -230,7 +232,7 @@ def test_github_issue(): assert issue.file_content == "This is the content of the file" # Test the summary method - summary = issue.summary() + summary = str(issue) expected_summary = ( f"Title: {issue.title}\n" f"Body: {issue.body}\n" From cb1b714e1b44945d3518aaf8501d6c7558850645 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 30 May 2024 10:18:37 +0100 Subject: [PATCH 41/47] fix errors --- examples/function_call/github_examples.py | 8 ++++---- pyproject.toml | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index ec603e622..be55506b5 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -48,15 +48,15 @@ def write_weekly_pr_summary(repo_name, model=None): of an open source project {repo_name} on the project's blog. """, ) - assistant_model_config = FunctionCallingConfig.from_openai_function_list( - function_list=[OpenAIFunction(toolkit.retrieve_pull_requests)], - kwargs=dict(temperature=0.0), + assistant_model_config = ChatGPTConfig( + tools=[OpenAIFunction(toolkit.retrieve_pull_requests)], + temperature=0.0, ) agent = ChatAgent( assistant_sys_msg, model_type=model, model_config=assistant_model_config, - function_list=[OpenAIFunction(toolkit.retrieve_pull_requests)], + tools=[OpenAIFunction(toolkit.retrieve_pull_requests)], ) agent.reset() diff --git a/pyproject.toml b/pyproject.toml index e8aef46f5..251ee8d87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ cohere = { version = "^4.56", optional = true } pytest = { version = "^7", optional = true} mock = { version = "^5", optional = true} + [tool.poetry.extras] test = ["pytest", "mock"] From ddfcfd91a50ed0598fe0b55460a95b5da7bc3632 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 30 May 2024 11:07:10 +0100 Subject: [PATCH 42/47] added max amount of PRs --- camel/toolkits/github_toolkit.py | 7 +++++-- examples/function_call/github_examples.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 8ea91c922..e63581ea2 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -226,13 +226,16 @@ def retrieve_issue(self, issue_number: int) -> Optional[str]: return str(issue) return None - def retrieve_pull_requests(self, days: int, state: str) -> List: + def retrieve_pull_requests( + self, days: int, state: str, max_prs: int + ) -> List: r"""Retrieves a summary of merged pull requests from the repository. The summary will be provided for the last specified number of days. Args: days (int): The number of days to retrieve merged pull requests for. state (str): A specific state of PRs to retrieve. Can be open or closed. + max_prs (int): The maximum number of PRs to retrieve. Returns: list: A list of merged pull request summaries. @@ -241,7 +244,7 @@ def retrieve_pull_requests(self, days: int, state: str) -> List: merged_prs = [] earliest_date: datetime = datetime.utcnow() - timedelta(days=days) - for pr in pull_requests: + for pr in pull_requests[:max_prs]: if ( pr.merged and pr.merged_at is not None diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index be55506b5..21ed2ef88 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -27,6 +27,7 @@ def write_weekly_pr_summary(repo_name, model=None): prompt = """ You need to write a summary of the pull requests that were merged in the last week. You can use the provided github function retrieve_pull_requests to retrieve the list of pull requests that were merged in the last week. + The maximum amount of PRs to analyze is 3. You have to pass the number of days as the first parameter to retrieve_pull_requests and state='closed' as the second parameter. The function will return a list of pull requests with the following properties: title, body, and diffs. Diffs is a list of dictionaries with the following properties: filename, diff. From 210d809f01c173403196af3a9281f759534701f6 Mon Sep 17 00:00:00 2001 From: Ruslan Serebriakov Date: Thu, 30 May 2024 11:12:08 +0100 Subject: [PATCH 43/47] update tests --- test/toolkits/test_github_toolkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/toolkits/test_github_toolkit.py b/test/toolkits/test_github_toolkit.py index a0728c043..803045a94 100644 --- a/test/toolkits/test_github_toolkit.py +++ b/test/toolkits/test_github_toolkit.py @@ -197,7 +197,7 @@ def test_retrieve_pull_requests(monkeypatch): github_toolkit.repo.get_pulls.return_value = [mock_pull_request] pull_requests = github_toolkit.retrieve_pull_requests( - days=7, state='closed' + days=7, state='closed', max_prs=3 ) # Assert the returned issue list expected_pull_request = GithubPullRequest( From 1cfc6c7cbed8cbe159555570fd50262b14c826cc Mon Sep 17 00:00:00 2001 From: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Date: Sun, 16 Jun 2024 22:33:06 +0800 Subject: [PATCH 44/47] Update camel/toolkits/github_toolkit.py --- camel/toolkits/github_toolkit.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index e63581ea2..922f744cc 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -81,9 +81,8 @@ class GithubPullRequestDiff: patch (str): The diff patch for the file. """ - def __init__(self, filename: str, patch: str) -> None: - self.filename = filename - self.patch = patch + filename: str + patch: str def __str__(self) -> str: r"""Returns a string representation of this diff.""" From 2995e55973b67710295de0051eec146c56d50ea1 Mon Sep 17 00:00:00 2001 From: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Date: Sun, 16 Jun 2024 22:33:11 +0800 Subject: [PATCH 45/47] Update camel/toolkits/github_toolkit.py --- camel/toolkits/github_toolkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 922f744cc..4715e801d 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -227,7 +227,7 @@ def retrieve_issue(self, issue_number: int) -> Optional[str]: def retrieve_pull_requests( self, days: int, state: str, max_prs: int - ) -> List: + ) -> List[str]: r"""Retrieves a summary of merged pull requests from the repository. The summary will be provided for the last specified number of days. From 2d3cf226f09c75df3b7ba6ff6d4ab2f50e7aae5d Mon Sep 17 00:00:00 2001 From: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Date: Sun, 16 Jun 2024 22:33:17 +0800 Subject: [PATCH 46/47] Update camel/toolkits/github_toolkit.py --- camel/toolkits/github_toolkit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index 4715e801d..623228d5a 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -237,7 +237,7 @@ def retrieve_pull_requests( max_prs (int): The maximum number of PRs to retrieve. Returns: - list: A list of merged pull request summaries. + List[str]: A list of merged pull request summaries. """ pull_requests = self.repo.get_pulls(state=state) merged_prs = [] From 7c9cb2dbc6a83da23e65eb4ebc3cb569ecfa19ce Mon Sep 17 00:00:00 2001 From: Wendong Date: Sun, 16 Jun 2024 22:37:55 +0800 Subject: [PATCH 47/47] update --- camel/toolkits/github_toolkit.py | 21 ++++++++---------- examples/function_call/github_examples.py | 27 +++++++++++++++-------- test/toolkits/test_github_toolkit.py | 6 +++-- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/camel/toolkits/github_toolkit.py b/camel/toolkits/github_toolkit.py index ae58fb400..caa8252fd 100644 --- a/camel/toolkits/github_toolkit.py +++ b/camel/toolkits/github_toolkit.py @@ -98,18 +98,13 @@ class GithubPullRequest: Attributes: title (str): The title of the GitHub pull request. body (str): The body/content of the GitHub pull request. - diffs (List[GithubPullRequestDiff]): A list of diffs for the pull request. + diffs (List[GithubPullRequestDiff]): A list of diffs for the pull + request. """ - def __init__( - self, - title: str, - body: str, - diffs: List[GithubPullRequestDiff], - ) -> None: - self.title = title - self.body = body - self.diffs = diffs + title: str + body: str + diffs: List[GithubPullRequestDiff] def __str__(self) -> str: r"""Returns a string representation of the pull request.""" @@ -240,8 +235,10 @@ def retrieve_pull_requests( The summary will be provided for the last specified number of days. Args: - days (int): The number of days to retrieve merged pull requests for. - state (str): A specific state of PRs to retrieve. Can be open or closed. + days (int): The number of days to retrieve merged pull requests + for. + state (str): A specific state of PRs to retrieve. Can be open or + closed. max_prs (int): The maximum number of PRs to retrieve. Returns: diff --git a/examples/function_call/github_examples.py b/examples/function_call/github_examples.py index b962ec3e6..cfd9ea657 100644 --- a/examples/function_call/github_examples.py +++ b/examples/function_call/github_examples.py @@ -25,18 +25,26 @@ def write_weekly_pr_summary(repo_name, model=None): prompt = """ - You need to write a summary of the pull requests that were merged in the last week. - You can use the provided github function retrieve_pull_requests to retrieve the list of pull requests that were merged in the last week. + You need to write a summary of the pull requests that were merged in the + last week. + You can use the provided github function retrieve_pull_requests to + retrieve the list of pull requests that were merged in the last week. The maximum amount of PRs to analyze is 3. - You have to pass the number of days as the first parameter to retrieve_pull_requests and state='closed' as the second parameter. - The function will return a list of pull requests with the following properties: title, body, and diffs. - Diffs is a list of dictionaries with the following properties: filename, diff. - You will have to look closely at each diff to understand the changes that were made in each pull request. - Output a twitter post that describes recent changes in the project and thanks the contributors. + You have to pass the number of days as the first parameter to + retrieve_pull_requests and state='closed' as the second parameter. + The function will return a list of pull requests with the following + properties: title, body, and diffs. + Diffs is a list of dictionaries with the following properties: filename, + diff. + You will have to look closely at each diff to understand the changes that + were made in each pull request. + Output a twitter post that describes recent changes in the project and + thanks the contributors. Here is an example of a summary for one pull request: 📢 We've improved function calling in the 🐪 CAMEL-AI framework! - This update enhances the handling of various docstring styles and supports enum types, ensuring more accurate and reliable function calls. + This update enhances the handling of various docstring styles and supports + enum types, ensuring more accurate and reliable function calls. Thanks to our contributor Jiahui Zhang for making this possible. """ print(Fore.YELLOW + f"Final prompt:\n{prompt}\n") @@ -45,7 +53,8 @@ def write_weekly_pr_summary(repo_name, model=None): assistant_sys_msg = BaseMessage.make_assistant_message( role_name="Marketing Manager", content=f""" - You are an experienced marketing manager responsible for posting weekly updates about the status + You are an experienced marketing manager responsible for posting + weekly updates about the status of an open source project {repo_name} on the project's blog. """, ) diff --git a/test/toolkits/test_github_toolkit.py b/test/toolkits/test_github_toolkit.py index 5fa54e84c..714697aff 100644 --- a/test/toolkits/test_github_toolkit.py +++ b/test/toolkits/test_github_toolkit.py @@ -193,10 +193,12 @@ def test_retrieve_pull_requests(monkeypatch): mock_file.filename = "path/to/file" mock_file.patch = "This is the diff of the file" - # Mock the get_files method of the mock_pull_request instance to return a list containing the mock file object + # Mock the get_files method of the mock_pull_request instance to return a + # list containing the mock file object mock_pull_request.get_files.return_value = [mock_file] - # Mock the get_issues method of the mock repo instance to return a list containing the mock issue object + # Mock the get_issues method of the mock repo instance to return a list + # containing the mock issue object github_toolkit.repo.get_pulls.return_value = [mock_pull_request] pull_requests = github_toolkit.retrieve_pull_requests(