Compare commits

..

45 Commits

Author SHA1 Message Date
2dcee63df5 fix improve 2023-11-27 12:32:53 +02:00
cddf183e03 Merge pull request #470 from Codium-ai/tr/glob
Enhance glob pattern handling and logging in file filtering
2023-11-22 23:19:09 -08:00
e80a0ed9c8 glob 2023-11-23 09:16:50 +02:00
d6d362b51e Merge pull request #469 from Codium-ai/mrT23-patch-1
Improve Documentation in Usage.md
2023-11-22 22:14:22 -08:00
4eff0282a1 Update Usage.md 2023-11-23 08:06:07 +02:00
8fc07df6ef Update INSTALL.md 2023-11-21 18:39:36 +02:00
84e4b607cc Merge pull request #467 from Codium-ai/ok/base_url
Add support for base_url in GitHub SDK
2023-11-21 16:51:16 +02:00
613ccb4c34 Add support for base_url in GitHub SDK 2023-11-21 16:48:36 +02:00
e95a6a8b07 Merge pull request #466 from Codium-ai/ok/gitlab_fix
Fix a bug in GitLab webhook
2023-11-21 16:36:40 +02:00
2add584fbc Fix a bug in GitLab webhook 2023-11-21 16:28:01 +02:00
54d7d59177 Update Usage.md 2023-11-20 20:06:07 +02:00
b3129c7dd9 Merge pull request #464 from Codium-ai/tr/more_protections
Refactor YAML parsing for improved error handling
2023-11-20 02:28:57 -08:00
3f76d95495 ScannerError 2023-11-20 10:35:35 +02:00
1b600cd85f Refactor YAML parsing for improved error handling 2023-11-20 10:30:59 +02:00
26cc26129c Merge pull request #463 from Codium-ai/tr/more_protections
minor fix
2023-11-19 07:45:39 -08:00
d1d7903e39 minor fix 2023-11-19 17:44:11 +02:00
dff4d1befc Merge pull request #462 from Codium-ai/tr/more_protections
Enhancements in YAML Parsing and Error Handling
2023-11-19 07:40:06 -08:00
3504a64269 protections 2023-11-19 17:35:40 +02:00
83247cadec protections 2023-11-19 17:30:57 +02:00
5ca1748b93 Merge pull request #460 from Codium-ai/tr/update_instructions
GFM mode for 'review' instructions
2023-11-19 01:20:36 -08:00
c7a681038d gfm instructions 2023-11-19 11:11:11 +02:00
eb977b4c24 gfm instructions 2023-11-19 11:02:11 +02:00
14a934b146 Update Usage.md 2023-11-17 10:41:52 +02:00
bc28d657b2 Merge pull request #438 from koid/fix/remove-unnecessary-setup
Removal of Redundant Logger Setup
2023-11-15 10:35:13 -08:00
416a5495da Merge pull request #453 from Codium-ai/tr/v_010
Version 0.10 Release and Workflow Update
2023-11-15 09:46:31 -08:00
a2b27dcac8 v10 2023-11-15 19:45:51 +02:00
d8e4e2e8fd Merge pull request #454 from Codium-ai/coditamar-bitbucket-doc-type
Update INSTALL.md
2023-11-15 09:44:03 -08:00
896a81d173 Update INSTALL.md 2023-11-15 15:20:50 +02:00
b216af8f04 v10 2023-11-15 14:49:18 +02:00
388cc740b6 Merge pull request #436 from rhyst/support-vertex-ai
Support Google's Vertex AI
2023-11-15 04:26:08 -08:00
6214494c84 Merge pull request #452 from Codium-ai/tr/review_extra_labels
Add Review Labels for Security and Effort Estimation
2023-11-15 04:25:03 -08:00
762a6981e1 extra_labels 2023-11-15 14:12:59 +02:00
b362c406bc Merge remote-tracking branch 'origin/main' into tr/review_extra_labels 2023-11-15 14:07:44 +02:00
7a342d3312 extra_labels 2023-11-15 14:07:32 +02:00
2e95988741 extra_labels 2023-11-15 14:04:17 +02:00
9478447141 extra_labels 2023-11-15 14:02:13 +02:00
082293b48c Merge pull request #451 from Codium-ai/tr/persistent_enhacments
Enhancement of Persistent Comments in PR Review
2023-11-15 03:55:15 -08:00
e1d92206f3 docs 2023-11-15 13:32:32 +02:00
557ec72bfe Update documentation for Vertex AI 2023-11-15 10:27:48 +00:00
6b4b16dcf9 Support Google's Vertex AI 2023-11-15 10:26:58 +00:00
c4899a6c54 bitbucket 2023-11-15 12:11:02 +02:00
24d82e65cb gitlab 2023-11-15 09:45:10 +02:00
2567a6cf27 gitlab 2023-11-15 09:40:45 +02:00
94cb6b9795 more feedback 2023-11-15 09:06:26 +02:00
f0991526b5 remove unnecessary setup_logger 2023-11-08 16:56:44 +09:00
28 changed files with 345 additions and 164 deletions

