Compare commits

..

70 Commits

Author SHA1 Message Date
5fb8bc1927 test 2023-11-20 00:42:29 +02:00
e9315c7d28 Merge commit 'e878bbbe369c90433c1b261b5479d23c47734539' into hl/test_docstring 2023-11-15 10:37:31 +02:00
e878bbbe36 Merge pull request #449 from zmeir/patch-1
Fix `get_user_description` in case `pr_description.enable_pr_type=false`
2023-11-14 22:09:59 -08:00
67b61d722d test 2023-11-14 20:31:01 +02:00
7d89b82967 Fix get_user_description in case pr_description.enable_pr_type=false
Fixes an issue when getting the user description after a PR-Agent description was already generated, in case the configuration setting `pr_description.enable_pr_type` was `false`.
2023-11-13 14:41:14 +02:00
c5f9bbbf92 Merge pull request #448 from Codium-ai/hl/optional_custom_labels
remove the "one or more" for custom labels
2023-11-13 13:51:18 +02:00
a5e5a82952 s 2023-11-13 13:49:16 +02:00
ccbb62b50a remove the "one or more" for custom labels 2023-11-13 13:47:06 +02:00
1df36c6a44 Merge pull request #446 from Codium-ai/tr/fix_cli_args
Handling CLI Arguments with Quotes in pr_agent
2023-11-12 17:29:38 +02:00
9e5e9afe92 Refactor CLI argument handling and request processing 2023-11-12 16:11:34 +02:00
5e43c202dd s1 2023-11-12 15:45:22 +02:00
37e6608e68 Merge pull request #444 from Codium-ai/tr/fallback_yaml
Implementing Fallback Mechanisms for YAML Parsing
2023-11-12 00:43:15 -08:00
f64d5f1e2a tests 2023-11-12 08:36:57 +02:00
8fdf174dec fallback 2023-11-10 18:44:19 +02:00
29d4f98b19 Merge pull request #441 from Codium-ai/tr/presistent_review
Add Persistent Review Feature to PR Agent
2023-11-09 05:26:51 -08:00
737792d83c publish_persistent_comment 2023-11-09 15:24:55 +02:00
7e5889061c publish_persistent_comment 2023-11-09 15:20:31 +02:00
755e04cf65 bitbucket finally works 2023-11-08 20:41:55 +02:00
44d6c95714 response 2023-11-08 20:38:18 +02:00
14610d5375 persistent
s
2023-11-08 20:16:08 +02:00
f9c832d6cb Merge pull request #439 from Codium-ai/tr/fixes_added_files
Enhancement of Patch Handling and PR Processing
2023-11-08 04:48:07 -08:00
c2bec614e5 s 2023-11-08 14:46:11 +02:00
49725e92f2 s 2023-11-08 14:41:15 +02:00
a1e32d8331 s 2023-11-08 14:36:59 +02:00
0293412a42 s 2023-11-08 14:31:08 +02:00
10ec0a1812 s 2023-11-08 14:21:03 +02:00
69b68b78f5 s 2023-11-08 14:17:59 +02:00
c5bc4b44ff fix added files 2023-11-08 12:51:30 +02:00
39e5102a2e fix added files 2023-11-08 12:47:18 +02:00
6c82bc9a3e Merge pull request #437 from Codium-ai/tr/new_gpt4
Introduce support for 'gpt-4-1106-preview' model and dynamic token limit calculation
2023-11-07 04:49:50 -08:00
54f41dd603 code 2023-11-07 14:41:15 +02:00
094f641fb5 code 2023-11-07 14:38:37 +02:00
a35a75eb34 get_max_tokens + added 'gpt-4-1106-preview' 2023-11-07 14:28:41 +02:00
5a7c118b56 Merge pull request #434 from Codium-ai/document_describe
Update DESCRIBE.md
2023-11-06 11:00:06 -08:00
cf9e0fbbc5 Update DESCRIBE.md 2023-11-06 17:55:58 +02:00
ef9af261ed Merge pull request #433 from Codium-ai/hl/user_labels
Keep user labels
2023-11-06 15:17:19 +02:00
ff79776410 Keep user labels 2023-11-06 15:14:08 +02:00
ec3f2fb485 Revert "generate labels keep user labels only"
This reverts commit 94a2a5e527.
2023-11-06 15:08:29 +02:00
94a2a5e527 generate labels keep user labels only 2023-11-06 15:07:06 +02:00
ea4bc548fc Merge pull request #432 from Codium-ai/hl/type_vs_labels
Support git providers with no label support
2023-11-06 14:38:29 +02:00
1eefd3365b Merge commit 'e352c98ce83bfbd99078f62d8705eb938a6ba5b5' into hl/type_vs_labels 2023-11-06 14:24:33 +02:00
db37ee819a support git providers with no label support 2023-11-06 14:11:49 +02:00
e352c98ce8 Merge pull request #431 from Codium-ai/hl/type_vs_labels
Refactoring PR Labels Handling and Display
2023-11-06 02:10:38 -08:00
e96b03da57 add configuration enable_pr_type 2023-11-06 11:58:26 +02:00
1d2aedf169 Don't Display pr labels in the text 2023-11-06 11:35:22 +02:00
4c484f8e86 Merge pull request #423 from zmeir/zmeir-external-incremental_review_thresholds
Implementing Thresholds for Incremental PR Reviews
2023-11-06 01:07:01 -08:00
8a79114ed9 Merge pull request #430 from Codium-ai/tr/fix_prompt
Fix PR Description Prompt and Data Preparation
2023-11-06 01:06:26 -08:00
cd69f43c77 Merge pull request #428 from Codium-ai/tr/fixes
Enhancements and Fixes in Bitbucket Provider
2023-11-06 01:06:15 -08:00
6d6d864417 fix prompt 2023-11-06 09:44:59 +02:00
b286c8ed20 Added documentation for the new configurations 2023-11-06 09:44:04 +02:00
7238c81f0c fix prompt 2023-11-06 09:41:26 +02:00
62412f8cd4 fix prompt 2023-11-06 09:38:39 +02:00
5d2bdadb45 fix prompt 2023-11-06 09:33:10 +02:00
06d030637c fix prompt 2023-11-06 09:32:46 +02:00
8e3fa3926a Extract incremental review checks to separate method 2023-11-06 09:21:22 +02:00
92071fcf1c Stack all incremental parameters 2023-11-06 09:13:04 +02:00
fed1c160eb files walkthrough bullets 2023-11-06 08:43:15 +02:00
e37daf6987 link to change 2023-11-06 08:27:34 +02:00
8fc663911f fixe bitbucket get_repo_settings bug 2023-11-06 08:15:43 +02:00
bb2760ae41 tools 2023-11-06 08:10:04 +02:00
3548b88463 type and labels 2023-11-05 15:48:39 +02:00
c917e48098 Merge pull request #427 from koid/fix/add-middleware
Adding Middleware to FastAPI Initialization
2023-11-05 01:40:37 -07:00
e6ef123ce5 add middleware when initializing fastapi 2023-11-05 15:38:19 +09:00
194bfe1193 Update INSTALL.md 2023-11-05 07:59:59 +02:00
e456cf36aa Merge pull request #425 from Codium-ai/ok/protect_apply_settings
Add exception handling for applying repo settings failure
2023-11-03 11:07:49 -07:00
fe3527de3c Add exception handling for applying repo settings failure 2023-11-03 12:23:49 +02:00
b99c769b53 Merge pull request #415 from zmeir/zmeir-patch-2
Refactor Command Handling for Different Triggers
2023-11-02 18:32:42 +02:00
60bdfb78df Merge pull request #424 from Codium-ai/ok/bitbucket_fix
Update Bitbucket Provider to Use 'position' Instead of 'start_line' for Inline Comments
2023-11-02 18:31:18 +02:00
c623c3baf4 Added new configurations to prevent too frequent incremental commits on push trigger 2023-11-02 12:24:54 +02:00
cf4d007737 Fix commands list for push trigger 2023-10-31 00:00:48 +02:00
28 changed files with 416 additions and 129 deletions

