From 5dc2595dcf9bbf24dcc745c1fa42d098579bd891 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 24 Dec 2023 08:30:35 +0200 Subject: [PATCH 1/5] feat: Refactor code suggestion handling and update YAML schema in pr_code_suggestions.py and pr_code_suggestions_prompts.toml - Update key names in pr_code_suggestions.py to use snake_case for consistency - Implement removal of invalid suggestions where existing code is equal to improved code - Update YAML parsing in _prepare_pr_code_suggestions method to include keys_fix_yaml parameter - Refactor push_inline_code_suggestions method to use updated key names - Update _prepare_prediction_extended method to use new key names - Refactor _prepare_markdown method to include suggestion label and use updated key names - Update instructions and YAML schema in pr_code_suggestions_prompts.toml to reflect changes in pr_code_suggestions.py - Remove redundant removal of invalid suggestions in rank_suggestions method --- .../settings/pr_code_suggestions_prompts.toml | 72 +++++++------------ pr_agent/tools/pr_code_suggestions.py | 60 +++++++++------- 2 files changed, 57 insertions(+), 75 deletions(-) diff --git a/pr_agent/settings/pr_code_suggestions_prompts.toml b/pr_agent/settings/pr_code_suggestions_prompts.toml index 001dbb278..6218e13a0 100644 --- a/pr_agent/settings/pr_code_suggestions_prompts.toml +++ b/pr_agent/settings/pr_code_suggestions_prompts.toml @@ -32,7 +32,7 @@ __old hunk__ Specific instructions: - Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions. -- Prioritize suggestions that address major problems, issues and bugs in the code. As a second priority, suggestions should focus on best practices, code readability, maintainability, enhancments, performance, and other aspects. +- Prioritize suggestions that address major problems, issues and bugs in the code. As a second priority, suggestions should focus on enhancment, best practice, performance, maintainability, and other aspects. - Don't suggest to add docstring, type hints, or comments. - Suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+'). - Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the '__new hunk__' code. @@ -49,65 +49,41 @@ Extra instructions from the user: ====== {%- endif %} +The output must be a YAML object equivalent to type $PRCodeSuggestins, according to the following Pydantic definitions: +===== +class CodeSuggestion(BaseModel): + relevant_file: str = Field(description="the relevant file full path") + suggestion_content: str = Field(description="a concrete suggestion for meaningfully improving the new code introduced in the PR") + existing_code: str = Field(description="a code snippet showing the relevant code lines from a '__new hunk__' section. It must be contiguous, correctly formatted and indented, and without line numbers.") + relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.") + relevant_lines_end: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion ends (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.") + improved_code: str = Field(description="a new code snippet that can be used to replace the relevant lines in '__new hunk__' code. Replacement suggestions should be complete, correctly formatted and indented, and without line numbers.") + label: str = Field(description="a single label for the suggestion, to help the user understand the suggestion type. For example: 'security', 'bug', 'performance', 'enhancement', 'possible issue', 'best practice', 'maintainability', etc. Other labels are also allowed.") + +class PRCodeSuggestins(BaseModel): + code_suggestions: List[CodeSuggestion] +===== -You must use the following YAML schema to format your answer: -```yaml -Code suggestions: - type: array - minItems: 1 - maxItems: {{ num_code_suggestions }} - uniqueItems: true - items: - relevant file: - type: string - description: the relevant file full path - suggestion content: - type: string - description: |- - a concrete suggestion for meaningfully improving the new PR code. - existing code: - type: string - description: |- - a code snippet showing the relevant code lines from a '__new hunk__' section. - It must be contiguous, correctly formatted and indented, and without line numbers. - relevant lines start: - type: integer - description: |- - The relevant line number from a '__new hunk__' section where the suggestion starts (inclusive). - Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above. - relevant lines end: - type: integer - description: |- - The relevant line number from a '__new hunk__' section where the suggestion ends (inclusive). - Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above. - improved code: - type: string - description: |- - a new code snippet that can be used to replace the relevant lines in '__new hunk__' code. - Replacement suggestions should be complete, correctly formatted and indented, and without line numbers. -``` Example output: ```yaml -Code suggestions: -- relevant file: |- +code_suggestions: +- relevant_file: |- src/file1.py - suggestion content: |- + suggestion_content: |- Add a docstring to func1() - existing code: |- + existing_code: |- def func1(): - relevant lines start: |- - 12 - relevant lines end: |- - 12 - improved code: |- + relevant_lines_start: 12 + relevant_lines_end: 12 + improved_code: |- + ... + label: |- ... -... ``` Each YAML output MUST be after a newline, indented, with block scalar indicator ('|-'). -Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields. """ user="""PR Info: diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index f2f612a1c..662d6f62c 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -65,14 +65,14 @@ async def run(self): data = self._prepare_pr_code_suggestions() else: data = await retry_with_fallback_models(self._prepare_prediction_extended) - if (not data) or (not 'Code suggestions' in data): + if (not data) or (not 'code_suggestions' in data): get_logger().info('No code suggestions found for PR.') return if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \ (self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions): get_logger().info('Ranking Suggestions...') - data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions']) + data['code_suggestions'] = await self.rank_suggestions(data['code_suggestions']) if get_settings().config.publish_output: get_logger().info('Pushing PR code suggestions...') @@ -116,27 +116,37 @@ async def _get_prediction(self, model: str): def _prepare_pr_code_suggestions(self) -> Dict: review = self.prediction.strip() - data = load_yaml(review) + data = load_yaml(review, keys_fix_yaml=["relevant_file", "suggestion_content", "existing_code", "improved_code"]) if isinstance(data, list): - data = {'Code suggestions': data} + data = {'code_suggestions': data} + + # remove invalid suggestions + suggestion_list = [] + for i, suggestion in enumerate(data['code_suggestions']): + if suggestion['existing_code'] != suggestion['improved_code']: + suggestion_list.append(suggestion) + else: + get_logger().debug(f"Skipping suggestion {i + 1}, because existing code is equal to improved code {suggestion['existing_code']}") + data['code_suggestions'] = suggestion_list + return data def push_inline_code_suggestions(self, data): code_suggestions = [] - if not data['Code suggestions']: + if not data['code_suggestions']: get_logger().info('No suggestions found to improve this PR.') return self.git_provider.publish_comment('No suggestions found to improve this PR.') - for d in data['Code suggestions']: + for d in data['code_suggestions']: try: if get_settings().config.verbosity_level >= 2: get_logger().info(f"suggestion: {d}") - relevant_file = d['relevant file'].strip() - relevant_lines_start = int(d['relevant lines start']) # absolute position - relevant_lines_end = int(d['relevant lines end']) - content = d['suggestion content'] - new_code_snippet = d['improved code'] + relevant_file = d['relevant_file'].strip() + relevant_lines_start = int(d['relevant_lines_start']) # absolute position + relevant_lines_end = int(d['relevant_lines_end']) + content = d['suggestion_content'].rstrip() + new_code_snippet = d['improved_code'].rstrip() if new_code_snippet: new_code_snippet = self.dedent_code(relevant_file, relevant_lines_start, new_code_snippet) @@ -195,8 +205,8 @@ async def _prepare_prediction_extended(self, model: str) -> dict: for prediction in prediction_list: self.prediction = prediction data_per_chunk = self._prepare_pr_code_suggestions() - if "Code suggestions" in data: - data["Code suggestions"].extend(data_per_chunk["Code suggestions"]) + if "code_suggestions" in data: + data["code_suggestions"].extend(data_per_chunk["code_suggestions"]) else: data.update(data_per_chunk) self.data = data @@ -214,11 +224,6 @@ async def rank_suggestions(self, data: List) -> List: """ suggestion_list = [] - # remove invalid suggestions - for i, suggestion in enumerate(data): - if suggestion['existing code'] != suggestion['improved code']: - suggestion_list.append(suggestion) - data_sorted = [[]] * len(suggestion_list) try: @@ -264,24 +269,25 @@ def publish_summarizes_suggestions(self, data: Dict): for ext in extensions: extension_to_language[ext] = language - for s in data['Code suggestions']: + for s in data['code_suggestions']: try: - extension_s = s['relevant file'].rsplit('.')[-1] - code_snippet_link = self.git_provider.get_line_link(s['relevant file'], s['relevant lines start'], - s['relevant lines end']) - data_markdown += f"\n💡 Suggestion:\n\n**{s['suggestion content']}**\n\n" + extension_s = s['relevant_file'].rsplit('.')[-1] + code_snippet_link = self.git_provider.get_line_link(s['relevant_file'], s['relevant_lines_start'], + s['relevant_lines_end']) + label = s['label'].strip() + data_markdown += f"\n💡 Type: [{label}]\n\n**{s['suggestion_content'].rstrip().rstrip()}**\n\n" if code_snippet_link: - data_markdown += f" File: [{s['relevant file']} ({s['relevant lines start']}-{s['relevant lines end']})]({code_snippet_link})\n\n" + data_markdown += f" File: [{s['relevant_file']} ({s['relevant_lines_start']}-{s['relevant_lines_end']})]({code_snippet_link})\n\n" else: - data_markdown += f"File: {s['relevant file']} ({s['relevant lines start']}-{s['relevant lines end']})\n\n" + data_markdown += f"File: {s['relevant_file']} ({s['relevant_lines_start']}-{s['relevant_lines_end']})\n\n" if self.git_provider.is_supported("gfm_markdown"): data_markdown += "
Example code:\n\n" data_markdown += f"___\n\n" language_name = "python" if extension_s and (extension_s in extension_to_language): language_name = extension_to_language[extension_s] - data_markdown += f"Existing code:\n```{language_name}\n{s['existing code']}\n```\n" - data_markdown += f"Improved code:\n```{language_name}\n{s['improved code']}\n```\n" + data_markdown += f"Existing code:\n```{language_name}\n{s['existing_code'].rstrip()}\n```\n" + data_markdown += f"Improved code:\n```{language_name}\n{s['improved_code'].rstrip()}\n```\n" if self.git_provider.is_supported("gfm_markdown"): data_markdown += "
\n" data_markdown += "\n___\n\n" From 5c49ff216a2fefb3f8de5de0a1a7f4a1bbcf8093 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 24 Dec 2023 09:44:08 +0200 Subject: [PATCH 2/5] feat: Update inline comment creation in git providers and improve code suggestion handling - Update `create_inline_comment` method in various git providers to include `absolute_position` parameter - Remove `create_inline_comment` method from providers that do not support inline comments - Enhance `find_line_number_of_relevant_line_in_file` function to handle absolute position - Modify `pr_code_suggestions.py` to handle improved code inclusion in suggestions - Add `include_improved_code` configuration option in `configuration.toml` and update documentation accordingly --- docs/IMPROVE.md | 1 + pr_agent/algo/pr_processing.py | 84 ++++++++++--------- .../git_providers/azuredevops_provider.py | 3 - pr_agent/git_providers/bitbucket_provider.py | 6 +- .../bitbucket_server_provider.py | 7 +- pr_agent/git_providers/codecommit_provider.py | 3 - pr_agent/git_providers/gerrit_provider.py | 5 -- pr_agent/git_providers/git_provider.py | 6 +- pr_agent/git_providers/github_provider.py | 8 +- pr_agent/git_providers/gitlab_provider.py | 2 +- pr_agent/git_providers/local_git_provider.py | 3 - pr_agent/settings/configuration.toml | 1 + pr_agent/tools/pr_code_suggestions.py | 39 ++++++--- 13 files changed, 95 insertions(+), 73 deletions(-) diff --git a/docs/IMPROVE.md b/docs/IMPROVE.md index 24d78cbd2..cb4b47dd3 100644 --- a/docs/IMPROVE.md +++ b/docs/IMPROVE.md @@ -26,6 +26,7 @@ Under the section 'pr_code_suggestions', the [configuration file](./../pr_agent/ - `num_code_suggestions`: number of code suggestions provided by the 'improve' tool. Default is 4. - `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...". - `rank_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is false. +- `include_improved_code`: if set to true, the tool will include an improved code implementation in the suggestion. Default is true. #### params for '/improve --extended' mode - `num_code_suggestions_per_chunk`: number of code suggestions provided by the 'improve' tool, per chunk. Default is 8. diff --git a/pr_agent/algo/pr_processing.py b/pr_agent/algo/pr_processing.py index 4c1352f0e..4b3805506 100644 --- a/pr_agent/algo/pr_processing.py +++ b/pr_agent/algo/pr_processing.py @@ -264,20 +264,11 @@ def _get_all_deployments(all_models: List[str]) -> List[str]: def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo], relevant_file: str, - relevant_line_in_file: str) -> Tuple[int, int]: - """ - Find the line number and absolute position of a relevant line in a file. - - Args: - diff_files (List[FilePatchInfo]): A list of FilePatchInfo objects representing the patches of files. - relevant_file (str): The name of the file where the relevant line is located. - relevant_line_in_file (str): The content of the relevant line. - - Returns: - Tuple[int, int]: A tuple containing the line number and absolute position of the relevant line in the file. - """ + relevant_line_in_file: str, + absolute_position: int = None) -> Tuple[int, int]: position = -1 - absolute_position = -1 + if absolute_position is None: + absolute_position = -1 re_hunk_header = re.compile( r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") @@ -285,31 +276,11 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo], if file.filename and (file.filename.strip() == relevant_file): patch = file.patch patch_lines = patch.splitlines() - - # try to find the line in the patch using difflib, with some margin of error - matches_difflib: list[str | Any] = difflib.get_close_matches(relevant_line_in_file, - patch_lines, n=3, cutoff=0.93) - if len(matches_difflib) == 1 and matches_difflib[0].startswith('+'): - relevant_line_in_file = matches_difflib[0] - delta = 0 start1, size1, start2, size2 = 0, 0, 0, 0 - for i, line in enumerate(patch_lines): - if line.startswith('@@'): - delta = 0 - match = re_hunk_header.match(line) - start1, size1, start2, size2 = map(int, match.groups()[:4]) - elif not line.startswith('-'): - delta += 1 - - if relevant_line_in_file in line and line[0] != '-': - position = i - absolute_position = start2 + delta - 1 - break - - if position == -1 and relevant_line_in_file[0] == '+': - no_plus_line = relevant_line_in_file[1:].lstrip() + if absolute_position != -1: # matching absolute to relative for i, line in enumerate(patch_lines): + # new hunk if line.startswith('@@'): delta = 0 match = re_hunk_header.match(line) @@ -317,12 +288,49 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo], elif not line.startswith('-'): delta += 1 - if no_plus_line in line and line[0] != '-': - # The model might add a '+' to the beginning of the relevant_line_in_file even if originally - # it's a context line + # + absolute_position_curr = start2 + delta - 1 + + if absolute_position_curr == absolute_position: + position = i + break + else: + # try to find the line in the patch using difflib, with some margin of error + matches_difflib: list[str | Any] = difflib.get_close_matches(relevant_line_in_file, + patch_lines, n=3, cutoff=0.93) + if len(matches_difflib) == 1 and matches_difflib[0].startswith('+'): + relevant_line_in_file = matches_difflib[0] + + + for i, line in enumerate(patch_lines): + if line.startswith('@@'): + delta = 0 + match = re_hunk_header.match(line) + start1, size1, start2, size2 = map(int, match.groups()[:4]) + elif not line.startswith('-'): + delta += 1 + + if relevant_line_in_file in line and line[0] != '-': position = i absolute_position = start2 + delta - 1 break + + if position == -1 and relevant_line_in_file[0] == '+': + no_plus_line = relevant_line_in_file[1:].lstrip() + for i, line in enumerate(patch_lines): + if line.startswith('@@'): + delta = 0 + match = re_hunk_header.match(line) + start1, size1, start2, size2 = map(int, match.groups()[:4]) + elif not line.startswith('-'): + delta += 1 + + if no_plus_line in line and line[0] != '-': + # The model might add a '+' to the beginning of the relevant_line_in_file even if originally + # it's a context line + position = i + absolute_position = start2 + delta - 1 + break return position, absolute_position diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index ca11b9d86..0552a63e9 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -174,9 +174,6 @@ def remove_initial_comment(self): def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): raise NotImplementedError("Azure DevOps provider does not support publishing inline comment yet") - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): - raise NotImplementedError("Azure DevOps provider does not support creating inline comments yet") - def publish_inline_comments(self, comments: list[dict]): raise NotImplementedError("Azure DevOps provider does not support publishing inline comments yet") diff --git a/pr_agent/git_providers/bitbucket_provider.py b/pr_agent/git_providers/bitbucket_provider.py index 23173f8e0..c98ebc058 100644 --- a/pr_agent/git_providers/bitbucket_provider.py +++ b/pr_agent/git_providers/bitbucket_provider.py @@ -201,8 +201,10 @@ def remove_comment(self, comment): get_logger().exception(f"Failed to remove comment, error: {e}") # funtion to create_inline_comment - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): - position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(), relevant_file.strip('`'), relevant_line_in_file) + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, absolute_position: int = None): + position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(), + relevant_file.strip('`'), + relevant_line_in_file, absolute_position) if position == -1: if get_settings().config.verbosity_level >= 2: get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}") diff --git a/pr_agent/git_providers/bitbucket_server_provider.py b/pr_agent/git_providers/bitbucket_server_provider.py index 902beb16a..2d96120ba 100644 --- a/pr_agent/git_providers/bitbucket_server_provider.py +++ b/pr_agent/git_providers/bitbucket_server_provider.py @@ -210,11 +210,14 @@ def remove_comment(self, comment): pass # funtion to create_inline_comment - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, + absolute_position: int = None): + position, absolute_position = find_line_number_of_relevant_line_in_file( self.get_diff_files(), relevant_file.strip('`'), - relevant_line_in_file + relevant_line_in_file, + absolute_position ) if position == -1: if get_settings().config.verbosity_level >= 2: diff --git a/pr_agent/git_providers/codecommit_provider.py b/pr_agent/git_providers/codecommit_provider.py index 286444c50..3c7b76974 100644 --- a/pr_agent/git_providers/codecommit_provider.py +++ b/pr_agent/git_providers/codecommit_provider.py @@ -229,9 +229,6 @@ def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet") - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): - raise NotImplementedError("CodeCommit provider does not support creating inline comments yet") - def publish_inline_comments(self, comments: list[dict]): raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet") diff --git a/pr_agent/git_providers/gerrit_provider.py b/pr_agent/git_providers/gerrit_provider.py index dbdbe82f8..f7dd05acc 100644 --- a/pr_agent/git_providers/gerrit_provider.py +++ b/pr_agent/git_providers/gerrit_provider.py @@ -380,11 +380,6 @@ def publish_inline_comment(self, body: str, relevant_file: str, 'Publishing inline comments is not implemented for the gerrit ' 'provider') - def create_inline_comment(self, body: str, relevant_file: str, - relevant_line_in_file: str): - raise NotImplementedError( - 'Creating inline comments is not implemented for the gerrit ' - 'provider') def publish_labels(self, labels): # Not applicable to the local git provider, diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 4c4684c30..b2d313229 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -106,9 +106,9 @@ def publish_persistent_comment(self, pr_comment: str, initial_header: str, updat def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): pass - @abstractmethod - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): - pass + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, + absolute_position: int = None): + raise NotImplementedError("This git provider does not support creating inline comments yet") @abstractmethod def publish_inline_comments(self, comments: list[dict]): diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index f365db848..a6ffbcdfb 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -206,8 +206,12 @@ def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)]) - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): - position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file) + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, + absolute_position: int = None): + position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, + relevant_file.strip('`'), + relevant_line_in_file, + absolute_position) if position == -1: if get_settings().config.verbosity_level >= 2: get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}") diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 902d63cb4..994ba156f 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -183,7 +183,7 @@ def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, target_file, target_line_no) - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, position: int = None): raise NotImplementedError("Gitlab provider does not support creating inline comments yet") def create_inline_comments(self, comments: list[dict]): diff --git a/pr_agent/git_providers/local_git_provider.py b/pr_agent/git_providers/local_git_provider.py index b3fad772a..3d45d35c0 100644 --- a/pr_agent/git_providers/local_git_provider.py +++ b/pr_agent/git_providers/local_git_provider.py @@ -121,9 +121,6 @@ def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): raise NotImplementedError('Publishing inline comments is not implemented for the local git provider') - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): - raise NotImplementedError('Creating inline comments is not implemented for the local git provider') - def publish_inline_comments(self, comments: list[dict]): raise NotImplementedError('Publishing inline comments is not implemented for the local git provider') diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index b079f170e..b83297017 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -62,6 +62,7 @@ include_generated_by_header=true [pr_code_suggestions] # /improve # num_code_suggestions=4 summarize = false +include_improved_code = true extra_instructions = "" rank_suggestions = false # params for '/improve --extended' mode diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index 662d6f62c..763e0388c 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -116,7 +116,8 @@ async def _get_prediction(self, model: str): def _prepare_pr_code_suggestions(self) -> Dict: review = self.prediction.strip() - data = load_yaml(review, keys_fix_yaml=["relevant_file", "suggestion_content", "existing_code", "improved_code"]) + data = load_yaml(review, + keys_fix_yaml=["relevant_file", "suggestion_content", "existing_code", "improved_code"]) if isinstance(data, list): data = {'code_suggestions': data} @@ -126,7 +127,8 @@ def _prepare_pr_code_suggestions(self) -> Dict: if suggestion['existing_code'] != suggestion['improved_code']: suggestion_list.append(suggestion) else: - get_logger().debug(f"Skipping suggestion {i + 1}, because existing code is equal to improved code {suggestion['existing_code']}") + get_logger().debug( + f"Skipping suggestion {i + 1}, because existing code is equal to improved code {suggestion['existing_code']}") data['code_suggestions'] = suggestion_list return data @@ -147,23 +149,40 @@ def push_inline_code_suggestions(self, data): relevant_lines_end = int(d['relevant_lines_end']) content = d['suggestion_content'].rstrip() new_code_snippet = d['improved_code'].rstrip() + label = d['label'].strip() if new_code_snippet: new_code_snippet = self.dedent_code(relevant_file, relevant_lines_start, new_code_snippet) - body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```" - code_suggestions.append({'body': body, 'relevant_file': relevant_file, - 'relevant_lines_start': relevant_lines_start, - 'relevant_lines_end': relevant_lines_end}) + if get_settings().pr_code_suggestions.include_improved_code: + body = f"**Suggestion:** {content} [{label}]\n```suggestion\n" + new_code_snippet + "\n```" + code_suggestions.append({'body': body, 'relevant_file': relevant_file, + 'relevant_lines_start': relevant_lines_start, + 'relevant_lines_end': relevant_lines_end}) + else: + if self.git_provider.is_supported("create_inline_comment"): + body = f"**Suggestion:** {content} [{label}]" + comment = self.git_provider.create_inline_comment(body, relevant_file, "", + absolute_position=relevant_lines_end) + if comment: + code_suggestions.append(comment) + else: + get_logger().error("Inline comments are not supported by the git provider") except Exception: if get_settings().config.verbosity_level >= 2: get_logger().info(f"Could not parse suggestion: {d}") - is_successful = self.git_provider.publish_code_suggestions(code_suggestions) + if get_settings().pr_code_suggestions.include_improved_code: + is_successful = self.git_provider.publish_code_suggestions(code_suggestions) + else: + is_successful = self.git_provider.publish_inline_comments(code_suggestions) if not is_successful: get_logger().info("Failed to publish code suggestions, trying to publish each suggestion separately") for code_suggestion in code_suggestions: - self.git_provider.publish_code_suggestions([code_suggestion]) + if get_settings().pr_code_suggestions.include_improved_code: + self.git_provider.publish_code_suggestions([code_suggestion]) + else: + self.git_provider.publish_inline_comments([code_suggestion]) def dedent_code(self, relevant_file, relevant_lines_start, new_code_snippet): try: # dedent code snippet @@ -275,7 +294,7 @@ def publish_summarizes_suggestions(self, data: Dict): code_snippet_link = self.git_provider.get_line_link(s['relevant_file'], s['relevant_lines_start'], s['relevant_lines_end']) label = s['label'].strip() - data_markdown += f"\n💡 Type: [{label}]\n\n**{s['suggestion_content'].rstrip().rstrip()}**\n\n" + data_markdown += f"\n💡 [{label}]\n\n**{s['suggestion_content'].rstrip().rstrip()}**\n\n" if code_snippet_link: data_markdown += f" File: [{s['relevant_file']} ({s['relevant_lines_start']}-{s['relevant_lines_end']})]({code_snippet_link})\n\n" else: @@ -296,5 +315,3 @@ def publish_summarizes_suggestions(self, data: Dict): self.git_provider.publish_comment(data_markdown) except Exception as e: get_logger().info(f"Failed to publish summarized code suggestions, error: {e}") - - From 47b267a73db6f7979a8bb38868294b72bf67a25f Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 24 Dec 2023 09:52:59 +0200 Subject: [PATCH 3/5] prompt --- .../settings/pr_code_suggestions_prompts.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pr_agent/settings/pr_code_suggestions_prompts.toml b/pr_agent/settings/pr_code_suggestions_prompts.toml index 6218e13a0..f152b92b7 100644 --- a/pr_agent/settings/pr_code_suggestions_prompts.toml +++ b/pr_agent/settings/pr_code_suggestions_prompts.toml @@ -49,18 +49,18 @@ Extra instructions from the user: ====== {%- endif %} -The output must be a YAML object equivalent to type $PRCodeSuggestins, according to the following Pydantic definitions: +The output must be a YAML object equivalent to type PRCodeSuggestions, according to the following Pydantic definitions: ===== class CodeSuggestion(BaseModel): relevant_file: str = Field(description="the relevant file full path") - suggestion_content: str = Field(description="a concrete suggestion for meaningfully improving the new code introduced in the PR") - existing_code: str = Field(description="a code snippet showing the relevant code lines from a '__new hunk__' section. It must be contiguous, correctly formatted and indented, and without line numbers.") - relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.") - relevant_lines_end: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion ends (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.") - improved_code: str = Field(description="a new code snippet that can be used to replace the relevant lines in '__new hunk__' code. Replacement suggestions should be complete, correctly formatted and indented, and without line numbers.") - label: str = Field(description="a single label for the suggestion, to help the user understand the suggestion type. For example: 'security', 'bug', 'performance', 'enhancement', 'possible issue', 'best practice', 'maintainability', etc. Other labels are also allowed.") - -class PRCodeSuggestins(BaseModel): + suggestion_content: str = Field(description="an actionable suggestion for meaningfully improving the new code introduced in the PR") + existing_code: str = Field(description="a code snippet, showing the relevant code lines from a '__new hunk__' section. It must be contiguous, correctly formatted and indented, and without line numbers") + relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above") + relevant_lines_end: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion ends (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above") + improved_code: str = Field(description="a new code snippet, that can be used to replace the relevant lines in '__new hunk__' code. Replacement suggestions should be complete, correctly formatted and indented, and without line numbers") + label: str = Field(description="a single label for the suggestion, to help the user understand the suggestion type. For example: 'security', 'bug', 'performance', 'enhancement', 'possible issue', 'best practice', 'maintainability', etc. Other labels are also allowed") + +class PRCodeSuggestions(BaseModel): code_suggestions: List[CodeSuggestion] ===== From 5fd3fdfae143ed73157b84321ff3ee68f5127767 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 24 Dec 2023 10:08:36 +0200 Subject: [PATCH 4/5] feat: Add loop to populate suggestion_list in pr_code_suggestions.py --- pr_agent/tools/pr_code_suggestions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index 763e0388c..b5006b855 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -243,6 +243,8 @@ async def rank_suggestions(self, data: List) -> List: """ suggestion_list = [] + for suggestion in data: + suggestion_list.append(suggestion) data_sorted = [[]] * len(suggestion_list) try: From 663632e2d9f0966b60b9127214d123f8aed1f2c2 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 24 Dec 2023 10:27:48 +0200 Subject: [PATCH 5/5] fixed bug --- pr_agent/git_providers/gitlab_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 994ba156f..2b7c1a933 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -183,7 +183,7 @@ def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, target_file, target_line_no) - def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, position: int = None): + def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, absolute_position: int = None): raise NotImplementedError("Gitlab provider does not support creating inline comments yet") def create_inline_comments(self, comments: list[dict]):