View File

@ -26,5 +26,7 @@ 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'
GITHUB_ACTION.AUTO_IMPROVE: 'true'

2
.pr_agent.toml Normal file
View File

@ -0,0 +1,2 @@
[pr_reviewer]
enable_review_labels_effort = true

View File

@ -1,5 +1,5 @@
### Installation ## Installation
To get started with PR-Agent quickly, you first need to acquire two tokens: To get started with PR-Agent quickly, you first need to acquire two tokens:
@ -101,6 +101,7 @@ python3 -m pr_agent.cli --pr_url <pr_url> ask <your question>
python3 -m pr_agent.cli --pr_url <pr_url> describe python3 -m pr_agent.cli --pr_url <pr_url> describe
python3 -m pr_agent.cli --pr_url <pr_url> improve python3 -m pr_agent.cli --pr_url <pr_url> improve
python3 -m pr_agent.cli --pr_url <pr_url> add_docs python3 -m pr_agent.cli --pr_url <pr_url> add_docs
python3 -m pr_agent.cli --pr_url <pr_url> generate_labels
python3 -m pr_agent.cli --issue_url <issue_url> similar_issue python3 -m pr_agent.cli --issue_url <issue_url> similar_issue
... ...
``` ```
@ -409,10 +410,12 @@ 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.
Note that comments on a PR are not supported in Bitbucket Pipeline.
### Run on a 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. ### Run using CodiumAI-hosted Bitbucket app
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.
======= =======

View File

@ -1,5 +1,24 @@
## Unreleased ## [Version 0.10] - 2023-11-15
- review tool now posts persistent comments by default - 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

View File