View File

@ -1,5 +1,5 @@
## Installation
### Installation
To get started with PR-Agent quickly, you first need to acquire two tokens:
@ -29,39 +29,43 @@ There are several ways to use PR-Agent:
### Use Docker image (no installation required)
To request a review for a PR, or ask a question about a PR, you can run directly from the Docker image. Here's how:
A list of the relevant tools can be found in the [tools guide](./docs/TOOLS_GUIDE.md).
For GitHub:
To invoke a tool (for example `review`), you can run directly from the Docker image. Here's how:
- For GitHub:
```
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent:latest --pr_url <pr_url> review
```
For GitLab:
- For GitLab:
```
docker run --rm -it -e OPENAI.KEY=<your key> -e CONFIG.GIT_PROVIDER=gitlab -e GITLAB.PERSONAL_ACCESS_TOKEN=<your token> codiumai/pr-agent:latest --pr_url <pr_url> review
```
For BitBucket:
Note: If you have a dedicated GitLab instance, you need to specify the custom url as variable:
```
docker run --rm -it -e OPENAI.KEY=<your key> -e CONFIG.GIT_PROVIDER=gitlab -e GITLAB.PERSONAL_ACCESS_TOKEN=<your token> GITLAB.URL=<your gitlab instance url> codiumai/pr-agent:latest --pr_url <pr_url> review
```
- For BitBucket:
```
docker run --rm -it -e CONFIG.GIT_PROVIDER=bitbucket -e OPENAI.KEY=$OPENAI_API_KEY -e BITBUCKET.BEARER_TOKEN=$BITBUCKET_BEARER_TOKEN codiumai/pr-agent:latest --pr_url=<pr_url> review
```
For other git providers, update CONFIG.GIT_PROVIDER accordingly, and check the `pr_agent/settings/.secrets_template.toml` file for the environment variables expected names and values.
Similarly, to ask a question about a PR, run the following command:
```
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> ask "<your question>"
```
A list of the relevant tools can be found in the [tools guide](./docs/TOOLS_GUIDE.md).
---
Note: If you want to ensure you're running a specific version of the Docker image, consider using the image's digest:
If you want to ensure you're running a specific version of the Docker image, consider using the image's digest:
```bash
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> review
```
in addition, you can run a [specific released versions](./RELEASE_NOTES.md) of pr-agent, for example:
Or you can run a [specific released versions](./RELEASE_NOTES.md) of pr-agent, for example:
```
codiumai/pr-agent@v0.8
codiumai/pr-agent@v0.9
```
---

View File

@ -1,3 +1,6 @@
## Unreleased
- review tool now posts persistent comments by default
## [Version 0.9] - 2023-10-29
- codiumai/pr-agent:0.9
- codiumai/pr-agent:0.9-github_app

View File

