From fa90b242e3bee1ce5cd1d11e850c693ffc8b6818 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Mon, 17 Jul 2023 08:09:56 +0300 Subject: [PATCH 01/13] pr_information_from_user_prompts --- pr_agent/settings/pr_information_from_user_prompts.toml | 9 +++++---- pr_agent/tools/pr_information_from_user.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pr_agent/settings/pr_information_from_user_prompts.toml b/pr_agent/settings/pr_information_from_user_prompts.toml index c4ba1d52..f32ec715 100644 --- a/pr_agent/settings/pr_information_from_user_prompts.toml +++ b/pr_agent/settings/pr_information_from_user_prompts.toml @@ -1,16 +1,17 @@ [pr_information_from_user_prompt] system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests. -Given the PR Info and the PR Git Diff, generate 4 questions about the PR for the PR author. +Given the PR Info and the PR Git Diff, generate 3 short questions about the PR code for the PR author. The goal of the questions is to help the language model understand the PR better, so the questions should be insightful, informative, non-trivial, and relevant to the PR. -Prefer yes\\no or multiple choice questions. If you have to ask open-ended questions, make sure they are not too difficult, and can be answered in a sentence or two. +You should prefer asking yes\\no questions, or multiple choice questions. Also add at least one open-ended question, but make sure they are not too difficult, and can be answered in a sentence or two. Example output: ' Questions to better understand the PR: -1. ... -2. ... +1) ... +2) ... ... +' """ user="""PR Info: diff --git a/pr_agent/tools/pr_information_from_user.py b/pr_agent/tools/pr_information_from_user.py index 50a42799..8e5a5d98 100644 --- a/pr_agent/tools/pr_information_from_user.py +++ b/pr_agent/tools/pr_information_from_user.py @@ -66,6 +66,6 @@ class PRInformationFromUser: model_output = self.prediction.strip() if settings.config.verbosity_level >= 2: logging.info(f"answer_str:\n{model_output}") - answer_str = f"{model_output}\n\n Please respond to the question above in the following format:\n\n" + \ - f"/answer \n\n" + f"Example:\n'\n/answer\n1. Yes, because ...\n2. No, because ...\n'" + answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\ + f"\n>/answer\n>1) ...\n>2) ...\n>...\n" return answer_str From f8f415eb75a8b55f2e60bdc3d0b2e93b16a63672 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Mon, 17 Jul 2023 15:49:29 +0300 Subject: [PATCH 02/13] stable --- pr_agent/agent/pr_agent.py | 11 +++++++++-- pr_agent/cli.py | 7 ++++++- pr_agent/servers/github_action_runner.py | 10 ++++++++-- pr_agent/settings/configuration.toml | 3 ++- pr_agent/settings/pr_reviewer_prompts.toml | 10 ++++++++++ pr_agent/tools/pr_reviewer.py | 22 ++++++++++++++++++++-- 6 files changed, 55 insertions(+), 8 deletions(-) diff --git a/pr_agent/agent/pr_agent.py b/pr_agent/agent/pr_agent.py index 9a5ccf36..5d1f5c76 100644 --- a/pr_agent/agent/pr_agent.py +++ b/pr_agent/agent/pr_agent.py @@ -2,8 +2,10 @@ import re from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions 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.config_loader import settings class PRAgent: @@ -11,8 +13,13 @@ class PRAgent: pass async def handle_request(self, pr_url, request) -> bool: - if any(cmd in request for cmd in ["/review", "/review_pr"]): - await PRReviewer(pr_url).review() + if any(cmd in request for cmd in ["/answer"]): + await PRReviewer(pr_url, is_answer=True).review() + elif any(cmd in request for cmd in ["/review", "/review_pr"]): + if settings.pr_reviewer.ask_and_reflect: + await PRInformationFromUser(pr_url).generate_questions() + else: + await PRReviewer(pr_url).review() elif any(cmd in request for cmd in ["/describe", "/describe_pr"]): await PRDescription(pr_url).describe() elif any(cmd in request for cmd in ["/improve", "/improve_code"]): diff --git a/pr_agent/cli.py b/pr_agent/cli.py index a430b4d3..e11ad677 100644 --- a/pr_agent/cli.py +++ b/pr_agent/cli.py @@ -30,7 +30,8 @@ improve / improve_code - Suggest improvements to the code in the PR as pull requ 'ask', 'ask_question', 'describe', 'describe_pr', 'improve', 'improve_code', - 'user_questions'], default='review') + 'user_questions', 'user_answers'], + default='review') parser.add_argument('rest', nargs=argparse.REMAINDER, default=[]) args = parser.parse_args() logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) @@ -60,6 +61,10 @@ improve / improve_code - Suggest improvements to the code in the PR as pull requ print(f"Asking the PR author questions: {args.pr_url}") reviewer = PRInformationFromUser(args.pr_url) asyncio.run(reviewer.generate_questions()) + elif command in ['user_answers']: + print(f"Processing author answers and sending review: {args.pr_url}") + reviewer = PRReviewer(args.pr_url, cli_mode=True, is_answer=True) + asyncio.run(reviewer.review()) else: print(f"Unknown command: {command}") parser.print_help() diff --git a/pr_agent/servers/github_action_runner.py b/pr_agent/servers/github_action_runner.py index ba6ffe9c..a856cc58 100644 --- a/pr_agent/servers/github_action_runner.py +++ b/pr_agent/servers/github_action_runner.py @@ -6,6 +6,7 @@ import re from pr_agent.config_loader import settings from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions 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 @@ -53,8 +54,13 @@ async def run_action(): pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None) if pr_url: body = comment_body.strip().lower() - if any(cmd in body for cmd in ["/review", "/review_pr"]): - await PRReviewer(pr_url).review() + if any(cmd in body for cmd in ["/answer"]): + await PRReviewer(pr_url, is_answer=True).review() + elif any(cmd in body for cmd in ["/review", "/review_pr", "/answer"]): + if settings.pr_reviewer.ask_and_reflect: + await PRInformationFromUser(pr_url).generate_questions() + else: + await PRReviewer(pr_url).review() elif any(cmd in body for cmd in ["/describe", "/describe_pr"]): await PRDescription(pr_url).describe() elif any(cmd in body for cmd in ["/improve", "/improve_code"]): diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index 44355877..01bf0a86 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -2,7 +2,7 @@ model="gpt-4-0613" git_provider="github" publish_review=true -verbosity_level=2 # 0,1,2 +verbosity_level=0 # 0,1,2 [pr_reviewer] require_focused_review=true @@ -10,6 +10,7 @@ require_tests_review=true require_security_review=true num_code_suggestions=3 inline_code_comments = true +ask_and_reflect=false [pr_questions] diff --git a/pr_agent/settings/pr_reviewer_prompts.toml b/pr_agent/settings/pr_reviewer_prompts.toml index af6f3ee8..309d95d8 100644 --- a/pr_agent/settings/pr_reviewer_prompts.toml +++ b/pr_agent/settings/pr_reviewer_prompts.toml @@ -108,6 +108,16 @@ Description: '{{description}}' Main language: {{language}} {%- endif %} +{%- if question_str %} +###### +Here are questions to better understand the PR. Use the answers to provide better feedback. + +{{question_str|trim}} + +User answers: +{{answer_str|trim}} +###### +{%- endif %} The PR Git Diff: ``` diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index 6a8f33cb..c6e9d9ff 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -15,12 +15,14 @@ from pr_agent.servers.help import bot_help_text, actions_help_text class PRReviewer: - def __init__(self, pr_url: str, cli_mode=False): + def __init__(self, pr_url: str, cli_mode=False, is_answer: bool = 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() ) + self.is_answer = is_answer + answer_str = question_str = self._get_user_answers() self.ai_handler = AiHandler() self.patches_diff = None self.prediction = None @@ -35,6 +37,9 @@ class PRReviewer: "require_security": settings.pr_reviewer.require_security_review, "require_focused": settings.pr_reviewer.require_focused_review, 'num_code_suggestions': settings.pr_reviewer.num_code_suggestions, + # + 'question_str': question_str, + 'answer_str': answer_str, } self.token_handler = TokenHandler(self.git_provider.pr, self.vars, @@ -118,4 +123,17 @@ class PRReviewer: relevant_line_in_file = d['relevant line in file'].strip() content = d['suggestion content'] - self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) \ No newline at end of file + self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) + + def _get_user_answers(self): + answer_str = question_str = "" + if self.is_answer: + discussion_messages = self.git_provider.pr.get_issue_comments() + for message in discussion_messages.reversed: + if "Questions to better understand the PR:" in message.body: + question_str = message.body + elif '/answer' in message.body: + answer_str = message.body + if answer_str and question_str: + break + return question_str, answer_str From ea27c63f13508b5dc24620716ef9c1dc930a4d2b Mon Sep 17 00:00:00 2001 From: mrT23 Date: Mon, 17 Jul 2023 15:59:57 +0300 Subject: [PATCH 03/13] Insights from user's answers --- pr_agent/cli.py | 2 +- pr_agent/settings/pr_reviewer_prompts.toml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pr_agent/cli.py b/pr_agent/cli.py index e11ad677..daf60d56 100644 --- a/pr_agent/cli.py +++ b/pr_agent/cli.py @@ -62,7 +62,7 @@ improve / improve_code - Suggest improvements to the code in the PR as pull requ reviewer = PRInformationFromUser(args.pr_url) asyncio.run(reviewer.generate_questions()) elif command in ['user_answers']: - print(f"Processing author answers and sending review: {args.pr_url}") + print(f"Processing author's answers and sending review: {args.pr_url}") reviewer = PRReviewer(args.pr_url, cli_mode=True, is_answer=True) asyncio.run(reviewer.review()) else: diff --git a/pr_agent/settings/pr_reviewer_prompts.toml b/pr_agent/settings/pr_reviewer_prompts.toml index 309d95d8..99e00e6b 100644 --- a/pr_agent/settings/pr_reviewer_prompts.toml +++ b/pr_agent/settings/pr_reviewer_prompts.toml @@ -23,6 +23,12 @@ You must use the following JSON schema to format your answer: "description": "yes\\no question: does this PR have relevant tests ?" }, {%- endif %} +{%- if question_str %} + "Insights from user's answers": { + "type": "string", + "description": "shortly summarize the insights you gained from the user's answers to the questions" + }, +{%- endif %} {%- if require_focused %} "Focused PR": { "type": "string", From 539edcad3c4968bb155c5629dc00dbe2f8530c71 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Mon, 17 Jul 2023 16:53:38 +0300 Subject: [PATCH 04/13] works --- pr_agent/settings/pr_reviewer_prompts.toml | 2 +- pr_agent/tools/pr_information_from_user.py | 2 +- pr_agent/tools/pr_reviewer.py | 14 +++++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pr_agent/settings/pr_reviewer_prompts.toml b/pr_agent/settings/pr_reviewer_prompts.toml index 99e00e6b..3a8f0b51 100644 --- a/pr_agent/settings/pr_reviewer_prompts.toml +++ b/pr_agent/settings/pr_reviewer_prompts.toml @@ -24,7 +24,7 @@ You must use the following JSON schema to format your answer: }, {%- endif %} {%- if question_str %} - "Insights from user's answers": { + "Insights from user's answer": { "type": "string", "description": "shortly summarize the insights you gained from the user's answers to the questions" }, diff --git a/pr_agent/tools/pr_information_from_user.py b/pr_agent/tools/pr_information_from_user.py index 8e5a5d98..adfd9733 100644 --- a/pr_agent/tools/pr_information_from_user.py +++ b/pr_agent/tools/pr_information_from_user.py @@ -21,7 +21,7 @@ class PRInformationFromUser: self.vars = { "title": self.git_provider.pr.title, "branch": self.git_provider.get_pr_branch(), - "description": self.git_provider.get_description(), + "description": self.git_provider.get_pr_description(), "language": self.main_pr_language, "diff": "", # empty diff for initial calculation } diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index c6e9d9ff..a05aaac1 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -118,12 +118,16 @@ class PRReviewer: except json.decoder.JSONDecodeError: data = try_fix_json(review) - for d in data['PR Feedback']['Code suggestions']: - relevant_file = d['relevant file'].strip() - relevant_line_in_file = d['relevant line in file'].strip() - content = d['suggestion content'] + if settings.config.pr_reviewer > 0: + try: + for d in data['PR Feedback']['Code suggestions']: + relevant_file = d['relevant file'].strip() + relevant_line_in_file = d['relevant line in file'].strip() + content = d['suggestion content'] - self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) + self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) + except KeyError: + pass def _get_user_answers(self): answer_str = question_str = "" From 5fbaa4366f3294fbd5d48d564aac7cde66561934 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Tue, 18 Jul 2023 08:05:42 +0300 Subject: [PATCH 05/13] publish_output instead publish_review --- pr_agent/settings/configuration.toml | 2 +- pr_agent/tools/pr_code_suggestions.py | 4 ++-- pr_agent/tools/pr_description.py | 4 ++-- pr_agent/tools/pr_information_from_user.py | 4 ++-- pr_agent/tools/pr_questions.py | 4 ++-- pr_agent/tools/pr_reviewer.py | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index 01bf0a86..6fc9cb71 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -1,7 +1,7 @@ [config] model="gpt-4-0613" git_provider="github" -publish_review=true +publish_output=true verbosity_level=0 # 0,1,2 [pr_reviewer] diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index c008368a..c75b3771 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -42,7 +42,7 @@ class PRCodeSuggestions: assert type(self.git_provider) != BitbucketProvider, "Bitbucket is not supported for now" logging.info('Generating code suggestions for PR...') - if settings.config.publish_review: + if settings.config.publish_output: self.git_provider.publish_comment("Preparing review...", is_temporary=True) logging.info('Getting PR diff...') @@ -56,7 +56,7 @@ class PRCodeSuggestions: self.prediction = await self._get_prediction() logging.info('Preparing PR review...') data = self._prepare_pr_code_suggestions() - if settings.config.publish_review: + if settings.config.publish_output: logging.info('Pushing PR review...') self.git_provider.remove_initial_comment() logging.info('Pushing inline code comments...') diff --git a/pr_agent/tools/pr_description.py b/pr_agent/tools/pr_description.py index fa0423d7..789f5f08 100644 --- a/pr_agent/tools/pr_description.py +++ b/pr_agent/tools/pr_description.py @@ -36,7 +36,7 @@ class PRDescription: async def describe(self): logging.info('Generating a PR description...') - if settings.config.publish_review: + if settings.config.publish_output: self.git_provider.publish_comment("Preparing pr description...", is_temporary=True) logging.info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) @@ -44,7 +44,7 @@ class PRDescription: self.prediction = await self._get_prediction() logging.info('Preparing answer...') pr_title, pr_body = self._prepare_pr_answer() - if settings.config.publish_review: + if settings.config.publish_output: logging.info('Pushing answer...') self.git_provider.publish_description(pr_title, pr_body) self.git_provider.remove_initial_comment() diff --git a/pr_agent/tools/pr_information_from_user.py b/pr_agent/tools/pr_information_from_user.py index adfd9733..a4120cd4 100644 --- a/pr_agent/tools/pr_information_from_user.py +++ b/pr_agent/tools/pr_information_from_user.py @@ -34,7 +34,7 @@ class PRInformationFromUser: async def generate_questions(self): logging.info('Generating question to the user...') - if settings.config.publish_review: + if settings.config.publish_output: self.git_provider.publish_comment("Preparing answer...", is_temporary=True) logging.info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) @@ -42,7 +42,7 @@ class PRInformationFromUser: self.prediction = await self._get_prediction() logging.info('Preparing questions...') pr_comment = self._prepare_pr_answer() - if settings.config.publish_review: + if settings.config.publish_output: logging.info('Pushing questions...') self.git_provider.publish_comment(pr_comment) self.git_provider.remove_initial_comment() diff --git a/pr_agent/tools/pr_questions.py b/pr_agent/tools/pr_questions.py index 8941957b..08af3797 100644 --- a/pr_agent/tools/pr_questions.py +++ b/pr_agent/tools/pr_questions.py @@ -36,7 +36,7 @@ class PRQuestions: async def answer(self): logging.info('Answering a PR question...') - if settings.config.publish_review: + if settings.config.publish_output: self.git_provider.publish_comment("Preparing answer...", is_temporary=True) logging.info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) @@ -44,7 +44,7 @@ class PRQuestions: self.prediction = await self._get_prediction() logging.info('Preparing answer...') pr_comment = self._prepare_pr_answer() - if settings.config.publish_review: + if settings.config.publish_output: logging.info('Pushing answer...') self.git_provider.publish_comment(pr_comment) self.git_provider.remove_initial_comment() diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index a05aaac1..e722971c 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -48,7 +48,7 @@ class PRReviewer: async def review(self): logging.info('Reviewing PR...') - if settings.config.publish_review: + if settings.config.publish_output: self.git_provider.publish_comment("Preparing review...", is_temporary=True) logging.info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) @@ -56,7 +56,7 @@ class PRReviewer: self.prediction = await self._get_prediction() logging.info('Preparing PR review...') pr_comment = self._prepare_pr_review() - if settings.config.publish_review: + if settings.config.publish_output: logging.info('Pushing PR review...') self.git_provider.publish_comment(pr_comment) self.git_provider.remove_initial_comment() From 51e08c3c2bcc076f55bccad9c1bc7721d35b06ab Mon Sep 17 00:00:00 2001 From: mrT23 Date: Tue, 18 Jul 2023 08:22:25 +0300 Subject: [PATCH 06/13] reflect and review + protections --- README.md | 7 +++++-- pr_agent/agent/pr_agent.py | 4 ++-- pr_agent/cli.py | 10 ++++++---- pr_agent/servers/github_action_runner.py | 5 +++-- pr_agent/tools/pr_information_from_user.py | 2 +- pr_agent/tools/pr_reviewer.py | 4 +++- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 25ddc715..f8658bdb 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ To set up your own PR-Agent, see the [Quickstart](#Quickstart) section | | Ask | :white_check_mark: | :white_check_mark: | | | | Auto-Description | :white_check_mark: | | | | | Improve Code | :white_check_mark: | :white_check_mark: | | +| | Reflect and Review | :white_check_mark: | | | | | | | | | | USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Tagging bot | :white_check_mark: | :white_check_mark: | | @@ -92,6 +93,7 @@ Examples for invoking the different tools via the [CLI](#quickstart): - **Describe**: python cli.py --pr-url= describe - **Improve**: python cli.py --pr-url= improve - **Ask**: python cli.py --pr-url= ask "Write me a poem about this PR" +- **Reflect**: python cli.py --pr-url= reflect "" is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50). @@ -120,12 +122,13 @@ Here are several ways to install and run PR-Agent: ## Usage and Tools -**PR-Agent** provides four types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"` and `"PR Code Sueggestions"`. +**PR-Agent** provides five types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"`, `"PR Code Sueggestions"` and `"PR Reflect and Review"`. - The "PR Reviewer" tool automatically analyzes PRs, and provides various types of feedback. - The "PR Q&A" tool answers free-text questions about the PR. - The "PR Description" tool automatically sets the PR Title and body. - The "PR Code Suggestion" tool provide inline code suggestions for the PR that can be applied and committed. +- The "PR Reflect and Review" tool first dialog with the user and asks him to reflect on the PR, and then provides a review. ## How it works @@ -138,11 +141,11 @@ Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more detai - [ ] Support open-source models, as a replacement for openai models. (Note - a minimal requirement for each open-source model is to have 8k+ context, and good support for generating json as an output) - [x] Support other Git providers, such as Gitlab and Bitbucket. - [ ] Develop additional logics for handling large PRs, and compressing git patches -- [ ] Dedicated tools and sub-tools for specific programming languages (Python, Javascript, Java, C++, etc) - [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags) - [ ] Adding more tools. Possible directions: - [x] PR description - [x] Inline code suggestions + - [x] Reflect and review - [ ] Enforcing CONTRIBUTING.md guidelines - [ ] Performance (are there any performance issues) - [ ] Documentation (is the PR properly documented) diff --git a/pr_agent/agent/pr_agent.py b/pr_agent/agent/pr_agent.py index 5d1f5c76..3709db1f 100644 --- a/pr_agent/agent/pr_agent.py +++ b/pr_agent/agent/pr_agent.py @@ -15,8 +15,8 @@ class PRAgent: async def handle_request(self, pr_url, request) -> bool: if any(cmd in request for cmd in ["/answer"]): await PRReviewer(pr_url, is_answer=True).review() - elif any(cmd in request for cmd in ["/review", "/review_pr"]): - if settings.pr_reviewer.ask_and_reflect: + elif any(cmd in request for cmd in ["/review", "/review_pr", "/reflect_and_review"]): + if settings.pr_reviewer.ask_and_reflect or any(cmd in request for cmd in ["/reflect_and_review"]): await PRInformationFromUser(pr_url).generate_questions() else: await PRReviewer(pr_url).review() diff --git a/pr_agent/cli.py b/pr_agent/cli.py index d681776a..ca9d5db0 100644 --- a/pr_agent/cli.py +++ b/pr_agent/cli.py @@ -18,20 +18,22 @@ For example: - cli.py --pr-url=... describe - cli.py --pr-url=... improve - cli.py --pr-url=... ask "write me a poem about this PR" +- cli.py --pr-url=... reflect Supported commands: review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement. 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. """) 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', - 'user_questions', 'user_answers'], - default='review') + 'reflect', 'review_after_reflect'], + default='review') parser.add_argument('rest', nargs=argparse.REMAINDER, default=[]) args = parser.parse_args() logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) @@ -57,11 +59,11 @@ improve / improve_code - Suggest improvements to the code in the PR as pull requ print(f"Reviewing PR: {args.pr_url}") reviewer = PRReviewer(args.pr_url, cli_mode=True) asyncio.run(reviewer.review()) - elif command in ['user_questions']: + elif command in ['reflect']: print(f"Asking the PR author questions: {args.pr_url}") reviewer = PRInformationFromUser(args.pr_url) asyncio.run(reviewer.generate_questions()) - elif command in ['user_answers']: + elif command in ['review_after_reflect']: print(f"Processing author's answers and sending review: {args.pr_url}") reviewer = PRReviewer(args.pr_url, cli_mode=True, is_answer=True) asyncio.run(reviewer.review()) diff --git a/pr_agent/servers/github_action_runner.py b/pr_agent/servers/github_action_runner.py index a856cc58..56a7bb5e 100644 --- a/pr_agent/servers/github_action_runner.py +++ b/pr_agent/servers/github_action_runner.py @@ -56,8 +56,9 @@ async def run_action(): body = comment_body.strip().lower() if any(cmd in body for cmd in ["/answer"]): await PRReviewer(pr_url, is_answer=True).review() - elif any(cmd in body for cmd in ["/review", "/review_pr", "/answer"]): - if settings.pr_reviewer.ask_and_reflect: + elif any(cmd in body for cmd in ["/review", "/review_pr", "/reflect_and_review"]): + if settings.pr_reviewer.ask_and_reflect or \ + any(cmd in body for cmd in ["/reflect_and_review"]): await PRInformationFromUser(pr_url).generate_questions() else: await PRReviewer(pr_url).review() diff --git a/pr_agent/tools/pr_information_from_user.py b/pr_agent/tools/pr_information_from_user.py index a4120cd4..ff78858f 100644 --- a/pr_agent/tools/pr_information_from_user.py +++ b/pr_agent/tools/pr_information_from_user.py @@ -35,7 +35,7 @@ class PRInformationFromUser: async def generate_questions(self): logging.info('Generating question to the user...') if settings.config.publish_output: - self.git_provider.publish_comment("Preparing answer...", is_temporary=True) + self.git_provider.publish_comment("Preparing questions...", is_temporary=True) logging.info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) logging.info('Getting AI prediction...') diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index 12d4df72..b086c9c0 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -9,7 +9,7 @@ from pr_agent.algo.pr_processing import get_pr_diff from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.utils import convert_to_markdown, try_fix_json from pr_agent.config_loader import settings -from pr_agent.git_providers import get_git_provider +from pr_agent.git_providers import get_git_provider, GithubProvider from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.servers.help import bot_help_text, actions_help_text @@ -22,6 +22,8 @@ class PRReviewer: self.git_provider.get_languages(), self.git_provider.get_files() ) self.is_answer = is_answer + if self.is_answer and type(self.git_provider) != GithubProvider: + raise Exception("Answer mode is only supported for Github for now") answer_str = question_str = self._get_user_answers() self.ai_handler = AiHandler() self.patches_diff = None From e5259e2f5c06d9931b1a3bc35101ec9f9dc9d1a0 Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Tue, 18 Jul 2023 10:17:09 +0300 Subject: [PATCH 07/13] Small refactor --- pr_agent/git_providers/bitbucket_provider.py | 8 ++++++++ pr_agent/git_providers/git_provider.py | 8 ++++++++ pr_agent/git_providers/github_provider.py | 6 ++++++ pr_agent/git_providers/gitlab_provider.py | 8 ++++++++ pr_agent/tools/pr_reviewer.py | 8 ++++---- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pr_agent/git_providers/bitbucket_provider.py b/pr_agent/git_providers/bitbucket_provider.py index e9946aa9..86e445ac 100644 --- a/pr_agent/git_providers/bitbucket_provider.py +++ b/pr_agent/git_providers/bitbucket_provider.py @@ -25,6 +25,11 @@ class BitbucketProvider: if pr_url: self.set_pr(pr_url) + def is_supported(self, capability: str) -> bool: + if capability == 'get_issue_comments': + return False + return True + def set_pr(self, pr_url: str): self.workspace_slug, self.repo_slug, self.pr_num = self._parse_pr_url(pr_url) self.pr = self._get_pr() @@ -74,6 +79,9 @@ class BitbucketProvider: def get_user_id(self): return 0 + def get_issue_comments(self): + raise NotImplementedError("Bitbucket provider does not support issue comments yet") + @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, int]: parsed_url = urlparse(pr_url) diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index ae39cc74..4beba204 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -21,6 +21,10 @@ class FilePatchInfo: class GitProvider(ABC): + @abstractmethod + def is_supported(self, capability: str) -> bool: + pass + @abstractmethod def get_diff_files(self) -> list[FilePatchInfo]: pass @@ -62,6 +66,10 @@ class GitProvider(ABC): def get_pr_description(self): pass + @abstractmethod + def get_issue_comments(self): + pass + def get_main_pr_language(languages, files) -> str: """ diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index 4f7d44bc..76f3a818 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -23,6 +23,9 @@ class GithubProvider(GitProvider): self.set_pr(pr_url) self.last_commit_id = list(self.pr.get_commits())[-1] + def is_supported(self, capability: str) -> bool: + return True + def set_pr(self, pr_url: str): self.repo, self.pr_num = self._parse_pr_url(pr_url) self.pr = self._get_pr() @@ -161,6 +164,9 @@ class GithubProvider(GitProvider): notifications = self.github_client.get_user().get_notifications(since=since) return notifications + def get_issue_comments(self): + return self.pr.get_issue_comments() + @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, int]: parsed_url = urlparse(pr_url) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 07a25e2f..c33016cc 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -31,6 +31,11 @@ class GitLabProvider(GitProvider): self.RE_HUNK_HEADER = re.compile( r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") + def is_supported(self, capability: str) -> bool: + if capability == 'get_issue_comments': + return False + return True + @property def pr(self): '''The GitLab terminology is merge request (MR) instead of pull request (PR)''' @@ -203,6 +208,9 @@ class GitLabProvider(GitProvider): def get_pr_description(self): return self.mr.description + def get_issue_comments(self): + raise NotImplementedError("GitLab provider does not support issue comments yet") + def _parse_merge_request_url(self, merge_request_url: str) -> Tuple[int, int]: parsed_url = urlparse(merge_request_url) diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index b086c9c0..e11a2042 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -9,7 +9,7 @@ from pr_agent.algo.pr_processing import get_pr_diff from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.utils import convert_to_markdown, try_fix_json from pr_agent.config_loader import settings -from pr_agent.git_providers import get_git_provider, GithubProvider +from pr_agent.git_providers import get_git_provider from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.servers.help import bot_help_text, actions_help_text @@ -22,8 +22,8 @@ class PRReviewer: self.git_provider.get_languages(), self.git_provider.get_files() ) self.is_answer = is_answer - if self.is_answer and type(self.git_provider) != GithubProvider: - raise Exception("Answer mode is only supported for Github for now") + if self.is_answer and not self.git_provider.is_supported("get_issue_comments"): + raise Exception(f"Answer mode is not supported for {settings.config.git_provider} for now") answer_str = question_str = self._get_user_answers() self.ai_handler = AiHandler() self.patches_diff = None @@ -139,7 +139,7 @@ class PRReviewer: def _get_user_answers(self): answer_str = question_str = "" if self.is_answer: - discussion_messages = self.git_provider.pr.get_issue_comments() + discussion_messages = self.git_provider.get_issue_comments() for message in discussion_messages.reversed: if "Questions to better understand the PR:" in message.body: question_str = message.body From a994ec1427ee016f1a0ae1054a777ad3036c573f Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Tue, 18 Jul 2023 10:19:32 +0300 Subject: [PATCH 08/13] Call PRAgent from github_action_runner.py --- pr_agent/servers/github_action_runner.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/pr_agent/servers/github_action_runner.py b/pr_agent/servers/github_action_runner.py index 56a7bb5e..31a4800d 100644 --- a/pr_agent/servers/github_action_runner.py +++ b/pr_agent/servers/github_action_runner.py @@ -3,6 +3,7 @@ import json import os import re +from pr_agent.agent.pr_agent import PRAgent from pr_agent.config_loader import settings from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions from pr_agent.tools.pr_description import PRDescription @@ -54,26 +55,7 @@ async def run_action(): pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None) if pr_url: body = comment_body.strip().lower() - if any(cmd in body for cmd in ["/answer"]): - await PRReviewer(pr_url, is_answer=True).review() - elif any(cmd in body for cmd in ["/review", "/review_pr", "/reflect_and_review"]): - if settings.pr_reviewer.ask_and_reflect or \ - any(cmd in body for cmd in ["/reflect_and_review"]): - await PRInformationFromUser(pr_url).generate_questions() - else: - await PRReviewer(pr_url).review() - elif any(cmd in body for cmd in ["/describe", "/describe_pr"]): - await PRDescription(pr_url).describe() - elif any(cmd in body for cmd in ["/improve", "/improve_code"]): - await PRCodeSuggestions(pr_url).suggest() - elif any(cmd in body for cmd in ["/ask", "/ask_question"]): - pattern = r'(/ask|/ask_question)\s*(.*)' - matches = re.findall(pattern, comment_body, re.IGNORECASE) - if matches: - question = matches[0][1] - await PRQuestions(pr_url, question).answer() - else: - print(f"Unknown command: {body}") + await PRAgent().handle_request(pr_url, body) if __name__ == '__main__': From fdeae9c2095353722f1d62fdcd3a8a8cb2e0447d Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Tue, 18 Jul 2023 10:20:52 +0300 Subject: [PATCH 09/13] Update pr_agent/agent/pr_agent.py --- pr_agent/agent/pr_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/agent/pr_agent.py b/pr_agent/agent/pr_agent.py index 3709db1f..672a7a94 100644 --- a/pr_agent/agent/pr_agent.py +++ b/pr_agent/agent/pr_agent.py @@ -16,7 +16,7 @@ class PRAgent: if any(cmd in request for cmd in ["/answer"]): await PRReviewer(pr_url, is_answer=True).review() elif any(cmd in request for cmd in ["/review", "/review_pr", "/reflect_and_review"]): - if settings.pr_reviewer.ask_and_reflect or any(cmd in request for cmd in ["/reflect_and_review"]): + if settings.pr_reviewer.ask_and_reflect or "/reflect_and_review" in request: await PRInformationFromUser(pr_url).generate_questions() else: await PRReviewer(pr_url).review() From f77a5f69297fb2755abced73d160e606124b5b4d Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Tue, 18 Jul 2023 10:31:24 +0300 Subject: [PATCH 10/13] Call PRAgent from github_action_runner.py --- pr_agent/tools/pr_reviewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index e11a2042..2ceeef8b 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -125,7 +125,7 @@ class PRReviewer: except json.decoder.JSONDecodeError: data = try_fix_json(review) - if settings.config.pr_reviewer > 0: + if settings.get("CONFIG.PR_REVIEWER.NUM_CODE_SUGGESTIONS", 0): try: for d in data['PR Feedback']['Code suggestions']: relevant_file = d['relevant file'].strip() From b3a1d456b26d245978f5481f812e2636c7a08094 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Tue, 18 Jul 2023 10:32:36 +0300 Subject: [PATCH 11/13] if settings.pr_reviewer.num_code_suggestions --- pr_agent/tools/pr_reviewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index b086c9c0..6be84a3a 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -125,7 +125,7 @@ class PRReviewer: except json.decoder.JSONDecodeError: data = try_fix_json(review) - if settings.config.pr_reviewer > 0: + if settings.pr_reviewer.num_code_suggestions > 0: try: for d in data['PR Feedback']['Code suggestions']: relevant_file = d['relevant file'].strip() From 62f08f4ec42820750efe698298d94f59d0f23c47 Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Tue, 18 Jul 2023 10:35:05 +0300 Subject: [PATCH 12/13] removed an unneeded file --- pics/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pics/.DS_Store diff --git a/pics/.DS_Store b/pics/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Tue, 18 Jul 2023 10:36:05 +0300 Subject: [PATCH 13/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8658bdb..5e56260a 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Here are several ways to install and run PR-Agent: - The "PR Q&A" tool answers free-text questions about the PR. - The "PR Description" tool automatically sets the PR Title and body. - The "PR Code Suggestion" tool provide inline code suggestions for the PR that can be applied and committed. -- The "PR Reflect and Review" tool first dialog with the user and asks him to reflect on the PR, and then provides a review. +- The "PR Reflect and Review" tool first initiates a dialog with the user and asks them to reflect on the PR, and then provides a review. ## How it works