@ -32,12 +32,19 @@ The [Tools Guide](./docs/TOOLS_GUIDE.md) provides a detailed description of the
#### Ignoring files from analysis #### Ignoring files from analysis
In some cases, you may want to exclude specific files or directories from the analysis performed by CodiumAI PR-Agent. This can be useful, for example, when you have files that are generated automatically or files that shouldn't be reviewed, like vendored code. In some cases, you may want to exclude specific files or directories from the analysis performed by CodiumAI PR-Agent. This can be useful, for example, when you have files that are generated automatically or files that shouldn't be reviewed, like vendored code.
To ignore files or directories, edit the **[ignore.toml](/pr_agent/settings/ignore.toml)** configuration file. This setting is also exposed the following environment variables: To ignore files or directories, edit the **[ignore.toml](/pr_agent/settings/ignore.toml)** configuration file. This setting also exposes the following environment variables:
- `IGNORE.GLOB` - `IGNORE.GLOB`
- `IGNORE.REGEX` - `IGNORE.REGEX`
See [dynaconf envvars documentation](https://www.dynaconf.com/envvars/). For example, to ignore python files in a PR with online usage, comment on a PR:
`/review --ignore.glob=['*.py']`
To ignore python files in all PRs, set in a configuration file:
```
[ignore]
glob = ['*.py']
```
#### git provider #### git provider
The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configuration file determines the GIT provider that will be used by PR-Agent. Currently, the following providers are supported: The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configuration file determines the GIT provider that will be used by PR-Agent. Currently, the following providers are supported:
@ -59,7 +66,7 @@ The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configu
### Working from a local repo (CLI) ### Working from a local repo (CLI)
When running from your local repo (CLI), your local configuration file will be used. When running from your local repo (CLI), your local configuration file will be used.
Examples for invoking the different tools via the CLI: Examples of invoking the different tools via the CLI:
- **Review**: `python -m pr_agent.cli --pr_url=<pr_url> review` - **Review**: `python -m pr_agent.cli --pr_url=<pr_url> review`
- **Describe**: `python -m pr_agent.cli --pr_url=<pr_url> describe` - **Describe**: `python -m pr_agent.cli --pr_url=<pr_url> describe`
@ -83,7 +90,7 @@ python -m pr_agent.cli --pr_url=<pr_url> /review --pr_reviewer.extra_instructio
publish_output=true publish_output=true
verbosity_level=2 verbosity_level=2
``` ```
This is useful for debugging or experimenting with the different tools. This is useful for debugging or experimenting with different tools.
### Online usage ### Online usage
@ -100,17 +107,17 @@ Commands for invoking the different tools via comments:
To edit a specific configuration value, just add `--config_path=<value>` to any command. To edit a specific configuration value, just add `--config_path=<value>` to any command.
For example if you want to edit the `review` tool configurations, you can run: For example, if you want to edit the `review` tool configurations, you can run:
``` ```
/review --pr_reviewer.extra_instructions="..." --pr_reviewer.require_score_review=false /review --pr_reviewer.extra_instructions="..." --pr_reviewer.require_score_review=false
``` ```
Any configuration value in [configuration file](pr_agent/settings/configuration.toml) file can be similarly edited. comment `/config` to see the list of available configurations. Any configuration value in [configuration file](pr_agent/settings/configuration.toml) file can be similarly edited. Comment `/config` to see the list of available configurations.
### Working with GitHub App ### Working with GitHub App
When running PR-Agent from GitHub App, the default [configuration file](pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded. When running PR-Agent from GitHub App, the default [configuration file](pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded.
By uploading a local `.pr_agent.toml` file, you can edit and customize any configuration parameter. By uploading a local `.pr_agent.toml` file to the root of the repo's main branch, you can edit and customize any configuration parameter.
For example, if you set in `.pr_agent.toml`: For example, if you set in `.pr_agent.toml`:
@ -119,7 +126,7 @@ For example, if you set in `.pr_agent.toml`:
num_code_suggestions=1 num_code_suggestions=1
``` ```
Than you will overwrite the default number of code suggestions to be 1. Then you will overwrite the default number of code suggestions to 1.
#### GitHub app automatic tools #### GitHub app automatic tools
The [github_app](pr_agent/settings/configuration.toml#L76) section defines GitHub app-specific configurations. The [github_app](pr_agent/settings/configuration.toml#L76) section defines GitHub app-specific configurations.
@ -133,7 +140,7 @@ The GitHub app can respond to the following actions on a PR:
4. `review_requested` - Specifically requesting review (in the PR reviewers list) from the `github-actions[bot]` user 4. `review_requested` - Specifically requesting review (in the PR reviewers list) from the `github-actions[bot]` user
The configuration parameter `handle_pr_actions` defines the list of actions for which the GitHub app will trigger the PR-Agent. The configuration parameter `handle_pr_actions` defines the list of actions for which the GitHub app will trigger the PR-Agent.
The configuration parameter `pr_commands` defines the list of tools that will be **run automatically** when one of the above action happens (e.g. a new PR is opened): The configuration parameter `pr_commands` defines the list of tools that will be **run automatically** when one of the above actions happens (e.g., a new PR is opened):
``` ```
[github_app] [github_app]
handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested'] handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested']
@ -173,11 +180,11 @@ 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 pushed to the PR, the PR-Agent will run the `describe` and incremental `auto_review` tools. This 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.
Much like the configurations for `pr_commands`, you can override the default tool paramteres by uploading a local configuration file to the root of your repo. Much like the configurations for `pr_commands`, you can override the default tool parameters by uploading a local configuration file to the root of your repo.
#### Editing the prompts #### Editing the prompts
The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder. The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder.
@ -303,6 +310,24 @@ 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"
fallback_models="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.

View File

@ -16,17 +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. - `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. - `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:

View File

@ -13,5 +13,9 @@ MAX_TOKENS = {
'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,
} }

View File

@ -23,7 +23,9 @@ 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"):
@ -31,7 +33,6 @@ class AiHandler:
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):
@ -54,8 +55,11 @@ class AiHandler:
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
except AttributeError as e: if get_settings().get("VERTEXAI.VERTEX_PROJECT", None):
raise ValueError("OpenAI key is required") from e litellm.vertex_project = get_settings().vertexai.vertex_project
litellm.vertex_location = get_settings().get(
"VERTEXAI.VERTEX_LOCATION", None
)
@property @property
def deployment_id(self): def deployment_id(self):

View File

@ -11,7 +11,12 @@ def filter_ignored(files):
try: try:
# load regex patterns, and translate glob patterns to regex # load regex patterns, and translate glob patterns to regex
patterns = get_settings().ignore.regex patterns = get_settings().ignore.regex
patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob] if isinstance(patterns, str):
patterns = [patterns]
glob_setting = get_settings().ignore.glob
if isinstance(glob_setting, str): # --ignore.glob=[.*utils.py], --ignore.glob=.*utils.py
glob_setting = glob_setting.strip('[]').split(",")
patterns += [fnmatch.translate(glob) for glob in glob_setting]
# compile all valid patterns # compile all valid patterns
compiled_patterns = [] compiled_patterns = []

