From 428e6382bd8ab88d0766e5a28cbee0b56052f1a9 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Thu, 5 Oct 2023 08:17:37 +0300 Subject: [PATCH 01/29] prompts minor update --- pr_agent/settings/pr_add_docs.toml | 2 ++ pr_agent/settings/pr_code_suggestions_prompts.toml | 13 +++++++------ pr_agent/settings/pr_description_prompts.toml | 2 ++ pr_agent/settings/pr_reviewer_prompts.toml | 7 +++++-- pr_agent/settings/pr_update_changelog_prompts.toml | 2 ++ 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pr_agent/settings/pr_add_docs.toml b/pr_agent/settings/pr_add_docs.toml index b552ec86..31b7195c 100644 --- a/pr_agent/settings/pr_add_docs.toml +++ b/pr_agent/settings/pr_add_docs.toml @@ -42,7 +42,9 @@ Specific instructions: {%- if extra_instructions %} Extra instructions from the user: +' {{ extra_instructions }} +' {%- endif %} You must use the following YAML schema to format your answer: diff --git a/pr_agent/settings/pr_code_suggestions_prompts.toml b/pr_agent/settings/pr_code_suggestions_prompts.toml index 68083945..a3eb93a1 100644 --- a/pr_agent/settings/pr_code_suggestions_prompts.toml +++ b/pr_agent/settings/pr_code_suggestions_prompts.toml @@ -1,6 +1,6 @@ [pr_code_suggestions_prompt] system="""You are a language model called PR-Code-Reviewer, that specializes in suggesting code improvements for Pull Request (PR). -Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR. +Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR (the '+' lines in the diff). Example for a PR Diff input: ' @@ -31,14 +31,13 @@ __old hunk__ ' Specific instructions: -- Provide up to {{ num_code_suggestions }} code suggestions. +- 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. - Don't suggest to add docstring, type hints, or comments. - Try to provide diverse and insightful suggestions. +- 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. - For each suggestion, make sure to take into consideration also the context, meaning the lines before and after the relevant code. +- 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. +- For each suggestion, make sure to take into consideration also the context, meaning the lines before and after the relevant code. - Provide the exact line numbers range (inclusive) for each issue. - Assume there is additional relevant code, that is not included in the diff. @@ -46,7 +45,9 @@ Specific instructions: {%- if extra_instructions %} Extra instructions from the user: +' {{ extra_instructions }} +' {%- endif %} You must use the following YAML schema to format your answer: diff --git a/pr_agent/settings/pr_description_prompts.toml b/pr_agent/settings/pr_description_prompts.toml index 43dd8e3b..c2c8e654 100644 --- a/pr_agent/settings/pr_description_prompts.toml +++ b/pr_agent/settings/pr_description_prompts.toml @@ -7,7 +7,9 @@ Your task is to provide full description of the PR content. {%- if extra_instructions %} Extra instructions from the user: +' {{ extra_instructions }} +' {% endif %} You must use the following YAML schema to format your answer: diff --git a/pr_agent/settings/pr_reviewer_prompts.toml b/pr_agent/settings/pr_reviewer_prompts.toml index c0599e50..657027af 100644 --- a/pr_agent/settings/pr_reviewer_prompts.toml +++ b/pr_agent/settings/pr_reviewer_prompts.toml @@ -35,7 +35,9 @@ The review should focus on new code added in the PR (lines starting with '+'), a {%- if extra_instructions %} Extra instructions from the user: +' {{ extra_instructions }} +' {% endif %} You must use the following YAML schema to format your answer: @@ -129,8 +131,7 @@ PR Feedback: Security concerns: type: string description: >- - yes\\no question: does this PR code introduce possible security concerns or - issues, like SQL injection, XSS, CSRF, and others ? If answered 'yes',explain your answer shortly + yes\\no question: does this PR code introduce possible vulnerabilities such as exposure of sensitive information (e.g., API keys, secrets, passwords), or security concerns like SQL injection, XSS, CSRF, and others ? If answered 'yes', explain your answer briefly. {%- endif %} ``` @@ -196,7 +197,9 @@ Here are questions to better understand the PR. Use the answers to provide bette {{question_str|trim}} User answers: +' {{answer_str|trim}} +' ###### {%- endif %} diff --git a/pr_agent/settings/pr_update_changelog_prompts.toml b/pr_agent/settings/pr_update_changelog_prompts.toml index 78b6a0b5..e9133e34 100644 --- a/pr_agent/settings/pr_update_changelog_prompts.toml +++ b/pr_agent/settings/pr_update_changelog_prompts.toml @@ -8,7 +8,9 @@ Your task is to update the CHANGELOG.md file of the project, to shortly summariz {%- if extra_instructions %} Extra instructions from the user: +' {{ extra_instructions }} +' {%- endif %} """ From 44239f1a79e3990da667038677d2830e8f4ebe11 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Thu, 5 Oct 2023 08:38:43 +0300 Subject: [PATCH 02/29] Patch Extra Lines --- Usage.md | 11 +++++++++++ pr_agent/algo/pr_processing.py | 13 +++++++------ pr_agent/settings/configuration.toml | 1 + pr_agent/tools/pr_add_docs.py | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Usage.md b/Usage.md index 6176eaf0..f79f7567 100644 --- a/Usage.md +++ b/Usage.md @@ -261,6 +261,17 @@ All PR-Agent tools have a parameter called `extra_instructions`, that enables to /update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..." ``` +#### Patch Extra Lines +By default, around any change in your PR, git patch provides 3 lines of context above and below the change. +For the `review`, `describe`, `ask` and `add_docs` tools, if the token budget allows, PR-Agent tries to increase the number of lines of context, via the parameter: +``` +[config] +patch_extra_lines=3 +``` +Increasing this number provides more context to the model, but will also increase the token budget. +If the PR is too large (see [PR Compression strategy](./PR_COMPRESSION.md)), we automatically set this number to 0 + + #### Azure DevOps provider To use Azure DevOps provider use the following settings in configuration.toml: ``` diff --git a/pr_agent/algo/pr_processing.py b/pr_agent/algo/pr_processing.py index 1c34e603..4d717202 100644 --- a/pr_agent/algo/pr_processing.py +++ b/pr_agent/algo/pr_processing.py @@ -21,7 +21,6 @@ MORE_MODIFIED_FILES_ = "More modified files:\n" OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1000 OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 600 -PATCH_EXTRA_LINES = 3 def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: str, add_line_numbers_to_hunks: bool = False, disable_extra_lines: bool = False) -> str: @@ -44,8 +43,9 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s """ if disable_extra_lines: - global PATCH_EXTRA_LINES PATCH_EXTRA_LINES = 0 + else: + PATCH_EXTRA_LINES = get_settings().config.patch_extra_lines try: diff_files = git_provider.get_diff_files() @@ -57,8 +57,8 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files) # generate a standard diff string, with patch extension - patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff(pr_languages, token_handler, - add_line_numbers_to_hunks) + patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff( + pr_languages, token_handler, add_line_numbers_to_hunks, patch_extra_lines=PATCH_EXTRA_LINES) # if we are under the limit, return the full diff if total_tokens + OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD < MAX_TOKENS[model]: @@ -80,7 +80,8 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s def pr_generate_extended_diff(pr_languages: list, token_handler: TokenHandler, - add_line_numbers_to_hunks: bool) -> Tuple[list, int, list]: + add_line_numbers_to_hunks: bool, + patch_extra_lines: int = 0) -> Tuple[list, int, list]: """ Generate a standard diff string with patch extension, while counting the number of tokens used and applying diff minimization techniques if needed. @@ -102,7 +103,7 @@ def pr_generate_extended_diff(pr_languages: list, continue # extend each patch with extra lines of context - extended_patch = extend_patch(original_file_content_str, patch, num_lines=PATCH_EXTRA_LINES) + extended_patch = extend_patch(original_file_content_str, patch, num_lines=patch_extra_lines) full_extended_patch = f"\n\n## {file.filename}\n\n{extended_patch}\n" if add_line_numbers_to_hunks: diff --git a/pr_agent/settings/configuration.toml b/pr_agent/settings/configuration.toml index 187ded56..9a03055f 100644 --- a/pr_agent/settings/configuration.toml +++ b/pr_agent/settings/configuration.toml @@ -10,6 +10,7 @@ use_repo_settings_file=true ai_timeout=180 max_description_tokens = 500 max_commits_tokens = 500 +patch_extra_lines = 3 secret_provider="google_cloud_storage" cli_mode=false diff --git a/pr_agent/tools/pr_add_docs.py b/pr_agent/tools/pr_add_docs.py index 4cc9102a..2769e9a9 100644 --- a/pr_agent/tools/pr_add_docs.py +++ b/pr_agent/tools/pr_add_docs.py @@ -68,7 +68,7 @@ class PRAddDocs: self.token_handler, model, add_line_numbers_to_hunks=True, - disable_extra_lines=True) + disable_extra_lines=False) logging.info('Getting AI prediction...') self.prediction = await self._get_prediction(model) From f3e794e50be5f1ee5bc9d55d70671d8797294ad6 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Thu, 5 Oct 2023 08:46:02 +0300 Subject: [PATCH 03/29] Patch Extra Lines --- Usage.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Usage.md b/Usage.md index f79f7567..e80fea7b 100644 --- a/Usage.md +++ b/Usage.md @@ -263,13 +263,26 @@ All PR-Agent tools have a parameter called `extra_instructions`, that enables to #### Patch Extra Lines By default, around any change in your PR, git patch provides 3 lines of context above and below the change. +``` +@@ -12,5 +12,5 @@ def func1(): + code line that already existed in the file... + code line that already existed in the file... + code line that already existed in the file.... +-code line that was removed in the PR ++new code line added in the PR + code line that already existed in the file... + code line that already existed in the file... + code line that already existed in the file... +``` + For the `review`, `describe`, `ask` and `add_docs` tools, if the token budget allows, PR-Agent tries to increase the number of lines of context, via the parameter: ``` [config] patch_extra_lines=3 ``` + Increasing this number provides more context to the model, but will also increase the token budget. -If the PR is too large (see [PR Compression strategy](./PR_COMPRESSION.md)), we automatically set this number to 0 +If the PR is too large (see [PR Compression strategy](./PR_COMPRESSION.md)), PR-Agent automatically sets this number to 0, using the original git patch. #### Azure DevOps provider From 088f256415ce7cdf85651870ff253c3c2f445b3e Mon Sep 17 00:00:00 2001 From: mrT23 Date: Thu, 5 Oct 2023 17:03:10 +0300 Subject: [PATCH 04/29] stable --- Usage.md | 7 +++++++ pr_agent/algo/utils.py | 2 ++ pr_agent/git_providers/gitlab_provider.py | 3 +-- pr_agent/tools/pr_code_suggestions.py | 6 +++--- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Usage.md b/Usage.md index e80fea7b..9f4af800 100644 --- a/Usage.md +++ b/Usage.md @@ -122,6 +122,13 @@ keep_original_user_title = false ``` When a new PR is opened, PR-Agent will run the `describe` tool with the above parameters. +To cancel the automatic run of all the tools, set: +``` +[github_app] +pr_commands = [] +``` + + Note that a local `.pr_agent.toml` file enables you to edit and customize the default parameters of any tool, not just the ones that are run automatically. #### Editing the prompts diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index c7923d16..6460c31d 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -99,6 +99,8 @@ def parse_code_suggestion(code_suggestions: dict) -> str: markdown_text += f"\n - **{sub_key}:** {sub_value}\n" else: markdown_text += f" **{sub_key}:** {sub_value}\n" + if "relevant line" not in sub_key.lower(): # nicer presentation + markdown_text = markdown_text.rstrip('\n') + "\\\n" markdown_text += "\n" return markdown_text diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 33cbee2b..350efcfc 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -175,8 +175,7 @@ class GitLabProvider(GitProvider): pos_obj['new_line'] = target_line_no - 1 pos_obj['old_line'] = source_line_no - 1 logging.debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}") - self.mr.discussions.create({'body': body, - 'position': pos_obj}) + self.mr.discussions.create({'body': body, 'position': pos_obj}) def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]: changes = self.mr.changes() # Retrieve the changes for the merge request once diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index 7f0b1264..e1d5206c 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -138,9 +138,9 @@ class PRCodeSuggestions: if get_settings().config.verbosity_level >= 2: logging.info(f"Could not parse suggestion: {d}") - is_successful = self.git_provider.publish_code_suggestions(code_suggestions) - if not is_successful: - logging.info("Failed to publish code suggestions, trying to publish each suggestion separately") + # is_successful = self.git_provider.publish_code_suggestions(code_suggestions) + if True: + # logging.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]) From e387086890a64d13c5d0c378c01f80f078d31181 Mon Sep 17 00:00:00 2001 From: jamesrom Date: Fri, 6 Oct 2023 01:43:35 +1100 Subject: [PATCH 05/29] Add support for ignoring files Add ignore.toml, configuration for ignoring files Add file_filter.py, for matching files against glob/regex patterns Update relevant code to use file filter +Tests --- pr_agent/algo/file_filter.py | 23 ++++++++++++ pr_agent/algo/pr_processing.py | 11 ++++-- pr_agent/config_loader.py | 17 +++++---- pr_agent/settings/ignore.toml | 5 +++ tests/unittest/test_file_filter.py | 59 ++++++++++++++++++++++++++++++ 5 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 pr_agent/algo/file_filter.py create mode 100644 pr_agent/settings/ignore.toml create mode 100644 tests/unittest/test_file_filter.py diff --git a/pr_agent/algo/file_filter.py b/pr_agent/algo/file_filter.py new file mode 100644 index 00000000..3dc78c64 --- /dev/null +++ b/pr_agent/algo/file_filter.py @@ -0,0 +1,23 @@ +import fnmatch +import re + +from pr_agent.config_loader import get_settings + +def filter_ignored(files): + """ + Filter out files that match the ignore patterns. + """ + + # load regex patterns, and translate glob patterns to regex + patterns = get_settings().ignore.regex + patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob] + + compiled_patterns = [re.compile(r) for r in patterns] + filenames = [file.filename for file in files] + + # keep filenames that don't match the ignore regex + for r in compiled_patterns: + filenames = [f for f in filenames if not r.match(f)] + + # map filenames back to files + return [file for file in files if file.filename in filenames] diff --git a/pr_agent/algo/pr_processing.py b/pr_agent/algo/pr_processing.py index 4d717202..4327a0f1 100644 --- a/pr_agent/algo/pr_processing.py +++ b/pr_agent/algo/pr_processing.py @@ -11,6 +11,7 @@ from github import RateLimitExceededException from pr_agent.algo import MAX_TOKENS from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions from pr_agent.algo.language_handler import sort_files_by_main_languages +from pr_agent.algo.file_filter import filter_ignored from pr_agent.algo.token_handler import TokenHandler, get_token_encoder from pr_agent.config_loader import get_settings from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider @@ -53,6 +54,8 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s logging.error(f"Rate limit exceeded for git provider API. original message {e}") raise + diff_files = filter_ignored(diff_files) + # get pr languages pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files) @@ -348,16 +351,16 @@ def get_pr_multi_diffs(git_provider: GitProvider, """ Retrieves the diff files from a Git provider, sorts them by main language, and generates patches for each file. The patches are split into multiple groups based on the maximum number of tokens allowed for the given model. - + Args: git_provider (GitProvider): An object that provides access to Git provider APIs. token_handler (TokenHandler): An object that handles tokens in the context of a pull request. model (str): The name of the model. max_calls (int, optional): The maximum number of calls to retrieve diff files. Defaults to 5. - + Returns: List[str]: A list of final diff strings, split into multiple groups based on the maximum number of tokens allowed for the given model. - + Raises: RateLimitExceededException: If the rate limit for the Git provider API is exceeded. """ @@ -367,6 +370,8 @@ def get_pr_multi_diffs(git_provider: GitProvider, logging.error(f"Rate limit exceeded for git provider API. original message {e}") raise + diff_files = filter_ignored(diff_files) + # Sort files by main language pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files) diff --git a/pr_agent/config_loader.py b/pr_agent/config_loader.py index 184adb82..80e091b8 100644 --- a/pr_agent/config_loader.py +++ b/pr_agent/config_loader.py @@ -12,18 +12,19 @@ global_settings = Dynaconf( envvar_prefix=False, merge_enabled=True, settings_files=[join(current_dir, f) for f in [ + "settings_prod/.secrets.toml" "settings/.secrets.toml", "settings/configuration.toml", + "settings/ignore.toml", "settings/language_extensions.toml", - "settings/pr_reviewer_prompts.toml", - "settings/pr_questions_prompts.toml", - "settings/pr_description_prompts.toml", - "settings/pr_code_suggestions_prompts.toml", - "settings/pr_sort_code_suggestions_prompts.toml", - "settings/pr_information_from_user_prompts.toml", - "settings/pr_update_changelog_prompts.toml", "settings/pr_add_docs.toml", - "settings_prod/.secrets.toml" + "settings/pr_code_suggestions_prompts.toml", + "settings/pr_description_prompts.toml", + "settings/pr_information_from_user_prompts.toml", + "settings/pr_questions_prompts.toml", + "settings/pr_reviewer_prompts.toml", + "settings/pr_sort_code_suggestions_prompts.toml", + "settings/pr_update_changelog_prompts.toml", ]] ) diff --git a/pr_agent/settings/ignore.toml b/pr_agent/settings/ignore.toml new file mode 100644 index 00000000..a59b810b --- /dev/null +++ b/pr_agent/settings/ignore.toml @@ -0,0 +1,5 @@ +[ignore] + +# Ignore files and directories matching these patterns. +glob = [] +regex = [] diff --git a/tests/unittest/test_file_filter.py b/tests/unittest/test_file_filter.py new file mode 100644 index 00000000..4856fbb4 --- /dev/null +++ b/tests/unittest/test_file_filter.py @@ -0,0 +1,59 @@ +import pytest +from pr_agent.algo.file_filter import filter_ignored +from pr_agent.config_loader import global_settings + +class TestIgnoreFilter: + def test_no_ignores(self): + """ + Test no files are ignored when no patterns are specified. + """ + files = [ + type('', (object,), {'filename': 'file1.py'})(), + type('', (object,), {'filename': 'file2.java'})(), + type('', (object,), {'filename': 'file3.cpp'})(), + type('', (object,), {'filename': 'file4.py'})(), + type('', (object,), {'filename': 'file5.py'})() + ] + assert filter_ignored(files) == files + + def test_glob_ignores(self, monkeypatch): + """ + Test files are ignored when glob patterns are specified. + """ + monkeypatch.setattr(global_settings.ignore, 'glob', ['*.py']) + + files = [ + type('', (object,), {'filename': 'file1.py'})(), + type('', (object,), {'filename': 'file2.java'})(), + type('', (object,), {'filename': 'file3.cpp'})(), + type('', (object,), {'filename': 'file4.py'})(), + type('', (object,), {'filename': 'file5.py'})() + ] + expected = [ + files[1], + files[2] + ] + + filtered_files = filter_ignored(files) + assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}." + + def test_regex_ignores(self, monkeypatch): + """ + Test files are ignored when regex patterns are specified. + """ + monkeypatch.setattr(global_settings.ignore, 'regex', ['^file[2-4]\..*$']) + + files = [ + type('', (object,), {'filename': 'file1.py'})(), + type('', (object,), {'filename': 'file2.java'})(), + type('', (object,), {'filename': 'file3.cpp'})(), + type('', (object,), {'filename': 'file4.py'})(), + type('', (object,), {'filename': 'file5.py'})() + ] + expected = [ + files[0], + files[4] + ] + + filtered_files = filter_ignored(files) + assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}." From 989c56220b1d00a5cecc089b4e34d8f5fac9712a Mon Sep 17 00:00:00 2001 From: mrT23 Date: Thu, 5 Oct 2023 17:48:36 +0300 Subject: [PATCH 06/29] add line number --- pr_agent/git_providers/gitlab_provider.py | 25 +++++++++++++++++++++++ pr_agent/tools/pr_reviewer.py | 15 ++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 350efcfc..15b76dd8 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -385,3 +385,28 @@ class GitLabProvider(GitProvider): return pr_id except: return "" + + # def generate_link_to_relevant_line_number(self, suggestion) -> str: + # try: + # relevant_file = suggestion['relevant file'].strip('`').strip("'") + # relevant_line_str = suggestion['relevant line'] + # if not relevant_line_str: + # return "" + # + # position, absolute_position = find_line_number_of_relevant_line_in_file \ + # (self.diff_files, relevant_file, relevant_line_str) + # + # if absolute_position != -1: + # # # link to right file only + # # link = f"https://github.com/{self.repo}/blob/{self.pr.head.sha}/{relevant_file}" \ + # # + "#" + f"L{absolute_position}" + # + # # link to diff + # sha_file = hashlib.sha1(relevant_file.encode('utf-8')).hexdigest() + # link = f"{self.pr.web_url}/diffs#{sha_file}_{absolute_position}_{absolute_position}" + # return link + # except Exception as e: + # if get_settings().config.verbosity_level >= 2: + # logging.info(f"Failed adding line link, error: {e}") + # + # return "" diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index 01e3f276..a0d9394c 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -209,6 +209,21 @@ class PRReviewer: link = self.git_provider.generate_link_to_relevant_line_number(suggestion) if link: suggestion['relevant line'] = f"[{suggestion['relevant line']}]({link})" + else: + try: + relevant_file = suggestion['relevant file'].strip('`').strip("'") + relevant_line_str = suggestion['relevant line'] + if not relevant_line_str: + return "" + + position, absolute_position = find_line_number_of_relevant_line_in_file( + self.git_provider.diff_files, relevant_file, relevant_line_str) + if absolute_position != -1: + suggestion[ + 'relevant line'] = f"{suggestion['relevant line']} (line {absolute_position})" + except: + pass + # Add incremental review section if self.incremental.is_incremental: From 72eecbbf616ecc1879ebaf74c75a784bef9256fd Mon Sep 17 00:00:00 2001 From: mrT23 Date: Thu, 5 Oct 2023 17:59:08 +0300 Subject: [PATCH 07/29] add line number --- pr_agent/git_providers/gitlab_provider.py | 50 +++++++++++------------ pr_agent/tools/pr_reviewer.py | 27 ++++++------ 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 15b76dd8..7a8e6bf9 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -1,3 +1,4 @@ +import hashlib import logging import re from typing import Optional, Tuple @@ -7,7 +8,7 @@ import gitlab from gitlab import GitlabGetError from ..algo.language_handler import is_valid_file -from ..algo.pr_processing import clip_tokens +from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file from ..algo.utils import load_large_diff from ..config_loader import get_settings from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider @@ -386,27 +387,26 @@ class GitLabProvider(GitProvider): except: return "" - # def generate_link_to_relevant_line_number(self, suggestion) -> str: - # try: - # relevant_file = suggestion['relevant file'].strip('`').strip("'") - # relevant_line_str = suggestion['relevant line'] - # if not relevant_line_str: - # return "" - # - # position, absolute_position = find_line_number_of_relevant_line_in_file \ - # (self.diff_files, relevant_file, relevant_line_str) - # - # if absolute_position != -1: - # # # link to right file only - # # link = f"https://github.com/{self.repo}/blob/{self.pr.head.sha}/{relevant_file}" \ - # # + "#" + f"L{absolute_position}" - # - # # link to diff - # sha_file = hashlib.sha1(relevant_file.encode('utf-8')).hexdigest() - # link = f"{self.pr.web_url}/diffs#{sha_file}_{absolute_position}_{absolute_position}" - # return link - # except Exception as e: - # if get_settings().config.verbosity_level >= 2: - # logging.info(f"Failed adding line link, error: {e}") - # - # return "" + def generate_link_to_relevant_line_number(self, suggestion) -> str: + try: + relevant_file = suggestion['relevant file'].strip('`').strip("'") + relevant_line_str = suggestion['relevant line'] + if not relevant_line_str: + return "" + + position, absolute_position = find_line_number_of_relevant_line_in_file \ + (self.diff_files, relevant_file, relevant_line_str) + + if absolute_position != -1: + # link to right file only + link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.target_branch}/{relevant_file}?ref_type=heads#L{absolute_position}" + + # # link to diff + # sha_file = hashlib.sha1(relevant_file.encode('utf-8')).hexdigest() + # link = f"{self.pr.web_url}/diffs#{sha_file}_{absolute_position}_{absolute_position}" + return link + except Exception as e: + if get_settings().config.verbosity_level >= 2: + logging.info(f"Failed adding line link, error: {e}") + + return "" diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index a0d9394c..b96d5379 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -210,19 +210,20 @@ class PRReviewer: if link: suggestion['relevant line'] = f"[{suggestion['relevant line']}]({link})" else: - try: - relevant_file = suggestion['relevant file'].strip('`').strip("'") - relevant_line_str = suggestion['relevant line'] - if not relevant_line_str: - return "" - - position, absolute_position = find_line_number_of_relevant_line_in_file( - self.git_provider.diff_files, relevant_file, relevant_line_str) - if absolute_position != -1: - suggestion[ - 'relevant line'] = f"{suggestion['relevant line']} (line {absolute_position})" - except: - pass + pass + # try: + # relevant_file = suggestion['relevant file'].strip('`').strip("'") + # relevant_line_str = suggestion['relevant line'] + # if not relevant_line_str: + # return "" + # + # position, absolute_position = find_line_number_of_relevant_line_in_file( + # self.git_provider.diff_files, relevant_file, relevant_line_str) + # if absolute_position != -1: + # suggestion[ + # 'relevant line'] = f"{suggestion['relevant line']} (line {absolute_position})" + # except: + # pass # Add incremental review section From 1bab26f1c57676f3c9cad801a935ff828244562a Mon Sep 17 00:00:00 2001 From: mrT23 Date: Thu, 5 Oct 2023 18:08:02 +0300 Subject: [PATCH 08/29] gfm_supported --- pr_agent/algo/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index 6460c31d..11f28e38 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -62,7 +62,7 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str: markdown_text += f"- {emoji} **{key}:**\n\n" for item in value: if isinstance(item, dict) and key.lower() == 'code feedback': - markdown_text += parse_code_suggestion(item) + markdown_text += parse_code_suggestion(item, gfm_supported) elif item: markdown_text += f" - {item}\n" if key.lower() == 'code feedback': @@ -76,7 +76,7 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str: return markdown_text -def parse_code_suggestion(code_suggestions: dict) -> str: +def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> str: """ Convert a dictionary of data into markdown format. @@ -99,8 +99,9 @@ def parse_code_suggestion(code_suggestions: dict) -> str: markdown_text += f"\n - **{sub_key}:** {sub_value}\n" else: markdown_text += f" **{sub_key}:** {sub_value}\n" - if "relevant line" not in sub_key.lower(): # nicer presentation - markdown_text = markdown_text.rstrip('\n') + "\\\n" + if not gfm_supported: + if "relevant line" not in sub_key.lower(): # nicer presentation + markdown_text = markdown_text.rstrip('\n') + "\\\n" markdown_text += "\n" return markdown_text From 2945c36899a1b4d6d9b450c2748af3f58de2f4a7 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Thu, 5 Oct 2023 18:21:52 +0300 Subject: [PATCH 09/29] source_branch --- 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 7a8e6bf9..2e6f2140 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -399,7 +399,7 @@ class GitLabProvider(GitProvider): if absolute_position != -1: # link to right file only - link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.target_branch}/{relevant_file}?ref_type=heads#L{absolute_position}" + link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{absolute_position}" # # link to diff # sha_file = hashlib.sha1(relevant_file.encode('utf-8')).hexdigest() From ea6253e2e8aba1ba768477b7055f94084c14e3ec Mon Sep 17 00:00:00 2001 From: mrT23 Date: Fri, 6 Oct 2023 08:12:11 +0300 Subject: [PATCH 10/29] revert azure --- pr_agent/algo/ai_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pr_agent/algo/ai_handler.py b/pr_agent/algo/ai_handler.py index 819ba25b..90247bde 100644 --- a/pr_agent/algo/ai_handler.py +++ b/pr_agent/algo/ai_handler.py @@ -92,6 +92,8 @@ class AiHandler: f"Generating completion with {model}" f"{(' from deployment ' + deployment_id) if deployment_id else ''}" ) + if self.azure: + model = self.azure + "/" + model response = await acompletion( model=model, deployment_id=deployment_id, @@ -100,7 +102,6 @@ class AiHandler: {"role": "user", "content": user} ], temperature=temperature, - azure=self.azure, force_timeout=get_settings().config.ai_timeout ) except (APIError, Timeout, TryAgain) as e: From fd8c90041c730be45df13a1fcd96b6f87a4a2ea0 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Fri, 6 Oct 2023 08:31:31 +0300 Subject: [PATCH 11/29] azure --- pr_agent/algo/ai_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/algo/ai_handler.py b/pr_agent/algo/ai_handler.py index 90247bde..ba285743 100644 --- a/pr_agent/algo/ai_handler.py +++ b/pr_agent/algo/ai_handler.py @@ -93,7 +93,7 @@ class AiHandler: f"{(' from deployment ' + deployment_id) if deployment_id else ''}" ) if self.azure: - model = self.azure + "/" + model + model = 'azure/' + model response = await acompletion( model=model, deployment_id=deployment_id, From b27f57d05d28d7b63434a60ee4472df4cfc4c2cf Mon Sep 17 00:00:00 2001 From: jamesrom Date: Fri, 6 Oct 2023 21:03:36 +1100 Subject: [PATCH 12/29] Update settings, documentation --- INSTALL.md | 26 +++++++++++++------------- pr_agent/config_loader.py | 12 ++++++------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index f4247ce2..492dc25c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -40,7 +40,7 @@ For other git providers, update CONFIG.GIT_PROVIDER accordingly, and check the ` ``` docker run --rm -it -e OPENAI.KEY= -e GITHUB.USER_TOKEN= codiumai/pr-agent --pr_url ask "" ``` -Note: If you want to ensure you're running a specific version of the Docker image, consider using the image's digest. +Note: If you want to ensure you're running a specific version of the Docker image, consider using the image's digest. The digest is a unique identifier for a specific version of an image. You can pull and run an image using its digest by referencing it like so: repository@sha256:digest. Always ensure you're using the correct and trusted digest for your operations. 1. To request a review for a PR using a specific digest, run the following command: @@ -89,17 +89,17 @@ chmod 600 pr_agent/settings/.secrets.toml ``` export PYTHONPATH=[$PYTHONPATH:] -python3 -m pr_agent.cli --pr_url /review -python3 -m pr_agent.cli --pr_url /ask -python3 -m pr_agent.cli --pr_url /describe -python3 -m pr_agent.cli --pr_url /improve +python3 -m pr_agent.cli --pr_url review +python3 -m pr_agent.cli --pr_url ask +python3 -m pr_agent.cli --pr_url describe +python3 -m pr_agent.cli --pr_url improve ``` --- ### Method 3: Run as a GitHub Action -You can use our pre-built Github Action Docker image to run PR-Agent as a Github Action. +You can use our pre-built Github Action Docker image to run PR-Agent as a Github Action. 1. Add the following file to your repository under `.github/workflows/pr_agent.yml`: @@ -153,7 +153,7 @@ OPENAI_KEY: The GITHUB_TOKEN secret is automatically created by GitHub. -3. Merge this change to your main branch. +3. Merge this change to your main branch. When you open your next PR, you should see a comment from `github-actions` bot with a review of your PR, and instructions on how to use the rest of the tools. 4. You may configure PR-Agent by adding environment variables under the env section corresponding to any configurable property in the [configuration](pr_agent/settings/configuration.toml) file. Some examples: @@ -221,12 +221,12 @@ git clone https://github.com/Codium-ai/pr-agent.git - Copy your app's webhook secret to the webhook_secret field. - Set deployment_type to 'app' in [configuration.toml](./pr_agent/settings/configuration.toml) -> The .secrets.toml file is not copied to the Docker image by default, and is only used for local development. +> The .secrets.toml file is not copied to the Docker image by default, and is only used for local development. > If you want to use the .secrets.toml file in your Docker image, you can add remove it from the .dockerignore file. -> In most production environments, you would inject the secrets file as environment variables or as mounted volumes. +> In most production environments, you would inject the secrets file as environment variables or as mounted volumes. > For example, in order to inject a secrets file as a volume in a Kubernetes environment you can update your pod spec to include the following, > assuming you have a secret named `pr-agent-settings` with a key named `.secrets.toml`: -``` +``` volumes: - name: settings-volume secret: @@ -322,7 +322,7 @@ Example IAM permissions to that user to allow access to CodeCommit: "codecommit:PostComment*", "codecommit:PutCommentReaction", "codecommit:UpdatePullRequestDescription", - "codecommit:UpdatePullRequestTitle" + "codecommit:UpdatePullRequestTitle" ], "Resource": "*" } @@ -366,8 +366,8 @@ WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))") - Your OpenAI key. - In the [gitlab] section, fill in personal_access_token and shared_secret. The access token can be a personal access token, or a group or project access token. - Set deployment_type to 'gitlab' in [configuration.toml](./pr_agent/settings/configuration.toml) -5. Create a webhook in GitLab. Set the URL to the URL of your app's server. Set the secret token to the generated secret from step 2. -In the "Trigger" section, check the ‘comments’ and ‘merge request events’ boxes. +5. Create a webhook in GitLab. Set the URL to the URL of your app's server. Set the secret token to the generated secret from step 2. +In the "Trigger" section, check the ‘comments’ and ‘merge request events’ boxes. 6. Test your installation by opening a merge request or commenting or a merge request using one of CodiumAI's commands. diff --git a/pr_agent/config_loader.py b/pr_agent/config_loader.py index 80e091b8..3b0b0360 100644 --- a/pr_agent/config_loader.py +++ b/pr_agent/config_loader.py @@ -12,19 +12,19 @@ global_settings = Dynaconf( envvar_prefix=False, merge_enabled=True, settings_files=[join(current_dir, f) for f in [ - "settings_prod/.secrets.toml" "settings/.secrets.toml", "settings/configuration.toml", "settings/ignore.toml", "settings/language_extensions.toml", - "settings/pr_add_docs.toml", - "settings/pr_code_suggestions_prompts.toml", - "settings/pr_description_prompts.toml", - "settings/pr_information_from_user_prompts.toml", - "settings/pr_questions_prompts.toml", "settings/pr_reviewer_prompts.toml", + "settings/pr_questions_prompts.toml", + "settings/pr_description_prompts.toml", + "settings/pr_code_suggestions_prompts.toml", "settings/pr_sort_code_suggestions_prompts.toml", + "settings/pr_information_from_user_prompts.toml", "settings/pr_update_changelog_prompts.toml", + "settings/pr_add_docs.toml", + "settings_prod/.secrets.toml" ]] ) From baa0e95227fc985cc6f4f28fbd088acb20a1f3fa Mon Sep 17 00:00:00 2001 From: jamesrom Date: Fri, 6 Oct 2023 21:53:10 +1100 Subject: [PATCH 13/29] Code comments for ignore.toml --- pr_agent/settings/ignore.toml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pr_agent/settings/ignore.toml b/pr_agent/settings/ignore.toml index a59b810b..429d3887 100644 --- a/pr_agent/settings/ignore.toml +++ b/pr_agent/settings/ignore.toml @@ -1,5 +1,11 @@ [ignore] -# Ignore files and directories matching these patterns. -glob = [] -regex = [] +glob = [ + # Ignore files and directories matching these glob patterns. + # See https://docs.python.org/3/library/glob.html + 'vendor/**', +] +regex = [ + # Ignore files and directories matching these regex patterns. + # See https://learnbyexample.github.io/python-regex-cheatsheet/ +] From 6dee18b24a1c34a2aac3e32572e584dd95dd43e5 Mon Sep 17 00:00:00 2001 From: jamesrom Date: Fri, 6 Oct 2023 22:13:03 +1100 Subject: [PATCH 14/29] Update usage documentation --- Usage.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/Usage.md b/Usage.md index 9f4af800..4a3855e4 100644 --- a/Usage.md +++ b/Usage.md @@ -29,6 +29,16 @@ In addition to general configuration options, each tool has its own configuratio The [Tools Guide](./docs/TOOLS_GUIDE.md) provides a detailed description of the different tools and their configurations. +#### Ignoring files from analysis +In some cases, you may want to exclude specific files or directories from the analysis performed by CodiumAI PR-Agent. This can be useful, for example, when you have files that are generated automatically or files that shouldn't be reviewed, like vendored code. + +To ignore files or directories, edit the **[ignore.toml](/pr_agent/settings/ignore.toml)** configuration file. This setting is also exposed the following environment variables: + + - `IGNORE.GLOB` + - `IGNORE.REGEX` + +See [dynaconf envvars documentation](https://www.dynaconf.com/envvars/). + #### git provider The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configuration file determines the GIT provider that will be used by PR-Agent. Currently, the following providers are supported: ` @@ -101,7 +111,7 @@ Any configuration value in [configuration file](pr_agent/settings/configuration. When running PR-Agent from [GitHub App](INSTALL.md#method-5-run-as-a-github-app), the default configurations from a pre-built docker will be initially loaded. #### GitHub app automatic tools -The [github_app](pr_agent/settings/configuration.toml#L56) section defines GitHub app specific configurations. +The [github_app](pr_agent/settings/configuration.toml#L56) section defines GitHub app specific configurations. An important parameter is `pr_commands`, which is a list of tools that will be **run automatically** when a new PR is opened: ``` [github_app] @@ -133,7 +143,7 @@ Note that a local `.pr_agent.toml` file enables you to edit and customize the de #### Editing the prompts The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder. -In practice, the prompts are loaded and stored as a standard setting object. +In practice, the prompts are loaded and stored as a standard setting object. Hence, editing them is similar to editing any other configuration value - just place the relevant key in `.pr_agent.toml`file, and override the default value. For example, if you want to edit the prompts of the [describe](./pr_agent/settings/pr_description_prompts.toml) tool, you can add the following to your `.pr_agent.toml` file: @@ -158,7 +168,7 @@ You can configure settings in GitHub action by adding environment variables unde PR_CODE_SUGGESTIONS.NUM_CODE_SUGGESTIONS: 6 # Increase number of code suggestions github_action.auto_review: "true" # Enable auto review github_action.auto_describe: "true" # Enable auto describe - github_action.auto_improve: "false" # Disable auto improve + github_action.auto_improve: "false" # Disable auto improve ``` specifically, `github_action.auto_review`, `github_action.auto_describe` and `github_action.auto_improve` are used to enable/disable automatic tools that run when a new PR is opened. @@ -171,7 +181,7 @@ To use a different model than the default (GPT-4), you need to edit [configurati For models and environments not from OPENAI, you might need to provide additional keys and other parameters. See below for instructions. #### Azure -To use Azure, set in your .secrets.toml: +To use Azure, set in your .secrets.toml: ``` api_key = "" # your azure api key api_type = "azure" @@ -180,16 +190,16 @@ api_base = "" # The base URL for your Azure OpenAI resource. e.g. "https:// Date: Fri, 6 Oct 2023 22:44:29 +1100 Subject: [PATCH 15/29] Simplify filter --- pr_agent/algo/file_filter.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pr_agent/algo/file_filter.py b/pr_agent/algo/file_filter.py index 3dc78c64..fb40cfda 100644 --- a/pr_agent/algo/file_filter.py +++ b/pr_agent/algo/file_filter.py @@ -10,14 +10,13 @@ def filter_ignored(files): # load regex patterns, and translate glob patterns to regex patterns = get_settings().ignore.regex - patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob] + patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob] + # compile regex patterns compiled_patterns = [re.compile(r) for r in patterns] - filenames = [file.filename for file in files] - # keep filenames that don't match the ignore regex + # keep filenames that _don't_ match the ignore regex for r in compiled_patterns: - filenames = [f for f in filenames if not r.match(f)] + files = [f for f in files if not r.match(f.filename)] - # map filenames back to files - return [file for file in files if file.filename in filenames] + return files From 92e9012fb63d21f2464f48979c9e642a43e9d6ed Mon Sep 17 00:00:00 2001 From: jamesrom Date: Sat, 7 Oct 2023 09:39:53 +1100 Subject: [PATCH 16/29] Error handling --- pr_agent/algo/file_filter.py | 25 +++++++++++++++++-------- tests/unittest/test_file_filter.py | 23 ++++++++++++++++++++++- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/pr_agent/algo/file_filter.py b/pr_agent/algo/file_filter.py index fb40cfda..cc466f57 100644 --- a/pr_agent/algo/file_filter.py +++ b/pr_agent/algo/file_filter.py @@ -8,15 +8,24 @@ def filter_ignored(files): Filter out files that match the ignore patterns. """ - # load regex patterns, and translate glob patterns to regex - patterns = get_settings().ignore.regex - patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob] + try: + # load regex patterns, and translate glob patterns to regex + patterns = get_settings().ignore.regex + patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob] - # compile regex patterns - compiled_patterns = [re.compile(r) for r in patterns] + # compile all valid patterns + compiled_patterns = [] + for r in patterns: + try: + compiled_patterns.append(re.compile(r)) + except re.error: + pass - # keep filenames that _don't_ match the ignore regex - for r in compiled_patterns: - files = [f for f in files if not r.match(f.filename)] + # keep filenames that _don't_ match the ignore regex + for r in compiled_patterns: + files = [f for f in files if not r.match(f.filename)] + + except Exception as e: + print(f"Could not filter file list: {e}") return files diff --git a/tests/unittest/test_file_filter.py b/tests/unittest/test_file_filter.py index 4856fbb4..43e9c9b4 100644 --- a/tests/unittest/test_file_filter.py +++ b/tests/unittest/test_file_filter.py @@ -14,7 +14,7 @@ class TestIgnoreFilter: type('', (object,), {'filename': 'file4.py'})(), type('', (object,), {'filename': 'file5.py'})() ] - assert filter_ignored(files) == files + assert filter_ignored(files) == files, "Expected all files to be returned when no ignore patterns are given." def test_glob_ignores(self, monkeypatch): """ @@ -57,3 +57,24 @@ class TestIgnoreFilter: filtered_files = filter_ignored(files) assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}." + + def test_invalid_regex(self, monkeypatch): + """ + Test invalid patterns are quietly ignored. + """ + monkeypatch.setattr(global_settings.ignore, 'regex', ['(((||', '^file[2-4]\..*$']) + + files = [ + type('', (object,), {'filename': 'file1.py'})(), + type('', (object,), {'filename': 'file2.java'})(), + type('', (object,), {'filename': 'file3.cpp'})(), + type('', (object,), {'filename': 'file4.py'})(), + type('', (object,), {'filename': 'file5.py'})() + ] + expected = [ + files[0], + files[4] + ] + + filtered_files = filter_ignored(files) + assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}." From 70c0ef5ce1f66b2ce0fcc8e6e2877575f3315757 Mon Sep 17 00:00:00 2001 From: Phill Zarfos Date: Sat, 7 Oct 2023 09:57:51 -0400 Subject: [PATCH 17/29] added checkmark on features table for update_changelog with CodeCommit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6fd1f9f6..f5bc474e 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ See the [Release notes](./RELEASE_NOTES.md) for updates on the latest changes. | | Improve Code | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: | -| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | | +| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | | | Find similar issue | :white_check_mark: | | | | | | | | Add Documentation | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | | | | | | | | From d77a71bf477222dd1ef268329002ba853c877765 Mon Sep 17 00:00:00 2001 From: Zohar Meir <33152084+zmeir@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:35:54 +0300 Subject: [PATCH 18/29] Small typo in DESCRIBE.md --- docs/DESCRIBE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DESCRIBE.md b/docs/DESCRIBE.md index bb07668c..05d1608a 100644 --- a/docs/DESCRIBE.md +++ b/docs/DESCRIBE.md @@ -23,7 +23,7 @@ Under the section 'pr_description', the [configuration file](./../pr_agent/setti - `add_original_user_description`: if set to true, the tool will add the original user description to the generated description. Default is false. -- `add_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is false. +- `keep_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is false. - `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...". @@ -48,4 +48,4 @@ pr_agent:walkthrough ``` The marker `pr_agent:pr_type` will be replaced with the PR type, `pr_agent:summary` will be replaced with the PR summary, and `pr_agent:walkthrough` will be replaced with the PR walkthrough. -- `include_generated_by_header`: if set to true, the tool will add a dedicated header: 'Generated by PR Agent at ...' to any automatic content. Default is true. \ No newline at end of file +- `include_generated_by_header`: if set to true, the tool will add a dedicated header: 'Generated by PR Agent at ...' to any automatic content. Default is true. From 8f9f09ecbf9af13ebfbdcb7e3a5e54c978105b0c Mon Sep 17 00:00:00 2001 From: Zohar Meir <33152084+zmeir@users.noreply.github.com> Date: Sun, 8 Oct 2023 16:19:11 +0300 Subject: [PATCH 19/29] Fixed help message for bot user This changes the help message to display properly when running a custom deployment of the PR-Agent app (i.e. not via GitHub Actions, and with the setting `github_app.override_deployment_type=false`) --- pr_agent/tools/pr_reviewer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index b96d5379..919e1800 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -241,7 +241,8 @@ class PRReviewer: # Add help text if not in CLI mode if not get_settings().get("CONFIG.CLI_MODE", False): markdown_text += "\n### How to use\n" - if user and '[bot]' not in user: + bot_user = "[bot]" if get_settings().github_app.override_deployment_type else get_settings().github_app.bot_user + if user and bot_user not in user: markdown_text += bot_help_text(user) else: markdown_text += actions_help_text From 1b3fb49f9c49367cd82fec1d8d73721382280231 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 8 Oct 2023 16:50:25 +0300 Subject: [PATCH 20/29] publish each suggestion seperatly only on gitlab --- pr_agent/git_providers/gitlab_provider.py | 4 ++++ pr_agent/tools/pr_code_suggestions.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 2e6f2140..de3762e6 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -196,6 +196,10 @@ class GitLabProvider(GitProvider): return self.last_diff # fallback to last_diff if no relevant diff is found def publish_code_suggestions(self, code_suggestions: list) -> bool: + if len(code_suggestions) > 1: + logging.debug(f"in gitlab we dont currently support publishing multiple code suggestions simultaneously") + return False + for suggestion in code_suggestions: try: body = suggestion['body'] diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index e1d5206c..d49459b0 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -138,8 +138,8 @@ class PRCodeSuggestions: if get_settings().config.verbosity_level >= 2: logging.info(f"Could not parse suggestion: {d}") - # is_successful = self.git_provider.publish_code_suggestions(code_suggestions) - if True: + is_successful = self.git_provider.publish_code_suggestions(code_suggestions) + if not is_successful: # logging.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]) From e6548f4fe1b06a75848748d9809195a9b371afe7 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 8 Oct 2023 16:57:22 +0300 Subject: [PATCH 21/29] simpler solution --- pr_agent/git_providers/gitlab_provider.py | 5 +---- pr_agent/tools/pr_code_suggestions.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index de3762e6..ae2d621f 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -196,10 +196,6 @@ class GitLabProvider(GitProvider): return self.last_diff # fallback to last_diff if no relevant diff is found def publish_code_suggestions(self, code_suggestions: list) -> bool: - if len(code_suggestions) > 1: - logging.debug(f"in gitlab we dont currently support publishing multiple code suggestions simultaneously") - return False - for suggestion in code_suggestions: try: body = suggestion['body'] @@ -231,6 +227,7 @@ class GitLabProvider(GitProvider): target_file, target_line_no) except Exception as e: logging.exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}") + return True def search_line(self, relevant_file, relevant_line_in_file): target_file = None diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index d49459b0..7f0b1264 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -140,7 +140,7 @@ class PRCodeSuggestions: is_successful = self.git_provider.publish_code_suggestions(code_suggestions) if not is_successful: - # logging.info("Failed to publish code suggestions, trying to publish each suggestion separately") + logging.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]) From da98fd712fb7aaf8d927504a79b5dbbd3fe75726 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Sun, 8 Oct 2023 16:58:22 +0300 Subject: [PATCH 22/29] note --- pr_agent/git_providers/gitlab_provider.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index ae2d621f..b8344fd4 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -227,6 +227,8 @@ class GitLabProvider(GitProvider): target_file, target_line_no) except Exception as e: logging.exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}") + + # note that we publish suggestions one-by-one. so, if one fails, the rest will still be published return True def search_line(self, relevant_file, relevant_line_in_file): From 8755f635b400929165d974992e80395086c08405 Mon Sep 17 00:00:00 2001 From: mrT23 Date: Tue, 10 Oct 2023 17:23:58 +0300 Subject: [PATCH 23/29] Update Usage.md --- Usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Usage.md b/Usage.md index 4a3855e4..d6941156 100644 --- a/Usage.md +++ b/Usage.md @@ -187,7 +187,7 @@ api_key = "" # your azure api key api_type = "azure" api_version = '2023-05-15' # Check Azure documentation for the current API version api_base = "" # The base URL for your Azure OpenAI resource. e.g. "https://.openai.azure.com" -deployment_id = "" # The deployment name you chose when you deployed the engine +openai.deployment_id = "" # The deployment name you chose when you deployed the engine ``` and From 18ee9d66b0aa21c3eb6ab19733159f939d9ce39e Mon Sep 17 00:00:00 2001 From: mrT23 Date: Fri, 13 Oct 2023 05:00:41 +0300 Subject: [PATCH 24/29] Update Usage.md --- Usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Usage.md b/Usage.md index d6941156..867ccc44 100644 --- a/Usage.md +++ b/Usage.md @@ -135,7 +135,7 @@ When a new PR is opened, PR-Agent will run the `describe` tool with the above pa To cancel the automatic run of all the tools, set: ``` [github_app] -pr_commands = [] +pr_commands = "" ``` From e7258e732be56fb7ce803c02d9a9ecb0d4cf887b Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Sat, 14 Oct 2023 01:39:05 +0300 Subject: [PATCH 25/29] Refactor repo-specific settings application into a utility function, fix merge bug --- pr_agent/agent/pr_agent.py | 19 +++--------------- pr_agent/git_providers/utils.py | 35 +++++++++++++++++++++++++++++++++ pr_agent/servers/github_app.py | 2 ++ 3 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 pr_agent/git_providers/utils.py diff --git a/pr_agent/agent/pr_agent.py b/pr_agent/agent/pr_agent.py index 3d819af5..476957d5 100644 --- a/pr_agent/agent/pr_agent.py +++ b/pr_agent/agent/pr_agent.py @@ -6,6 +6,7 @@ import tempfile from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider +from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.tools.pr_add_docs import PRAddDocs from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions from pr_agent.tools.pr_description import PRDescription @@ -44,22 +45,7 @@ class PRAgent: async def handle_request(self, pr_url, request, notify=None) -> bool: # First, apply repo specific settings if exists - if get_settings().config.use_repo_settings_file: - repo_settings_file = None - try: - git_provider = get_git_provider()(pr_url) - repo_settings = git_provider.get_repo_settings() - if repo_settings: - repo_settings_file = None - fd, repo_settings_file = tempfile.mkstemp(suffix='.toml') - os.write(fd, repo_settings) - get_settings().load_file(repo_settings_file) - finally: - if repo_settings_file: - try: - os.remove(repo_settings_file) - except Exception as e: - logging.error(f"Failed to remove temporary settings file {repo_settings_file}", e) + apply_repo_settings(pr_url) # Then, apply user specific settings if exists request = request.replace("'", "\\'") @@ -84,3 +70,4 @@ class PRAgent: else: return False return True + diff --git a/pr_agent/git_providers/utils.py b/pr_agent/git_providers/utils.py new file mode 100644 index 00000000..d2dc79af --- /dev/null +++ b/pr_agent/git_providers/utils.py @@ -0,0 +1,35 @@ +import copy +import logging +import os +import tempfile + +from dynaconf import Dynaconf + +from pr_agent.config_loader import get_settings +from pr_agent.git_providers import get_git_provider + + +def apply_repo_settings(pr_url): + if get_settings().config.use_repo_settings_file: + repo_settings_file = None + try: + git_provider = get_git_provider()(pr_url) + repo_settings = git_provider.get_repo_settings() + if repo_settings: + repo_settings_file = None + fd, repo_settings_file = tempfile.mkstemp(suffix='.toml') + os.write(fd, repo_settings) + new_settings = Dynaconf(settings_files=[repo_settings_file]) + for section, contents in new_settings.as_dict().items(): + section_dict = copy.deepcopy(get_settings().as_dict().get(section, {})) + for key, value in contents.items(): + section_dict[key] = value + get_settings().unset(section) + get_settings().set(section, section_dict, merge=False) + + finally: + if repo_settings_file: + try: + os.remove(repo_settings_file) + except Exception as e: + logging.error(f"Failed to remove temporary settings file {repo_settings_file}", e) diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index c9f25124..272d9d0f 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -15,6 +15,7 @@ from pr_agent.agent.pr_agent import PRAgent from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings, global_settings from pr_agent.git_providers import get_git_provider +from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.servers.utils import verify_signature logging.basicConfig(stream=sys.stdout, level=logging.INFO) @@ -124,6 +125,7 @@ async def handle_request(body: Dict[str, Any], event: str): # avoid double reviews when opening a PR for the first time return {} logging.info(f"Performing review because of event={event} and action={action}") + apply_repo_settings(api_url) for command in get_settings().github_app.pr_commands: split_command = command.split(" ") command = split_command[0] From c324d88be3ccbacc8c7c844e2657136e17df95a8 Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Mon, 16 Oct 2023 14:56:00 +0300 Subject: [PATCH 26/29] Refactor logging system to use custom logger across the codebase --- pr_agent/agent/pr_agent.py | 8 +-- pr_agent/algo/ai_handler.py | 21 +++---- pr_agent/algo/git_patch_processing.py | 9 +-- pr_agent/algo/pr_processing.py | 22 ++++---- pr_agent/algo/utils.py | 16 +++--- pr_agent/cli.py | 4 +- .../git_providers/azuredevops_provider.py | 11 ++-- pr_agent/git_providers/bitbucket_provider.py | 14 ++--- pr_agent/git_providers/codecommit_provider.py | 23 ++++---- pr_agent/git_providers/gerrit_provider.py | 29 +++++----- pr_agent/git_providers/git_provider.py | 7 ++- pr_agent/git_providers/github_provider.py | 37 ++++++------- pr_agent/git_providers/gitlab_provider.py | 29 +++++----- pr_agent/git_providers/local_git_provider.py | 4 +- pr_agent/git_providers/utils.py | 4 +- pr_agent/log/__init__.py | 40 ++++++++++++++ pr_agent/servers/bitbucket_app.py | 13 ++--- pr_agent/servers/gerrit_server.py | 11 ++-- pr_agent/servers/github_app.py | 47 ++++++++-------- pr_agent/servers/github_polling.py | 9 ++- pr_agent/servers/gitlab_webhook.py | 11 ++-- pr_agent/servers/serverless.py | 6 +- pr_agent/tools/pr_add_docs.py | 37 +++++++------ pr_agent/tools/pr_code_suggestions.py | 51 ++++++++--------- pr_agent/tools/pr_config.py | 11 ++-- pr_agent/tools/pr_description.py | 23 ++++---- pr_agent/tools/pr_information_from_user.py | 18 +++--- pr_agent/tools/pr_questions.py | 18 +++--- pr_agent/tools/pr_reviewer.py | 34 ++++++------ pr_agent/tools/pr_similar_issue.py | 55 +++++++++---------- pr_agent/tools/pr_update_changelog.py | 26 ++++----- requirements.txt | 3 +- 32 files changed, 340 insertions(+), 311 deletions(-) create mode 100644 pr_agent/log/__init__.py diff --git a/pr_agent/agent/pr_agent.py b/pr_agent/agent/pr_agent.py index 476957d5..cd2bf2cc 100644 --- a/pr_agent/agent/pr_agent.py +++ b/pr_agent/agent/pr_agent.py @@ -1,21 +1,17 @@ -import logging -import os import shlex -import tempfile from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings -from pr_agent.git_providers import get_git_provider from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.tools.pr_add_docs import PRAddDocs from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions +from pr_agent.tools.pr_config import PRConfig from pr_agent.tools.pr_description import PRDescription from pr_agent.tools.pr_information_from_user import PRInformationFromUser -from pr_agent.tools.pr_similar_issue import PRSimilarIssue from pr_agent.tools.pr_questions import PRQuestions from pr_agent.tools.pr_reviewer import PRReviewer +from pr_agent.tools.pr_similar_issue import PRSimilarIssue from pr_agent.tools.pr_update_changelog import PRUpdateChangelog -from pr_agent.tools.pr_config import PRConfig command2class = { "auto_review": PRReviewer, diff --git a/pr_agent/algo/ai_handler.py b/pr_agent/algo/ai_handler.py index ba285743..c3989563 100644 --- a/pr_agent/algo/ai_handler.py +++ b/pr_agent/algo/ai_handler.py @@ -1,4 +1,3 @@ -import logging import os import litellm @@ -7,6 +6,8 @@ from litellm import acompletion from openai.error import APIError, RateLimitError, Timeout, TryAgain from retry import retry from pr_agent.config_loader import get_settings +from pr_agent.log import get_logger + OPENAI_RETRIES = 5 @@ -88,34 +89,34 @@ class AiHandler: try: deployment_id = self.deployment_id if get_settings().config.verbosity_level >= 2: - logging.debug( + get_logger().debug( f"Generating completion with {model}" f"{(' from deployment ' + deployment_id) if deployment_id else ''}" ) if self.azure: model = 'azure/' + model + messages = [{"role": "system", "content": system}, {"role": "user", "content": user}] response = await acompletion( model=model, deployment_id=deployment_id, - messages=[ - {"role": "system", "content": system}, - {"role": "user", "content": user} - ], + messages=messages, temperature=temperature, force_timeout=get_settings().config.ai_timeout ) except (APIError, Timeout, TryAgain) as e: - logging.error("Error during OpenAI inference: ", e) + get_logger().error("Error during OpenAI inference: ", e) raise except (RateLimitError) as e: - logging.error("Rate limit error during OpenAI inference: ", e) + get_logger().error("Rate limit error during OpenAI inference: ", e) raise except (Exception) as e: - logging.error("Unknown error during OpenAI inference: ", e) + get_logger().error("Unknown error during OpenAI inference: ", e) raise TryAgain from e if response is None or len(response["choices"]) == 0: raise TryAgain resp = response["choices"][0]['message']['content'] finish_reason = response["choices"][0]["finish_reason"] - print(resp, finish_reason) + usage = response.get("usage") + get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason, + model=model, usage=usage) return resp, finish_reason diff --git a/pr_agent/algo/git_patch_processing.py b/pr_agent/algo/git_patch_processing.py index 58d05235..4c20ea48 100644 --- a/pr_agent/algo/git_patch_processing.py +++ b/pr_agent/algo/git_patch_processing.py @@ -1,8 +1,9 @@ from __future__ import annotations -import logging + import re from pr_agent.config_loader import get_settings +from pr_agent.log import get_logger def extend_patch(original_file_str, patch_str, num_lines) -> str: @@ -63,7 +64,7 @@ def extend_patch(original_file_str, patch_str, num_lines) -> str: extended_patch_lines.append(line) except Exception as e: if get_settings().config.verbosity_level >= 2: - logging.error(f"Failed to extend patch: {e}") + get_logger().error(f"Failed to extend patch: {e}") return patch_str # finish previous hunk @@ -134,14 +135,14 @@ def handle_patch_deletions(patch: str, original_file_content_str: str, if not new_file_content_str: # logic for handling deleted files - don't show patch, just show that the file was deleted if get_settings().config.verbosity_level > 0: - logging.info(f"Processing file: {file_name}, minimizing deletion file") + get_logger().info(f"Processing file: {file_name}, minimizing deletion file") patch = None # file was deleted else: patch_lines = patch.splitlines() patch_new = omit_deletion_hunks(patch_lines) if patch != patch_new: if get_settings().config.verbosity_level > 0: - logging.info(f"Processing file: {file_name}, hunks were deleted") + get_logger().info(f"Processing file: {file_name}, hunks were deleted") patch = patch_new return patch diff --git a/pr_agent/algo/pr_processing.py b/pr_agent/algo/pr_processing.py index 4327a0f1..63bd02eb 100644 --- a/pr_agent/algo/pr_processing.py +++ b/pr_agent/algo/pr_processing.py @@ -1,7 +1,6 @@ from __future__ import annotations import difflib -import logging import re import traceback from typing import Any, Callable, List, Tuple @@ -15,6 +14,7 @@ from pr_agent.algo.file_filter import filter_ignored from pr_agent.algo.token_handler import TokenHandler, get_token_encoder from pr_agent.config_loader import get_settings from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider +from pr_agent.log import get_logger DELETED_FILES_ = "Deleted files:\n" @@ -51,7 +51,7 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s try: diff_files = git_provider.get_diff_files() except RateLimitExceededException as e: - logging.error(f"Rate limit exceeded for git provider API. original message {e}") + get_logger().error(f"Rate limit exceeded for git provider API. original message {e}") raise diff_files = filter_ignored(diff_files) @@ -180,7 +180,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo # Hard Stop, no more tokens if total_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD: - logging.warning(f"File was fully skipped, no more tokens: {file.filename}.") + get_logger().warning(f"File was fully skipped, no more tokens: {file.filename}.") continue # If the patch is too large, just show the file name @@ -189,7 +189,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo # TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens # until we meet the requirements if get_settings().config.verbosity_level >= 2: - logging.warning(f"Patch too large, minimizing it, {file.filename}") + get_logger().warning(f"Patch too large, minimizing it, {file.filename}") if not modified_files_list: total_tokens += token_handler.count_tokens(MORE_MODIFIED_FILES_) modified_files_list.append(file.filename) @@ -204,7 +204,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo patches.append(patch_final) total_tokens += token_handler.count_tokens(patch_final) if get_settings().config.verbosity_level >= 2: - logging.info(f"Tokens: {total_tokens}, last filename: {file.filename}") + get_logger().info(f"Tokens: {total_tokens}, last filename: {file.filename}") return patches, modified_files_list, deleted_files_list @@ -218,7 +218,7 @@ async def retry_with_fallback_models(f: Callable): get_settings().set("openai.deployment_id", deployment_id) return await f(model) except Exception as e: - logging.warning( + get_logger().warning( f"Failed to generate prediction with {model}" f"{(' from deployment ' + deployment_id) if deployment_id else ''}: " f"{traceback.format_exc()}" @@ -340,7 +340,7 @@ def clip_tokens(text: str, max_tokens: int) -> str: clipped_text = text[:num_output_chars] return clipped_text except Exception as e: - logging.warning(f"Failed to clip tokens: {e}") + get_logger().warning(f"Failed to clip tokens: {e}") return text @@ -367,7 +367,7 @@ def get_pr_multi_diffs(git_provider: GitProvider, try: diff_files = git_provider.get_diff_files() except RateLimitExceededException as e: - logging.error(f"Rate limit exceeded for git provider API. original message {e}") + get_logger().error(f"Rate limit exceeded for git provider API. original message {e}") raise diff_files = filter_ignored(diff_files) @@ -387,7 +387,7 @@ def get_pr_multi_diffs(git_provider: GitProvider, for file in sorted_files: if call_number > max_calls: if get_settings().config.verbosity_level >= 2: - logging.info(f"Reached max calls ({max_calls})") + get_logger().info(f"Reached max calls ({max_calls})") break original_file_content_str = file.base_file @@ -410,13 +410,13 @@ def get_pr_multi_diffs(git_provider: GitProvider, total_tokens = token_handler.prompt_tokens call_number += 1 if get_settings().config.verbosity_level >= 2: - logging.info(f"Call number: {call_number}") + get_logger().info(f"Call number: {call_number}") if patch: patches.append(patch) total_tokens += new_patch_tokens if get_settings().config.verbosity_level >= 2: - logging.info(f"Tokens: {total_tokens}, last filename: {file.filename}") + get_logger().info(f"Tokens: {total_tokens}, last filename: {file.filename}") # Add the last chunk if patches: diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index 11f28e38..4e88b33e 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -2,7 +2,6 @@ from __future__ import annotations import difflib import json -import logging import re import textwrap from datetime import datetime @@ -11,6 +10,7 @@ from typing import Any, List import yaml from starlette_context import context from pr_agent.config_loader import get_settings, global_settings +from pr_agent.log import get_logger def get_setting(key: str) -> Any: @@ -159,7 +159,7 @@ def try_fix_json(review, max_iter=10, code_suggestions=False): iter_count += 1 if not valid_json: - logging.error("Unable to decode JSON response from AI") + get_logger().error("Unable to decode JSON response from AI") data = {} return data @@ -230,7 +230,7 @@ def load_large_diff(filename, new_file_content_str: str, original_file_content_s diff = difflib.unified_diff(original_file_content_str.splitlines(keepends=True), new_file_content_str.splitlines(keepends=True)) if get_settings().config.verbosity_level >= 2: - logging.warning(f"File was modified, but no patch was found. Manually creating patch: {filename}.") + get_logger().warning(f"File was modified, but no patch was found. Manually creating patch: {filename}.") patch = ''.join(diff) except Exception: pass @@ -262,12 +262,12 @@ def update_settings_from_args(args: List[str]) -> List[str]: vals = arg.split('=', 1) if len(vals) != 2: if len(vals) > 2: # --extended is a valid argument - logging.error(f'Invalid argument format: {arg}') + get_logger().error(f'Invalid argument format: {arg}') other_args.append(arg) continue key, value = _fix_key_value(*vals) get_settings().set(key, value) - logging.info(f'Updated setting {key} to: "{value}"') + get_logger().info(f'Updated setting {key} to: "{value}"') else: other_args.append(arg) return other_args @@ -279,7 +279,7 @@ def _fix_key_value(key: str, value: str): try: value = yaml.safe_load(value) except Exception as e: - logging.error(f"Failed to parse YAML for config override {key}={value}", exc_info=e) + get_logger().error(f"Failed to parse YAML for config override {key}={value}", exc_info=e) return key, value @@ -288,7 +288,7 @@ def load_yaml(review_text: str) -> dict: try: data = yaml.safe_load(review_text) except Exception as e: - logging.error(f"Failed to parse AI prediction: {e}") + get_logger().error(f"Failed to parse AI prediction: {e}") data = try_fix_yaml(review_text) return data @@ -299,7 +299,7 @@ def try_fix_yaml(review_text: str) -> dict: review_text_lines_tmp = '\n'.join(review_text_lines[:-i]) try: data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader) - logging.info(f"Successfully parsed AI prediction after removing {i} lines") + get_logger().info(f"Successfully parsed AI prediction after removing {i} lines") break except: pass diff --git a/pr_agent/cli.py b/pr_agent/cli.py index 07c37f5e..6728db9f 100644 --- a/pr_agent/cli.py +++ b/pr_agent/cli.py @@ -1,11 +1,12 @@ import argparse import asyncio -import logging import os from pr_agent.agent.pr_agent import PRAgent, commands from pr_agent.config_loader import get_settings +from pr_agent.log import setup_logger +setup_logger() def run(inargs=None): parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage= @@ -47,7 +48,6 @@ For example: 'python cli.py --pr_url=... review --pr_reviewer.extra_instructions parser.print_help() return - logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) command = args.command.lower() get_settings().set("CONFIG.CLI_MODE", True) if args.issue_url: diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index e0f4760d..9e9497c7 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -1,10 +1,11 @@ import json -import logging from typing import Optional, Tuple from urllib.parse import urlparse import os +from ..log import get_logger + AZURE_DEVOPS_AVAILABLE = True try: from msrest.authentication import BasicAuthentication @@ -55,7 +56,7 @@ class AzureDevopsProvider: path=".pr_agent.toml") return contents except Exception as e: - logging.exception("get repo settings error") + get_logger().exception("get repo settings error") return "" def get_files(self): @@ -110,7 +111,7 @@ class AzureDevopsProvider: new_file_content_str = new_file_content_str.content except Exception as error: - logging.error("Failed to retrieve new file content of %s at version %s. Error: %s", file, version, str(error)) + get_logger().error("Failed to retrieve new file content of %s at version %s. Error: %s", file, version, str(error)) new_file_content_str = "" edit_type = EDIT_TYPE.MODIFIED @@ -131,7 +132,7 @@ class AzureDevopsProvider: include_content=True) original_file_content_str = original_file_content_str.content except Exception as error: - logging.error("Failed to retrieve original file content of %s at version %s. Error: %s", file, version, str(error)) + get_logger().error("Failed to retrieve original file content of %s at version %s. Error: %s", file, version, str(error)) original_file_content_str = "" patch = load_large_diff(file, new_file_content_str, original_file_content_str) @@ -166,7 +167,7 @@ class AzureDevopsProvider: pull_request_id=self.pr_num, git_pull_request_to_update=updated_pr) except Exception as e: - logging.exception(f"Could not update pull request {self.pr_num} description: {e}") + get_logger().exception(f"Could not update pull request {self.pr_num} description: {e}") def remove_initial_comment(self): return "" # not implemented yet diff --git a/pr_agent/git_providers/bitbucket_provider.py b/pr_agent/git_providers/bitbucket_provider.py index 56b9f711..fef51794 100644 --- a/pr_agent/git_providers/bitbucket_provider.py +++ b/pr_agent/git_providers/bitbucket_provider.py @@ -1,5 +1,4 @@ import json -import logging from typing import Optional, Tuple from urllib.parse import urlparse @@ -7,8 +6,9 @@ import requests from atlassian.bitbucket import Cloud from starlette_context import context -from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file +from ..algo.pr_processing import find_line_number_of_relevant_line_in_file from ..config_loader import get_settings +from ..log import get_logger from .git_provider import FilePatchInfo, GitProvider @@ -61,14 +61,14 @@ class BitbucketProvider(GitProvider): if not relevant_lines_start or relevant_lines_start == -1: if get_settings().config.verbosity_level >= 2: - logging.exception( + get_logger().exception( f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}" ) continue if relevant_lines_end < relevant_lines_start: if get_settings().config.verbosity_level >= 2: - logging.exception( + get_logger().exception( f"Failed to publish code suggestion, " f"relevant_lines_end is {relevant_lines_end} and " f"relevant_lines_start is {relevant_lines_start}" @@ -97,7 +97,7 @@ class BitbucketProvider(GitProvider): return True except Exception as e: if get_settings().config.verbosity_level >= 2: - logging.error(f"Failed to publish code suggestion, error: {e}") + get_logger().error(f"Failed to publish code suggestion, error: {e}") return False def is_supported(self, capability: str) -> bool: @@ -144,7 +144,7 @@ class BitbucketProvider(GitProvider): for comment in self.temp_comments: self.pr.delete(f"comments/{comment}") except Exception as e: - logging.exception(f"Failed to remove temp comments, error: {e}") + get_logger().exception(f"Failed to remove temp comments, error: {e}") # funtion to create_inline_comment @@ -152,7 +152,7 @@ class BitbucketProvider(GitProvider): position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(), relevant_file.strip('`'), relevant_line_in_file) if position == -1: if get_settings().config.verbosity_level >= 2: - logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") + get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}") subject_type = "FILE" else: subject_type = "LINE" diff --git a/pr_agent/git_providers/codecommit_provider.py b/pr_agent/git_providers/codecommit_provider.py index 5fa8c873..4e12f96e 100644 --- a/pr_agent/git_providers/codecommit_provider.py +++ b/pr_agent/git_providers/codecommit_provider.py @@ -1,17 +1,16 @@ -import logging import os import re from collections import Counter from typing import List, Optional, Tuple from urllib.parse import urlparse -from ..algo.language_handler import is_valid_file, language_extension_map -from ..algo.pr_processing import clip_tokens -from ..algo.utils import load_large_diff -from ..config_loader import get_settings -from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider, IncrementalPR from pr_agent.git_providers.codecommit_client import CodeCommitClient +from ..algo.language_handler import is_valid_file, language_extension_map +from ..algo.utils import load_large_diff +from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider +from ..log import get_logger + class PullRequestCCMimic: """ @@ -166,7 +165,7 @@ class CodeCommitProvider(GitProvider): def publish_comment(self, pr_comment: str, is_temporary: bool = False): if is_temporary: - logging.info(pr_comment) + get_logger().info(pr_comment) return pr_comment = CodeCommitProvider._remove_markdown_html(pr_comment) @@ -188,12 +187,12 @@ class CodeCommitProvider(GitProvider): for suggestion in code_suggestions: # Verify that each suggestion has the required keys if not all(key in suggestion for key in ["body", "relevant_file", "relevant_lines_start"]): - logging.warning(f"Skipping code suggestion #{counter}: Each suggestion must have 'body', 'relevant_file', 'relevant_lines_start' keys") + get_logger().warning(f"Skipping code suggestion #{counter}: Each suggestion must have 'body', 'relevant_file', 'relevant_lines_start' keys") continue # Publish the code suggestion to CodeCommit try: - logging.debug(f"Code Suggestion #{counter} in file: {suggestion['relevant_file']}: {suggestion['relevant_lines_start']}") + get_logger().debug(f"Code Suggestion #{counter} in file: {suggestion['relevant_file']}: {suggestion['relevant_lines_start']}") self.codecommit_client.publish_comment( repo_name=self.repo_name, pr_number=self.pr_num, @@ -296,11 +295,11 @@ class CodeCommitProvider(GitProvider): return self.codecommit_client.get_file(self.repo_name, settings_filename, self.pr.source_commit, optional=True) def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: - logging.info("CodeCommit provider does not support eyes reaction yet") + get_logger().info("CodeCommit provider does not support eyes reaction yet") return True def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: - logging.info("CodeCommit provider does not support removing reactions yet") + get_logger().info("CodeCommit provider does not support removing reactions yet") return True @staticmethod @@ -366,7 +365,7 @@ class CodeCommitProvider(GitProvider): # TODO: implement support for multiple targets in one CodeCommit PR # for now, we are only using the first target in the PR if len(response.targets) > 1: - logging.warning( + get_logger().warning( "Multiple targets in one PR is not supported for CodeCommit yet. Continuing, using the first target only..." ) diff --git a/pr_agent/git_providers/gerrit_provider.py b/pr_agent/git_providers/gerrit_provider.py index dd56803a..ae017fbd 100644 --- a/pr_agent/git_providers/gerrit_provider.py +++ b/pr_agent/git_providers/gerrit_provider.py @@ -1,5 +1,4 @@ import json -import logging import os import pathlib import shutil @@ -7,18 +6,16 @@ import subprocess import uuid from collections import Counter, namedtuple from pathlib import Path -from tempfile import mkdtemp, NamedTemporaryFile +from tempfile import NamedTemporaryFile, mkdtemp import requests import urllib3.util from git import Repo from pr_agent.config_loader import get_settings -from pr_agent.git_providers.git_provider import GitProvider, FilePatchInfo, \ - EDIT_TYPE +from pr_agent.git_providers.git_provider import EDIT_TYPE, FilePatchInfo, GitProvider from pr_agent.git_providers.local_git_provider import PullRequestMimic - -logger = logging.getLogger(__name__) +from pr_agent.log import get_logger def _call(*command, **kwargs) -> (int, str, str): @@ -33,42 +30,42 @@ def _call(*command, **kwargs) -> (int, str, str): def clone(url, directory): - logger.info("Cloning %s to %s", url, directory) + get_logger().info("Cloning %s to %s", url, directory) stdout = _call('git', 'clone', "--depth", "1", url, directory) - logger.info(stdout) + get_logger().info(stdout) def fetch(url, refspec, cwd): - logger.info("Fetching %s %s", url, refspec) + get_logger().info("Fetching %s %s", url, refspec) stdout = _call( 'git', 'fetch', '--depth', '2', url, refspec, cwd=cwd ) - logger.info(stdout) + get_logger().info(stdout) def checkout(cwd): - logger.info("Checking out") + get_logger().info("Checking out") stdout = _call('git', 'checkout', "FETCH_HEAD", cwd=cwd) - logger.info(stdout) + get_logger().info(stdout) def show(*args, cwd=None): - logger.info("Show") + get_logger().info("Show") return _call('git', 'show', *args, cwd=cwd) def diff(*args, cwd=None): - logger.info("Diff") + get_logger().info("Diff") patch = _call('git', 'diff', *args, cwd=cwd) if not patch: - logger.warning("No changes found") + get_logger().warning("No changes found") return return patch def reset_local_changes(cwd): - logger.info("Reset local changes") + get_logger().info("Reset local changes") _call('git', 'checkout', "--force", cwd=cwd) diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index a4a242f3..c6740467 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -1,4 +1,3 @@ -import logging from abc import ABC, abstractmethod from dataclasses import dataclass @@ -6,6 +5,8 @@ from dataclasses import dataclass from enum import Enum from typing import Optional +from pr_agent.log import get_logger + class EDIT_TYPE(Enum): ADDED = 1 @@ -136,7 +137,7 @@ def get_main_pr_language(languages, files) -> str: """ main_language_str = "" if not languages: - logging.info("No languages detected") + get_logger().info("No languages detected") return main_language_str try: @@ -172,7 +173,7 @@ def get_main_pr_language(languages, files) -> str: main_language_str = top_language except Exception as e: - logging.exception(e) + get_logger().exception(e) pass return main_language_str diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index e5f62eb3..cf04ecfe 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -1,20 +1,19 @@ -import logging import hashlib - from datetime import datetime -from typing import Optional, Tuple, Any +from typing import Optional, Tuple from urllib.parse import urlparse -from github import AppAuthentication, Auth, Github, GithubException, Reaction +from github import AppAuthentication, Auth, Github, GithubException from retry import retry from starlette_context import context -from .git_provider import FilePatchInfo, GitProvider, IncrementalPR from ..algo.language_handler import is_valid_file +from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file from ..algo.utils import load_large_diff -from ..algo.pr_processing import find_line_number_of_relevant_line_in_file, clip_tokens from ..config_loader import get_settings +from ..log import get_logger from ..servers.utils import RateLimitExceeded +from .git_provider import FilePatchInfo, GitProvider, IncrementalPR class GithubProvider(GitProvider): @@ -58,7 +57,7 @@ class GithubProvider(GitProvider): self.file_set = dict() for commit in self.incremental.commits_range: if commit.commit.message.startswith(f"Merge branch '{self._get_repo().default_branch}'"): - logging.info(f"Skipping merge commit {commit.commit.message}") + get_logger().info(f"Skipping merge commit {commit.commit.message}") continue self.file_set.update({file.filename: file for file in commit.files}) @@ -130,7 +129,7 @@ class GithubProvider(GitProvider): return diff_files except GithubException.RateLimitExceededException as e: - logging.error(f"Rate limit exceeded for GitHub API. Original message: {e}") + get_logger().error(f"Rate limit exceeded for GitHub API. Original message: {e}") raise RateLimitExceeded("Rate limit exceeded for GitHub API.") from e def publish_description(self, pr_title: str, pr_body: str): @@ -138,7 +137,7 @@ class GithubProvider(GitProvider): def publish_comment(self, pr_comment: str, is_temporary: bool = False): if is_temporary and not get_settings().config.publish_output_progress: - logging.debug(f"Skipping publish_comment for temporary comment: {pr_comment}") + get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}") return response = self.pr.create_issue_comment(pr_comment) if hasattr(response, "user") and hasattr(response.user, "login"): @@ -156,7 +155,7 @@ class GithubProvider(GitProvider): position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file) if position == -1: if get_settings().config.verbosity_level >= 2: - logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") + get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}") subject_type = "FILE" else: subject_type = "LINE" @@ -179,13 +178,13 @@ class GithubProvider(GitProvider): if not relevant_lines_start or relevant_lines_start == -1: if get_settings().config.verbosity_level >= 2: - logging.exception( + get_logger().exception( f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}") continue if relevant_lines_end < relevant_lines_start: if get_settings().config.verbosity_level >= 2: - logging.exception(f"Failed to publish code suggestion, " + get_logger().exception(f"Failed to publish code suggestion, " f"relevant_lines_end is {relevant_lines_end} and " f"relevant_lines_start is {relevant_lines_start}") continue @@ -212,7 +211,7 @@ class GithubProvider(GitProvider): return True except Exception as e: if get_settings().config.verbosity_level >= 2: - logging.error(f"Failed to publish code suggestion, error: {e}") + get_logger().error(f"Failed to publish code suggestion, error: {e}") return False def remove_initial_comment(self): @@ -221,7 +220,7 @@ class GithubProvider(GitProvider): if comment.is_temporary: comment.delete() except Exception as e: - logging.exception(f"Failed to remove initial comment, error: {e}") + get_logger().exception(f"Failed to remove initial comment, error: {e}") def get_title(self): return self.pr.title @@ -269,7 +268,7 @@ class GithubProvider(GitProvider): reaction = self.pr.get_issue_comment(issue_comment_id).create_reaction("eyes") return reaction.id except Exception as e: - logging.exception(f"Failed to add eyes reaction, error: {e}") + get_logger().exception(f"Failed to add eyes reaction, error: {e}") return None def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool: @@ -277,7 +276,7 @@ class GithubProvider(GitProvider): self.pr.get_issue_comment(issue_comment_id).delete_reaction(reaction_id) return True except Exception as e: - logging.exception(f"Failed to remove eyes reaction, error: {e}") + get_logger().exception(f"Failed to remove eyes reaction, error: {e}") return False @@ -396,13 +395,13 @@ class GithubProvider(GitProvider): "PUT", f"{self.pr.issue_url}/labels", input=post_parameters ) except Exception as e: - logging.exception(f"Failed to publish labels, error: {e}") + get_logger().exception(f"Failed to publish labels, error: {e}") def get_labels(self): try: return [label.name for label in self.pr.labels] except Exception as e: - logging.exception(f"Failed to get labels, error: {e}") + get_logger().exception(f"Failed to get labels, error: {e}") return [] def get_commit_messages(self): @@ -444,7 +443,7 @@ class GithubProvider(GitProvider): return link except Exception as e: if get_settings().config.verbosity_level >= 2: - logging.info(f"Failed adding line link, error: {e}") + get_logger().info(f"Failed adding line link, error: {e}") return "" diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index b8344fd4..0d09e622 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -1,5 +1,4 @@ import hashlib -import logging import re from typing import Optional, Tuple from urllib.parse import urlparse @@ -12,8 +11,8 @@ from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_ from ..algo.utils import load_large_diff from ..config_loader import get_settings from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider +from ..log import get_logger -logger = logging.getLogger() class DiffNotFoundError(Exception): """Raised when the diff for a merge request cannot be found.""" @@ -59,7 +58,7 @@ class GitLabProvider(GitProvider): try: self.last_diff = self.mr.diffs.list(get_all=True)[-1] except IndexError as e: - logger.error(f"Could not get diff for merge request {self.id_mr}") + get_logger().error(f"Could not get diff for merge request {self.id_mr}") raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") from e @@ -99,7 +98,7 @@ class GitLabProvider(GitProvider): if isinstance(new_file_content_str, bytes): new_file_content_str = bytes.decode(new_file_content_str, 'utf-8') except UnicodeDecodeError: - logging.warning( + get_logger().warning( f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}") edit_type = EDIT_TYPE.MODIFIED @@ -135,7 +134,7 @@ class GitLabProvider(GitProvider): self.mr.description = pr_body self.mr.save() except Exception as e: - logging.exception(f"Could not update merge request {self.id_mr} description: {e}") + get_logger().exception(f"Could not update merge request {self.id_mr} description: {e}") def publish_comment(self, mr_comment: str, is_temporary: bool = False): comment = self.mr.notes.create({'body': mr_comment}) @@ -157,12 +156,12 @@ class GitLabProvider(GitProvider): def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int, source_line_no: int, target_file: str,target_line_no: int) -> None: if not found: - logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") + get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}") else: # in order to have exact sha's we have to find correct diff for this change diff = self.get_relevant_diff(relevant_file, relevant_line_in_file) if diff is None: - logger.error(f"Could not get diff for merge request {self.id_mr}") + get_logger().error(f"Could not get diff for merge request {self.id_mr}") raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") pos_obj = {'position_type': 'text', 'new_path': target_file.filename, @@ -175,23 +174,23 @@ class GitLabProvider(GitProvider): else: pos_obj['new_line'] = target_line_no - 1 pos_obj['old_line'] = source_line_no - 1 - logging.debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}") + get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}") self.mr.discussions.create({'body': body, 'position': pos_obj}) def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]: changes = self.mr.changes() # Retrieve the changes for the merge request once if not changes: - logging.error('No changes found for the merge request.') + get_logger().error('No changes found for the merge request.') return None all_diffs = self.mr.diffs.list(get_all=True) if not all_diffs: - logging.error('No diffs found for the merge request.') + get_logger().error('No diffs found for the merge request.') return None for diff in all_diffs: for change in changes['changes']: if change['new_path'] == relevant_file and relevant_line_in_file in change['diff']: return diff - logging.debug( + get_logger().debug( f'No relevant diff found for {relevant_file} {relevant_line_in_file}. Falling back to last diff.') return self.last_diff # fallback to last_diff if no relevant diff is found @@ -226,7 +225,7 @@ class GitLabProvider(GitProvider): self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, target_file, target_line_no) except Exception as e: - logging.exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}") + get_logger().exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}") # note that we publish suggestions one-by-one. so, if one fails, the rest will still be published return True @@ -290,7 +289,7 @@ class GitLabProvider(GitProvider): for comment in self.temp_comments: comment.delete() except Exception as e: - logging.exception(f"Failed to remove temp comments, error: {e}") + get_logger().exception(f"Failed to remove temp comments, error: {e}") def get_title(self): return self.mr.title @@ -358,7 +357,7 @@ class GitLabProvider(GitProvider): self.mr.labels = list(set(pr_types)) self.mr.save() except Exception as e: - logging.exception(f"Failed to publish labels, error: {e}") + get_logger().exception(f"Failed to publish labels, error: {e}") def publish_inline_comments(self, comments: list[dict]): pass @@ -410,6 +409,6 @@ class GitLabProvider(GitProvider): return link except Exception as e: if get_settings().config.verbosity_level >= 2: - logging.info(f"Failed adding line link, error: {e}") + get_logger().info(f"Failed adding line link, error: {e}") return "" diff --git a/pr_agent/git_providers/local_git_provider.py b/pr_agent/git_providers/local_git_provider.py index ac750371..5fa9f0be 100644 --- a/pr_agent/git_providers/local_git_provider.py +++ b/pr_agent/git_providers/local_git_provider.py @@ -1,4 +1,3 @@ -import logging from collections import Counter from pathlib import Path from typing import List @@ -7,6 +6,7 @@ from git import Repo from pr_agent.config_loader import _find_repository_root, get_settings from pr_agent.git_providers.git_provider import EDIT_TYPE, FilePatchInfo, GitProvider +from pr_agent.log import get_logger class PullRequestMimic: @@ -49,7 +49,7 @@ class LocalGitProvider(GitProvider): """ Prepare the repository for PR-mimic generation. """ - logging.debug('Preparing repository for PR-mimic generation...') + get_logger().debug('Preparing repository for PR-mimic generation...') if self.repo.is_dirty(): raise ValueError('The repository is not in a clean state. Please commit or stash pending changes.') if self.target_branch_name not in self.repo.heads: diff --git a/pr_agent/git_providers/utils.py b/pr_agent/git_providers/utils.py index d2dc79af..c8d80dfc 100644 --- a/pr_agent/git_providers/utils.py +++ b/pr_agent/git_providers/utils.py @@ -1,5 +1,4 @@ import copy -import logging import os import tempfile @@ -7,6 +6,7 @@ from dynaconf import Dynaconf from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider +from pr_agent.log import get_logger def apply_repo_settings(pr_url): @@ -32,4 +32,4 @@ def apply_repo_settings(pr_url): try: os.remove(repo_settings_file) except Exception as e: - logging.error(f"Failed to remove temporary settings file {repo_settings_file}", e) + get_logger().error(f"Failed to remove temporary settings file {repo_settings_file}", e) diff --git a/pr_agent/log/__init__.py b/pr_agent/log/__init__.py new file mode 100644 index 00000000..665988ef --- /dev/null +++ b/pr_agent/log/__init__.py @@ -0,0 +1,40 @@ +import json +import logging +import sys +from enum import Enum + +from loguru import logger + + +class LoggingFormat(str, Enum): + CONSOLE = "CONSOLE" + JSON = "JSON" + + +def json_format(record: dict) -> str: + return record["message"] + + +def setup_logger(level: str = "INFO", fmt: LoggingFormat = LoggingFormat.CONSOLE): + level: int = logging.getLevelName(level.upper()) + if type(level) is not int: + level = logging.INFO + + if fmt == LoggingFormat.JSON: + logger.remove(None) + logger.add( + sys.stdout, + level=level, + format="{message}", + colorize=False, + serialize=True, + ) + elif fmt == LoggingFormat.CONSOLE: + logger.remove(None) + logger.add(sys.stdout, level=level, colorize=True) + + return logger + + +def get_logger(*args, **kwargs): + return logger diff --git a/pr_agent/servers/bitbucket_app.py b/pr_agent/servers/bitbucket_app.py index cc6491d4..00f83cfe 100644 --- a/pr_agent/servers/bitbucket_app.py +++ b/pr_agent/servers/bitbucket_app.py @@ -1,9 +1,7 @@ import copy import hashlib import json -import logging import os -import sys import time import jwt @@ -18,9 +16,10 @@ from starlette_context.middleware import RawContextMiddleware from pr_agent.agent.pr_agent import PRAgent from pr_agent.config_loader import get_settings, global_settings +from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.secret_providers import get_secret_provider -logging.basicConfig(stream=sys.stdout, level=logging.INFO) +setup_logger(fmt=LoggingFormat.JSON) router = APIRouter() secret_provider = get_secret_provider() @@ -49,7 +48,7 @@ async def get_bearer_token(shared_secret: str, client_key: str): bearer_token = response.json()["access_token"] return bearer_token except Exception as e: - logging.error(f"Failed to get bearer token: {e}") + get_logger().error(f"Failed to get bearer token: {e}") raise e @router.get("/") @@ -60,7 +59,7 @@ async def handle_manifest(request: Request, response: Response): manifest = manifest.replace("app_key", get_settings().bitbucket.app_key) manifest = manifest.replace("base_url", get_settings().bitbucket.base_url) except: - logging.error("Failed to replace api_key in Bitbucket manifest, trying to continue") + get_logger().error("Failed to replace api_key in Bitbucket manifest, trying to continue") manifest_obj = json.loads(manifest) return JSONResponse(manifest_obj) @@ -92,7 +91,7 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req comment_body = data["data"]["comment"]["content"]["raw"] await agent.handle_request(pr_url, comment_body) except Exception as e: - logging.error(f"Failed to handle webhook: {e}") + get_logger().error(f"Failed to handle webhook: {e}") background_tasks.add_task(inner) return "OK" @@ -115,7 +114,7 @@ async def handle_installed_webhooks(request: Request, response: Response): } secret_provider.store_secret(username, json.dumps(secrets)) except Exception as e: - logging.error(f"Failed to register user: {e}") + get_logger().error(f"Failed to register user: {e}") return JSONResponse({"error": "Unable to register user"}, status_code=500) @router.post("/uninstalled") diff --git a/pr_agent/servers/gerrit_server.py b/pr_agent/servers/gerrit_server.py index 04232ea9..1783f6b9 100644 --- a/pr_agent/servers/gerrit_server.py +++ b/pr_agent/servers/gerrit_server.py @@ -1,6 +1,4 @@ import copy -import logging -import sys from enum import Enum from json import JSONDecodeError @@ -12,9 +10,10 @@ from starlette_context import context from starlette_context.middleware import RawContextMiddleware from pr_agent.agent.pr_agent import PRAgent -from pr_agent.config_loader import global_settings, get_settings +from pr_agent.config_loader import get_settings, global_settings +from pr_agent.log import get_logger, setup_logger -logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) +setup_logger() router = APIRouter() @@ -35,7 +34,7 @@ class Item(BaseModel): @router.post("/api/v1/gerrit/{action}") async def handle_gerrit_request(action: Action, item: Item): - logging.debug("Received a Gerrit request") + get_logger().debug("Received a Gerrit request") context["settings"] = copy.deepcopy(global_settings) if action == Action.ask: @@ -54,7 +53,7 @@ async def get_body(request): try: body = await request.json() except JSONDecodeError as e: - logging.error("Error parsing request body", e) + get_logger().error("Error parsing request body", e) return {} return body diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index 272d9d0f..c598049e 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -1,6 +1,4 @@ import copy -import logging -import sys import os import time from typing import Any, Dict @@ -16,9 +14,11 @@ from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings, global_settings from pr_agent.git_providers import get_git_provider from pr_agent.git_providers.utils import apply_repo_settings +from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.servers.utils import verify_signature -logging.basicConfig(stream=sys.stdout, level=logging.INFO) +setup_logger(fmt=LoggingFormat.JSON) + router = APIRouter() @@ -29,11 +29,11 @@ async def handle_github_webhooks(request: Request, response: Response): Verifies the request signature, parses the request body, and passes it to the handle_request function for further processing. """ - logging.debug("Received a GitHub webhook") + get_logger().debug("Received a GitHub webhook") body = await get_body(request) - logging.debug(f'Request body:\n{body}') + get_logger().debug(f'Request body:\n{body}') installation_id = body.get("installation", {}).get("id") context["installation_id"] = installation_id context["settings"] = copy.deepcopy(global_settings) @@ -45,13 +45,13 @@ async def handle_github_webhooks(request: Request, response: Response): @router.post("/api/v1/marketplace_webhooks") async def handle_marketplace_webhooks(request: Request, response: Response): body = await get_body(request) - logging.info(f'Request body:\n{body}') + get_logger().info(f'Request body:\n{body}') async def get_body(request): try: body = await request.json() except Exception as e: - logging.error("Error parsing request body", e) + get_logger().error("Error parsing request body", e) raise HTTPException(status_code=400, detail="Error parsing request body") from e webhook_secret = getattr(get_settings().github, 'webhook_secret', None) if webhook_secret: @@ -77,8 +77,8 @@ async def handle_request(body: Dict[str, Any], event: str): return {} agent = PRAgent() bot_user = get_settings().github_app.bot_user - logging.info(f"action: '{action}'") - logging.info(f"event: '{event}'") + sender = body.get("sender", {}).get("login") + log_context = {"action": action, "event": event, "sender": sender} if get_settings().github_app.duplicate_requests_cache and _is_duplicate_request(body): return {} @@ -88,22 +88,23 @@ async def handle_request(body: Dict[str, Any], event: str): if "comment" not in body: return {} comment_body = body.get("comment", {}).get("body") - sender = body.get("sender", {}).get("login") if sender and bot_user in sender: - logging.info(f"Ignoring comment from {bot_user} user") + get_logger().info(f"Ignoring comment from {bot_user} user") return {} - logging.info(f"Processing comment from {sender} user") + get_logger().info(f"Processing comment from {sender} user") if "issue" in body and "pull_request" in body["issue"] and "url" in body["issue"]["pull_request"]: api_url = body["issue"]["pull_request"]["url"] elif "comment" in body and "pull_request_url" in body["comment"]: api_url = body["comment"]["pull_request_url"] else: return {} - logging.info(body) - logging.info(f"Handling comment because of event={event} and action={action}") + log_context["api_url"] = api_url + get_logger().info(body) + get_logger().info(f"Handling comment because of event={event} and action={action}") comment_id = body.get("comment", {}).get("id") provider = get_git_provider()(pr_url=api_url) - await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id)) + with get_logger().contextualize(**log_context): + await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id)) # handle pull_request event: # automatically review opened/reopened/ready_for_review PRs as long as they're not in draft, @@ -115,6 +116,7 @@ async def handle_request(body: Dict[str, Any], event: str): api_url = pull_request.get("url") if not api_url: return {} + log_context["api_url"] = api_url if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user: return {} if action in get_settings().github_app.handle_pr_actions: @@ -124,7 +126,7 @@ async def handle_request(body: Dict[str, Any], event: str): if pull_request.get("created_at") == pull_request.get("updated_at"): # avoid double reviews when opening a PR for the first time return {} - logging.info(f"Performing review because of event={event} and action={action}") + get_logger().info(f"Performing review because of event={event} and action={action}") apply_repo_settings(api_url) for command in get_settings().github_app.pr_commands: split_command = command.split(" ") @@ -132,11 +134,12 @@ async def handle_request(body: Dict[str, Any], event: str): args = split_command[1:] other_args = update_settings_from_args(args) new_command = ' '.join([command] + other_args) - logging.info(body) - logging.info(f"Performing command: {new_command}") - await agent.handle_request(api_url, new_command) + get_logger().info(body) + get_logger().info(f"Performing command: {new_command}") + with get_logger().contextualize(**log_context): + await agent.handle_request(api_url, new_command) - logging.info("event or action does not require handling") + get_logger().info("event or action does not require handling") return {} @@ -146,7 +149,7 @@ def _is_duplicate_request(body: Dict[str, Any]) -> bool: This function checks if the request is duplicate and if so - ignores it. """ request_hash = hash(str(body)) - logging.info(f"request_hash: {request_hash}") + get_logger().info(f"request_hash: {request_hash}") request_time = time.monotonic() ttl = get_settings().github_app.duplicate_requests_cache_ttl # in seconds to_delete = [key for key, key_time in _duplicate_requests_cache.items() if request_time - key_time > ttl] @@ -155,7 +158,7 @@ def _is_duplicate_request(body: Dict[str, Any]) -> bool: is_duplicate = request_hash in _duplicate_requests_cache _duplicate_requests_cache[request_hash] = request_time if is_duplicate: - logging.info(f"Ignoring duplicate request {request_hash}") + get_logger().info(f"Ignoring duplicate request {request_hash}") return is_duplicate diff --git a/pr_agent/servers/github_polling.py b/pr_agent/servers/github_polling.py index fdd6642d..1363b941 100644 --- a/pr_agent/servers/github_polling.py +++ b/pr_agent/servers/github_polling.py @@ -1,6 +1,4 @@ import asyncio -import logging -import sys from datetime import datetime, timezone import aiohttp @@ -8,9 +6,10 @@ import aiohttp from pr_agent.agent.pr_agent import PRAgent from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider +from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.servers.help import bot_help_text -logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) +setup_logger(fmt=LoggingFormat.JSON) NOTIFICATION_URL = "https://api.github.com/notifications" @@ -94,7 +93,7 @@ async def polling_loop(): comment_body = comment['body'] if 'body' in comment else '' commenter_github_user = comment['user']['login'] \ if 'user' in comment else '' - logging.info(f"Commenter: {commenter_github_user}\nComment: {comment_body}") + get_logger().info(f"Commenter: {commenter_github_user}\nComment: {comment_body}") user_tag = "@" + user_id if user_tag not in comment_body: continue @@ -112,7 +111,7 @@ async def polling_loop(): print(f"Failed to fetch notifications. Status code: {response.status}") except Exception as e: - logging.error(f"Exception during processing of a notification: {e}") + get_logger().error(f"Exception during processing of a notification: {e}") if __name__ == '__main__': diff --git a/pr_agent/servers/gitlab_webhook.py b/pr_agent/servers/gitlab_webhook.py index 8321cd60..9cddc27e 100644 --- a/pr_agent/servers/gitlab_webhook.py +++ b/pr_agent/servers/gitlab_webhook.py @@ -1,7 +1,5 @@ import copy import json -import logging -import sys import uvicorn from fastapi import APIRouter, FastAPI, Request, status @@ -14,9 +12,10 @@ from starlette_context.middleware import RawContextMiddleware from pr_agent.agent.pr_agent import PRAgent from pr_agent.config_loader import get_settings, global_settings +from pr_agent.log import get_logger, setup_logger from pr_agent.secret_providers import get_secret_provider -logging.basicConfig(stream=sys.stdout, level=logging.INFO) +setup_logger() router = APIRouter() secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None @@ -33,7 +32,7 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request): context["settings"] = copy.deepcopy(global_settings) context["settings"].gitlab.personal_access_token = gitlab_token except Exception as e: - logging.error(f"Failed to validate secret {request_token}: {e}") + get_logger().error(f"Failed to validate secret {request_token}: {e}") return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"})) elif get_settings().get("GITLAB.SHARED_SECRET"): secret = get_settings().get("GITLAB.SHARED_SECRET") @@ -45,9 +44,9 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request): if not gitlab_token: return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"})) data = await request.json() - logging.info(json.dumps(data)) + get_logger().info(json.dumps(data)) if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']: - logging.info(f"A merge request has been opened: {data['object_attributes'].get('title')}") + get_logger().info(f"A merge request has been opened: {data['object_attributes'].get('title')}") url = data['object_attributes'].get('url') background_tasks.add_task(PRAgent().handle_request, url, "/review") elif data.get('object_kind') == 'note' and data['event_type'] == 'note': diff --git a/pr_agent/servers/serverless.py b/pr_agent/servers/serverless.py index 42178431..91596315 100644 --- a/pr_agent/servers/serverless.py +++ b/pr_agent/servers/serverless.py @@ -1,12 +1,10 @@ -import logging - from fastapi import FastAPI from mangum import Mangum +from pr_agent.log import setup_logger from pr_agent.servers.github_app import router -logger = logging.getLogger() -logger.setLevel(logging.DEBUG) +setup_logger() app = FastAPI() app.include_router(router) diff --git a/pr_agent/tools/pr_add_docs.py b/pr_agent/tools/pr_add_docs.py index 2769e9a9..c6499bb5 100644 --- a/pr_agent/tools/pr_add_docs.py +++ b/pr_agent/tools/pr_add_docs.py @@ -1,16 +1,17 @@ import copy -import logging import textwrap -from typing import List, Dict +from typing import Dict + 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, get_pr_multi_diffs +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.algo.utils import load_yaml from pr_agent.config_loader import get_settings -from pr_agent.git_providers import BitbucketProvider, get_git_provider +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 class PRAddDocs: @@ -43,34 +44,34 @@ class PRAddDocs: async def run(self): try: - logging.info('Generating code Docs for PR...') + get_logger().info('Generating code Docs for PR...') if get_settings().config.publish_output: self.git_provider.publish_comment("Generating Documentation...", is_temporary=True) - logging.info('Preparing PR documentation...') + get_logger().info('Preparing PR documentation...') await retry_with_fallback_models(self._prepare_prediction) data = self._prepare_pr_code_docs() if (not data) or (not 'Code Documentation' in data): - logging.info('No code documentation found for PR.') + get_logger().info('No code documentation found for PR.') return if get_settings().config.publish_output: - logging.info('Pushing PR documentation...') + get_logger().info('Pushing PR documentation...') self.git_provider.remove_initial_comment() - logging.info('Pushing inline code documentation...') + get_logger().info('Pushing inline code documentation...') self.push_inline_docs(data) except Exception as e: - logging.error(f"Failed to generate code documentation for PR, error: {e}") + get_logger().error(f"Failed to generate code documentation for PR, error: {e}") async def _prepare_prediction(self, model: str): - logging.info('Getting PR diff...') + get_logger().info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model, add_line_numbers_to_hunks=True, disable_extra_lines=False) - logging.info('Getting AI prediction...') + get_logger().info('Getting AI prediction...') self.prediction = await self._get_prediction(model) async def _get_prediction(self, model: str): @@ -80,8 +81,8 @@ class PRAddDocs: system_prompt = environment.from_string(get_settings().pr_add_docs_prompt.system).render(variables) user_prompt = environment.from_string(get_settings().pr_add_docs_prompt.user).render(variables) if get_settings().config.verbosity_level >= 2: - logging.info(f"\nSystem prompt:\n{system_prompt}") - logging.info(f"\nUser prompt:\n{user_prompt}") + get_logger().info(f"\nSystem prompt:\n{system_prompt}") + get_logger().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) @@ -103,7 +104,7 @@ class PRAddDocs: for d in data['Code Documentation']: try: if get_settings().config.verbosity_level >= 2: - logging.info(f"add_docs: {d}") + get_logger().info(f"add_docs: {d}") relevant_file = d['relevant file'].strip() relevant_line = int(d['relevant line']) # absolute position documentation = d['documentation'] @@ -118,11 +119,11 @@ class PRAddDocs: 'relevant_lines_end': relevant_line}) except Exception: if get_settings().config.verbosity_level >= 2: - logging.info(f"Could not parse code docs: {d}") + get_logger().info(f"Could not parse code docs: {d}") is_successful = self.git_provider.publish_code_suggestions(docs) if not is_successful: - logging.info("Failed to publish code docs, trying to publish each docs separately") + get_logger().info("Failed to publish code docs, trying to publish each docs separately") for doc_suggestion in docs: self.git_provider.publish_code_suggestions([doc_suggestion]) @@ -154,7 +155,7 @@ class PRAddDocs: new_code_snippet = new_code_snippet.rstrip() + "\n" + original_initial_line except Exception as e: if get_settings().config.verbosity_level >= 2: - logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}") + get_logger().info(f"Could not dedent code snippet for file {relevant_file}, error: {e}") return new_code_snippet diff --git a/pr_agent/tools/pr_code_suggestions.py b/pr_agent/tools/pr_code_suggestions.py index 7f0b1264..9e8d7f15 100644 --- a/pr_agent/tools/pr_code_suggestions.py +++ b/pr_agent/tools/pr_code_suggestions.py @@ -1,16 +1,17 @@ import copy -import logging import textwrap -from typing import List, Dict +from typing import Dict, List + 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, get_pr_multi_diffs +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.config_loader import get_settings -from pr_agent.git_providers import BitbucketProvider, get_git_provider +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 class PRCodeSuggestions: @@ -52,42 +53,42 @@ class PRCodeSuggestions: async def run(self): try: - logging.info('Generating code suggestions for PR...') + get_logger().info('Generating code suggestions for PR...') if get_settings().config.publish_output: self.git_provider.publish_comment("Preparing review...", is_temporary=True) - logging.info('Preparing PR review...') + get_logger().info('Preparing PR review...') if not self.is_extended: await retry_with_fallback_models(self._prepare_prediction) 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): - logging.info('No code suggestions found for PR.') + 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): - logging.info('Ranking Suggestions...') + get_logger().info('Ranking Suggestions...') data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions']) if get_settings().config.publish_output: - logging.info('Pushing PR review...') + get_logger().info('Pushing PR review...') self.git_provider.remove_initial_comment() - logging.info('Pushing inline code suggestions...') + get_logger().info('Pushing inline code suggestions...') self.push_inline_code_suggestions(data) except Exception as e: - logging.error(f"Failed to generate code suggestions for PR, error: {e}") + get_logger().error(f"Failed to generate code suggestions for PR, error: {e}") async def _prepare_prediction(self, model: str): - logging.info('Getting PR diff...') + get_logger().info('Getting PR diff...') 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...') + get_logger().info('Getting AI prediction...') self.prediction = await self._get_prediction(model) async def _get_prediction(self, model: str): @@ -97,8 +98,8 @@ class PRCodeSuggestions: system_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.system).render(variables) user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables) if get_settings().config.verbosity_level >= 2: - logging.info(f"\nSystem prompt:\n{system_prompt}") - logging.info(f"\nUser prompt:\n{user_prompt}") + get_logger().info(f"\nSystem prompt:\n{system_prompt}") + get_logger().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) @@ -120,7 +121,7 @@ class PRCodeSuggestions: for d in data['Code suggestions']: try: if get_settings().config.verbosity_level >= 2: - logging.info(f"suggestion: {d}") + 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']) @@ -136,11 +137,11 @@ class PRCodeSuggestions: 'relevant_lines_end': relevant_lines_end}) except Exception: if get_settings().config.verbosity_level >= 2: - logging.info(f"Could not parse suggestion: {d}") + get_logger().info(f"Could not parse suggestion: {d}") is_successful = self.git_provider.publish_code_suggestions(code_suggestions) if not is_successful: - logging.info("Failed to publish code suggestions, trying to publish each suggestion separately") + 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]) @@ -162,19 +163,19 @@ class PRCodeSuggestions: new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n') except Exception as e: if get_settings().config.verbosity_level >= 2: - logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}") + get_logger().info(f"Could not dedent code snippet for file {relevant_file}, error: {e}") return new_code_snippet async def _prepare_prediction_extended(self, model: str) -> dict: - logging.info('Getting PR diff...') + get_logger().info('Getting PR diff...') patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model, max_calls=get_settings().pr_code_suggestions.max_number_of_calls) - logging.info('Getting multi AI predictions...') + get_logger().info('Getting multi AI predictions...') prediction_list = [] for i, patches_diff in enumerate(patches_diff_list): - logging.info(f"Processing chunk {i + 1} of {len(patches_diff_list)}") + get_logger().info(f"Processing chunk {i + 1} of {len(patches_diff_list)}") self.patches_diff = patches_diff prediction = await self._get_prediction(model) prediction_list.append(prediction) @@ -222,8 +223,8 @@ class PRCodeSuggestions: variables) user_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.user).render(variables) if get_settings().config.verbosity_level >= 2: - logging.info(f"\nSystem prompt:\n{system_prompt}") - logging.info(f"\nUser prompt:\n{user_prompt}") + get_logger().info(f"\nSystem prompt:\n{system_prompt}") + get_logger().info(f"\nUser prompt:\n{user_prompt}") response, finish_reason = await self.ai_handler.chat_completion(model=model, system=system_prompt, user=user_prompt) @@ -238,7 +239,7 @@ class PRCodeSuggestions: data_sorted = data_sorted[:new_len] except Exception as e: if get_settings().config.verbosity_level >= 1: - logging.info(f"Could not sort suggestions, error: {e}") + get_logger().info(f"Could not sort suggestions, error: {e}") data_sorted = suggestion_list return data_sorted diff --git a/pr_agent/tools/pr_config.py b/pr_agent/tools/pr_config.py index 0dc35918..44ab7e23 100644 --- a/pr_agent/tools/pr_config.py +++ b/pr_agent/tools/pr_config.py @@ -1,7 +1,6 @@ -import logging - from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider +from pr_agent.log import get_logger class PRConfig: @@ -19,11 +18,11 @@ class PRConfig: self.git_provider = get_git_provider()(pr_url) async def run(self): - logging.info('Getting configuration settings...') - logging.info('Preparing configs...') + get_logger().info('Getting configuration settings...') + get_logger().info('Preparing configs...') pr_comment = self._prepare_pr_configs() if get_settings().config.publish_output: - logging.info('Pushing configs...') + get_logger().info('Pushing configs...') self.git_provider.publish_comment(pr_comment) self.git_provider.remove_initial_comment() return "" @@ -44,5 +43,5 @@ class PRConfig: comment_str += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}" comment_str += " " if get_settings().config.verbosity_level >= 2: - logging.info(f"comment_str:\n{comment_str}") + get_logger().info(f"comment_str:\n{comment_str}") return comment_str diff --git a/pr_agent/tools/pr_description.py b/pr_agent/tools/pr_description.py index 41f0b781..d74ee665 100644 --- a/pr_agent/tools/pr_description.py +++ b/pr_agent/tools/pr_description.py @@ -1,7 +1,5 @@ import copy -import json import re -import logging from typing import List, Tuple from jinja2 import Environment, StrictUndefined @@ -13,6 +11,7 @@ from pr_agent.algo.utils import load_yaml 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 class PRDescription: @@ -65,13 +64,13 @@ class PRDescription: """ try: - logging.info(f"Generating a PR description {self.pr_id}") + get_logger().info(f"Generating a PR description {self.pr_id}") if get_settings().config.publish_output: self.git_provider.publish_comment("Preparing PR description...", is_temporary=True) await retry_with_fallback_models(self._prepare_prediction) - logging.info(f"Preparing answer {self.pr_id}") + get_logger().info(f"Preparing answer {self.pr_id}") if self.prediction: self._prepare_data() else: @@ -88,7 +87,7 @@ class PRDescription: full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}" if get_settings().config.publish_output: - logging.info(f"Pushing answer {self.pr_id}") + get_logger().info(f"Pushing answer {self.pr_id}") if get_settings().pr_description.publish_description_as_comment: self.git_provider.publish_comment(full_markdown_description) else: @@ -100,7 +99,7 @@ class PRDescription: self.git_provider.publish_labels(pr_labels + current_labels) self.git_provider.remove_initial_comment() except Exception as e: - logging.error(f"Error generating PR description {self.pr_id}: {e}") + get_logger().error(f"Error generating PR description {self.pr_id}: {e}") return "" @@ -121,9 +120,9 @@ class PRDescription: if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description: return None - logging.info(f"Getting PR diff {self.pr_id}") + get_logger().info(f"Getting PR diff {self.pr_id}") self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) - logging.info(f"Getting AI prediction {self.pr_id}") + get_logger().info(f"Getting AI prediction {self.pr_id}") self.prediction = await self._get_prediction(model) async def _get_prediction(self, model: str) -> str: @@ -144,8 +143,8 @@ class PRDescription: user_prompt = environment.from_string(get_settings().pr_description_prompt.user).render(variables) if get_settings().config.verbosity_level >= 2: - logging.info(f"\nSystem prompt:\n{system_prompt}") - logging.info(f"\nUser prompt:\n{user_prompt}") + get_logger().info(f"\nSystem prompt:\n{system_prompt}") + get_logger().info(f"\nUser prompt:\n{user_prompt}") response, finish_reason = await self.ai_handler.chat_completion( model=model, @@ -178,7 +177,7 @@ class PRDescription: return pr_types def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]: - logging.info(f"Using description marker replacements {self.pr_id}") + get_logger().info(f"Using description marker replacements {self.pr_id}") title = self.vars["title"] body = self.user_description if get_settings().pr_description.include_generated_by_header: @@ -252,6 +251,6 @@ class PRDescription: pr_body += "\n___\n" if get_settings().config.verbosity_level >= 2: - logging.info(f"title:\n{title}\n{pr_body}") + get_logger().info(f"title:\n{title}\n{pr_body}") return title, pr_body \ No newline at end of file diff --git a/pr_agent/tools/pr_information_from_user.py b/pr_agent/tools/pr_information_from_user.py index c049250f..059966e1 100644 --- a/pr_agent/tools/pr_information_from_user.py +++ b/pr_agent/tools/pr_information_from_user.py @@ -1,5 +1,4 @@ import copy -import logging from jinja2 import Environment, StrictUndefined @@ -9,6 +8,7 @@ from pr_agent.algo.token_handler import TokenHandler 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 class PRInformationFromUser: @@ -34,22 +34,22 @@ class PRInformationFromUser: self.prediction = None async def run(self): - logging.info('Generating question to the user...') + get_logger().info('Generating question to the user...') if get_settings().config.publish_output: self.git_provider.publish_comment("Preparing questions...", is_temporary=True) await retry_with_fallback_models(self._prepare_prediction) - logging.info('Preparing questions...') + get_logger().info('Preparing questions...') pr_comment = self._prepare_pr_answer() if get_settings().config.publish_output: - logging.info('Pushing questions...') + get_logger().info('Pushing questions...') self.git_provider.publish_comment(pr_comment) self.git_provider.remove_initial_comment() return "" async def _prepare_prediction(self, model): - logging.info('Getting PR diff...') + get_logger().info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) - logging.info('Getting AI prediction...') + get_logger().info('Getting AI prediction...') self.prediction = await self._get_prediction(model) async def _get_prediction(self, model: str): @@ -59,8 +59,8 @@ class PRInformationFromUser: system_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.system).render(variables) user_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.user).render(variables) if get_settings().config.verbosity_level >= 2: - logging.info(f"\nSystem prompt:\n{system_prompt}") - logging.info(f"\nUser prompt:\n{user_prompt}") + get_logger().info(f"\nSystem prompt:\n{system_prompt}") + get_logger().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 @@ -68,7 +68,7 @@ class PRInformationFromUser: def _prepare_pr_answer(self) -> str: model_output = self.prediction.strip() if get_settings().config.verbosity_level >= 2: - logging.info(f"answer_str:\n{model_output}") + get_logger().info(f"answer_str:\n{model_output}") answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\ "\n>/answer\n>1) ...\n>2) ...\n>...\n" return answer_str diff --git a/pr_agent/tools/pr_questions.py b/pr_agent/tools/pr_questions.py index 959bebe7..7740fd4a 100644 --- a/pr_agent/tools/pr_questions.py +++ b/pr_agent/tools/pr_questions.py @@ -1,5 +1,4 @@ import copy -import logging from jinja2 import Environment, StrictUndefined @@ -9,6 +8,7 @@ from pr_agent.algo.token_handler import TokenHandler 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 class PRQuestions: @@ -44,22 +44,22 @@ class PRQuestions: return question_str async def run(self): - logging.info('Answering a PR question...') + get_logger().info('Answering a PR question...') if get_settings().config.publish_output: self.git_provider.publish_comment("Preparing answer...", is_temporary=True) await retry_with_fallback_models(self._prepare_prediction) - logging.info('Preparing answer...') + get_logger().info('Preparing answer...') pr_comment = self._prepare_pr_answer() if get_settings().config.publish_output: - logging.info('Pushing answer...') + get_logger().info('Pushing answer...') self.git_provider.publish_comment(pr_comment) self.git_provider.remove_initial_comment() return "" async def _prepare_prediction(self, model: str): - logging.info('Getting PR diff...') + get_logger().info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) - logging.info('Getting AI prediction...') + get_logger().info('Getting AI prediction...') self.prediction = await self._get_prediction(model) async def _get_prediction(self, model: str): @@ -69,8 +69,8 @@ class PRQuestions: system_prompt = environment.from_string(get_settings().pr_questions_prompt.system).render(variables) user_prompt = environment.from_string(get_settings().pr_questions_prompt.user).render(variables) if get_settings().config.verbosity_level >= 2: - logging.info(f"\nSystem prompt:\n{system_prompt}") - logging.info(f"\nUser prompt:\n{user_prompt}") + get_logger().info(f"\nSystem prompt:\n{system_prompt}") + get_logger().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 @@ -79,5 +79,5 @@ class PRQuestions: answer_str = f"Question: {self.question_str}\n\n" answer_str += f"Answer:\n{self.prediction.strip()}\n\n" if get_settings().config.verbosity_level >= 2: - logging.info(f"answer_str:\n{answer_str}") + get_logger().info(f"answer_str:\n{answer_str}") return answer_str diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index 919e1800..ed99ddf6 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -1,6 +1,4 @@ import copy -import json -import logging from collections import OrderedDict from typing import List, Tuple @@ -9,13 +7,13 @@ from jinja2 import Environment, StrictUndefined from yaml import SafeLoader from pr_agent.algo.ai_handler import AiHandler -from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models, \ - find_line_number_of_relevant_line_in_file, clip_tokens +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.algo.utils import convert_to_markdown, try_fix_json, try_fix_yaml, load_yaml +from pr_agent.algo.utils import convert_to_markdown, load_yaml, try_fix_yaml 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 IncrementalPR, get_main_pr_language +from pr_agent.log import get_logger from pr_agent.servers.help import actions_help_text, bot_help_text @@ -98,29 +96,29 @@ class PRReviewer: try: if self.is_auto and not get_settings().pr_reviewer.automatic_review: - logging.info(f'Automatic review is disabled {self.pr_url}') + get_logger().info(f'Automatic review is disabled {self.pr_url}') return None - logging.info(f'Reviewing PR: {self.pr_url} ...') + get_logger().info(f'Reviewing PR: {self.pr_url} ...') if get_settings().config.publish_output: self.git_provider.publish_comment("Preparing review...", is_temporary=True) await retry_with_fallback_models(self._prepare_prediction) - logging.info('Preparing PR review...') + get_logger().info('Preparing PR review...') pr_comment = self._prepare_pr_review() if get_settings().config.publish_output: - logging.info('Pushing PR review...') + get_logger().info('Pushing PR review...') self.git_provider.publish_comment(pr_comment) self.git_provider.remove_initial_comment() if get_settings().pr_reviewer.inline_code_comments: - logging.info('Pushing inline code comments...') + get_logger().info('Pushing inline code comments...') self._publish_inline_code_comments() except Exception as e: - logging.error(f"Failed to review PR: {e}") + get_logger().error(f"Failed to review PR: {e}") async def _prepare_prediction(self, model: str) -> None: """ @@ -132,9 +130,9 @@ class PRReviewer: Returns: None """ - logging.info('Getting PR diff...') + get_logger().info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) - logging.info('Getting AI prediction...') + get_logger().info('Getting AI prediction...') self.prediction = await self._get_prediction(model) async def _get_prediction(self, model: str) -> str: @@ -155,8 +153,8 @@ class PRReviewer: user_prompt = environment.from_string(get_settings().pr_review_prompt.user).render(variables) if get_settings().config.verbosity_level >= 2: - logging.info(f"\nSystem prompt:\n{system_prompt}") - logging.info(f"\nUser prompt:\n{user_prompt}") + get_logger().info(f"\nSystem prompt:\n{system_prompt}") + get_logger().info(f"\nUser prompt:\n{user_prompt}") response, finish_reason = await self.ai_handler.chat_completion( model=model, @@ -249,7 +247,7 @@ class PRReviewer: # Log markdown response if verbosity level is high if get_settings().config.verbosity_level >= 2: - logging.info(f"Markdown response:\n{markdown_text}") + get_logger().info(f"Markdown response:\n{markdown_text}") if markdown_text == None or len(markdown_text) == 0: markdown_text = "" @@ -268,7 +266,7 @@ class PRReviewer: try: data = yaml.load(review_text, Loader=SafeLoader) except Exception as e: - logging.error(f"Failed to parse AI prediction: {e}") + get_logger().error(f"Failed to parse AI prediction: {e}") data = try_fix_yaml(review_text) comments: List[str] = [] @@ -277,7 +275,7 @@ class PRReviewer: relevant_line_in_file = suggestion.get('relevant line', '').strip() content = suggestion.get('suggestion', '') if not relevant_file or not relevant_line_in_file or not content: - logging.info("Skipping inline comment with missing file/line/content") + get_logger().info("Skipping inline comment with missing file/line/content") continue if self.git_provider.is_supported("create_inline_comment"): diff --git a/pr_agent/tools/pr_similar_issue.py b/pr_agent/tools/pr_similar_issue.py index d7b6a799..4bca8169 100644 --- a/pr_agent/tools/pr_similar_issue.py +++ b/pr_agent/tools/pr_similar_issue.py @@ -1,18 +1,17 @@ -import copy -import json -import logging from enum import Enum -from typing import List, Tuple -import pinecone +from typing import List + import openai import pandas as pd +import pinecone +from pinecone_datasets import Dataset, DatasetMetadata from pydantic import BaseModel, Field from pr_agent.algo import MAX_TOKENS from pr_agent.algo.token_handler import TokenHandler from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider -from pinecone_datasets import Dataset, DatasetMetadata +from pr_agent.log import get_logger MODEL = "text-embedding-ada-002" @@ -62,11 +61,11 @@ class PRSimilarIssue: upsert = False if run_from_scratch or upsert: # index the entire repo - logging.info('Indexing the entire repo...') + get_logger().info('Indexing the entire repo...') - logging.info('Getting issues...') + get_logger().info('Getting issues...') issues = list(repo_obj.get_issues(state='all')) - logging.info('Done') + get_logger().info('Done') self._update_index_with_issues(issues, repo_name_for_index, upsert=upsert) else: # update index if needed pinecone_index = pinecone.Index(index_name=index_name) @@ -92,20 +91,20 @@ class PRSimilarIssue: break if issues_to_update: - logging.info(f'Updating index with {counter} new issues...') + get_logger().info(f'Updating index with {counter} new issues...') self._update_index_with_issues(issues_to_update, repo_name_for_index, upsert=True) else: - logging.info('No new issues to update') + get_logger().info('No new issues to update') async def run(self): - logging.info('Getting issue...') + get_logger().info('Getting issue...') repo_name, original_issue_number = self.git_provider._parse_issue_url(self.issue_url.split('=')[-1]) issue_main = self.git_provider.repo_obj.get_issue(original_issue_number) issue_str, comments, number = self._process_issue(issue_main) openai.api_key = get_settings().openai.key - logging.info('Done') + get_logger().info('Done') - logging.info('Querying...') + get_logger().info('Querying...') res = openai.Embedding.create(input=[issue_str], engine=MODEL) embeds = [record['embedding'] for record in res['data']] pinecone_index = pinecone.Index(index_name=self.index_name) @@ -127,9 +126,9 @@ class PRSimilarIssue: else: relevant_comment_number_list.append(-1) score_list.append(str("{:.2f}".format(r['score']))) - logging.info('Done') + get_logger().info('Done') - logging.info('Publishing response...') + get_logger().info('Publishing response...') similar_issues_str = "### Similar Issues\n___\n\n" for i, issue_number_similar in enumerate(relevant_issues_number_list): issue = self.git_provider.repo_obj.get_issue(issue_number_similar) @@ -140,8 +139,8 @@ class PRSimilarIssue: similar_issues_str += f"{i + 1}. **[{title}]({url})** (score={score_list[i]})\n\n" if get_settings().config.publish_output: response = issue_main.create_comment(similar_issues_str) - logging.info(similar_issues_str) - logging.info('Done') + get_logger().info(similar_issues_str) + get_logger().info('Done') def _process_issue(self, issue): header = issue.title @@ -155,7 +154,7 @@ class PRSimilarIssue: return issue_str, comments, number def _update_index_with_issues(self, issues_list, repo_name_for_index, upsert=False): - logging.info('Processing issues...') + get_logger().info('Processing issues...') corpus = Corpus() example_issue_record = Record( id=f"example_issue_{repo_name_for_index}", @@ -171,9 +170,9 @@ class PRSimilarIssue: counter += 1 if counter % 100 == 0: - logging.info(f"Scanned {counter} issues") + get_logger().info(f"Scanned {counter} issues") if counter >= self.max_issues_to_scan: - logging.info(f"Scanned {self.max_issues_to_scan} issues, stopping") + get_logger().info(f"Scanned {self.max_issues_to_scan} issues, stopping") break issue_str, comments, number = self._process_issue(issue) @@ -210,9 +209,9 @@ class PRSimilarIssue: ) corpus.append(comment_record) df = pd.DataFrame(corpus.dict()["documents"]) - logging.info('Done') + get_logger().info('Done') - logging.info('Embedding...') + get_logger().info('Embedding...') openai.api_key = get_settings().openai.key list_to_encode = list(df["text"].values) try: @@ -220,7 +219,7 @@ class PRSimilarIssue: embeds = [record['embedding'] for record in res['data']] except: embeds = [] - logging.error('Failed to embed entire list, embedding one by one...') + get_logger().error('Failed to embed entire list, embedding one by one...') for i, text in enumerate(list_to_encode): try: res = openai.Embedding.create(input=[text], engine=MODEL) @@ -231,21 +230,21 @@ class PRSimilarIssue: meta = DatasetMetadata.empty() meta.dense_model.dimension = len(embeds[0]) ds = Dataset.from_pandas(df, meta) - logging.info('Done') + get_logger().info('Done') api_key = get_settings().pinecone.api_key environment = get_settings().pinecone.environment if not upsert: - logging.info('Creating index from scratch...') + get_logger().info('Creating index from scratch...') ds.to_pinecone_index(self.index_name, api_key=api_key, environment=environment) else: - logging.info('Upserting index...') + get_logger().info('Upserting index...') namespace = "" batch_size: int = 100 concurrency: int = 10 pinecone.init(api_key=api_key, environment=environment) ds._upsert_to_index(self.index_name, namespace, batch_size, concurrency) - logging.info('Done') + get_logger().info('Done') class IssueLevel(str, Enum): diff --git a/pr_agent/tools/pr_update_changelog.py b/pr_agent/tools/pr_update_changelog.py index 547ce84b..a5f24e0d 100644 --- a/pr_agent/tools/pr_update_changelog.py +++ b/pr_agent/tools/pr_update_changelog.py @@ -1,5 +1,4 @@ import copy -import logging from datetime import date from time import sleep from typing import Tuple @@ -10,8 +9,9 @@ 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 get_settings -from pr_agent.git_providers import GithubProvider, get_git_provider +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 CHANGELOG_LINES = 50 @@ -48,26 +48,26 @@ class PRUpdateChangelog: async def run(self): # assert type(self.git_provider) == GithubProvider, "Currently only Github is supported" - logging.info('Updating the changelog...') + get_logger().info('Updating the changelog...') if get_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...') + get_logger().info('Preparing PR changelog updates...') new_file_content, answer = self._prepare_changelog_update() if get_settings().config.publish_output: self.git_provider.remove_initial_comment() - logging.info('Publishing changelog updates...') + get_logger().info('Publishing changelog updates...') if self.commit_changelog: - logging.info('Pushing PR changelog updates to repo...') + get_logger().info('Pushing PR changelog updates to repo...') self._push_changelog_update(new_file_content, answer) else: - logging.info('Publishing PR changelog as comment...') + get_logger().info('Publishing PR changelog as comment...') self.git_provider.publish_comment(f"**Changelog updates:**\n\n{answer}") async def _prepare_prediction(self, model: str): - logging.info('Getting PR diff...') + get_logger().info('Getting PR diff...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model) - logging.info('Getting AI prediction...') + get_logger().info('Getting AI prediction...') self.prediction = await self._get_prediction(model) async def _get_prediction(self, model: str): @@ -77,8 +77,8 @@ class PRUpdateChangelog: system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables) user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables) if get_settings().config.verbosity_level >= 2: - logging.info(f"\nSystem prompt:\n{system_prompt}") - logging.info(f"\nUser prompt:\n{user_prompt}") + get_logger().info(f"\nSystem prompt:\n{system_prompt}") + get_logger().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) @@ -100,7 +100,7 @@ class PRUpdateChangelog: "\n>'/update_changelog --pr_update_changelog.push_changelog_changes=true'\n" if get_settings().config.verbosity_level >= 2: - logging.info(f"answer:\n{answer}") + get_logger().info(f"answer:\n{answer}") return new_file_content, answer @@ -149,7 +149,7 @@ Example: except Exception: self.changelog_file_str = "" if self.commit_changelog: - logging.info("No CHANGELOG.md file found in the repository. Creating one...") + get_logger().info("No CHANGELOG.md file found in the repository. Creating one...") changelog_file = self.git_provider.repo_obj.create_file(path="CHANGELOG.md", message='add CHANGELOG.md', content="", diff --git a/requirements.txt b/requirements.txt index 8791a115..8589b30b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,5 @@ ujson==5.8.0 azure-devops==7.1.0b3 msrest==0.7.1 pinecone-client -pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main \ No newline at end of file +pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main +loguru==0.7.2 From 83e670c5df9a045a459d2737838152dde1e47bc7 Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Mon, 16 Oct 2023 16:13:09 +0300 Subject: [PATCH 27/29] Enhance logging context in github_app server with server type --- pr_agent/servers/github_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pr_agent/servers/github_app.py b/pr_agent/servers/github_app.py index c598049e..37f96e2d 100644 --- a/pr_agent/servers/github_app.py +++ b/pr_agent/servers/github_app.py @@ -78,7 +78,7 @@ async def handle_request(body: Dict[str, Any], event: str): agent = PRAgent() bot_user = get_settings().github_app.bot_user sender = body.get("sender", {}).get("login") - log_context = {"action": action, "event": event, "sender": sender} + log_context = {"action": action, "event": event, "sender": sender, "server_type": "github_app"} if get_settings().github_app.duplicate_requests_cache and _is_duplicate_request(body): return {} From 7ffdf8de37ce1db98ebd09a83a4c64ceb174e09b Mon Sep 17 00:00:00 2001 From: Ori Kotek Date: Mon, 16 Oct 2023 16:25:57 +0300 Subject: [PATCH 28/29] Remove verbosity level check in handle_patch_deletions test --- tests/unittest/test_handle_patch_deletions.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/unittest/test_handle_patch_deletions.py b/tests/unittest/test_handle_patch_deletions.py index 152ea4b2..e44c0d77 100644 --- a/tests/unittest/test_handle_patch_deletions.py +++ b/tests/unittest/test_handle_patch_deletions.py @@ -43,18 +43,6 @@ class TestHandlePatchDeletions: assert handle_patch_deletions(patch, original_file_content_str, new_file_content_str, file_name) == patch.rstrip() - # Tests that handle_patch_deletions logs a message when verbosity_level is greater than 0 - def test_handle_patch_deletions_happy_path_verbosity_level_greater_than_0(self, caplog): - patch = '--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n-bar\n+baz\n' - original_file_content_str = 'foo\nbar\n' - new_file_content_str = '' - file_name = 'file.py' - get_settings().config.verbosity_level = 1 - - with caplog.at_level(logging.INFO): - handle_patch_deletions(patch, original_file_content_str, new_file_content_str, file_name) - assert any("Processing file" in message for message in caplog.messages) - # Tests that handle_patch_deletions returns 'File was deleted' when new_file_content_str is empty def test_handle_patch_deletions_edge_case_new_file_content_empty(self): patch = '--- a/file.py\n+++ b/file.py\n@@ -1,2 +1,2 @@\n-foo\n-bar\n' From e6f1e0520a11d96dc026f7ccb4adf11c6392fe7c Mon Sep 17 00:00:00 2001 From: Marius Anderie Date: Mon, 16 Oct 2023 20:38:14 +0200 Subject: [PATCH 29/29] remove azure.com url restriction --- pr_agent/git_providers/azuredevops_provider.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 9e9497c7..6a404532 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -236,9 +236,6 @@ class AzureDevopsProvider: def _parse_pr_url(pr_url: str) -> Tuple[str, int]: parsed_url = urlparse(pr_url) - if 'azure.com' not in parsed_url.netloc: - raise ValueError("The provided URL is not a valid Azure DevOps URL") - path_parts = parsed_url.path.strip('/').split('/') if len(path_parts) < 6 or path_parts[4] != 'pullrequest':