@ -173,7 +173,7 @@ push_commands = [
"/auto_review -i --pr_reviewer.remove_previous_review_comment=true",
]
```
The means that when new code is pused to the PR, the PR-Agent will run the `describe` and incremental `auto_review` tools.
The means that when new code is pushed to the PR, the PR-Agent will run the `describe` and incremental `auto_review` tools.
For the describe tool, the `add_original_user_description` and `keep_original_user_title` parameters will be set to true.
For the `auto_review` tool, it will run in incremental mode, and the `remove_previous_review_comment` parameter will be set to true.

View File

@ -27,6 +27,8 @@ Under the section 'pr_description', the [configuration file](./../pr_agent/setti
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
- `enable_pr_type`: if set to false, it will not show the `PR type` as a text value in the description content. Default is true.
### Markers template
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.

View File

@ -24,17 +24,33 @@ Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings
- `num_code_suggestions`: number of code suggestions provided by the 'review' tool. Default is 4.
- `inline_code_comments`: if set to true, the tool will publish the code suggestions as comments on the code diff. Default is false.
- `automatic_review`: if set to false, no automatic reviews will be done. Default is true.
- `remove_previous_review_comment`: if set to true, the tool will remove the previous review comment before adding a new one. Default is false.
- `persistent_comment`: if set to true, the review comment will be persistent. Default is true.
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
#### Incremental Mode
For an incremental review, which only considers changes since the last PR-Agent review, this can be useful when working on the PR in an iterative manner, and you want to focus on the changes since the last review instead of reviewing the entire PR again, the following command can be used:
```
/improve -i
/review -i
```
Note that the incremental mode is only available for GitHub.
<kbd><img src=./../pics/incremental_review.png width="768"></kbd>
Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review -i' tool.
These configurations can be used to control the rate at which the incremental review tool will create new review comments when invoked automatically, to prevent making too much noise in the PR.
- `minimal_commits_for_incremental_review`: Minimal number of commits since the last review that are required to create incremental review.
If there are less than the specified number of commits since the last review, the tool will not perform any action.
Default is 0 - the tool will always run, no matter how many commits since the last review.
- `minimal_minutes_for_incremental_review`: Minimal number of minutes that need to pass since the last reviewed commit to create incremental review.
If less that the specified number of minutes have passed between the last reviewed commit and running this command, the tool will not perform any action.
Default is 0 - the tool will always run, no matter how much time have passed since the last reviewed commit.
- `require_all_thresholds_for_incremental_review`: If set to true, all the previous thresholds must be met for incremental review to run. If false, only one is enough to run the tool.
For example, if `minimal_commits_for_incremental_review=2` and `minimal_minutes_for_incremental_review=2`, and we have 3 commits since the last review, but the last reviewed commit is from 1 minute ago:
When `require_all_thresholds_for_incremental_review=true` the incremental review __will not__ run, because only 1 out of 2 conditions were met (we have enough commits but the last review is too recent),
but when `require_all_thresholds_for_incremental_review=false` the incremental review __will__ run, because one condition is enough (we have 3 commits which is more than the configured 2).
Default is false - the tool will run as long as at least once conditions is met.
#### PR Reflection
By invoking:
```

View File

@ -46,10 +46,13 @@ class PRAgent:
apply_repo_settings(pr_url)
# Then, apply user specific settings if exists
request = request.replace("'", "\\'")
lexer = shlex.shlex(request, posix=True)
lexer.whitespace_split = True
action, *args = list(lexer)
if isinstance(request, str):
request = request.replace("'", "\\'")
lexer = shlex.shlex(request, posix=True)
lexer.whitespace_split = True
action, *args = list(lexer)
else:
action, *args = request
args = update_settings_from_args(args)
action = action.lstrip("/").lower()

View File

@ -8,6 +8,7 @@ MAX_TOKENS = {
'gpt-4': 8000,
'gpt-4-0613': 8000,
'gpt-4-32k': 32000,
'gpt-4-1106-preview': 128000, # 128K, but may be limited by config.max_model_tokens
'claude-instant-1': 100000,
'claude-2': 100000,
'command-nightly': 4096,

View File

@ -23,7 +23,7 @@ def filter_ignored(files):
# 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)]
files = [f for f in files if (f.filename and not r.match(f.filename))]
except Exception as e:
print(f"Could not filter file list: {e}")

View File

@ -3,6 +3,7 @@ from __future__ import annotations
import re
from pr_agent.config_loader import get_settings
from pr_agent.git_providers.git_provider import EDIT_TYPE
from pr_agent.log import get_logger
@ -115,7 +116,7 @@ def omit_deletion_hunks(patch_lines) -> str:
def handle_patch_deletions(patch: str, original_file_content_str: str,
new_file_content_str: str, file_name: str) -> str:
new_file_content_str: str, file_name: str, edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN) -> str:
"""
Handle entire file or deletion patches.
@ -132,7 +133,7 @@ def handle_patch_deletions(patch: str, original_file_content_str: str,
str: The modified patch with deletion hunks omitted.
"""
if not new_file_content_str:
if not new_file_content_str and edit_type != EDIT_TYPE.ADDED:
# logic for handling deleted files - don't show patch, just show that the file was deleted
if get_settings().config.verbosity_level > 0:
get_logger().info(f"Processing file: {file_name}, minimizing deletion file")

View File