View File

@ -282,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()

View File

@ -282,42 +282,43 @@ def _fix_key_value(key: str, value: str):
try: try:
value = yaml.safe_load(value) value = yaml.safe_load(value)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to parse YAML for config override {key}={value}", exc_info=e) get_logger().debug(f"Failed to parse YAML for config override {key}={value}", exc_info=e)
return key, value return key, value
def load_yaml(review_text: str) -> dict: def load_yaml(response_text: str) -> dict:
test = 1 response_text = response_text.removeprefix('```yaml').rstrip('`')
review_text = review_text.removeprefix('```yaml').rstrip('`')
try: try:
data = yaml.safe_load(review_text) data = yaml.safe_load(response_text)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to parse AI prediction: {e}") get_logger().error(f"Failed to parse AI prediction: {e}")
data = try_fix_yaml(review_text) data = try_fix_yaml(response_text)
return data return data
def try_fix_yaml(review_text: str) -> dict: def try_fix_yaml(response_text: str) -> dict:
review_text_lines = review_text.split('\n') response_text_lines = response_text.split('\n')
keys = ['relevant line:', 'suggestion content:', 'relevant file:']
# first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...' # first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...'
review_text_lines_copy = review_text_lines.copy() response_text_lines_copy = response_text_lines.copy()
for i in range(0, len(review_text_lines_copy)): for i in range(0, len(response_text_lines_copy)):
if 'relevant line:' in review_text_lines_copy[i] and not '|-' in review_text_lines_copy[i]: for key in keys:
review_text_lines_copy[i] = review_text_lines_copy[i].replace('relevant line: ', if key in response_text_lines_copy[i] and not '|-' in response_text_lines_copy[i]:
'relevant line: |-\n ') response_text_lines_copy[i] = response_text_lines_copy[i].replace(f'{key}',
f'{key} |-\n ')
try: try:
data = yaml.load('\n'.join(review_text_lines_copy), Loader=yaml.SafeLoader) data = yaml.safe_load('\n'.join(response_text_lines_copy))
get_logger().info(f"Successfully parsed AI prediction after adding |-\n to relevant line") get_logger().info(f"Successfully parsed AI prediction after adding |-\n")
return data return data
except: except:
get_logger().debug(f"Failed to parse AI prediction after adding |-\n to relevant line") get_logger().info(f"Failed to parse AI prediction after adding |-\n")
# second fallback - try to remove last lines # second fallback - try to remove last lines
data = {} data = {}
for i in range(1, len(review_text_lines)): for i in range(1, len(response_text_lines)):
review_text_lines_tmp = '\n'.join(review_text_lines[:-i]) response_text_lines_tmp = '\n'.join(response_text_lines[:-i])
try: try:
data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader) data = yaml.safe_load(response_text_lines_tmp,)
get_logger().info(f"Successfully parsed AI prediction after removing {i} lines") get_logger().info(f"Successfully parsed AI prediction after removing {i} lines")
break break
except: except:

View File

@ -153,17 +153,29 @@ class BitbucketProvider(GitProvider):
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
def publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str): 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: try:
for comment in self.pr.comments(): for comment in self.pr.comments():
body = comment.raw body = comment.raw
if initial_text in body: if initial_header in body:
if updated_text: latest_commit_url = self.get_latest_commit_url()
pr_comment_updated = pr_comment.replace(initial_text, updated_text) 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: else:
pr_comment_updated = pr_comment 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}} d = {"content": {"raw": pr_comment_updated}}
response = comment._update_data(comment.put(None, data=d)) 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 return
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}") get_logger().exception(f"Failed to update persistent review, error: {e}")

View File

@ -40,45 +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
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
@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
@ -117,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
@ -132,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:

View File

@ -154,16 +154,28 @@ 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 publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str): 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()) prev_comments = list(self.pr.get_issue_comments())
for comment in prev_comments: for comment in prev_comments:
body = comment.body body = comment.body
if body.startswith(initial_text): if body.startswith(initial_header):
if updated_text: latest_commit_url = self.get_latest_commit_url()
pr_comment_updated = pr_comment.replace(initial_text, updated_text) 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: else:
pr_comment_updated = pr_comment 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) response = comment.edit(pr_comment_updated)
self.publish_comment(
f"**[Persistent review]({comment_url})** updated to latest commit {latest_commit_url}")
return return
self.publish_comment(pr_comment) self.publish_comment(pr_comment)
@ -393,7 +405,7 @@ class GithubProvider(GitProvider):
raise ValueError("GitHub app installation ID is required when using GitHub app deployment") raise ValueError("GitHub app installation ID is required when using GitHub app deployment")
auth = AppAuthentication(app_id=app_id, private_key=private_key, auth = AppAuthentication(app_id=app_id, private_key=private_key,
installation_id=self.installation_id) installation_id=self.installation_id)
return Github(app_auth=auth) return Github(app_auth=auth, base_url=get_settings().github.base_url)
if deployment_type == 'user': if deployment_type == 'user':
try: try:
@ -402,7 +414,7 @@ class GithubProvider(GitProvider):
raise ValueError( raise ValueError(
"GitHub token is required when using user deployment. See: " "GitHub token is required when using user deployment. See: "
"https://github.com/Codium-ai/pr-agent#method-2-run-from-source") from e "https://github.com/Codium-ai/pr-agent#method-2-run-from-source") from e
return Github(auth=Auth.Token(token)) return Github(auth=Auth.Token(token), base_url=get_settings().github.base_url)
def _get_repo(self): def _get_repo(self):
if hasattr(self, 'repo_obj') and \ if hasattr(self, 'repo_obj') and \

View File

@ -136,15 +136,27 @@ 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 publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str): 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: try:
for comment in self.mr.notes.list(get_all=True)[::-1]: for comment in self.mr.notes.list(get_all=True)[::-1]:
if comment.body.startswith(initial_text): if comment.body.startswith(initial_header):
if updated_text: latest_commit_url = self.get_latest_commit_url()
pr_comment_updated = pr_comment.replace(initial_text, updated_text) 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: else:
pr_comment_updated = pr_comment 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}) 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 return
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to update persistent review, error: {e}") get_logger().exception(f"Failed to update persistent review, error: {e}")

View File

