diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index 87411430..9a150864 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -474,4 +474,13 @@ def clip_tokens(text: str, max_tokens: int, add_three_dots=True) -> str: return clipped_text except Exception as e: get_logger().warning(f"Failed to clip tokens: {e}") - return text \ No newline at end of file + return text + +def replace_code_tags(text): + """ + Replace odd instances of ` with and even instances of ` with + """ + parts = text.split('`') + for i in range(1, len(parts), 2): + parts[i] = '' + parts[i] + '' + return ''.join(parts) \ No newline at end of file diff --git a/pr_agent/settings/pr_code_suggestions_prompts.toml b/pr_agent/settings/pr_code_suggestions_prompts.toml index 5aa3cce2..2fb224c7 100644 --- a/pr_agent/settings/pr_code_suggestions_prompts.toml +++ b/pr_agent/settings/pr_code_suggestions_prompts.toml @@ -47,14 +47,15 @@ Extra instructions from the user: ====== {%- endif %} -The output must be a YAML object equivalent to type PRCodeSuggestions, 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="an actionable suggestion for meaningfully improving the new code introduced in the PR") {%- if summarize_mode %} - existing_code: str = Field(description="a short code snippet from a '__new hunk__' section to illustrate the relevant existing code. Don't show the line numbers. Shorten parts of the code ('...') if needed") - improved_code: str = Field(description="a short code snippet to illustrate the improved code, after applying the suggestion. Shorten parts of the code ('...') if needed") + existing_code: str = Field(description="a short code snippet from a '__new hunk__' section to illustrate the relevant existing code. Don't show the line numbers.") + improved_code: str = Field(description="a short code snippet to illustrate the improved code, after applying the suggestion.") + one_sentence_summary:str = Field(description="a short summary of the suggestion action, in a single sentence. Focus on the 'what'. Be general, and avoid method or variable names.") {%- else %} existing_code: str = Field(description="a code snippet, demonstrating the relevant code lines from a '__new hunk__' section. It must be contiguous, correctly formatted and indented, and without line numbers") 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") @@ -75,12 +76,23 @@ code_suggestions: src/file1.py suggestion_content: |- Add a docstring to func1() +{%- if summarize_mode %} + existing_code: |- + def func1(): + improved_code: |- + ... + one_sentence_summary: |- + ... + relevant_lines_start: 12 + relevant_lines_end: 12 +{%- else %} existing_code: |- def func1(): relevant_lines_start: 12 relevant_lines_end: 12 improved_code: |- ... +{%- endif %} label: |- ... ``` diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index b4f7a973..ddc7913e 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -8,12 +8,13 @@ from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models from pr_agent.algo.token_handler import TokenHandler -from pr_agent.algo.utils import load_yaml +from pr_agent.algo.utils import load_yaml, replace_code_tags from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.log import get_logger - +from pr_agent.tools.pr_description import insert_br_after_x_chars +import difflib class PRCodeSuggestions: def __init__(self, pr_url: str, cli_mode=False, args: list = None, @@ -299,7 +300,7 @@ class PRCodeSuggestions: def publish_summarizes_suggestions(self, data: Dict): try: - data_markdown = "## PR Code Suggestions\n\n" + pr_body = "## PR Code Suggestions\n\n" language_extension_map_org = get_settings().language_extension_map_org extension_to_language = {} @@ -307,30 +308,77 @@ class PRCodeSuggestions: for ext in extensions: extension_to_language[ext] = language - 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']) - label = s['label'].strip() - 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" + pr_body += "" + header = f"Suggestions" + delta = 77 + header += "  " * delta + pr_body += f"""""" + pr_body += """""" + suggestions_labels = dict() + # add all suggestions related to to each label + for suggestion in data['code_suggestions']: + label = suggestion['label'].strip().strip("'").strip('"') + if label not in suggestions_labels: + suggestions_labels[label] = [] + suggestions_labels[label].append(suggestion) + + for label, suggestions in suggestions_labels.items(): + pr_body += f"""""" + pr_body += f"""""" + pr_body += """
{header}
{label}""" + # pr_body += f"""
{len(suggestions)} suggestions""" + pr_body += f"""""" + for suggestion in suggestions: + + relevant_file = suggestion['relevant_file'].strip() + relevant_lines_start = int(suggestion['relevant_lines_start']) + relevant_lines_end = int(suggestion['relevant_lines_end']) + range_str = "" + if relevant_lines_start == relevant_lines_end: + range_str = f"[{relevant_lines_start}]" else: - 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'].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" - except Exception as e: - get_logger().error(f"Could not parse suggestion: {s}, error: {e}") - self.git_provider.publish_comment(data_markdown) + range_str = f"[{relevant_lines_start}-{relevant_lines_end}]" + code_snippet_link = self.git_provider.get_line_link(relevant_file, relevant_lines_start, + relevant_lines_end) + # add html table for each suggestion + + suggestion_content = suggestion['suggestion_content'].rstrip().rstrip() + + suggestion_content = insert_br_after_x_chars(suggestion_content, 90) + # pr_body += f"" + + pr_body += """
{suggestion_content}" + existing_code = suggestion['existing_code'].rstrip()+"\n" + improved_code = suggestion['improved_code'].rstrip()+"\n" + + diff = difflib.unified_diff(existing_code.split('\n'), + improved_code.split('\n'), n=999) + patch_orig = "\n".join(diff) + patch = "\n".join(patch_orig.splitlines()[5:]).strip('\n') + + example_code = "" + example_code += f"```diff\n{patch}\n```\n" + + pr_body += f"""
""" + suggestion_summary = suggestion['one_sentence_summary'].strip() + if '`' in suggestion_summary: + suggestion_summary = replace_code_tags(suggestion_summary) + suggestion_summary = suggestion_summary + max((77-len(suggestion_summary)), 0)*" " + pr_body += f"""\n\n
{suggestion_summary}\n\n___\n\n""" + + pr_body += f""" + + +**{suggestion_content}** + +[{relevant_file} {range_str}]({code_snippet_link}) + +{example_code} +""" + pr_body += f"
" + pr_body += f"
""" + # pr_body += "
" + pr_body += """
""" + self.git_provider.publish_comment(pr_body) except Exception as e: get_logger().info(f"Failed to publish summarized code suggestions, error: {e}") diff --git a/pr_agent/tools/pr_description.py b/pr_agent/tools/pr_description.py index 71906f0e..51df9ee4 100644 --- a/pr_agent/tools/pr_description.py +++ b/pr_agent/tools/pr_description.py @@ -399,7 +399,7 @@ class PRDescription: filename = filename.strip() link = self.git_provider.get_line_link(filename, relevant_line_start=-1) - file_change_description = self._insert_br_after_x_chars(file_change_description, x=(delta - 5)) + file_change_description = insert_br_after_x_chars(file_change_description, x=(delta - 5)) pr_body += f""" @@ -427,25 +427,25 @@ class PRDescription: pass return pr_body - def _insert_br_after_x_chars(self, text, x=70): - """ - Insert
into a string after a word that increases its length above x characters. - """ - if len(text) < x: - return text +def insert_br_after_x_chars(text, x=70): + """ + Insert
into a string after a word that increases its length above x characters. + """ + if len(text) < x: + return text - words = text.split(' ') - new_text = "" - current_length = 0 + words = text.split(' ') + new_text = "" + current_length = 0 - for word in words: - # Check if adding this word exceeds x characters - if current_length + len(word) > x: - new_text += "
" # Insert line break - current_length = 0 # Reset counter + for word in words: + # Check if adding this word exceeds x characters + if current_length + len(word) > x: + new_text += "
" # Insert line break + current_length = 0 # Reset counter - # Add the word to the new text - new_text += word + " " - current_length += len(word) + 1 # Add 1 for the space + # Add the word to the new text + new_text += word + " " + current_length += len(word) + 1 # Add 1 for the space - return new_text.strip() # Remove trailing space + return new_text.strip() # Remove trailing space