@ -7,18 +7,20 @@ from typing import Any, Callable, List, Tuple
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.algo.utils import get_max_tokens
from pr_agent.config_loader import get_settings
from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider
from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
from pr_agent.log import get_logger
DELETED_FILES_ = "Deleted files:\n"
MORE_MODIFIED_FILES_ = "More modified files:\n"
MORE_MODIFIED_FILES_ = "Additional modified files (insufficient token budget to process):\n"
ADDED_FILES_ = "Additional added files (insufficient token budget to process):\n"
OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1000
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 600
@ -64,14 +66,17 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
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]:
if total_tokens + OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD < get_max_tokens(model):
return "\n".join(patches_extended)
# if we are over the limit, start pruning
patches_compressed, modified_file_names, deleted_file_names = \
patches_compressed, modified_file_names, deleted_file_names, added_file_names = \
pr_generate_compressed_diff(pr_languages, token_handler, model, add_line_numbers_to_hunks)
final_diff = "\n".join(patches_compressed)
if added_file_names:
added_list_str = ADDED_FILES_ + "\n".join(added_file_names)
final_diff = final_diff + "\n\n" + added_list_str
if modified_file_names:
modified_list_str = MORE_MODIFIED_FILES_ + "\n".join(modified_file_names)
final_diff = final_diff + "\n\n" + modified_list_str
@ -122,7 +127,7 @@ def pr_generate_extended_diff(pr_languages: list,
def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, model: str,
convert_hunks_to_line_numbers: bool) -> Tuple[list, list, list]:
convert_hunks_to_line_numbers: bool) -> Tuple[list, list, list, list]:
"""
Generate a compressed diff string for a pull request, using diff minimization techniques to reduce the number of
tokens used.
@ -148,6 +153,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
"""
patches = []
added_files_list = []
modified_files_list = []
deleted_files_list = []
# sort each one of the languages in top_langs by the number of tokens in the diff
@ -165,7 +171,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
# removing delete-only hunks
patch = handle_patch_deletions(patch, original_file_content_str,
new_file_content_str, file.filename)
new_file_content_str, file.filename, file.edit_type)
if patch is None:
if not deleted_files_list:
total_tokens += token_handler.count_tokens(DELETED_FILES_)
@ -179,21 +185,26 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
new_patch_tokens = token_handler.count_tokens(patch)
# Hard Stop, no more tokens
if total_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD:
if total_tokens > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD:
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
if total_tokens + new_patch_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD:
if total_tokens + new_patch_tokens > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD:
# Current logic is to skip the patch if it's too large
# 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:
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)
total_tokens += token_handler.count_tokens(file.filename) + 1
if file.edit_type == EDIT_TYPE.ADDED:
if not added_files_list:
total_tokens += token_handler.count_tokens(ADDED_FILES_)
added_files_list.append(file.filename)
else:
if not modified_files_list:
total_tokens += token_handler.count_tokens(MORE_MODIFIED_FILES_)
modified_files_list.append(file.filename)
total_tokens += token_handler.count_tokens(file.filename) + 1
continue
if patch:
@ -206,7 +217,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Tokens: {total_tokens}, last filename: {file.filename}")
return patches, modified_files_list, deleted_files_list
return patches, modified_files_list, deleted_files_list, added_files_list
async def retry_with_fallback_models(f: Callable):
@ -397,13 +408,13 @@ def get_pr_multi_diffs(git_provider: GitProvider,
continue
# Remove delete-only hunks
patch = handle_patch_deletions(patch, original_file_content_str, new_file_content_str, file.filename)
patch = handle_patch_deletions(patch, original_file_content_str, new_file_content_str, file.filename, file.edit_type)
if patch is None:
continue
patch = convert_to_hunks_with_lines_numbers(patch, file)
new_patch_tokens = token_handler.count_tokens(patch)
if patch and (total_tokens + new_patch_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD):
if patch and (total_tokens + new_patch_tokens > get_max_tokens(model) - OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD):
final_diff = "\n".join(patches)
final_diff_list.append(final_diff)
patches = []

View File

@ -9,6 +9,8 @@ from typing import Any, List
import yaml
from starlette_context import context
from pr_agent.algo import MAX_TOKENS
from pr_agent.config_loader import get_settings, global_settings
from pr_agent.log import get_logger
@ -285,6 +287,7 @@ def _fix_key_value(key: str, value: str):
def load_yaml(review_text: str) -> dict:
test = 1
review_text = review_text.removeprefix('```yaml').rstrip('`')
try:
data = yaml.safe_load(review_text)
@ -295,6 +298,21 @@ def load_yaml(review_text: str) -> dict:
def try_fix_yaml(review_text: str) -> dict:
review_text_lines = review_text.split('\n')
# first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...'
review_text_lines_copy = review_text_lines.copy()
for i in range(0, len(review_text_lines_copy)):
if 'relevant line:' in review_text_lines_copy[i] and not '|-' in review_text_lines_copy[i]:
review_text_lines_copy[i] = review_text_lines_copy[i].replace('relevant line: ',
'relevant line: |-\n ')
try:
data = yaml.load('\n'.join(review_text_lines_copy), Loader=yaml.SafeLoader)
get_logger().info(f"Successfully parsed AI prediction after adding |-\n to relevant line")
return data
except:
get_logger().debug(f"Failed to parse AI prediction after adding |-\n to relevant line")
# second fallback - try to remove last lines
data = {}
for i in range(1, len(review_text_lines)):
review_text_lines_tmp = '\n'.join(review_text_lines[:-i])
@ -324,3 +342,35 @@ def set_custom_labels(variables):
final_labels += f" - {k} ({v['description']})\n"
variables["custom_labels"] = final_labels
variables["custom_labels_examples"] = f" - {list(labels.keys())[0]}"
def get_user_labels(current_labels: List[str] = None):
"""
Only keep labels that has been added by the user
"""
try:
if current_labels is None:
current_labels = []
user_labels = []
for label in current_labels:
if label.lower() in ['bug fix', 'tests', 'refactoring', 'enhancement', 'documentation', 'other']:
continue
if get_settings().config.enable_custom_labels:
if label in get_settings().custom_labels:
continue
user_labels.append(label)
if user_labels:
get_logger().info(f"Keeping user labels: {user_labels}")
except Exception as e:
get_logger().exception(f"Failed to get user labels: {e}")
return current_labels
return user_labels
def get_max_tokens(model):
settings = get_settings()
max_tokens_model = MAX_TOKENS[model]
if settings.config.max_model_tokens:
max_tokens_model = min(settings.config.max_model_tokens, max_tokens_model)
# get_logger().debug(f"limiting max tokens to {max_tokens_model}")
return max_tokens_model

View File

@ -8,6 +8,8 @@ from pr_agent.log import setup_logger
setup_logger()
def run(inargs=None):
parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage=
"""\
@ -51,9 +53,9 @@ For example: 'python cli.py --pr_url=... review --pr_reviewer.extra_instructions
command = args.command.lower()
get_settings().set("CONFIG.CLI_MODE", True)
if args.issue_url:
result = asyncio.run(PRAgent().handle_request(args.issue_url, command + " " + " ".join(args.rest)))
result = asyncio.run(PRAgent().handle_request(args.issue_url, [command] + args.rest))
else:
result = asyncio.run(PRAgent().handle_request(args.pr_url, command + " " + " ".join(args.rest)))
result = asyncio.run(PRAgent().handle_request(args.pr_url, [command] + args.rest))
if not result:
parser.print_help()

View File

@ -9,7 +9,7 @@ from starlette_context import context
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
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
class BitbucketProvider(GitProvider):
@ -32,8 +32,10 @@ class BitbucketProvider(GitProvider):
self.repo = None
self.pr_num = None
self.pr = None
self.pr_url = pr_url
self.temp_comments = []
self.incremental = incremental
self.diff_files = None
if pr_url:
self.set_pr(pr_url)
self.bitbucket_comment_api_url = self.pr._BitbucketBase__data["links"]["comments"]["href"]
@ -44,6 +46,8 @@ class BitbucketProvider(GitProvider):
url = (f"https://api.bitbucket.org/2.0/repositories/{self.workspace_slug}/{self.repo_slug}/src/"
f"{self.pr.destination_branch}/.pr_agent.toml")
response = requests.request("GET", url, headers=self.headers)
if response.status_code == 404: # not found
return ""
contents = response.text.encode('utf-8')
return contents
except Exception:
@ -114,6 +118,9 @@ class BitbucketProvider(GitProvider):
return [diff.new.path for diff in self.pr.diffstat()]
def get_diff_files(self) -> list[FilePatchInfo]:
if self.diff_files:
return self.diff_files
diffs = self.pr.diffstat()
diff_split = [
"diff --git%s" % x for x in self.pr.diff().split("diff --git") if x.strip()
@ -125,16 +132,44 @@ class BitbucketProvider(GitProvider):
diff.old.get_data("links")
)
new_file_content_str = self._get_pr_file_content(diff.new.get_data("links"))
diff_files.append(
FilePatchInfo(
original_file_content_str,
new_file_content_str,
diff_split[index],
diff.new.path,
)
file_patch_canonic_structure = FilePatchInfo(
original_file_content_str,
new_file_content_str,
diff_split[index],
diff.new.path,
)
if diff.data['status'] == 'added':
file_patch_canonic_structure.edit_type = EDIT_TYPE.ADDED
elif diff.data['status'] == 'removed':
file_patch_canonic_structure.edit_type = EDIT_TYPE.DELETED
elif diff.data['status'] == 'modified':
file_patch_canonic_structure.edit_type = EDIT_TYPE.MODIFIED
elif diff.data['status'] == 'renamed':
file_patch_canonic_structure.edit_type = EDIT_TYPE.RENAMED
diff_files.append(file_patch_canonic_structure)
self.diff_files = diff_files
return diff_files
def publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str):
try:
for comment in self.pr.comments():
body = comment.raw
if initial_text in body:
if updated_text:
pr_comment_updated = pr_comment.replace(initial_text, updated_text)
else:
pr_comment_updated = pr_comment
d = {"content": {"raw": pr_comment_updated}}
response = comment._update_data(comment.put(None, data=d))
return
except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}")
pass
self.publish_comment(pr_comment)
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
comment = self.pr.comment(pr_comment)
if is_temporary:
@ -181,6 +216,26 @@ class BitbucketProvider(GitProvider):
)
return response
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 ""
diff_files = self.get_diff_files()
position, absolute_position = find_line_number_of_relevant_line_in_file \
(diff_files, relevant_file, relevant_line_str)
if absolute_position != -1 and self.pr_url:
link = f"{self.pr_url}/#L{relevant_file}T{absolute_position}"
return link
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Failed adding line link, error: {e}")
return ""
def publish_inline_comments(self, comments: list[dict]):
for comment in comments:
self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
@ -260,6 +315,11 @@ class BitbucketProvider(GitProvider):
})
response = requests.request("PUT", self.bitbucket_pull_request_api_url, headers=self.headers, data=payload)
try:
if response.status_code != 200:
get_logger().info(f"Failed to update description, error code: {response.status_code}")
except:
pass
return response
# bitbucket does not support labels

View File

@ -13,6 +13,7 @@ class EDIT_TYPE(Enum):
DELETED = 2
MODIFIED = 3
RENAMED = 4
UNKNOWN = 5
@dataclass
@ -22,7 +23,7 @@ class FilePatchInfo:
patch: str
filename: str
tokens: int = -1
edit_type: EDIT_TYPE = EDIT_TYPE.MODIFIED
edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN
old_filename: str = None
@ -43,6 +44,9 @@ class GitProvider(ABC):
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
pass
def publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str):
self.publish_comment(pr_comment)
@abstractmethod
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass
@ -94,16 +98,16 @@ class GitProvider(ABC):
def get_pr_description(self, *, full: bool = True) -> str:
from pr_agent.config_loader import get_settings
from pr_agent.algo.pr_processing import clip_tokens
max_tokens = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
max_tokens_description = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
description = self.get_pr_description_full() if full else self.get_user_description()
if max_tokens:
return clip_tokens(description, max_tokens)
if max_tokens_description:
return clip_tokens(description, max_tokens_description)
return description
def get_user_description(self) -> str:
description = (self.get_pr_description_full() or "").strip()
# if the existing description wasn't generated by the pr-agent, just return it as-is
if not description.startswith("## PR Type"):
if not any(description.startswith(header) for header in ("## PR Type", "## PR Description")):
return description
# if the existing description was generated by the pr-agent, but it doesn't contain the user description,
# return nothing (empty string) because it means there is no user description
@ -153,6 +157,8 @@ def get_main_pr_language(languages, files) -> str:
# validate that the specific commit uses the main language
extension_list = []
for file in files:
if not file:
continue
if isinstance(file, str):
file = FilePatchInfo(base_file=None, head_file=None, patch=None, filename=file)
extension_list.append(file.filename.rsplit('.')[-1])
@ -190,6 +196,13 @@ class IncrementalPR:
def __init__(self, is_incremental: bool = False):
self.is_incremental = is_incremental
self.commits_range = None
self.first_new_commit_sha = None
self.last_seen_commit_sha = None
self.first_new_commit = None
self.last_seen_commit = None
@property
def first_new_commit_sha(self):
return None if self.first_new_commit is None else self.first_new_commit.sha
@property
def last_seen_commit_sha(self):
return None if self.last_seen_commit is None else self.last_seen_commit.sha

View File

@ -13,7 +13,7 @@ from ..algo.utils import load_large_diff
from ..config_loader import get_settings
from ..log import get_logger
from ..servers.utils import RateLimitExceeded
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR, EDIT_TYPE
class GithubProvider(GitProvider):
@ -66,10 +66,10 @@ class GithubProvider(GitProvider):
first_new_commit_index = None
for index in range(len(self.commits) - 1, -1, -1):
if self.commits[index].commit.author.date > last_review_time:
self.incremental.first_new_commit_sha = self.commits[index].sha
self.incremental.first_new_commit = self.commits[index]
first_new_commit_index = index
else:
self.incremental.last_seen_commit_sha = self.commits[index].sha
self.incremental.last_seen_commit = self.commits[index]
break
return self.commits[first_new_commit_index:] if first_new_commit_index is not None else []
@ -129,7 +129,20 @@ class GithubProvider(GitProvider):
if not patch:
patch = load_large_diff(file.filename, new_file_content_str, original_file_content_str)
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str, patch, file.filename))
if file.status == 'added':
edit_type = EDIT_TYPE.ADDED
elif file.status == 'removed':
edit_type = EDIT_TYPE.DELETED
elif file.status == 'renamed':
edit_type = EDIT_TYPE.RENAMED
elif file.status == 'modified':
edit_type = EDIT_TYPE.MODIFIED
else:
get_logger().error(f"Unknown edit type: {file.status}")
edit_type = EDIT_TYPE.UNKNOWN
file_patch_canonical_structure = FilePatchInfo(original_file_content_str, new_file_content_str, patch,
file.filename, edit_type=edit_type)
diff_files.append(file_patch_canonical_structure)
self.diff_files = diff_files
return diff_files
@ -141,10 +154,24 @@ class GithubProvider(GitProvider):
def publish_description(self, pr_title: str, pr_body: str):
self.pr.edit(title=pr_title, body=pr_body)
def publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str):
prev_comments = list(self.pr.get_issue_comments())
for comment in prev_comments:
body = comment.body
if body.startswith(initial_text):
if updated_text:
pr_comment_updated = pr_comment.replace(initial_text, updated_text)
else:
pr_comment_updated = pr_comment
response = comment.edit(pr_comment_updated)
return
self.publish_comment(pr_comment)
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if is_temporary and not get_settings().config.publish_output_progress:
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"):
self.github_user_id = response.user.login

View File

@ -136,6 +136,21 @@ class GitLabProvider(GitProvider):
except Exception as e:
get_logger().exception(f"Could not update merge request {self.id_mr} description: {e}")
def publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str):
try:
for comment in self.mr.notes.list(get_all=True)[::-1]:
if comment.body.startswith(initial_text):
if updated_text:
pr_comment_updated = pr_comment.replace(initial_text, updated_text)
else:
pr_comment_updated = pr_comment
response = self.mr.notes.update(comment.id, {'body': pr_comment_updated})
return
except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}")
pass
self.publish_comment(pr_comment)
def publish_comment(self, mr_comment: str, is_temporary: bool = False):
comment = self.mr.notes.create({'body': mr_comment})
if is_temporary:

View File

@ -27,7 +27,8 @@ def apply_repo_settings(pr_url):
get_settings().unset(section)
get_settings().set(section, section_dict, merge=False)
get_logger().info(f"Applying repo settings for section {section}, contents: {contents}")
except Exception as e:
get_logger().exception("Failed to apply repo settings", e)
finally:
if repo_settings_file:
try:

View File

@ -122,7 +122,7 @@ async def handle_request(body: Dict[str, Any], event: str):
if body.get("requested_reviewer", {}).get("login", "") != bot_user:
return {}
get_logger().info(f"Performing review for {api_url=} because of {event=} and {action=}")
await _perform_commands(agent, body, api_url, log_context)
await _perform_commands("pr_commands", agent, body, api_url, log_context)
# handle pull_request event with synchronize action - "push trigger" for new commits
elif event == 'pull_request' and action == 'synchronize' and get_settings().github_app.handle_push_trigger:
@ -174,7 +174,7 @@ async def handle_request(body: Dict[str, Any], event: str):
get_logger().info(f"Skipping incremental review because there was no initial review for {api_url=} yet")
return {}
get_logger().info(f"Performing incremental review for {api_url=} because of {event=} and {action=}")
await _perform_commands(agent, body, api_url, log_context)
await _perform_commands("push_commands", agent, body, api_url, log_context)
finally:
# release the waiting task block
@ -203,9 +203,9 @@ def _check_pull_request_event(action: str, body: dict, log_context: dict, bot_us
return pull_request, api_url
async def _perform_commands(agent: PRAgent, body: dict, api_url: str, log_context: dict):
async def _perform_commands(commands_conf: str, agent: PRAgent, body: dict, api_url: str, log_context: dict):
apply_repo_settings(api_url)
commands = get_settings().github_app.pr_commands
commands = get_settings().get(f"github_app.{commands_conf}")
for command in commands:
split_command = command.split(" ")
command = split_command[0]

View File

@ -1,12 +1,15 @@
from fastapi import FastAPI
from mangum import Mangum
from starlette.middleware import Middleware
from starlette_context.middleware import RawContextMiddleware
from pr_agent.log import setup_logger
from pr_agent.servers.github_app import router
setup_logger()
app = FastAPI()
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router)
handler = Mangum(app, lifespan="off")

View File

@ -1,15 +1,16 @@
[config]
model="gpt-4"
model="gpt-4" # "gpt-4-1106-preview"
fallback_models=["gpt-3.5-turbo-16k"]
git_provider="github"
publish_output=true
publish_output_progress=true
verbosity_level=0 # 0,1,2
verbosity_level=2 # 0,1,2
use_extra_bad_extensions=false
use_repo_settings_file=true
ai_timeout=180
max_description_tokens = 500
max_commits_tokens = 500
max_model_tokens = 32000 # Limits the maximum number of tokens that can be used by any model, regardless of the model's default capabilities.
patch_extra_lines = 3
secret_provider="google_cloud_storage"
cli_mode=false
@ -25,7 +26,12 @@ inline_code_comments = false
ask_and_reflect=false
automatic_review=true
remove_previous_review_comment=false
persistent_comment=true
extra_instructions = ""
# specific configurations for incremental review (/review -i)
require_all_thresholds_for_incremental_review=false
minimal_commits_for_incremental_review=0
minimal_minutes_for_incremental_review=0
[pr_description] # /describe #
publish_labels=true
@ -34,6 +40,7 @@ add_original_user_description=false
keep_original_user_title=false
use_bullet_points=true
extra_instructions = ""
enable_pr_type=true
# markers
use_description_markers=false
@ -105,6 +112,9 @@ push_commands = [
--pr_reviewer.num_code_suggestions=0 \
--pr_reviewer.inline_code_comments=false \
--pr_reviewer.remove_previous_review_comment=true \
--pr_reviewer.require_all_thresholds_for_incremental_review=false \
--pr_reviewer.minimal_commits_for_incremental_review=5 \
--pr_reviewer.minimal_minutes_for_incremental_review=30 \
--pr_reviewer.extra_instructions='' \
"""
]

