diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..12dfefea --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +## [Unreleased] - 2023-07-23 + +### Added +- '/describe' operation now updates also the label of the PR + +### Changed + +### Fixed \ No newline at end of file diff --git a/pr_agent/cli.py b/pr_agent/cli.py index 4477016c..2bae6bd4 100644 --- a/pr_agent/cli.py +++ b/pr_agent/cli.py @@ -8,6 +8,7 @@ from pr_agent.tools.pr_description import PRDescription from pr_agent.tools.pr_information_from_user import PRInformationFromUser from pr_agent.tools.pr_questions import PRQuestions from pr_agent.tools.pr_reviewer import PRReviewer +from pr_agent.tools.pr_update_changelog import PRUpdateChangelog def run(args=None): @@ -27,13 +28,15 @@ ask / ask_question [question] - Ask a question about the PR. describe / describe_pr - Modify the PR title and description based on the PR's contents. improve / improve_code - Suggest improvements to the code in the PR as pull request comments ready to commit. reflect - Ask the PR author questions about the PR. +update_changelog - Update the changelog based on the PR's contents. """) parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True) parser.add_argument('command', type=str, help='The', choices=['review', 'review_pr', 'ask', 'ask_question', 'describe', 'describe_pr', 'improve', 'improve_code', - 'reflect', 'review_after_reflect'], + 'reflect', 'review_after_reflect', + 'update_changelog'], default='review') parser.add_argument('rest', nargs=argparse.REMAINDER, default=[]) args = parser.parse_args(args) @@ -49,7 +52,8 @@ reflect - Ask the PR author questions about the PR. 'review': _handle_review_command, 'review_pr': _handle_review_command, 'reflect': _handle_reflect_command, - 'review_after_reflect': _handle_review_after_reflect_command + 'review_after_reflect': _handle_review_after_reflect_command, + 'update_changelog': _handle_update_changelog, } if command in commands: commands[command](args.pr_url, args.rest) @@ -96,6 +100,10 @@ def _handle_review_after_reflect_command(pr_url: str, rest: list): reviewer = PRReviewer(pr_url, cli_mode=True, is_answer=True) asyncio.run(reviewer.review()) +def _handle_update_changelog(pr_url: str, rest: list): + print(f"Updating changlog for: {pr_url}") + reviewer = PRUpdateChangelog(pr_url, cli_mode=True) + asyncio.run(reviewer.update_changelog()) if __name__ == '__main__': run() diff --git a/pr_agent/config_loader.py b/pr_agent/config_loader.py index 7841f0b7..69d20d88 100644 --- a/pr_agent/config_loader.py +++ b/pr_agent/config_loader.py @@ -19,6 +19,7 @@ settings = Dynaconf( "settings/pr_description_prompts.toml", "settings/pr_code_suggestions_prompts.toml", "settings/pr_information_from_user_prompts.toml", + "settings/pr_update_changelog.toml", "settings_prod/.secrets.toml" ]] ) diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index fbf8ffec..33b84f79 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -1,10 +1,10 @@ [config] model="gpt-4" -fallback-models=["gpt-3.5-turbo-16k", "gpt-3.5-turbo"] +fallback-models=["gpt-3.5-turbo-16k"] git_provider="github" -publish_output=true +publish_output=false publish_output_progress=true -verbosity_level=0 # 0,1,2 +verbosity_level=2 # 0,1,2 use_extra_bad_extensions=false [pr_reviewer] @@ -24,6 +24,9 @@ publish_description_as_comment=false [pr_code_suggestions] num_code_suggestions=4 +[pr_update_changelog] +push_changelog_changes=false + [github] # The type of deployment to create. Valid values are 'app' or 'user'. deployment_type = "user" diff --git a/pr_agent/settings/pr_update_changelog.toml b/pr_agent/settings/pr_update_changelog.toml new file mode 100644 index 00000000..9fb386ea --- /dev/null +++ b/pr_agent/settings/pr_update_changelog.toml @@ -0,0 +1,30 @@ +[pr_update_changelog_prompt] +system="""You are a language model called CodiumAI-PR-Code-Reviewer. +Your task is to update the CHANGELOG.md file of the project, based on the PR diff. +The update should be short and concise. It should match the existing CHANGELOG.md format. + +Note that the output should be only the added lines to the CHANGELOG.md file, and nothing else. + +""" + +user="""PR Info: +Title: '{{title}}' +Branch: '{{branch}}' +Description: '{{description}}' +{%- if language %} +Main language: {{language}} +{%- endif %} + + +The PR Diff: +``` +{{diff}} +``` + +The current CHANGELOG.md: +``` +{{changelog_file}} +``` + +Response: +""" diff --git a/pr_agent/tools/pr_update_changelog.py b/pr_agent/tools/pr_update_changelog.py new file mode 100644 index 00000000..71e5e6e7 --- /dev/null +++ b/pr_agent/tools/pr_update_changelog.py @@ -0,0 +1,103 @@ +import copy +import json +import logging +import textwrap +from typing import Tuple + +from jinja2 import Environment, StrictUndefined + +from pr_agent.algo.ai_handler import AiHandler +from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models +from pr_agent.algo.token_handler import TokenHandler +from pr_agent.config_loader import settings +from pr_agent.git_providers import get_git_provider, GithubProvider +from pr_agent.git_providers.git_provider import get_main_pr_language + + +class PRUpdateChangelog: + def __init__(self, pr_url: str, cli_mode=False): + + self.git_provider = get_git_provider()(pr_url) + self.main_language = get_main_pr_language( + self.git_provider.get_languages(), self.git_provider.get_files() + ) + max_lines=50 + try: + self.changelog_file = self.git_provider.repo_obj.get_contents("CHANGELOG.md", ref=self.git_provider.get_pr_branch()) + changelog_file_lines = self.changelog_file.decoded_content.decode().splitlines() + changelog_file_lines = changelog_file_lines[:max_lines] + self.changelog_file_str = "\n".join(changelog_file_lines) + except: + raise Exception("No CHANGELOG.md file found in the repository") + + self.ai_handler = AiHandler() + self.patches_diff = None + self.prediction = None + self.cli_mode = cli_mode + self.vars = { + "title": self.git_provider.pr.title, + "branch": self.git_provider.get_pr_branch(), + "description": self.git_provider.get_pr_description(), + "language": self.main_language, + "diff": "", # empty diff for initial calculation + "changelog_file": self.changelog_file_str, + } + self.token_handler = TokenHandler(self.git_provider.pr, + self.vars, + settings.pr_update_changelog_prompt.system, + settings.pr_update_changelog_prompt.user) + + async def update_changelog(self): + assert type(self.git_provider) == GithubProvider, "Currently only Github is supported" + + logging.info('Updating the changelog...') + if settings.config.publish_output: + self.git_provider.publish_comment("Preparing changelog updates...", is_temporary=True) + await retry_with_fallback_models(self._prepare_prediction) + logging.info('Preparing PR changelog updates...') + new_file_content, answer = self._prepare_changelog_update() + if settings.config.publish_output or True: + self.git_provider.remove_initial_comment() + logging.info('publishing changelog updates...') + self.git_provider.publish_comment(f"**Changelog updates:**\n\n{answer}") + if settings.pr_update_changelog_prompt.push_changelog_changes: + logging.info('Pushing PR changelog updates...') + self.push_changelog_update(new_file_content) + + async def _prepare_prediction(self, model: str): + logging.info('Getting PR diff...') + # we are using extended hunk with line numbers for code suggestions + self.patches_diff = get_pr_diff(self.git_provider, + self.token_handler, + model, + add_line_numbers_to_hunks=True, + disable_extra_lines=True) + logging.info('Getting AI prediction...') + self.prediction = await self._get_prediction(model) + + async def _get_prediction(self, model: str): + variables = copy.deepcopy(self.vars) + variables["diff"] = self.patches_diff # update diff + environment = Environment(undefined=StrictUndefined) + system_prompt = environment.from_string(settings.pr_update_changelog_prompt.system).render(variables) + user_prompt = environment.from_string(settings.pr_update_changelog_prompt.user).render(variables) + if settings.config.verbosity_level >= 2: + logging.info(f"\nSystem prompt:\n{system_prompt}") + logging.info(f"\nUser prompt:\n{user_prompt}") + response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2, + system=system_prompt, user=user_prompt) + + return response + + def _prepare_changelog_update(self) -> Tuple[str,str]: + answer = self.prediction.strip().strip("```").strip() + new_file_content = answer.strip().strip("```").strip() + "\n\n" + self.changelog_file.decoded_content.decode() + + return new_file_content, answer + + def push_changelog_update(self, new_file_content): + self.git_provider.repo_obj.update_file(path=self.changelog_file.path, + message="Update CHANGELOG.md", + content=new_file_content, + sha=self.changelog_file.sha, + branch=self.git_provider.get_pr_branch()) \ No newline at end of file