@ -65,13 +65,13 @@ async def run_action():
if action in ["opened", "reopened"]: if action in ["opened", "reopened"]:
pr_url = event_payload.get("pull_request", {}).get("url") pr_url = event_payload.get("pull_request", {}).get("url")
if pr_url: if pr_url:
auto_review = os.environ.get('github_action.auto_review', None) auto_review = get_settings().get('GITHUB_ACTION.AUTO_REVIEW', None)
if auto_review is None or (isinstance(auto_review, str) and auto_review.lower() == 'true'): if auto_review is None or (isinstance(auto_review, str) and auto_review.lower() == 'true'):
await PRReviewer(pr_url).run() await PRReviewer(pr_url).run()
auto_describe = os.environ.get('github_action.auto_describe', None) auto_describe = get_settings().get('GITHUB_ACTION.AUTO_DESCRIBE', None)
if isinstance(auto_describe, str) and auto_describe.lower() == 'true': if isinstance(auto_describe, str) and auto_describe.lower() == 'true':
await PRDescription(pr_url).run() await PRDescription(pr_url).run()
auto_improve = os.environ.get('github_action.auto_improve', None) auto_improve = get_settings().get('GITHUB_ACTION.AUTO_IMPROVE', None)
if isinstance(auto_improve, str) and auto_improve.lower() == 'true': if isinstance(auto_improve, str) and auto_improve.lower() == 'true':
await PRCodeSuggestions(pr_url).run() await PRCodeSuggestions(pr_url).run()

View File

@ -38,7 +38,7 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
try: try:
secret_dict = json.loads(secret) secret_dict = json.loads(secret)
gitlab_token = secret_dict["gitlab_token"] gitlab_token = secret_dict["gitlab_token"]
log_context["sender"] = secret_dict["id"] log_context["sender"] = secret_dict.get("token_name", secret_dict.get("id", "unknown"))
context["settings"] = copy.deepcopy(global_settings) context["settings"] = copy.deepcopy(global_settings)
context["settings"].gitlab.personal_access_token = gitlab_token context["settings"].gitlab.personal_access_token = gitlab_token
except Exception as e: except Exception as e:

View File

@ -1,12 +1,14 @@
commands_text = "> **/review [-i]**: Request a review of your Pull Request. For an incremental review, which only " \ commands_text = "> **/review**: Request a review of your Pull Request.\n" \
"considers changes since the last review, include the '-i' option.\n" \ "> **/describe**: Update the PR title and description based on the contents of the PR.\n" \
"> **/describe**: Modify the PR title and description based on the contents of the PR.\n" \ "> **/improve [--extended]**: Suggest code improvements. Extended mode provides a higher quality feedback.\n" \
"> **/improve [--extended]**: Suggest improvements to the code in the PR. Extended mode employs several calls, and provides a more thorough feedback. \n" \ "> **/ask \\<QUESTION\\>**: Ask a question about the PR.\n" \
"> **/ask \\<QUESTION\\>**: Pose a question about the PR.\n" \ "> **/update_changelog**: Update the changelog based on the PR's contents.\n" \
"> **/update_changelog**: Update the changelog based on the PR's contents.\n\n" \ "> **/add_docs**: Generate docstring for new components introduced in the PR.\n" \
">To edit any configuration parameter from **configuration.toml**, add --config_path=new_value\n" \ "> **/generate_labels**: Generate labels for the PR based on the PR's contents.\n" \
"> see the [tools guide](https://github.com/Codium-ai/pr-agent/blob/main/docs/TOOLS_GUIDE.md) for more details.\n\n" \
">To edit any configuration parameter from the [configuration.toml](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml), add --config_path=new_value.\n" \
">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \ ">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \
">To list the possible configuration parameters, use the **/config** command.\n" \ ">To list the possible configuration parameters, add a **/config** comment.\n" \
def bot_help_text(user: str): def bot_help_text(user: str):

View File

@ -3,10 +3,8 @@ from mangum import Mangum
from starlette.middleware import Middleware from starlette.middleware import Middleware
from starlette_context.middleware import RawContextMiddleware from starlette_context.middleware import RawContextMiddleware
from pr_agent.log import setup_logger
from pr_agent.servers.github_app import router from pr_agent.servers.github_app import router
setup_logger()
middleware = [Middleware(RawContextMiddleware)] middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware) app = FastAPI(middleware=middleware)

