mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-07 22:30:38 +08:00
Compare commits
55 Commits
hl/keep_on
...
v0.10
Author | SHA1 | Date | |
---|---|---|---|
416a5495da | |||
a2b27dcac8 | |||
d8e4e2e8fd | |||
896a81d173 | |||
b216af8f04 | |||
388cc740b6 | |||
6214494c84 | |||
762a6981e1 | |||
b362c406bc | |||
7a342d3312 | |||
2e95988741 | |||
9478447141 | |||
082293b48c | |||
e1d92206f3 | |||
557ec72bfe | |||
6b4b16dcf9 | |||
c4899a6c54 | |||
24d82e65cb | |||
2567a6cf27 | |||
94cb6b9795 | |||
e878bbbe36 | |||
7d89b82967 | |||
c5f9bbbf92 | |||
a5e5a82952 | |||
ccbb62b50a | |||
1df36c6a44 | |||
9e5e9afe92 | |||
5e43c202dd | |||
37e6608e68 | |||
f64d5f1e2a | |||
8fdf174dec | |||
29d4f98b19 | |||
737792d83c | |||
7e5889061c | |||
755e04cf65 | |||
44d6c95714 | |||
14610d5375 | |||
f9c832d6cb | |||
c2bec614e5 | |||
49725e92f2 | |||
a1e32d8331 | |||
0293412a42 | |||
10ec0a1812 | |||
69b68b78f5 | |||
c5bc4b44ff | |||
39e5102a2e | |||
6c82bc9a3e | |||
54f41dd603 | |||
094f641fb5 | |||
a35a75eb34 | |||
5a7c118b56 | |||
cf9e0fbbc5 | |||
ef9af261ed | |||
ff79776410 | |||
ec3f2fb485 |
1
.github/workflows/pr-agent-review.yaml
vendored
1
.github/workflows/pr-agent-review.yaml
vendored
@ -26,5 +26,6 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PINECONE.API_KEY: ${{ secrets.PINECONE_API_KEY }}
|
PINECONE.API_KEY: ${{ secrets.PINECONE_API_KEY }}
|
||||||
PINECONE.ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }}
|
PINECONE.ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }}
|
||||||
|
GITHUB_ACTION.AUTO_REVIEW: true
|
||||||
|
|
||||||
|
|
||||||
|
2
.pr_agent.toml
Normal file
2
.pr_agent.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[pr_reviewer]
|
||||||
|
enable_review_labels_effort = true
|
@ -410,9 +410,9 @@ BITBUCKET_BEARER_TOKEN: <your token>
|
|||||||
You can get a Bitbucket token for your repository by following Repository Settings -> Security -> Access Tokens.
|
You can get a Bitbucket token for your repository by following Repository Settings -> Security -> Access Tokens.
|
||||||
|
|
||||||
|
|
||||||
### Run on a hosted Bitbucket app
|
### Run using CodiumAI-hosted Bitbucket app
|
||||||
|
|
||||||
Please contact <support@codium.ai> if you're interested in a hosted BitBucket app solution that provides full functionality including PR reviews and comment handling. It's based on the [bitbucket_app.py](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/git_providers/bitbucket_provider.py) implmentation.
|
Please contact <support@codium.ai> or visit [CodiumAI pricing page](https://www.codium.ai/pricing/) if you're interested in a hosted BitBucket app solution that provides full functionality including PR reviews and comment handling. It's based on the [bitbucket_app.py](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/git_providers/bitbucket_provider.py) implementation.
|
||||||
|
|
||||||
|
|
||||||
=======
|
=======
|
||||||
|
@ -1,3 +1,25 @@
|
|||||||
|
## [Version 0.10] - 2023-11-15
|
||||||
|
- codiumai/pr-agent:0.10
|
||||||
|
- codiumai/pr-agent:0.10-github_app
|
||||||
|
- codiumai/pr-agent:0.10-bitbucket-app
|
||||||
|
- codiumai/pr-agent:0.10-gitlab_webhook
|
||||||
|
- codiumai/pr-agent:0.10-github_polling
|
||||||
|
- codiumai/pr-agent:0.10-github_action
|
||||||
|
|
||||||
|
### Added::Algo
|
||||||
|
- Review tool now works with [persistent comments](https://github.com/Codium-ai/pr-agent/pull/451) by default
|
||||||
|
- Bitbucket now publishes review suggestions with [code links](https://github.com/Codium-ai/pr-agent/pull/428)
|
||||||
|
- Enabling to limit [max number of tokens](https://github.com/Codium-ai/pr-agent/pull/437/files)
|
||||||
|
- Support ['gpt-4-1106-preview'](https://github.com/Codium-ai/pr-agent/pull/437/files) model
|
||||||
|
- Support for Google's [Vertex AI](https://github.com/Codium-ai/pr-agent/pull/436)
|
||||||
|
- Implementing [thresholds](https://github.com/Codium-ai/pr-agent/pull/423) for incremental PR reviews
|
||||||
|
- Decoupled custom labels from [PR type](https://github.com/Codium-ai/pr-agent/pull/431)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Fixed bug in [parsing quotes](https://github.com/Codium-ai/pr-agent/pull/446) in CLI
|
||||||
|
- Preserve [user-added labels](https://github.com/Codium-ai/pr-agent/pull/433) in pull requests
|
||||||
|
- Bug fixes in GitLab and BitBucket
|
||||||
|
|
||||||
## [Version 0.9] - 2023-10-29
|
## [Version 0.9] - 2023-10-29
|
||||||
- codiumai/pr-agent:0.9
|
- codiumai/pr-agent:0.9
|
||||||
- codiumai/pr-agent:0.9-github_app
|
- codiumai/pr-agent:0.9-github_app
|
||||||
|
19
Usage.md
19
Usage.md
@ -173,7 +173,7 @@ push_commands = [
|
|||||||
"/auto_review -i --pr_reviewer.remove_previous_review_comment=true",
|
"/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 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.
|
For the `auto_review` tool, it will run in incremental mode, and the `remove_previous_review_comment` parameter will be set to true.
|
||||||
|
|
||||||
@ -303,6 +303,23 @@ key = ...
|
|||||||
|
|
||||||
Also review the [AiHandler](pr_agent/algo/ai_handler.py) file for instruction how to set keys for other models.
|
Also review the [AiHandler](pr_agent/algo/ai_handler.py) file for instruction how to set keys for other models.
|
||||||
|
|
||||||
|
#### Vertex AI
|
||||||
|
|
||||||
|
To use Google's Vertex AI platform and its associated models (chat-bison/codechat-bison) set:
|
||||||
|
|
||||||
|
```
|
||||||
|
[config] # in configuration.toml
|
||||||
|
model = "vertex_ai/codechat-bison"
|
||||||
|
|
||||||
|
[vertexai] # in .secrets.toml
|
||||||
|
vertex_project = "my-google-cloud-project"
|
||||||
|
vertex_location = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
Your [application default credentials](https://cloud.google.com/docs/authentication/application-default-credentials) will be used for authentication so there is no need to set explicit credentials in most environments.
|
||||||
|
|
||||||
|
If you do want to set explicit credentials then you can use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable set to a path to a json credentials file.
|
||||||
|
|
||||||
### Working with large PRs
|
### Working with large PRs
|
||||||
|
|
||||||
The default mode of CodiumAI is to have a single call per tool, using GPT-4, which has a token limit of 8000 tokens.
|
The default mode of CodiumAI is to have a single call per tool, using GPT-4, which has a token limit of 8000 tokens.
|
||||||
|
@ -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 ...".
|
- `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)
|
- 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 template
|
||||||
|
|
||||||
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
|
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
|
||||||
|
@ -16,15 +16,22 @@ The `review` tool can also be triggered automatically every time a new PR is ope
|
|||||||
|
|
||||||
Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review' tool:
|
Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review' tool:
|
||||||
|
|
||||||
|
#### enable\\disable features
|
||||||
- `require_focused_review`: if set to true, the tool will add a section - 'is the PR a focused one'. Default is false.
|
- `require_focused_review`: if set to true, the tool will add a section - 'is the PR a focused one'. Default is false.
|
||||||
- `require_score_review`: if set to true, the tool will add a section that scores the PR. Default is false.
|
- `require_score_review`: if set to true, the tool will add a section that scores the PR. Default is false.
|
||||||
- `require_tests_review`: if set to true, the tool will add a section that checks if the PR contains tests. Default is true.
|
- `require_tests_review`: if set to true, the tool will add a section that checks if the PR contains tests. Default is true.
|
||||||
- `require_security_review`: if set to true, the tool will add a section that checks if the PR contains security issues. Default is true.
|
- `require_security_review`: if set to true, the tool will add a section that checks if the PR contains security issues. Default is true.
|
||||||
- `require_estimate_effort_to_review`: if set to true, the tool will add a section that estimates thed effort needed to review the PR. Default is true.
|
- `require_estimate_effort_to_review`: if set to true, the tool will add a section that estimates thed effort needed to review the PR. Default is true.
|
||||||
|
#### general options
|
||||||
- `num_code_suggestions`: number of code suggestions provided by the 'review' tool. Default is 4.
|
- `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.
|
- `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.
|
- `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, meaning that every new review request will edit the previous one. Default is true.
|
||||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
||||||
|
#### review labels
|
||||||
|
- `enable_review_labels_security`: if set to true, the tool will publish a 'possible security issue' label if it detects a security issue. Default is true.
|
||||||
|
- `enable_review_labels_effort`: if set to true, the tool will publish a 'Review effort [1-5]: x' label. Default is false.
|
||||||
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
|
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
|
||||||
#### Incremental Mode
|
#### 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:
|
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:
|
||||||
|
@ -46,10 +46,13 @@ class PRAgent:
|
|||||||
apply_repo_settings(pr_url)
|
apply_repo_settings(pr_url)
|
||||||
|
|
||||||
# Then, apply user specific settings if exists
|
# Then, apply user specific settings if exists
|
||||||
request = request.replace("'", "\\'")
|
if isinstance(request, str):
|
||||||
lexer = shlex.shlex(request, posix=True)
|
request = request.replace("'", "\\'")
|
||||||
lexer.whitespace_split = True
|
lexer = shlex.shlex(request, posix=True)
|
||||||
action, *args = list(lexer)
|
lexer.whitespace_split = True
|
||||||
|
action, *args = list(lexer)
|
||||||
|
else:
|
||||||
|
action, *args = request
|
||||||
args = update_settings_from_args(args)
|
args = update_settings_from_args(args)
|
||||||
|
|
||||||
action = action.lstrip("/").lower()
|
action = action.lstrip("/").lower()
|
||||||
|
@ -8,9 +8,14 @@ MAX_TOKENS = {
|
|||||||
'gpt-4': 8000,
|
'gpt-4': 8000,
|
||||||
'gpt-4-0613': 8000,
|
'gpt-4-0613': 8000,
|
||||||
'gpt-4-32k': 32000,
|
'gpt-4-32k': 32000,
|
||||||
|
'gpt-4-1106-preview': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||||
'claude-instant-1': 100000,
|
'claude-instant-1': 100000,
|
||||||
'claude-2': 100000,
|
'claude-2': 100000,
|
||||||
'command-nightly': 4096,
|
'command-nightly': 4096,
|
||||||
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
|
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
|
||||||
'meta-llama/Llama-2-7b-chat-hf': 4096
|
'meta-llama/Llama-2-7b-chat-hf': 4096,
|
||||||
|
'vertex_ai/codechat-bison': 6144,
|
||||||
|
'vertex_ai/codechat-bison-32k': 32000,
|
||||||
|
'codechat-bison': 6144,
|
||||||
|
'codechat-bison-32k': 32000,
|
||||||
}
|
}
|
||||||
|
@ -23,39 +23,43 @@ class AiHandler:
|
|||||||
Initializes the OpenAI API key and other settings from a configuration file.
|
Initializes the OpenAI API key and other settings from a configuration file.
|
||||||
Raises a ValueError if the OpenAI key is missing.
|
Raises a ValueError if the OpenAI key is missing.
|
||||||
"""
|
"""
|
||||||
try:
|
self.azure = False
|
||||||
|
|
||||||
|
if get_settings().get("OPENAI.KEY", None):
|
||||||
openai.api_key = get_settings().openai.key
|
openai.api_key = get_settings().openai.key
|
||||||
litellm.openai_key = get_settings().openai.key
|
litellm.openai_key = get_settings().openai.key
|
||||||
if get_settings().get("litellm.use_client"):
|
if get_settings().get("litellm.use_client"):
|
||||||
litellm_token = get_settings().get("litellm.LITELLM_TOKEN")
|
litellm_token = get_settings().get("litellm.LITELLM_TOKEN")
|
||||||
assert litellm_token, "LITELLM_TOKEN is required"
|
assert litellm_token, "LITELLM_TOKEN is required"
|
||||||
os.environ["LITELLM_TOKEN"] = litellm_token
|
os.environ["LITELLM_TOKEN"] = litellm_token
|
||||||
litellm.use_client = True
|
litellm.use_client = True
|
||||||
self.azure = False
|
if get_settings().get("OPENAI.ORG", None):
|
||||||
if get_settings().get("OPENAI.ORG", None):
|
litellm.organization = get_settings().openai.org
|
||||||
litellm.organization = get_settings().openai.org
|
if get_settings().get("OPENAI.API_TYPE", None):
|
||||||
if get_settings().get("OPENAI.API_TYPE", None):
|
if get_settings().openai.api_type == "azure":
|
||||||
if get_settings().openai.api_type == "azure":
|
self.azure = True
|
||||||
self.azure = True
|
litellm.azure_key = get_settings().openai.key
|
||||||
litellm.azure_key = get_settings().openai.key
|
if get_settings().get("OPENAI.API_VERSION", None):
|
||||||
if get_settings().get("OPENAI.API_VERSION", None):
|
litellm.api_version = get_settings().openai.api_version
|
||||||
litellm.api_version = get_settings().openai.api_version
|
if get_settings().get("OPENAI.API_BASE", None):
|
||||||
if get_settings().get("OPENAI.API_BASE", None):
|
litellm.api_base = get_settings().openai.api_base
|
||||||
litellm.api_base = get_settings().openai.api_base
|
if get_settings().get("ANTHROPIC.KEY", None):
|
||||||
if get_settings().get("ANTHROPIC.KEY", None):
|
litellm.anthropic_key = get_settings().anthropic.key
|
||||||
litellm.anthropic_key = get_settings().anthropic.key
|
if get_settings().get("COHERE.KEY", None):
|
||||||
if get_settings().get("COHERE.KEY", None):
|
litellm.cohere_key = get_settings().cohere.key
|
||||||
litellm.cohere_key = get_settings().cohere.key
|
if get_settings().get("REPLICATE.KEY", None):
|
||||||
if get_settings().get("REPLICATE.KEY", None):
|
litellm.replicate_key = get_settings().replicate.key
|
||||||
litellm.replicate_key = get_settings().replicate.key
|
if get_settings().get("REPLICATE.KEY", None):
|
||||||
if get_settings().get("REPLICATE.KEY", None):
|
litellm.replicate_key = get_settings().replicate.key
|
||||||
litellm.replicate_key = get_settings().replicate.key
|
if get_settings().get("HUGGINGFACE.KEY", None):
|
||||||
if get_settings().get("HUGGINGFACE.KEY", None):
|
litellm.huggingface_key = get_settings().huggingface.key
|
||||||
litellm.huggingface_key = get_settings().huggingface.key
|
if get_settings().get("HUGGINGFACE.API_BASE", None):
|
||||||
if get_settings().get("HUGGINGFACE.API_BASE", None):
|
litellm.api_base = get_settings().huggingface.api_base
|
||||||
litellm.api_base = get_settings().huggingface.api_base
|
if get_settings().get("VERTEXAI.VERTEX_PROJECT", None):
|
||||||
except AttributeError as e:
|
litellm.vertex_project = get_settings().vertexai.vertex_project
|
||||||
raise ValueError("OpenAI key is required") from e
|
litellm.vertex_location = get_settings().get(
|
||||||
|
"VERTEXAI.VERTEX_LOCATION", None
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def deployment_id(self):
|
def deployment_id(self):
|
||||||
|
@ -23,7 +23,7 @@ def filter_ignored(files):
|
|||||||
|
|
||||||
# keep filenames that _don't_ match the ignore regex
|
# keep filenames that _don't_ match the ignore regex
|
||||||
for r in compiled_patterns:
|
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:
|
except Exception as e:
|
||||||
print(f"Could not filter file list: {e}")
|
print(f"Could not filter file list: {e}")
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
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
|
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,
|
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.
|
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.
|
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
|
# logic for handling deleted files - don't show patch, just show that the file was deleted
|
||||||
if get_settings().config.verbosity_level > 0:
|
if get_settings().config.verbosity_level > 0:
|
||||||
get_logger().info(f"Processing file: {file_name}, minimizing deletion file")
|
get_logger().info(f"Processing file: {file_name}, minimizing deletion file")
|
||||||
|
@ -7,18 +7,20 @@ from typing import Any, Callable, List, Tuple
|
|||||||
|
|
||||||
from github import RateLimitExceededException
|
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.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.language_handler import sort_files_by_main_languages
|
||||||
from pr_agent.algo.file_filter import filter_ignored
|
from pr_agent.algo.file_filter import filter_ignored
|
||||||
from pr_agent.algo.token_handler import TokenHandler, get_token_encoder
|
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.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
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
DELETED_FILES_ = "Deleted files:\n"
|
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_SOFT_THRESHOLD = 1000
|
||||||
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 600
|
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)
|
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 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)
|
return "\n".join(patches_extended)
|
||||||
|
|
||||||
# if we are over the limit, start pruning
|
# 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)
|
pr_generate_compressed_diff(pr_languages, token_handler, model, add_line_numbers_to_hunks)
|
||||||
|
|
||||||
final_diff = "\n".join(patches_compressed)
|
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:
|
if modified_file_names:
|
||||||
modified_list_str = MORE_MODIFIED_FILES_ + "\n".join(modified_file_names)
|
modified_list_str = MORE_MODIFIED_FILES_ + "\n".join(modified_file_names)
|
||||||
final_diff = final_diff + "\n\n" + modified_list_str
|
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,
|
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
|
Generate a compressed diff string for a pull request, using diff minimization techniques to reduce the number of
|
||||||
tokens used.
|
tokens used.
|
||||||
@ -148,6 +153,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
patches = []
|
patches = []
|
||||||
|
added_files_list = []
|
||||||
modified_files_list = []
|
modified_files_list = []
|
||||||
deleted_files_list = []
|
deleted_files_list = []
|
||||||
# sort each one of the languages in top_langs by the number of tokens in the diff
|
# 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
|
# removing delete-only hunks
|
||||||
patch = handle_patch_deletions(patch, original_file_content_str,
|
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 patch is None:
|
||||||
if not deleted_files_list:
|
if not deleted_files_list:
|
||||||
total_tokens += token_handler.count_tokens(DELETED_FILES_)
|
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)
|
new_patch_tokens = token_handler.count_tokens(patch)
|
||||||
|
|
||||||
# Hard Stop, no more tokens
|
# 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}.")
|
get_logger().warning(f"File was fully skipped, no more tokens: {file.filename}.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If the patch is too large, just show the file name
|
# 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
|
# 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
|
# TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens
|
||||||
# until we meet the requirements
|
# until we meet the requirements
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().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:
|
if file.edit_type == EDIT_TYPE.ADDED:
|
||||||
total_tokens += token_handler.count_tokens(MORE_MODIFIED_FILES_)
|
if not added_files_list:
|
||||||
modified_files_list.append(file.filename)
|
total_tokens += token_handler.count_tokens(ADDED_FILES_)
|
||||||
total_tokens += token_handler.count_tokens(file.filename) + 1
|
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
|
continue
|
||||||
|
|
||||||
if patch:
|
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:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().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
|
return patches, modified_files_list, deleted_files_list, added_files_list
|
||||||
|
|
||||||
|
|
||||||
async def retry_with_fallback_models(f: Callable):
|
async def retry_with_fallback_models(f: Callable):
|
||||||
@ -271,7 +282,7 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
|
|||||||
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
||||||
|
|
||||||
for file in diff_files:
|
for file in diff_files:
|
||||||
if file.filename.strip() == relevant_file:
|
if file.filename and (file.filename.strip() == relevant_file):
|
||||||
patch = file.patch
|
patch = file.patch
|
||||||
patch_lines = patch.splitlines()
|
patch_lines = patch.splitlines()
|
||||||
|
|
||||||
@ -397,13 +408,13 @@ def get_pr_multi_diffs(git_provider: GitProvider,
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Remove delete-only hunks
|
# 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:
|
if patch is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
patch = convert_to_hunks_with_lines_numbers(patch, file)
|
patch = convert_to_hunks_with_lines_numbers(patch, file)
|
||||||
new_patch_tokens = token_handler.count_tokens(patch)
|
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 = "\n".join(patches)
|
||||||
final_diff_list.append(final_diff)
|
final_diff_list.append(final_diff)
|
||||||
patches = []
|
patches = []
|
||||||
|
@ -9,6 +9,8 @@ from typing import Any, List
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from starlette_context import context
|
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.config_loader import get_settings, global_settings
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
@ -295,6 +297,21 @@ def load_yaml(review_text: str) -> dict:
|
|||||||
|
|
||||||
def try_fix_yaml(review_text: str) -> dict:
|
def try_fix_yaml(review_text: str) -> dict:
|
||||||
review_text_lines = review_text.split('\n')
|
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 = {}
|
data = {}
|
||||||
for i in range(1, len(review_text_lines)):
|
for i in range(1, len(review_text_lines)):
|
||||||
review_text_lines_tmp = '\n'.join(review_text_lines[:-i])
|
review_text_lines_tmp = '\n'.join(review_text_lines[:-i])
|
||||||
@ -326,18 +343,33 @@ def set_custom_labels(variables):
|
|||||||
variables["custom_labels_examples"] = f" - {list(labels.keys())[0]}"
|
variables["custom_labels_examples"] = f" - {list(labels.keys())[0]}"
|
||||||
|
|
||||||
|
|
||||||
def get_user_labels(current_labels):
|
def get_user_labels(current_labels: List[str] = None):
|
||||||
## Only keep labels that has been added by the user
|
"""
|
||||||
if current_labels is None:
|
Only keep labels that has been added by the user
|
||||||
current_labels = []
|
"""
|
||||||
user_labels = []
|
try:
|
||||||
for label in current_labels:
|
if current_labels is None:
|
||||||
if label in ['Bug fix', 'Tests', 'Refactoring', 'Enhancement', 'Documentation', 'Other']:
|
current_labels = []
|
||||||
continue
|
user_labels = []
|
||||||
if get_settings().config.enable_custom_labels:
|
for label in current_labels:
|
||||||
if label in get_settings().custom_labels:
|
if label.lower() in ['bug fix', 'tests', 'refactoring', 'enhancement', 'documentation', 'other']:
|
||||||
continue
|
continue
|
||||||
user_labels.append(label)
|
if get_settings().config.enable_custom_labels:
|
||||||
if user_labels:
|
if label in get_settings().custom_labels:
|
||||||
get_logger().info(f"Keeping user labels: {user_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
|
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
|
||||||
|
@ -8,6 +8,8 @@ from pr_agent.log import setup_logger
|
|||||||
|
|
||||||
setup_logger()
|
setup_logger()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def run(inargs=None):
|
def run(inargs=None):
|
||||||
parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage=
|
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()
|
command = args.command.lower()
|
||||||
get_settings().set("CONFIG.CLI_MODE", True)
|
get_settings().set("CONFIG.CLI_MODE", True)
|
||||||
if args.issue_url:
|
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:
|
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:
|
if not result:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from starlette_context import context
|
|||||||
from ..algo.pr_processing import 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 ..config_loader import get_settings
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .git_provider import FilePatchInfo, GitProvider
|
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
|
||||||
|
|
||||||
|
|
||||||
class BitbucketProvider(GitProvider):
|
class BitbucketProvider(GitProvider):
|
||||||
@ -132,17 +132,56 @@ class BitbucketProvider(GitProvider):
|
|||||||
diff.old.get_data("links")
|
diff.old.get_data("links")
|
||||||
)
|
)
|
||||||
new_file_content_str = self._get_pr_file_content(diff.new.get_data("links"))
|
new_file_content_str = self._get_pr_file_content(diff.new.get_data("links"))
|
||||||
diff_files.append(
|
file_patch_canonic_structure = FilePatchInfo(
|
||||||
FilePatchInfo(
|
original_file_content_str,
|
||||||
original_file_content_str,
|
new_file_content_str,
|
||||||
new_file_content_str,
|
diff_split[index],
|
||||||
diff_split[index],
|
diff.new.path,
|
||||||
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
|
self.diff_files = diff_files
|
||||||
return diff_files
|
return diff_files
|
||||||
|
|
||||||
|
def get_latest_commit_url(self):
|
||||||
|
return self.pr.data['source']['commit']['links']['html']['href']
|
||||||
|
|
||||||
|
def get_comment_url(self, comment):
|
||||||
|
return comment.data['links']['html']['href']
|
||||||
|
|
||||||
|
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True):
|
||||||
|
try:
|
||||||
|
for comment in self.pr.comments():
|
||||||
|
body = comment.raw
|
||||||
|
if initial_header in body:
|
||||||
|
latest_commit_url = self.get_latest_commit_url()
|
||||||
|
comment_url = self.get_comment_url(comment)
|
||||||
|
if update_header:
|
||||||
|
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n"
|
||||||
|
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
|
||||||
|
else:
|
||||||
|
pr_comment_updated = pr_comment
|
||||||
|
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message")
|
||||||
|
d = {"content": {"raw": pr_comment_updated}}
|
||||||
|
response = comment._update_data(comment.put(None, data=d))
|
||||||
|
self.publish_comment(
|
||||||
|
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}")
|
||||||
|
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):
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||||
comment = self.pr.comment(pr_comment)
|
comment = self.pr.comment(pr_comment)
|
||||||
if is_temporary:
|
if is_temporary:
|
||||||
@ -288,6 +327,11 @@ class BitbucketProvider(GitProvider):
|
|||||||
})
|
})
|
||||||
|
|
||||||
response = requests.request("PUT", self.bitbucket_pull_request_api_url, headers=self.headers, data=payload)
|
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
|
return response
|
||||||
|
|
||||||
# bitbucket does not support labels
|
# bitbucket does not support labels
|
||||||
|
@ -13,6 +13,7 @@ class EDIT_TYPE(Enum):
|
|||||||
DELETED = 2
|
DELETED = 2
|
||||||
MODIFIED = 3
|
MODIFIED = 3
|
||||||
RENAMED = 4
|
RENAMED = 4
|
||||||
|
UNKNOWN = 5
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -22,7 +23,7 @@ class FilePatchInfo:
|
|||||||
patch: str
|
patch: str
|
||||||
filename: str
|
filename: str
|
||||||
tokens: int = -1
|
tokens: int = -1
|
||||||
edit_type: EDIT_TYPE = EDIT_TYPE.MODIFIED
|
edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN
|
||||||
old_filename: str = None
|
old_filename: str = None
|
||||||
|
|
||||||
|
|
||||||
@ -39,42 +40,10 @@ class GitProvider(ABC):
|
|||||||
def publish_description(self, pr_title: str, pr_body: str):
|
def publish_description(self, pr_title: str, pr_body: str):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def publish_inline_comments(self, comments: list[dict]):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def publish_code_suggestions(self, code_suggestions: list) -> bool:
|
def publish_code_suggestions(self, code_suggestions: list) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def publish_labels(self, labels):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_labels(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def remove_initial_comment(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def remove_comment(self, comment):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_languages(self):
|
def get_languages(self):
|
||||||
pass
|
pass
|
||||||
@ -94,16 +63,16 @@ class GitProvider(ABC):
|
|||||||
def get_pr_description(self, *, full: bool = True) -> str:
|
def get_pr_description(self, *, full: bool = True) -> str:
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.algo.pr_processing import clip_tokens
|
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()
|
description = self.get_pr_description_full() if full else self.get_user_description()
|
||||||
if max_tokens:
|
if max_tokens_description:
|
||||||
return clip_tokens(description, max_tokens)
|
return clip_tokens(description, max_tokens_description)
|
||||||
return description
|
return description
|
||||||
|
|
||||||
def get_user_description(self) -> str:
|
def get_user_description(self) -> str:
|
||||||
description = (self.get_pr_description_full() or "").strip()
|
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 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
|
return description
|
||||||
# if the existing description was generated by the pr-agent, but it doesn't contain the user 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
|
# return nothing (empty string) because it means there is no user description
|
||||||
@ -113,11 +82,54 @@ class GitProvider(ABC):
|
|||||||
return description.split("## User Description:", 1)[1].strip()
|
return description.split("## User Description:", 1)[1].strip()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_issue_comments(self):
|
def get_repo_settings(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_pr_id(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
#### comments operations ####
|
||||||
|
@abstractmethod
|
||||||
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool):
|
||||||
|
self.publish_comment(pr_comment)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_repo_settings(self):
|
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def publish_inline_comments(self, comments: list[dict]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_initial_comment(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_comment(self, comment):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_issue_comments(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_comment_url(self, comment) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
#### labels operations ####
|
||||||
|
@abstractmethod
|
||||||
|
def publish_labels(self, labels):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_labels(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -128,11 +140,12 @@ class GitProvider(ABC):
|
|||||||
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
|
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
#### commits operations ####
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_commit_messages(self):
|
def get_commit_messages(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_pr_id(self):
|
def get_latest_commit_url(self) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_main_pr_language(languages, files) -> str:
|
def get_main_pr_language(languages, files) -> str:
|
||||||
@ -153,6 +166,8 @@ def get_main_pr_language(languages, files) -> str:
|
|||||||
# validate that the specific commit uses the main language
|
# validate that the specific commit uses the main language
|
||||||
extension_list = []
|
extension_list = []
|
||||||
for file in files:
|
for file in files:
|
||||||
|
if not file:
|
||||||
|
continue
|
||||||
if isinstance(file, str):
|
if isinstance(file, str):
|
||||||
file = FilePatchInfo(base_file=None, head_file=None, patch=None, filename=file)
|
file = FilePatchInfo(base_file=None, head_file=None, patch=None, filename=file)
|
||||||
extension_list.append(file.filename.rsplit('.')[-1])
|
extension_list.append(file.filename.rsplit('.')[-1])
|
||||||
|
@ -13,7 +13,7 @@ from ..algo.utils import load_large_diff
|
|||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ..servers.utils import RateLimitExceeded
|
from ..servers.utils import RateLimitExceeded
|
||||||
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR
|
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR, EDIT_TYPE
|
||||||
|
|
||||||
|
|
||||||
class GithubProvider(GitProvider):
|
class GithubProvider(GitProvider):
|
||||||
@ -129,7 +129,20 @@ class GithubProvider(GitProvider):
|
|||||||
if not patch:
|
if not patch:
|
||||||
patch = load_large_diff(file.filename, new_file_content_str, original_file_content_str)
|
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
|
self.diff_files = diff_files
|
||||||
return diff_files
|
return diff_files
|
||||||
@ -141,10 +154,36 @@ class GithubProvider(GitProvider):
|
|||||||
def publish_description(self, pr_title: str, pr_body: str):
|
def publish_description(self, pr_title: str, pr_body: str):
|
||||||
self.pr.edit(title=pr_title, body=pr_body)
|
self.pr.edit(title=pr_title, body=pr_body)
|
||||||
|
|
||||||
|
def get_latest_commit_url(self) -> str:
|
||||||
|
return self.last_commit_id.html_url
|
||||||
|
|
||||||
|
def get_comment_url(self, comment) -> str:
|
||||||
|
return comment.html_url
|
||||||
|
|
||||||
|
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True):
|
||||||
|
prev_comments = list(self.pr.get_issue_comments())
|
||||||
|
for comment in prev_comments:
|
||||||
|
body = comment.body
|
||||||
|
if body.startswith(initial_header):
|
||||||
|
latest_commit_url = self.get_latest_commit_url()
|
||||||
|
comment_url = self.get_comment_url(comment)
|
||||||
|
if update_header:
|
||||||
|
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n"
|
||||||
|
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
|
||||||
|
else:
|
||||||
|
pr_comment_updated = pr_comment
|
||||||
|
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message")
|
||||||
|
response = comment.edit(pr_comment_updated)
|
||||||
|
self.publish_comment(
|
||||||
|
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}")
|
||||||
|
return
|
||||||
|
self.publish_comment(pr_comment)
|
||||||
|
|
||||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||||
if is_temporary and not get_settings().config.publish_output_progress:
|
if is_temporary and not get_settings().config.publish_output_progress:
|
||||||
get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}")
|
get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}")
|
||||||
return
|
return
|
||||||
|
|
||||||
response = self.pr.create_issue_comment(pr_comment)
|
response = self.pr.create_issue_comment(pr_comment)
|
||||||
if hasattr(response, "user") and hasattr(response.user, "login"):
|
if hasattr(response, "user") and hasattr(response.user, "login"):
|
||||||
self.github_user_id = response.user.login
|
self.github_user_id = response.user.login
|
||||||
|
@ -136,6 +136,33 @@ class GitLabProvider(GitProvider):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().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 get_latest_commit_url(self):
|
||||||
|
return self.mr.commits().next().web_url
|
||||||
|
|
||||||
|
def get_comment_url(self, comment):
|
||||||
|
return f"{self.mr.web_url}#note_{comment.id}"
|
||||||
|
|
||||||
|
def publish_persistent_comment(self, pr_comment: str, initial_header: str, update_header: bool = True):
|
||||||
|
try:
|
||||||
|
for comment in self.mr.notes.list(get_all=True)[::-1]:
|
||||||
|
if comment.body.startswith(initial_header):
|
||||||
|
latest_commit_url = self.get_latest_commit_url()
|
||||||
|
comment_url = self.get_comment_url(comment)
|
||||||
|
if update_header:
|
||||||
|
updated_header = f"{initial_header}\n\n### (review updated until commit {latest_commit_url})\n"
|
||||||
|
pr_comment_updated = pr_comment.replace(initial_header, updated_header)
|
||||||
|
else:
|
||||||
|
pr_comment_updated = pr_comment
|
||||||
|
get_logger().info(f"Persistent mode- updating comment {comment_url} to latest review message")
|
||||||
|
response = self.mr.notes.update(comment.id, {'body': pr_comment_updated})
|
||||||
|
self.publish_comment(
|
||||||
|
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}")
|
||||||
|
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):
|
def publish_comment(self, mr_comment: str, is_temporary: bool = False):
|
||||||
comment = self.mr.notes.create({'body': mr_comment})
|
comment = self.mr.notes.create({'body': mr_comment})
|
||||||
if is_temporary:
|
if is_temporary:
|
||||||
|
@ -36,6 +36,10 @@ api_base = "" # the base url for your huggingface inference endpoint
|
|||||||
[ollama]
|
[ollama]
|
||||||
api_base = "" # the base url for your local Llama 2, Code Llama, and other models inference endpoint. Acquire through https://ollama.ai/
|
api_base = "" # the base url for your local Llama 2, Code Llama, and other models inference endpoint. Acquire through https://ollama.ai/
|
||||||
|
|
||||||
|
[vertexai]
|
||||||
|
vertex_project = "" # the google cloud platform project name for your vertexai deployment
|
||||||
|
vertex_location = "" # the google cloud platform location for your vertexai deployment
|
||||||
|
|
||||||
[github]
|
[github]
|
||||||
# ---- Set the following only for deployment type == "user"
|
# ---- Set the following only for deployment type == "user"
|
||||||
user_token = "" # A GitHub personal access token with 'repo' scope.
|
user_token = "" # A GitHub personal access token with 'repo' scope.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[config]
|
[config]
|
||||||
model="gpt-4"
|
model="gpt-4" # "gpt-4-1106-preview"
|
||||||
fallback_models=["gpt-3.5-turbo-16k"]
|
fallback_models=["gpt-3.5-turbo-16k"]
|
||||||
git_provider="github"
|
git_provider="github"
|
||||||
publish_output=true
|
publish_output=true
|
||||||
@ -10,22 +10,29 @@ use_repo_settings_file=true
|
|||||||
ai_timeout=180
|
ai_timeout=180
|
||||||
max_description_tokens = 500
|
max_description_tokens = 500
|
||||||
max_commits_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
|
patch_extra_lines = 3
|
||||||
secret_provider="google_cloud_storage"
|
secret_provider="google_cloud_storage"
|
||||||
cli_mode=false
|
cli_mode=false
|
||||||
|
|
||||||
[pr_reviewer] # /review #
|
[pr_reviewer] # /review #
|
||||||
|
# enable/disable features
|
||||||
require_focused_review=false
|
require_focused_review=false
|
||||||
require_score_review=false
|
require_score_review=false
|
||||||
require_tests_review=true
|
require_tests_review=true
|
||||||
require_security_review=true
|
require_security_review=true
|
||||||
require_estimate_effort_to_review=true
|
require_estimate_effort_to_review=true
|
||||||
|
# general options
|
||||||
num_code_suggestions=4
|
num_code_suggestions=4
|
||||||
inline_code_comments = false
|
inline_code_comments = false
|
||||||
ask_and_reflect=false
|
ask_and_reflect=false
|
||||||
automatic_review=true
|
automatic_review=true
|
||||||
remove_previous_review_comment=false
|
remove_previous_review_comment=false
|
||||||
|
persistent_comment=true
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
|
# review labels
|
||||||
|
enable_review_labels_security=true
|
||||||
|
enable_review_labels_effort=false
|
||||||
# specific configurations for incremental review (/review -i)
|
# specific configurations for incremental review (/review -i)
|
||||||
require_all_thresholds_for_incremental_review=false
|
require_all_thresholds_for_incremental_review=false
|
||||||
minimal_commits_for_incremental_review=0
|
minimal_commits_for_incremental_review=0
|
||||||
|
@ -16,7 +16,7 @@ You must use the following YAML schema to format your answer:
|
|||||||
PR Type:
|
PR Type:
|
||||||
type: array
|
type: array
|
||||||
{%- if enable_custom_labels %}
|
{%- 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 %}
|
{%- endif %}
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
@ -30,7 +30,7 @@ PR Type:
|
|||||||
{%- if enable_custom_labels %}
|
{%- if enable_custom_labels %}
|
||||||
PR Labels:
|
PR Labels:
|
||||||
type: array
|
type: array
|
||||||
description: One or more labels that describe the PR labels. 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.
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
|
@ -93,7 +93,7 @@ PR Analysis:
|
|||||||
description: >-
|
description: >-
|
||||||
Estimate, on a scale of 1-5 (inclusive), the time and effort required to review this PR by an experienced and knowledgeable developer. 1 means short and easy review , 5 means long and hard review.
|
Estimate, on a scale of 1-5 (inclusive), the time and effort required to review this PR by an experienced and knowledgeable developer. 1 means short and easy review , 5 means long and hard review.
|
||||||
Take into account the size, complexity, quality, and the needed changes of the PR code diff.
|
Take into account the size, complexity, quality, and the needed changes of the PR code diff.
|
||||||
Explain your answer shortly (1-2 sentences).
|
Explain your answer shortly (1-2 sentences). Use the format: '1, because ...'
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
PR Feedback:
|
PR Feedback:
|
||||||
General suggestions:
|
General suggestions:
|
||||||
@ -130,7 +130,8 @@ PR Feedback:
|
|||||||
Security concerns:
|
Security concerns:
|
||||||
type: string
|
type: string
|
||||||
description: >-
|
description: >-
|
||||||
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.
|
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 ? Answer 'No' if there are no possible issues.
|
||||||
|
Answer 'Yes, because ...' if there are security concerns or issues. Explain your answer shortly.
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -152,7 +153,8 @@ PR Analysis:
|
|||||||
Focused PR: no, because ...
|
Focused PR: no, because ...
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if require_estimate_effort_to_review %}
|
{%- if require_estimate_effort_to_review %}
|
||||||
Estimated effort to review [1-5]: 3, because ...
|
Estimated effort to review [1-5]: |-
|
||||||
|
3, because ...
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
PR Feedback:
|
PR Feedback:
|
||||||
General PR suggestions: |-
|
General PR suggestions: |-
|
||||||
|
@ -158,6 +158,9 @@ class PRDescription:
|
|||||||
user=user_prompt
|
user=user_prompt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().info(f"\nAI response:\n{response}")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _prepare_data(self):
|
def _prepare_data(self):
|
||||||
|
@ -10,7 +10,7 @@ from yaml import SafeLoader
|
|||||||
from pr_agent.algo.ai_handler import AiHandler
|
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.pr_processing import get_pr_diff, retry_with_fallback_models
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import convert_to_markdown, load_yaml, try_fix_yaml, set_custom_labels
|
from pr_agent.algo.utils import convert_to_markdown, load_yaml, try_fix_yaml, set_custom_labels, get_user_labels
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
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.git_providers.git_provider import IncrementalPR, get_main_pr_language
|
||||||
@ -117,7 +117,15 @@ class PRReviewer:
|
|||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
get_logger().info('Pushing PR review...')
|
get_logger().info('Pushing PR review...')
|
||||||
previous_review_comment = self._get_previous_review_comment()
|
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_header="## PR Analysis",
|
||||||
|
update_header=True)
|
||||||
|
else:
|
||||||
|
self.git_provider.publish_comment(pr_comment)
|
||||||
|
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
if previous_review_comment:
|
if previous_review_comment:
|
||||||
self._remove_previous_review_comment(previous_review_comment)
|
self._remove_previous_review_comment(previous_review_comment)
|
||||||
@ -156,7 +164,6 @@ class PRReviewer:
|
|||||||
variables["diff"] = self.patches_diff # update diff
|
variables["diff"] = self.patches_diff # update diff
|
||||||
|
|
||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
# set_custom_labels(variables)
|
|
||||||
system_prompt = environment.from_string(get_settings().pr_review_prompt.system).render(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)
|
user_prompt = environment.from_string(get_settings().pr_review_prompt.user).render(variables)
|
||||||
|
|
||||||
@ -171,6 +178,9 @@ class PRReviewer:
|
|||||||
user=user_prompt
|
user=user_prompt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().info(f"\nAI response:\n{response}")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _prepare_pr_review(self) -> str:
|
def _prepare_pr_review(self) -> str:
|
||||||
@ -245,6 +255,9 @@ class PRReviewer:
|
|||||||
else:
|
else:
|
||||||
markdown_text += actions_help_text
|
markdown_text += actions_help_text
|
||||||
|
|
||||||
|
# Add custom labels from the review prediction (effort, security)
|
||||||
|
self.set_review_labels(data)
|
||||||
|
|
||||||
# Log markdown response if verbosity level is high
|
# Log markdown response if verbosity level is high
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Markdown response:\n{markdown_text}")
|
get_logger().info(f"Markdown response:\n{markdown_text}")
|
||||||
@ -365,3 +378,28 @@ class PRReviewer:
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def set_review_labels(self, data):
|
||||||
|
if (get_settings().pr_reviewer.enable_review_labels_security or
|
||||||
|
get_settings().pr_reviewer.enable_review_labels_effort):
|
||||||
|
try:
|
||||||
|
review_labels = []
|
||||||
|
if get_settings().pr_reviewer.enable_review_labels_effort:
|
||||||
|
estimated_effort = data['PR Analysis']['Estimated effort to review [1-5]']
|
||||||
|
estimated_effort_number = int(estimated_effort.split(',')[0])
|
||||||
|
if 1 <= estimated_effort_number <= 5: # 1, because ...
|
||||||
|
review_labels.append(f'Review effort [1-5]: {estimated_effort_number}')
|
||||||
|
if get_settings().pr_reviewer.enable_review_labels_security:
|
||||||
|
security_concerns = data['PR Analysis']['Security concerns'] # yes, because ...
|
||||||
|
security_concerns_bool = 'yes' in security_concerns.lower() or 'true' in security_concerns.lower()
|
||||||
|
if security_concerns_bool:
|
||||||
|
review_labels.append('Possible security concern')
|
||||||
|
|
||||||
|
if review_labels:
|
||||||
|
current_labels = self.git_provider.get_labels()
|
||||||
|
current_labels_filtered = [label for label in current_labels if
|
||||||
|
not label.lower().startswith('review effort [1-5]:') and not label.lower().startswith(
|
||||||
|
'possible security concern')]
|
||||||
|
self.git_provider.publish_labels(review_labels + current_labels_filtered)
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().error(f"Failed to set review labels, error: {e}")
|
||||||
|
@ -8,8 +8,8 @@ import pinecone
|
|||||||
from pinecone_datasets import Dataset, DatasetMetadata
|
from pinecone_datasets import Dataset, DatasetMetadata
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from pr_agent.algo import MAX_TOKENS
|
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
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.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
@ -197,7 +197,7 @@ class PRSimilarIssue:
|
|||||||
username = issue.user.login
|
username = issue.user.login
|
||||||
created_at = str(issue.created_at)
|
created_at = str(issue.created_at)
|
||||||
if len(issue_str) < 8000 or \
|
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(
|
issue_record = Record(
|
||||||
id=issue_key + "." + "issue",
|
id=issue_key + "." + "issue",
|
||||||
text=issue_str,
|
text=issue_str,
|
||||||
|
@ -13,7 +13,7 @@ atlassian-python-api==3.39.0
|
|||||||
GitPython==3.1.32
|
GitPython==3.1.32
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
starlette-context==0.3.6
|
starlette-context==0.3.6
|
||||||
litellm~=0.1.574
|
litellm==0.12.5
|
||||||
boto3==1.28.25
|
boto3==1.28.25
|
||||||
google-cloud-storage==2.10.0
|
google-cloud-storage==2.10.0
|
||||||
ujson==5.8.0
|
ujson==5.8.0
|
||||||
@ -22,3 +22,4 @@ msrest==0.7.1
|
|||||||
pinecone-client
|
pinecone-client
|
||||||
pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main
|
pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main
|
||||||
loguru==0.7.2
|
loguru==0.7.2
|
||||||
|
google-cloud-aiplatform==1.35.0
|
||||||
|
31
tests/unittest/try_fix_yaml.py
Normal file
31
tests/unittest/try_fix_yaml.py
Normal 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
|
Reference in New Issue
Block a user