View File

@ -16,7 +16,7 @@ You must use the following YAML schema to format your answer:
PR Type:
type: array
{%- if enable_custom_labels %}
description: One or more labels that describe the PR type. Don't output the description in the parentheses.
description: Labels that are applicable to the Pull Request. Don't output the description in the parentheses. If none of the labels is relevant to the PR, output an empty array.
{%- endif %}
items:
type: string
@ -39,7 +39,6 @@ PR Type:
{{ custom_labels_examples }}
{%- else %}
- Bug fix
- Tests
{%- endif %}
```

View File

@ -1,9 +1,10 @@
[pr_description_prompt]
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
Your task is to provide full description of the PR content.
- Make sure not to focus the new PR code (the '+' lines).
Your task is to provide full description of a Pull Request (PR) content.
- Make sure to focus on the new PR code (the '+' lines).
- Notice that the 'Previous title', 'Previous description' and 'Commit messages' sections may be partial, simplistic, non-informative or not up-to-date. Hence, compare them to the PR diff code, and use them only as a reference.
- If needed, each YAML output should be in block scalar format ('|-')
- Emphasize first the most important changes, and then the less important ones.
- If needed, each YAML output should be in block scalar format ('|-')
{%- if extra_instructions %}
Extra instructions from the user:
@ -18,22 +19,22 @@ PR Title:
type: string
description: an informative title for the PR, describing its main theme
PR Type:
type: array
type: string
enum:
- Bug fix
- Tests
- Refactoring
- Enhancement
- Documentation
- Other
{%- if enable_custom_labels %}
description: One or more labels that describe the PR type. Don't output the description in the parentheses.
{%- endif %}
PR Labels:
type: array
description: Labels that are applicable to the Pull Request. Don't output the description in the parentheses. If none of the labels is relevant to the PR, output an empty array.
items:
type: string
enum:
{%- if enable_custom_labels %}
{{ custom_labels }}
{%- else %}
- Bug fix
- Tests
- Refactoring
- Enhancement
- Documentation
- Other
{%- endif %}
PR Description:
type: string
@ -51,6 +52,7 @@ PR Main Files Walkthrough:
changes in file:
type: string
description: minimal and concise description of the changes in the relevant file
```
Example output:
@ -58,10 +60,11 @@ Example output:
PR Title: |-
...
PR Type:
...
{%- if enable_custom_labels %}
{{ custom_labels_examples }}
{%- else %}
- Bug fix
PR Labels:
- ...
- ...
{%- endif %}
PR Description: |-
...