View File

@ -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.

View File

@ -4,7 +4,7 @@ fallback_models=["gpt-3.5-turbo-16k"]
git_provider="github" git_provider="github"
publish_output=true publish_output=true
publish_output_progress=true publish_output_progress=true
verbosity_level=2 # 0,1,2 verbosity_level=0 # 0,1,2
use_extra_bad_extensions=false use_extra_bad_extensions=false
use_repo_settings_file=true use_repo_settings_file=true
ai_timeout=180 ai_timeout=180
@ -16,11 +16,13 @@ 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
@ -28,6 +30,9 @@ automatic_review=true
remove_previous_review_comment=false remove_previous_review_comment=false
persistent_comment=true 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
@ -74,6 +79,7 @@ extra_instructions = ""
# The type of deployment to create. Valid values are 'app' or 'user'. # The type of deployment to create. Valid values are 'app' or 'user'.
deployment_type = "user" deployment_type = "user"
ratelimit_retries = 5 ratelimit_retries = 5
base_url = "https://api.github.com"
[github_action] [github_action]
# auto_review = true # set as env var in .github/workflows/pr-agent.yaml # auto_review = true # set as env var in .github/workflows/pr-agent.yaml

View File

@ -90,16 +90,19 @@ Code suggestions:
Example output: Example output:
```yaml ```yaml
Code suggestions: Code suggestions:
- relevant file: |- - relevant file: |-
src/file1.py src/file1.py
suggestion content: |- suggestion content: |-
Add a docstring to func1() Add a docstring to func1()
existing code: |- existing code: |-
def func1(): def func1():
relevant lines start: 12 relevant lines start: |-
relevant lines end: 12 12
relevant lines end: |-
12
improved code: |- improved code: |-
... ...
...
``` ```

View File

@ -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 %}
``` ```

View File

@ -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):

View File

@ -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
@ -121,8 +121,8 @@ class PRReviewer:
# publish the review # publish the review
if get_settings().pr_reviewer.persistent_comment and not self.incremental.is_incremental: if get_settings().pr_reviewer.persistent_comment and not self.incremental.is_incremental:
self.git_provider.publish_persistent_comment(pr_comment, self.git_provider.publish_persistent_comment(pr_comment,
initial_text="## PR Analysis", initial_header="## PR Analysis",
updated_text="## PR Analysis (updated)") update_header=True)
else: else:
self.git_provider.publish_comment(pr_comment) self.git_provider.publish_comment(pr_comment)
@ -178,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:
@ -246,11 +249,18 @@ class PRReviewer:
# Add help text if not in CLI mode # Add help text if not in CLI mode
if not get_settings().get("CONFIG.CLI_MODE", False): if not get_settings().get("CONFIG.CLI_MODE", False):
markdown_text += "\n### How to use\n" markdown_text += "\n### How to use\n"
if self.git_provider.is_supported("gfm_markdown"):
markdown_text += "\n**<details><summary> Instructions**</summary>\n"
bot_user = "[bot]" if get_settings().github_app.override_deployment_type else get_settings().github_app.bot_user bot_user = "[bot]" if get_settings().github_app.override_deployment_type else get_settings().github_app.bot_user
if user and bot_user not in user: if user and bot_user not in user:
markdown_text += bot_help_text(user) markdown_text += bot_help_text(user)
else: else:
markdown_text += actions_help_text markdown_text += actions_help_text
if self.git_provider.is_supported("gfm_markdown"):
markdown_text += "\n</details>\n"
# 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:
@ -268,14 +278,7 @@ class PRReviewer:
if get_settings().pr_reviewer.num_code_suggestions == 0: if get_settings().pr_reviewer.num_code_suggestions == 0:
return return
review_text = self.prediction.strip() data = load_yaml(self.prediction.strip())
review_text = review_text.removeprefix('```yaml').rstrip('`')
try:
data = yaml.load(review_text, Loader=SafeLoader)
except Exception as e:
get_logger().error(f"Failed to parse AI prediction: {e}")
data = try_fix_yaml(review_text)
comments: List[str] = [] comments: List[str] = []
for suggestion in data.get('PR Feedback', {}).get('Code feedback', []): for suggestion in data.get('PR Feedback', {}).get('Code feedback', []):
relevant_file = suggestion.get('relevant file', '').strip() relevant_file = suggestion.get('relevant file', '').strip()
@ -372,3 +375,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}")