View File

@ -51,22 +51,13 @@ PR Analysis:
description: summary of the PR in 2-3 sentences.
Type of PR:
type: string
{%- if enable_custom_labels %}
description: One or more labels that describe the PR type. Don't output the description in the parentheses.
{%- endif %}
items:
type: string
enum:
{%- if enable_custom_labels %}
{{ custom_labels }}
{%- else %}
- Bug fix
- Tests
- Refactoring
- Enhancement
- Documentation
- Other
{%- endif %}
{%- if require_score %}
Score:
type: int
@ -161,7 +152,8 @@ PR Analysis:
Focused PR: no, because ...
{%- endif %}
{%- if require_estimate_effort_to_review %}
Estimated effort to review [1-5]: 3, because ...
Estimated effort to review [1-5]: |-
3, because ...
{%- endif %}
PR Feedback:
General PR suggestions: |-

View File

@ -7,7 +7,7 @@ from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import load_yaml, set_custom_labels
from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels
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
@ -98,9 +98,9 @@ class PRDescription:
self.git_provider.publish_description(pr_title, pr_body)
if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"):
current_labels = self.git_provider.get_labels()
if current_labels is None:
current_labels = []
self.git_provider.publish_labels(pr_labels + current_labels)
user_labels = get_user_labels(current_labels)
self.git_provider.publish_labels(pr_labels + user_labels)
self.git_provider.remove_initial_comment()
except Exception as e:
get_logger().error(f"Error generating PR description {self.pr_id}: {e}")
@ -172,12 +172,16 @@ class PRDescription:
pr_types = []
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'PR Type' in self.data:
if 'PR Labels' in self.data:
if type(self.data['PR Labels']) == list:
pr_types = self.data['PR Labels']
elif type(self.data['PR Labels']) == str:
pr_types = self.data['PR Labels'].split(',')
elif 'PR Type' in self.data:
if type(self.data['PR Type']) == list:
pr_types = self.data['PR Type']
elif type(self.data['PR Type']) == str:
pr_types = self.data['PR Type'].split(',')
return pr_types
def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]:
@ -223,6 +227,11 @@ class PRDescription:
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
markdown_text = ""
# Don't display 'PR Labels'
if 'PR Labels' in self.data and self.git_provider.is_supported("get_labels"):
self.data.pop('PR Labels')
if not get_settings().pr_description.enable_pr_type:
self.data.pop('PR Type')
for key, value in self.data.items():
markdown_text += f"## {key}\n\n"
markdown_text += f"{value}\n\n"
@ -248,7 +257,7 @@ class PRDescription:
for file in value:
filename = file['filename'].replace("'", "`")
description = file['changes in file']
pr_body += f'`{filename}`: {description}\n'
pr_body += f'- `{filename}`: {description}\n'
if self.git_provider.is_supported("gfm_markdown"):
pr_body +="</details>\n"
else:

View File

@ -7,7 +7,7 @@ from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import load_yaml, set_custom_labels
from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels
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
@ -82,11 +82,17 @@ class PRGenerateLabels:
if get_settings().config.publish_output:
get_logger().info(f"Pushing labels {self.pr_id}")
current_labels = self.git_provider.get_labels()
user_labels = get_user_labels(current_labels)
pr_labels = pr_labels + user_labels
if self.git_provider.is_supported("get_labels"):
current_labels = self.git_provider.get_labels()
if current_labels is None:
current_labels = []
self.git_provider.publish_labels(pr_labels + current_labels)
self.git_provider.publish_labels(pr_labels)
elif pr_labels:
value = ', '.join(v for v in pr_labels)
pr_labels_text = f"## PR Labels:\n{value}\n"
self.git_provider.publish_comment(pr_labels_text, is_temporary=False)
self.git_provider.remove_initial_comment()
except Exception as e:
get_logger().error(f"Error generating PR labels {self.pr_id}: {e}")

View File

@ -1,4 +1,5 @@
import copy
import datetime
from collections import OrderedDict
from typing import List, Tuple
@ -100,8 +101,7 @@ class PRReviewer:
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
get_logger().info(f'Automatic review is disabled {self.pr_url}')
return None
if self.is_auto and self.incremental.is_incremental and not self.incremental.first_new_commit_sha:
get_logger().info(f"Incremental review is enabled for {self.pr_url} but there are no new commits")
if self.incremental.is_incremental and not self._can_run_incremental_review():
return None
get_logger().info(f'Reviewing PR: {self.pr_url} ...')
@ -117,7 +117,15 @@ class PRReviewer:
if get_settings().config.publish_output:
get_logger().info('Pushing PR review...')
previous_review_comment = self._get_previous_review_comment()
self.git_provider.publish_comment(pr_comment)
# publish the review
if get_settings().pr_reviewer.persistent_comment and not self.incremental.is_incremental:
self.git_provider.publish_persistent_comment(pr_comment,
initial_text="## PR Analysis",
updated_text="## PR Analysis (updated)")
else:
self.git_provider.publish_comment(pr_comment)
self.git_provider.remove_initial_comment()
if previous_review_comment:
self._remove_previous_review_comment(previous_review_comment)
@ -156,7 +164,6 @@ class PRReviewer:
variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined)
set_custom_labels(variables)
system_prompt = environment.from_string(get_settings().pr_review_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_review_prompt.user).render(variables)
@ -217,19 +224,6 @@ class PRReviewer:
suggestion['relevant line'] = f"[{suggestion['relevant line']}]({link})"
else:
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
@ -347,3 +341,34 @@ class PRReviewer:
self.git_provider.remove_comment(comment)
except Exception as e:
get_logger().exception(f"Failed to remove previous review comment, error: {e}")
def _can_run_incremental_review(self) -> bool:
"""Checks if we can run incremental review according the various configurations and previous review"""
# checking if running is auto mode but there are no new commits
if self.is_auto and not self.incremental.first_new_commit_sha:
get_logger().info(f"Incremental review is enabled for {self.pr_url} but there are no new commits")
return False
# checking if there are enough commits to start the review
num_new_commits = len(self.incremental.commits_range)
num_commits_threshold = get_settings().pr_reviewer.minimal_commits_for_incremental_review
not_enough_commits = num_new_commits < num_commits_threshold
# checking if the commits are not too recent to start the review
recent_commits_threshold = datetime.datetime.now() - datetime.timedelta(
minutes=get_settings().pr_reviewer.minimal_minutes_for_incremental_review
)
last_seen_commit_date = (
self.incremental.last_seen_commit.commit.author.date if self.incremental.last_seen_commit else None
)
all_commits_too_recent = (
last_seen_commit_date > recent_commits_threshold if self.incremental.last_seen_commit else False
)
# check all the thresholds or just one to start the review
condition = any if get_settings().pr_reviewer.require_all_thresholds_for_incremental_review else all
if condition((not_enough_commits, all_commits_too_recent)):
get_logger().info(
f"Incremental review is enabled for {self.pr_url} but didn't pass the threshold check to run:"
f"\n* Number of new commits = {num_new_commits} (threshold is {num_commits_threshold})"
f"\n* Last seen commit date = {last_seen_commit_date} (threshold is {recent_commits_threshold})"
)
return False
return True

View File

@ -8,8 +8,8 @@ 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.algo.utils import get_max_tokens
from pr_agent.config_loader import get_settings
from pr_agent.git_providers import get_git_provider
from pr_agent.log import get_logger
@ -197,7 +197,7 @@ class PRSimilarIssue:
username = issue.user.login
created_at = str(issue.created_at)
if len(issue_str) < 8000 or \
self.token_handler.count_tokens(issue_str) < MAX_TOKENS[MODEL]: # fast reject first
self.token_handler.count_tokens(issue_str) < get_max_tokens(MODEL): # fast reject first
issue_record = Record(
id=issue_key + "." + "issue",
text=issue_str,

View File

@ -0,0 +1,31 @@
# Generated by CodiumAI
from pr_agent.algo.utils import try_fix_yaml
import pytest
class TestTryFixYaml:
# The function successfully parses a valid YAML string.
def test_valid_yaml(self):
review_text = "key: value\n"
expected_output = {"key": "value"}
assert try_fix_yaml(review_text) == expected_output
# The function adds '|-' to 'relevant line:' if it is not already present and successfully parses the YAML string.
def test_add_relevant_line(self):
review_text = "relevant line: value: 3\n"
expected_output = {"relevant line": "value: 3"}
assert try_fix_yaml(review_text) == expected_output
# The function removes the last line(s) of the YAML string and successfully parses the YAML string.
def test_remove_last_line(self):
review_text = "key: value\nextra invalid line\n"
expected_output = {"key": "value"}
assert try_fix_yaml(review_text) == expected_output
# The YAML string is empty.
def test_empty_yaml_fixed(self):
review_text = ""
assert try_fix_yaml(review_text) is None