View File

@ -8,6 +8,7 @@ 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.algo.utils import get_max_tokens
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings

View File

@ -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

View File

@ -2,6 +2,9 @@
# Generated by CodiumAI # Generated by CodiumAI
import pytest import pytest
import yaml
from yaml.scanner import ScannerError
from pr_agent.algo.utils import load_yaml from pr_agent.algo.utils import load_yaml
@ -12,7 +15,7 @@ class TestLoadYaml:
expected_output = {'name': 'John Smith', 'age': 35} expected_output = {'name': 'John Smith', 'age': 35}
assert load_yaml(yaml_str) == expected_output assert load_yaml(yaml_str) == expected_output
def test_load_complicated_yaml(self): def test_load_invalid_yaml1(self):
yaml_str = \ yaml_str = \
'''\ '''\
PR Analysis: PR Analysis:
@ -26,7 +29,23 @@ PR Feedback:
Code feedback: Code feedback:
- relevant file: pr_agent/settings/pr_description_prompts.toml - relevant file: pr_agent/settings/pr_description_prompts.toml
suggestion: Consider using a more descriptive variable name than 'user' for the command prompt. A more descriptive name would make the code more readable and maintainable. [medium] suggestion: Consider using a more descriptive variable name than 'user' for the command prompt. A more descriptive name would make the code more readable and maintainable. [medium]
relevant line: 'user="""PR Info:' relevant line: user="""PR Info: aaa
Security concerns: No''' Security concerns: No'''
expected_output = {'PR Analysis': {'Main theme': 'Enhancing the `/describe` command prompt by adding title and description', 'Type of PR': 'Enhancement', 'Relevant tests added': False, 'Focused PR': 'Yes, the PR is focused on enhancing the `/describe` command prompt.'}, 'PR Feedback': {'General suggestions': 'The PR seems to be well-structured and focused on a specific enhancement. However, it would be beneficial to add tests to ensure the new feature works as expected.', 'Code feedback': [{'relevant file': 'pr_agent/settings/pr_description_prompts.toml', 'suggestion': "Consider using a more descriptive variable name than 'user' for the command prompt. A more descriptive name would make the code more readable and maintainable. [medium]", 'relevant line': 'user="""PR Info:'}], 'Security concerns': False}} with pytest.raises(ScannerError):
yaml.safe_load(yaml_str)
expected_output = {'PR Analysis': {'Main theme': 'Enhancing the `/describe` command prompt by adding title and description', 'Type of PR': 'Enhancement', 'Relevant tests added': False, 'Focused PR': 'Yes, the PR is focused on enhancing the `/describe` command prompt.'}, 'PR Feedback': {'General suggestions': 'The PR seems to be well-structured and focused on a specific enhancement. However, it would be beneficial to add tests to ensure the new feature works as expected.', 'Code feedback': [{'relevant file': 'pr_agent/settings/pr_description_prompts.toml', 'suggestion': "Consider using a more descriptive variable name than 'user' for the command prompt. A more descriptive name would make the code more readable and maintainable. [medium]", 'relevant line': 'user="""PR Info: aaa'}], 'Security concerns': False}}
assert load_yaml(yaml_str) == expected_output assert load_yaml(yaml_str) == expected_output
def test_load_invalid_yaml2(self):
yaml_str = '''\
- relevant file: src/app.py:
suggestion content: The print statement is outside inside the if __name__ ==: \
'''
with pytest.raises(ScannerError):
yaml.safe_load(yaml_str)
expected_output =[{'relevant file': 'src/app.py:',
'suggestion content': 'The print statement is outside inside the if __name__ ==: '}]
assert load_yaml(yaml_str) == expected_output