Compare commits

..

3 Commits

Author SHA1 Message Date
5fb8bc1927 test 2023-11-20 00:42:29 +02:00
e9315c7d28 Merge commit 'e878bbbe369c90433c1b261b5479d23c47734539' into hl/test_docstring 2023-11-15 10:37:31 +02:00
67b61d722d test 2023-11-14 20:31:01 +02:00
53 changed files with 400 additions and 1234 deletions

View File

@ -26,7 +26,5 @@ 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'

View File

@ -1,6 +0,0 @@
[pr_reviewer]
enable_review_labels_effort = true
[pr_code_suggestions]
summarize=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:
@ -25,7 +25,6 @@ There are several ways to use PR-Agent:
**BitBucket specific methods** **BitBucket specific methods**
- [Run as a Bitbucket Pipeline](INSTALL.md#run-as-a-bitbucket-pipeline) - [Run as a Bitbucket Pipeline](INSTALL.md#run-as-a-bitbucket-pipeline)
- [Run on a hosted app](INSTALL.md#run-on-a-hosted-bitbucket-app) - [Run on a hosted app](INSTALL.md#run-on-a-hosted-bitbucket-app)
- [Bitbucket server and data center](INSTALL.md#bitbucket-server-and-data-center)
--- ---
### Use Docker image (no installation required) ### Use Docker image (no installation required)
@ -102,7 +101,6 @@ 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
... ...
``` ```
@ -411,49 +409,10 @@ 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
### 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.
### Bitbucket Server and Data Center
Login into your on-prem instance of Bitbucket with your service account username and password.
Navigate to `Manage account`, `HTTP Access tokens`, `Create Token`.
Generate the token and add it to .secret.toml under `bitbucket_server` section
```toml
[bitbucket_server]
bearer_token = "<your key>"
```
#### Run it as CLI
Modify `configuration.toml`:
```toml
git_provider="bitbucket_server"
```
and pass the Pull request URL:
```shell
python cli.py --pr_url https://git.onpreminstanceofbitbucket.com/projects/PROJECT/repos/REPO/pull-requests/1 review
```
#### Run it as service
To run pr-agent as webhook, build the docker image:
```
docker build . -t codiumai/pr-agent:bitbucket_server_webhook --target bitbucket_server_webhook -f docker/Dockerfile
docker push codiumai/pr-agent:bitbucket_server_webhook # Push to your Docker repository
```
Navigate to `Projects` or `Repositories`, `Settings`, `Webhooks`, `Create Webhook`.
Fill the name and URL, Authentication None select the Pull Request Opened checkbox to receive that event as webhook.
The url should be ends with `/webhook`, example: https://domain.com/webhook
======= =======

View File

@ -1,24 +1,5 @@
## [Version 0.10] - 2023-11-15 ## Unreleased
- codiumai/pr-agent:0.10 - review tool now posts persistent comments by default
- 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,19 +32,12 @@ 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 also exposes the following environment variables: To ignore files or directories, edit the **[ignore.toml](/pr_agent/settings/ignore.toml)** configuration file. This setting is also exposed the following environment variables:
- `IGNORE.GLOB` - `IGNORE.GLOB`
- `IGNORE.REGEX` - `IGNORE.REGEX`
For example, to ignore python files in a PR with online usage, comment on a PR: See [dynaconf envvars documentation](https://www.dynaconf.com/envvars/).
`/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:
@ -66,7 +59,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 of invoking the different tools via the CLI: Examples for 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`
@ -90,7 +83,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 different tools. This is useful for debugging or experimenting with the different tools.
### Online usage ### Online usage
@ -107,17 +100,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 to the root of the repo's main branch, you can edit and customize any configuration parameter. Note that you need to upload `.pr_agent.toml` prior to creating a PR, in order for the configuration to take effect. By uploading a local `.pr_agent.toml` file, 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`:
@ -126,7 +119,7 @@ For example, if you set in `.pr_agent.toml`:
num_code_suggestions=1 num_code_suggestions=1
``` ```
Then you will overwrite the default number of code suggestions to 1. Than you will overwrite the default number of code suggestions to be 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.
@ -140,7 +133,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 actions 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 action 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']
@ -180,11 +173,11 @@ push_commands = [
"/auto_review -i --pr_reviewer.remove_previous_review_comment=true", "/auto_review -i --pr_reviewer.remove_previous_review_comment=true",
] ]
``` ```
This means that when new code is pushed 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.
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. 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.
#### 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.
@ -310,24 +303,6 @@ 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

@ -14,10 +14,6 @@ FROM base as bitbucket_app
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_app.py"] CMD ["python", "pr_agent/servers/bitbucket_app.py"]
FROM base as bitbucket_server_webhook
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_server_webhook.py"]
FROM base as github_polling FROM base as github_polling
ADD pr_agent pr_agent ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_polling.py"] CMD ["python", "pr_agent/servers/github_polling.py"]

View File

@ -1,6 +1,6 @@
# Improve Tool # Improve Tool
The `improve` tool scans the PR code changes, and automatically generates committable suggestions for improving the PR code. The `improve` tool scans the PR code changes, and automatically generate committable suggestions for improving the PR code.
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```
/improve /improve
@ -17,7 +17,7 @@ An extended mode, which does not involve PR Compression and provides more compre
/improve --extended /improve --extended
``` ```
Note that the extended mode divides the PR code changes into chunks, up to the token limits, where each chunk is handled separately (multiple calls to GPT-4). Note that the extended mode divides the PR code changes into chunks, up to the token limits, where each chunk is handled separately (multiple calls to GPT-4).
Hence, the total number of suggestions is proportional to the number of chunks, i.e., the size of the PR. Hence, the total number of suggestions is proportional to the number of chunks, i.e. the size of the PR.
### Configuration options ### Configuration options
@ -33,23 +33,13 @@ Under the section 'pr_code_suggestions', the [configuration file](./../pr_agent/
- `max_number_of_calls`: maximum number of chunks. Default is 5. - `max_number_of_calls`: maximum number of chunks. Default is 5.
- `final_clip_factor`: factor to remove suggestions with low confidence. Default is 0.9. - `final_clip_factor`: factor to remove suggestions with low confidence. Default is 0.9.
#### summarize mode
- `summarize`: if set to true, the tool will present the code suggestions in a compact way. Default is false.
In this mode, instead of presenting committable suggestions, the different suggestions will be combined into a single compact comment, with significantly smaller PR footprint.
For example:
`/improve --pr_code_suggestions.summarize=true`
<kbd><img src=./../pics/improved_summerize_open.png width="768"></kbd>
#### A note on code suggestions quality #### A note on code suggestions quality
- With the current level of AI for code (GPT-4), mistakes can happen. Not all the suggestions will be perfect, and a user should not accept all of them automatically. - With current level of AI for code (GPT-4), mistakes can happen. Not all the suggestions will be perfect, and a user should not accept all of them automatically.
- Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_code_suggestions_prompts.toml#L34). Instead, they aim to give deep feedback and raise questions, ideas and thoughts to the user, who can then use his judgment, experience, and understanding of the code base. - Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_code_suggestions_prompts.toml#L34). Instead, they aim to give deep feedback and raise questions, ideas and thoughts to the user, who can then use his judgment, experience, and understanding of the code base.
- Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project. - Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project.
- Best quality will be obtained by using 'improve --extended' mode. - Best quality will be obtained by using 'improve --extended' mode.

View File

@ -16,22 +16,17 @@ 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, meaning that every new review request will edit the previous one. Default is true. - `persistent_comment`: if set to true, the review comment will be persistent. Default is true.
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...". - `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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View File

@ -13,9 +13,5 @@ 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,43 +23,39 @@ 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.
""" """
self.azure = False try:
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
if get_settings().get("OPENAI.ORG", None): self.azure = False
litellm.organization = get_settings().openai.org if get_settings().get("OPENAI.ORG", None):
if get_settings().get("OPENAI.API_TYPE", None): litellm.organization = get_settings().openai.org
if get_settings().openai.api_type == "azure": if get_settings().get("OPENAI.API_TYPE", None):
self.azure = True if get_settings().openai.api_type == "azure":
litellm.azure_key = get_settings().openai.key self.azure = True
if get_settings().get("OPENAI.API_VERSION", None): litellm.azure_key = get_settings().openai.key
litellm.api_version = get_settings().openai.api_version if get_settings().get("OPENAI.API_VERSION", None):
if get_settings().get("OPENAI.API_BASE", None): litellm.api_version = get_settings().openai.api_version
litellm.api_base = get_settings().openai.api_base if get_settings().get("OPENAI.API_BASE", None):
if get_settings().get("ANTHROPIC.KEY", None): litellm.api_base = get_settings().openai.api_base
litellm.anthropic_key = get_settings().anthropic.key if get_settings().get("ANTHROPIC.KEY", None):
if get_settings().get("COHERE.KEY", None): litellm.anthropic_key = get_settings().anthropic.key
litellm.cohere_key = get_settings().cohere.key if get_settings().get("COHERE.KEY", None):
if get_settings().get("REPLICATE.KEY", None): litellm.cohere_key = get_settings().cohere.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("REPLICATE.KEY", None):
if get_settings().get("HUGGINGFACE.KEY", None): litellm.replicate_key = get_settings().replicate.key
litellm.huggingface_key = get_settings().huggingface.key if get_settings().get("HUGGINGFACE.KEY", None):
if get_settings().get("HUGGINGFACE.API_BASE", None): litellm.huggingface_key = get_settings().huggingface.key
litellm.api_base = get_settings().huggingface.api_base if get_settings().get("HUGGINGFACE.API_BASE", None):
if get_settings().get("VERTEXAI.VERTEX_PROJECT", None): litellm.api_base = get_settings().huggingface.api_base
litellm.vertex_project = get_settings().vertexai.vertex_project except AttributeError as e:
litellm.vertex_location = get_settings().get( raise ValueError("OpenAI key is required") from e
"VERTEXAI.VERTEX_LOCATION", None
)
@property @property
def deployment_id(self): def deployment_id(self):

View File

@ -11,12 +11,7 @@ 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
if isinstance(patterns, str): patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob]
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

@ -3,7 +3,8 @@ from typing import Dict
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
language_extension_map_org = get_settings().language_extension_map_org
language_extension_map = {k.lower(): v for k, v in language_extension_map_org.items()}
# Bad Extensions, source: https://github.com/EleutherAI/github-downloader/blob/345e7c4cbb9e0dc8a0615fd995a08bf9d73b3fe6/download_repo_text.py # noqa: E501 # Bad Extensions, source: https://github.com/EleutherAI/github-downloader/blob/345e7c4cbb9e0dc8a0615fd995a08bf9d73b3fe6/download_repo_text.py # noqa: E501
bad_extensions = get_settings().bad_extensions.default bad_extensions = get_settings().bad_extensions.default
@ -28,8 +29,6 @@ def sort_files_by_main_languages(languages: Dict, files: list):
# languages_sorted = sorted(languages, key=lambda x: x[1], reverse=True) # languages_sorted = sorted(languages, key=lambda x: x[1], reverse=True)
# get all extensions for the languages # get all extensions for the languages
main_extensions = [] main_extensions = []
language_extension_map_org = get_settings().language_extension_map_org
language_extension_map = {k.lower(): v for k, v in language_extension_map_org.items()}
for language in languages_sorted_list: for language in languages_sorted_list:
if language.lower() in language_extension_map: if language.lower() in language_extension_map:
main_extensions.append(language_extension_map[language.lower()]) main_extensions.append(language_extension_map[language.lower()])

View File

@ -10,7 +10,7 @@ from github import RateLimitExceededException
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 from pr_agent.algo.token_handler import TokenHandler, get_token_encoder
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
from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider, EDIT_TYPE from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
@ -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 and (file.filename.strip() == relevant_file): if file.filename.strip() == relevant_file:
patch = file.patch patch = file.patch
patch_lines = patch.splitlines() patch_lines = patch.splitlines()
@ -326,6 +326,35 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
return position, absolute_position return position, absolute_position
def clip_tokens(text: str, max_tokens: int) -> str:
"""
Clip the number of tokens in a string to a maximum number of tokens.
Args:
text (str): The string to clip.
max_tokens (int): The maximum number of tokens allowed in the string.
Returns:
str: The clipped string.
"""
if not text:
return text
try:
encoder = get_token_encoder()
num_input_tokens = len(encoder.encode(text))
if num_input_tokens <= max_tokens:
return text
num_chars = len(text)
chars_per_token = num_chars / num_input_tokens
num_output_chars = int(chars_per_token * max_tokens)
clipped_text = text[:num_output_chars]
return clipped_text
except Exception as e:
get_logger().warning(f"Failed to clip tokens: {e}")
return text
def get_pr_multi_diffs(git_provider: GitProvider, def get_pr_multi_diffs(git_provider: GitProvider,
token_handler: TokenHandler, token_handler: TokenHandler,
model: str, model: str,

View File

@ -11,7 +11,6 @@ import yaml
from starlette_context import context from starlette_context import context
from pr_agent.algo import MAX_TOKENS from pr_agent.algo import MAX_TOKENS
from pr_agent.algo.token_handler import get_token_encoder
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
@ -58,8 +57,7 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
emoji = emojis.get(key, "") emoji = emojis.get(key, "")
if key.lower() == 'code feedback': if key.lower() == 'code feedback':
if gfm_supported: if gfm_supported:
markdown_text += f"\n\n- " markdown_text += f"\n\n- **<details><summary> { emoji } Code feedback:**</summary>\n\n"
markdown_text += f"<details><summary> { emoji } Code feedback:</summary>\n\n"
else: else:
markdown_text += f"\n\n- **{emoji} Code feedback:**\n\n" markdown_text += f"\n\n- **{emoji} Code feedback:**\n\n"
else: else:
@ -100,9 +98,9 @@ def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> s
markdown_text += f" - **{code_key}:**\n{code_str_indented}\n" markdown_text += f" - **{code_key}:**\n{code_str_indented}\n"
else: else:
if "relevant file" in sub_key.lower(): if "relevant file" in sub_key.lower():
markdown_text += f"\n - **{sub_key}:** {sub_value} \n" markdown_text += f"\n - **{sub_key}:** {sub_value}\n"
else: else:
markdown_text += f" **{sub_key}:** {sub_value} \n" markdown_text += f" **{sub_key}:** {sub_value}\n"
if not gfm_supported: if not gfm_supported:
if "relevant line" not in sub_key.lower(): # nicer presentation if "relevant line" not in sub_key.lower(): # nicer presentation
# markdown_text = markdown_text.rstrip('\n') + "\\\n" # works for gitlab # markdown_text = markdown_text.rstrip('\n') + "\\\n" # works for gitlab
@ -284,43 +282,42 @@ 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().debug(f"Failed to parse YAML for config override {key}={value}", exc_info=e) get_logger().error(f"Failed to parse YAML for config override {key}={value}", exc_info=e)
return key, value return key, value
def load_yaml(response_text: str) -> dict: def load_yaml(review_text: str) -> dict:
response_text = response_text.removeprefix('```yaml').rstrip('`') test = 1
review_text = review_text.removeprefix('```yaml').rstrip('`')
try: try:
data = yaml.safe_load(response_text) data = yaml.safe_load(review_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(response_text) data = try_fix_yaml(review_text)
return data return data
def try_fix_yaml(response_text: str) -> dict: def try_fix_yaml(review_text: str) -> dict:
response_text_lines = response_text.split('\n') review_text_lines = review_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 ...'
response_text_lines_copy = response_text_lines.copy() review_text_lines_copy = review_text_lines.copy()
for i in range(0, len(response_text_lines_copy)): for i in range(0, len(review_text_lines_copy)):
for key in keys: if 'relevant line:' in review_text_lines_copy[i] and not '|-' in review_text_lines_copy[i]:
if key in response_text_lines_copy[i] and not '|-' in response_text_lines_copy[i]: review_text_lines_copy[i] = review_text_lines_copy[i].replace('relevant line: ',
response_text_lines_copy[i] = response_text_lines_copy[i].replace(f'{key}', 'relevant line: |-\n ')
f'{key} |-\n ')
try: try:
data = yaml.safe_load('\n'.join(response_text_lines_copy)) data = yaml.load('\n'.join(review_text_lines_copy), Loader=yaml.SafeLoader)
get_logger().info(f"Successfully parsed AI prediction after adding |-\n") get_logger().info(f"Successfully parsed AI prediction after adding |-\n to relevant line")
return data return data
except: except:
get_logger().info(f"Failed to parse AI prediction after adding |-\n") get_logger().debug(f"Failed to parse AI prediction after adding |-\n to relevant line")
# second fallback - try to remove last lines # second fallback - try to remove last lines
data = {} data = {}
for i in range(1, len(response_text_lines)): for i in range(1, len(review_text_lines)):
response_text_lines_tmp = '\n'.join(response_text_lines[:-i]) review_text_lines_tmp = '\n'.join(review_text_lines[:-i])
try: try:
data = yaml.safe_load(response_text_lines_tmp,) data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader)
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:
@ -340,15 +337,12 @@ def set_custom_labels(variables):
labels_list = f" - {labels_list}" if labels_list else "" labels_list = f" - {labels_list}" if labels_list else ""
variables["custom_labels"] = labels_list variables["custom_labels"] = labels_list
return return
#final_labels = "" final_labels = ""
#for k, v in labels.items():
# final_labels += f" - {k} ({v['description']})\n"
#variables["custom_labels"] = final_labels
#variables["custom_labels_examples"] = f" - {list(labels.keys())[0]}"
variables["custom_labels_class"] = "class Label(str, Enum):"
for k, v in labels.items(): for k, v in labels.items():
description = v['description'].strip('\n').replace('\n', '\\n') final_labels += f" - {k} ({v['description']})\n"
variables["custom_labels_class"] += f"\n {k.lower().replace(' ', '_')} = '{k}' # {description}" variables["custom_labels"] = final_labels
variables["custom_labels_examples"] = f" - {list(labels.keys())[0]}"
def get_user_labels(current_labels: List[str] = None): def get_user_labels(current_labels: List[str] = None):
""" """
@ -380,34 +374,3 @@ def get_max_tokens(model):
max_tokens_model = min(settings.config.max_model_tokens, max_tokens_model) max_tokens_model = min(settings.config.max_model_tokens, max_tokens_model)
# get_logger().debug(f"limiting max tokens to {max_tokens_model}") # get_logger().debug(f"limiting max tokens to {max_tokens_model}")
return max_tokens_model return max_tokens_model
def clip_tokens(text: str, max_tokens: int, add_three_dots=True) -> str:
"""
Clip the number of tokens in a string to a maximum number of tokens.
Args:
text (str): The string to clip.
max_tokens (int): The maximum number of tokens allowed in the string.
add_three_dots (bool, optional): A boolean indicating whether to add three dots at the end of the clipped
Returns:
str: The clipped string.
"""
if not text:
return text
try:
encoder = get_token_encoder()
num_input_tokens = len(encoder.encode(text))
if num_input_tokens <= max_tokens:
return text
num_chars = len(text)
chars_per_token = num_chars / num_input_tokens
num_output_chars = int(chars_per_token * max_tokens)
clipped_text = text[:num_output_chars]
if add_three_dots:
clipped_text += "...(truncated)"
return clipped_text
except Exception as e:
get_logger().warning(f"Failed to clip tokens: {e}")
return text

View File

@ -23,22 +23,18 @@ For example:
- cli.py --issue_url=... similar_issue - cli.py --issue_url=... similar_issue
Supported commands: Supported commands:
- review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement. -review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement.
- ask / ask_question [question] - Ask a question about the PR. -ask / ask_question [question] - Ask a question about the PR.
- describe / describe_pr - Modify the PR title and description based on the PR's contents. -describe / describe_pr - Modify the PR title and description based on the PR's contents.
- improve / improve_code - Suggest improvements to the code in the PR as pull request comments ready to commit. -improve / improve_code - Suggest improvements to the code in the PR as pull request comments ready to commit.
Extended mode ('improve --extended') employs several calls, and provides a more thorough feedback Extended mode ('improve --extended') employs several calls, and provides a more thorough feedback
- reflect - Ask the PR author questions about the PR. -reflect - Ask the PR author questions about the PR.
- update_changelog - Update the changelog based on the PR's contents. -update_changelog - Update the changelog based on the PR's contents.
- add_docs
- generate_labels
Configuration: Configuration:

View File

@ -1,6 +1,5 @@
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
from pr_agent.git_providers.bitbucket_server_provider import BitbucketServerProvider
from pr_agent.git_providers.codecommit_provider import CodeCommitProvider from pr_agent.git_providers.codecommit_provider import CodeCommitProvider
from pr_agent.git_providers.github_provider import GithubProvider from pr_agent.git_providers.github_provider import GithubProvider
from pr_agent.git_providers.gitlab_provider import GitLabProvider from pr_agent.git_providers.gitlab_provider import GitLabProvider
@ -13,7 +12,6 @@ _GIT_PROVIDERS = {
'github': GithubProvider, 'github': GithubProvider,
'gitlab': GitLabProvider, 'gitlab': GitLabProvider,
'bitbucket': BitbucketProvider, 'bitbucket': BitbucketProvider,
'bitbucket_server': BitbucketServerProvider,
'azure': AzureDevopsProvider, 'azure': AzureDevopsProvider,
'codecommit': CodeCommitProvider, 'codecommit': CodeCommitProvider,
'local' : LocalGitProvider, 'local' : LocalGitProvider,

View File

@ -14,8 +14,9 @@ try:
except ImportError: except ImportError:
AZURE_DEVOPS_AVAILABLE = False AZURE_DEVOPS_AVAILABLE = False
from ..algo.pr_processing import clip_tokens
from ..config_loader import get_settings from ..config_loader import get_settings
from ..algo.utils import load_large_diff, clip_tokens from ..algo.utils import load_large_diff
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from .git_provider import EDIT_TYPE, FilePatchInfo from .git_provider import EDIT_TYPE, FilePatchInfo

View File

@ -153,29 +153,17 @@ class BitbucketProvider(GitProvider):
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
def get_latest_commit_url(self): def publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str):
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_header in body: if initial_text in body:
latest_commit_url = self.get_latest_commit_url() if updated_text:
comment_url = self.get_comment_url(comment) pr_comment_updated = pr_comment.replace(initial_text, updated_text)
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}")
@ -228,10 +216,6 @@ class BitbucketProvider(GitProvider):
) )
return response return response
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
link = f"{self.pr_url}/#L{relevant_file}T{relevant_line_start}"
return link
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
relevant_file = suggestion['relevant file'].strip('`').strip("'") relevant_file = suggestion['relevant file'].strip('`').strip("'")
@ -254,15 +238,7 @@ class BitbucketProvider(GitProvider):
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
for comment in comments: for comment in comments:
if 'position' in comment: self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
elif 'start_line' in comment: # multi-line comment
# note that bitbucket does not seem to support range - only a comment on a single line - https://community.developer.atlassian.com/t/api-post-endpoint-for-inline-pull-request-comments/60452
self.publish_inline_comment(comment['body'], comment['start_line'], comment['path'])
elif 'line' in comment: # single-line comment
self.publish_inline_comment(comment['body'], comment['line'], comment['path'])
else:
get_logger().error(f"Could not publish inline comment {comment}")
def get_title(self): def get_title(self):
return self.pr.title return self.pr.title

View File

@ -1,351 +0,0 @@
import json
from typing import Optional, Tuple
from urllib.parse import urlparse
import requests
from atlassian.bitbucket import Bitbucket
from starlette_context import context
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file
from ..algo.utils import load_large_diff
from ..config_loader import get_settings
from ..log import get_logger
class BitbucketServerProvider(GitProvider):
def __init__(
self, pr_url: Optional[str] = None, incremental: Optional[bool] = False
):
s = requests.Session()
try:
bearer = context.get("bitbucket_bearer_token", None)
s.headers["Authorization"] = f"Bearer {bearer}"
except Exception:
s.headers[
"Authorization"
] = f'Bearer {get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN", None)}'
s.headers["Content-Type"] = "application/json"
self.headers = s.headers
self.bitbucket_server_url = None
self.workspace_slug = None
self.repo_slug = None
self.repo = None
self.pr_num = None
self.pr = None
self.pr_url = pr_url
self.temp_comments = []
self.incremental = incremental
self.diff_files = None
self.bitbucket_pull_request_api_url = pr_url
self.bitbucket_server_url = self._parse_bitbucket_server(url=pr_url)
self.bitbucket_client = Bitbucket(url=self.bitbucket_server_url,
token=get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN", None))
if pr_url:
self.set_pr(pr_url)
def get_repo_settings(self):
try:
url = (f"{self.bitbucket_server_url}/projects/{self.workspace_slug}/repos/{self.repo_slug}/src/"
f"{self.pr.destination_branch}/.pr_agent.toml")
response = requests.request("GET", url, headers=self.headers)
if response.status_code == 404: # not found
return ""
contents = response.text.encode('utf-8')
return contents
except Exception:
return ""
def publish_code_suggestions(self, code_suggestions: list) -> bool:
"""
Publishes code suggestions as comments on the PR.
"""
post_parameters_list = []
for suggestion in code_suggestions:
body = suggestion["body"]
relevant_file = suggestion["relevant_file"]
relevant_lines_start = suggestion["relevant_lines_start"]
relevant_lines_end = suggestion["relevant_lines_end"]
if not relevant_lines_start or relevant_lines_start == -1:
if get_settings().config.verbosity_level >= 2:
get_logger().exception(
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
)
continue
if relevant_lines_end < relevant_lines_start:
if get_settings().config.verbosity_level >= 2:
get_logger().exception(
f"Failed to publish code suggestion, "
f"relevant_lines_end is {relevant_lines_end} and "
f"relevant_lines_start is {relevant_lines_start}"
)
continue
if relevant_lines_end > relevant_lines_start:
post_parameters = {
"body": body,
"path": relevant_file,
"line": relevant_lines_end,
"start_line": relevant_lines_start,
"start_side": "RIGHT",
}
else: # API is different for single line comments
post_parameters = {
"body": body,
"path": relevant_file,
"line": relevant_lines_start,
"side": "RIGHT",
}
post_parameters_list.append(post_parameters)
try:
self.publish_inline_comments(post_parameters_list)
return True
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().error(f"Failed to publish code suggestion, error: {e}")
return False
def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'get_labels', 'gfm_markdown']:
return False
return True
def set_pr(self, pr_url: str):
self.workspace_slug, self.repo_slug, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr()
def get_file(self, path: str, commit_id: str):
file_content = ""
try:
file_content = self.bitbucket_client.get_content_of_file(self.workspace_slug,
self.repo_slug,
path,
commit_id)
except requests.HTTPError as e:
get_logger().debug(f"File {path} not found at commit id: {commit_id}")
return file_content
def get_files(self):
changes = self.bitbucket_client.get_pull_requests_changes(self.workspace_slug, self.repo_slug, self.pr_num)
diffstat = [change["path"]['toString'] for change in changes]
return diffstat
def get_diff_files(self) -> list[FilePatchInfo]:
if self.diff_files:
return self.diff_files
commits_in_pr = self.bitbucket_client.get_pull_requests_commits(
self.workspace_slug,
self.repo_slug,
self.pr_num
)
commit_list = list(commits_in_pr)
base_sha, head_sha = commit_list[0]['parents'][0]['id'], commit_list[-1]['id']
diff_files = []
original_file_content_str = ""
new_file_content_str = ""
changes = self.bitbucket_client.get_pull_requests_changes(self.workspace_slug, self.repo_slug, self.pr_num)
for change in changes:
file_path = change['path']['toString']
match change['type']:
case 'ADD':
edit_type = EDIT_TYPE.ADDED
new_file_content_str = self.get_file(file_path, head_sha)
if isinstance(new_file_content_str, (bytes, bytearray)):
new_file_content_str = new_file_content_str.decode("utf-8")
original_file_content_str = ""
case 'DELETE':
edit_type = EDIT_TYPE.DELETED
new_file_content_str = ""
original_file_content_str = self.get_file(file_path, base_sha)
if isinstance(original_file_content_str, (bytes, bytearray)):
original_file_content_str = original_file_content_str.decode("utf-8")
case 'RENAME':
edit_type = EDIT_TYPE.RENAMED
case _:
edit_type = EDIT_TYPE.MODIFIED
original_file_content_str = self.get_file(file_path, base_sha)
if isinstance(original_file_content_str, (bytes, bytearray)):
original_file_content_str = original_file_content_str.decode("utf-8")
new_file_content_str = self.get_file(file_path, head_sha)
if isinstance(new_file_content_str, (bytes, bytearray)):
new_file_content_str = new_file_content_str.decode("utf-8")
patch = load_large_diff(file_path, new_file_content_str, original_file_content_str)
diff_files.append(
FilePatchInfo(
original_file_content_str,
new_file_content_str,
patch,
file_path,
edit_type=edit_type,
)
)
self.diff_files = diff_files
return diff_files
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
if not is_temporary:
self.bitbucket_client.add_pull_request_comment(self.workspace_slug, self.repo_slug, self.pr_num, pr_comment)
def remove_initial_comment(self):
try:
for comment in self.temp_comments:
self.remove_comment(comment)
except ValueError as e:
get_logger().exception(f"Failed to remove temp comments, error: {e}")
def remove_comment(self, comment):
pass
# funtion to create_inline_comment
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
position, absolute_position = find_line_number_of_relevant_line_in_file(
self.get_diff_files(),
relevant_file.strip('`'),
relevant_line_in_file
)
if position == -1:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
subject_type = "FILE"
else:
subject_type = "LINE"
path = relevant_file.strip()
return dict(body=body, path=path, position=absolute_position) if subject_type == "LINE" else {}
def publish_inline_comment(self, comment: str, from_line: int, file: str):
payload = {
"text": comment,
"severity": "NORMAL",
"anchor": {
"diffType": "EFFECTIVE",
"path": file,
"lineType": "ADDED",
"line": from_line,
"fileType": "TO"
}
}
response = requests.post(url=self._get_pr_comments_url(), json=payload, headers=self.headers)
return response
def generate_link_to_relevant_line_number(self, suggestion) -> str:
try:
relevant_file = suggestion['relevant file'].strip('`').strip("'")
relevant_line_str = suggestion['relevant line']
if not relevant_line_str:
return ""
diff_files = self.get_diff_files()
position, absolute_position = find_line_number_of_relevant_line_in_file \
(diff_files, relevant_file, relevant_line_str)
if absolute_position != -1 and self.pr_url:
link = f"{self.pr_url}/#L{relevant_file}T{absolute_position}"
return link
except Exception as e:
if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Failed adding line link, error: {e}")
return ""
def publish_inline_comments(self, comments: list[dict]):
for comment in comments:
self.publish_inline_comment(comment['body'], comment['position'], comment['path'])
def get_title(self):
return self.pr.title
def get_languages(self):
return {"yaml": 0} # devops LOL
def get_pr_branch(self):
return self.pr.fromRef['displayId']
def get_pr_description_full(self):
return self.pr.description
def get_user_id(self):
return 0
def get_issue_comments(self):
raise NotImplementedError(
"Bitbucket provider does not support issue comments yet"
)
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
return True
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
return True
@staticmethod
def _parse_bitbucket_server(url: str) -> str:
parsed_url = urlparse(url)
return f"{parsed_url.scheme}://{parsed_url.netloc}"
@staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]:
parsed_url = urlparse(pr_url)
path_parts = parsed_url.path.strip("/").split("/")
if len(path_parts) < 6 or path_parts[4] != "pull-requests":
raise ValueError(
"The provided URL does not appear to be a Bitbucket PR URL"
)
workspace_slug = path_parts[1]
repo_slug = path_parts[3]
try:
pr_number = int(path_parts[5])
except ValueError as e:
raise ValueError("Unable to convert PR number to integer") from e
return workspace_slug, repo_slug, pr_number
def _get_repo(self):
if self.repo is None:
self.repo = self.bitbucket_client.get_repo(self.workspace_slug, self.repo_slug)
return self.repo
def _get_pr(self):
pr = self.bitbucket_client.get_pull_request(self.workspace_slug, self.repo_slug, pull_request_id=self.pr_num)
return type('new_dict', (object,), pr)
def _get_pr_file_content(self, remote_link: str):
return ""
def get_commit_messages(self):
def get_commit_messages(self):
raise NotImplementedError("Get commit messages function not implemented yet.")
# bitbucket does not support labels
def publish_description(self, pr_title: str, description: str):
payload = json.dumps({
"description": description,
"title": pr_title
})
response = requests.put(url=self.bitbucket_pull_request_api_url, headers=self.headers, data=payload)
return response
# bitbucket does not support labels
def publish_labels(self, pr_types: list):
pass
# bitbucket does not support labels
def get_labels(self):
pass
def _get_pr_comments_url(self):
return f"{self.bitbucket_server_url}/rest/api/latest/projects/{self.workspace_slug}/repos/{self.repo_slug}/pull-requests/{self.pr_num}/comments"

View File

@ -6,9 +6,9 @@ from urllib.parse import urlparse
from pr_agent.git_providers.codecommit_client import CodeCommitClient from pr_agent.git_providers.codecommit_client import CodeCommitClient
from ..algo.language_handler import is_valid_file, language_extension_map
from ..algo.utils import load_large_diff from ..algo.utils import load_large_diff
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
from ..config_loader import get_settings
from ..log import get_logger from ..log import get_logger
@ -269,8 +269,6 @@ class CodeCommitProvider(GitProvider):
# where each dictionary item is a language name. # where each dictionary item is a language name.
# We build that language->extension dictionary here in main_extensions_flat. # We build that language->extension dictionary here in main_extensions_flat.
main_extensions_flat = {} main_extensions_flat = {}
language_extension_map_org = get_settings().language_extension_map_org
language_extension_map = {k.lower(): v for k, v in language_extension_map_org.items()}
for language, extensions in language_extension_map.items(): for language, extensions in language_extension_map.items():
for ext in extensions: for ext in extensions:
main_extensions_flat[ext] = language main_extensions_flat[ext] = language

View File

@ -5,7 +5,6 @@ from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger from pr_agent.log import get_logger
@ -41,10 +40,45 @@ 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
@ -63,7 +97,7 @@ 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.utils import clip_tokens from pr_agent.algo.pr_processing import clip_tokens
max_tokens_description = 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_description: if max_tokens_description:
@ -82,58 +116,12 @@ class GitProvider(ABC):
# otherwise, extract the original user description from the existing pr-agent description and return it # otherwise, extract the original user description from the existing pr-agent description and return it
return description.split("## User Description:", 1)[1].strip() return description.split("## User Description:", 1)[1].strip()
@abstractmethod
def get_repo_settings(self):
pass
def get_pr_id(self):
return ""
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
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
@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
def remove_initial_comment(self):
pass
@abstractmethod
def remove_comment(self, comment):
pass
@abstractmethod @abstractmethod
def get_issue_comments(self): def get_issue_comments(self):
pass pass
def get_comment_url(self, comment) -> str:
return ""
#### labels operations ####
@abstractmethod @abstractmethod
def publish_labels(self, labels): def get_repo_settings(self):
pass
@abstractmethod
def get_labels(self):
pass pass
@abstractmethod @abstractmethod
@ -144,12 +132,11 @@ 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_latest_commit_url(self) -> str: def get_pr_id(self):
return "" return ""
def get_main_pr_language(languages, files) -> str: def get_main_pr_language(languages, files) -> str:
@ -177,42 +164,26 @@ def get_main_pr_language(languages, files) -> str:
extension_list.append(file.filename.rsplit('.')[-1]) extension_list.append(file.filename.rsplit('.')[-1])
# get the most common extension # get the most common extension
most_common_extension = '.' + max(set(extension_list), key=extension_list.count) most_common_extension = max(set(extension_list), key=extension_list.count)
try:
language_extension_map_org = get_settings().language_extension_map_org
language_extension_map = {k.lower(): v for k, v in language_extension_map_org.items()}
if top_language in language_extension_map and most_common_extension in language_extension_map[top_language]: # look for a match. TBD: add more languages, do this systematically
main_language_str = top_language if most_common_extension == 'py' and top_language == 'python' or \
else: most_common_extension == 'js' and top_language == 'javascript' or \
for language, extensions in language_extension_map.items(): most_common_extension == 'ts' and top_language == 'typescript' or \
if most_common_extension in extensions: most_common_extension == 'go' and top_language == 'go' or \
main_language_str = language most_common_extension == 'java' and top_language == 'java' or \
break most_common_extension == 'c' and top_language == 'c' or \
except Exception as e: most_common_extension == 'cpp' and top_language == 'c++' or \
get_logger().exception(f"Failed to get main language: {e}") most_common_extension == 'cs' and top_language == 'c#' or \
pass most_common_extension == 'swift' and top_language == 'swift' or \
most_common_extension == 'php' and top_language == 'php' or \
## old approach: most_common_extension == 'rb' and top_language == 'ruby' or \
# most_common_extension = max(set(extension_list), key=extension_list.count) most_common_extension == 'rs' and top_language == 'rust' or \
# if most_common_extension == 'py' and top_language == 'python' or \ most_common_extension == 'scala' and top_language == 'scala' or \
# most_common_extension == 'js' and top_language == 'javascript' or \ most_common_extension == 'kt' and top_language == 'kotlin' or \
# most_common_extension == 'ts' and top_language == 'typescript' or \ most_common_extension == 'pl' and top_language == 'perl' or \
# most_common_extension == 'tsx' and top_language == 'typescript' or \ most_common_extension == top_language:
# most_common_extension == 'go' and top_language == 'go' or \ main_language_str = top_language
# most_common_extension == 'java' and top_language == 'java' or \
# most_common_extension == 'c' and top_language == 'c' or \
# most_common_extension == 'cpp' and top_language == 'c++' or \
# most_common_extension == 'cs' and top_language == 'c#' or \
# most_common_extension == 'swift' and top_language == 'swift' or \
# most_common_extension == 'php' and top_language == 'php' or \
# most_common_extension == 'rb' and top_language == 'ruby' or \
# most_common_extension == 'rs' and top_language == 'rust' or \
# most_common_extension == 'scala' and top_language == 'scala' or \
# most_common_extension == 'kt' and top_language == 'kotlin' or \
# most_common_extension == 'pl' and top_language == 'perl' or \
# most_common_extension == top_language:
# main_language_str = top_language
except Exception as e: except Exception as e:
get_logger().exception(e) get_logger().exception(e)

View File

@ -8,8 +8,8 @@ from retry import retry
from starlette_context import context from starlette_context import context
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file
from ..algo.utils import load_large_diff, clip_tokens 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
@ -154,28 +154,16 @@ 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: def publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: 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_header): if body.startswith(initial_text):
latest_commit_url = self.get_latest_commit_url() if updated_text:
comment_url = self.get_comment_url(comment) pr_comment_updated = pr_comment.replace(initial_text, updated_text)
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)
@ -405,7 +393,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, base_url=get_settings().github.base_url) return Github(app_auth=auth)
if deployment_type == 'user': if deployment_type == 'user':
try: try:
@ -414,7 +402,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), base_url=get_settings().github.base_url) return Github(auth=Auth.Token(token))
def _get_repo(self): def _get_repo(self):
if hasattr(self, 'repo_obj') and \ if hasattr(self, 'repo_obj') and \
@ -501,15 +489,6 @@ class GithubProvider(GitProvider):
return "" return ""
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
sha_file = hashlib.sha256(relevant_file.encode('utf-8')).hexdigest()
if relevant_line_end:
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{relevant_line_start}-R{relevant_line_end}"
else:
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{relevant_line_start}"
return link
def get_pr_id(self): def get_pr_id(self):
try: try:
pr_id = f"{self.repo}/{self.pr_num}" pr_id = f"{self.repo}/{self.pr_num}"

View File

@ -7,8 +7,8 @@ import gitlab
from gitlab import GitlabGetError from gitlab import GitlabGetError
from ..algo.language_handler import is_valid_file from ..algo.language_handler import is_valid_file
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file
from ..algo.utils import load_large_diff, clip_tokens from ..algo.utils import load_large_diff
from ..config_loader import get_settings from ..config_loader import get_settings
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
from ..log import get_logger from ..log import get_logger
@ -43,7 +43,7 @@ class GitLabProvider(GitProvider):
self.incremental = incremental self.incremental = incremental
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']: # gfm_markdown is supported in gitlab ! if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'gfm_markdown']:
return False return False
return True return True
@ -136,27 +136,15 @@ 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): def publish_persistent_comment(self, pr_comment: str, initial_text: str, updated_text: str):
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_header): if comment.body.startswith(initial_text):
latest_commit_url = self.get_latest_commit_url() if updated_text:
comment_url = self.get_comment_url(comment) pr_comment_updated = pr_comment.replace(initial_text, updated_text)
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}")
@ -422,14 +410,6 @@ class GitLabProvider(GitProvider):
except: except:
return "" return ""
def get_line_link(self, relevant_file: str, relevant_line_start: int, relevant_line_end: int = None) -> str:
if relevant_line_end:
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{relevant_line_start}-L{relevant_line_end}"
else:
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{relevant_line_start}"
return link
def generate_link_to_relevant_line_number(self, suggestion) -> str: def generate_link_to_relevant_line_number(self, suggestion) -> str:
try: try:
relevant_file = suggestion['relevant file'].strip('`').strip("'") relevant_file = suggestion['relevant file'].strip('`').strip("'")

View File

@ -1,64 +0,0 @@
import json
import uvicorn
from fastapi import APIRouter, FastAPI
from fastapi.encoders import jsonable_encoder
from starlette import status
from starlette.background import BackgroundTasks
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette_context.middleware import RawContextMiddleware
from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
router = APIRouter()
def handle_request(background_tasks: BackgroundTasks, url: str, body: str, log_context: dict):
log_context["action"] = body
log_context["event"] = "pull_request" if body == "review" else "comment"
log_context["api_url"] = url
with get_logger().contextualize(**log_context):
background_tasks.add_task(PRAgent().handle_request, url, body)
@router.post("/webhook")
async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
log_context = {"server_type": "bitbucket_server"}
data = await request.json()
get_logger().info(json.dumps(data))
pr_id = data['pullRequest']['id']
repository_name = data['pullRequest']['toRef']['repository']['slug']
project_name = data['pullRequest']['toRef']['repository']['project']['key']
bitbucket_server = get_settings().get("BITBUCKET_SERVER.URL")
pr_url = f"{bitbucket_server}/projects/{project_name}/repos/{repository_name}/pull-requests/{pr_id}"
log_context["api_url"] = pr_url
log_context["event"] = "pull_request"
handle_request(background_tasks, pr_url, "review", log_context)
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
@router.get("/")
async def root():
return {"status": "ok"}
def start():
bitbucket_server_url = get_settings().get("BITBUCKET_SERVER.URL", None)
if not bitbucket_server_url:
raise ValueError("BITBUCKET_SERVER.URL is not set")
get_settings().config.git_provider = "bitbucket_server"
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router)
uvicorn.run(app, host="0.0.0.0", port=3000)
if __name__ == '__main__':
start()

View File

@ -1,7 +1,6 @@
import asyncio import asyncio
import json import json
import os import os
from typing import Union
from pr_agent.agent.pr_agent import PRAgent from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
@ -13,22 +12,6 @@ from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_reviewer import PRReviewer from pr_agent.tools.pr_reviewer import PRReviewer
def is_true(value: Union[str, bool]) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() == 'true'
return False
def get_setting_or_env(key: str, default: Union[str, bool] = None) -> Union[str, bool]:
try:
value = get_settings().get(key, default)
except AttributeError: # TBD still need to debug why this happens on GitHub Actions
value = os.getenv(key, None) or os.getenv(key.upper(), None) or os.getenv(key.lower(), None) or default
return value
async def run_action(): async def run_action():
# Get environment variables # Get environment variables
GITHUB_EVENT_NAME = os.environ.get('GITHUB_EVENT_NAME') GITHUB_EVENT_NAME = os.environ.get('GITHUB_EVENT_NAME')
@ -82,14 +65,14 @@ 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 = get_setting_or_env("GITHUB_ACTION.AUTO_REVIEW", None) auto_review = os.environ.get('github_action.auto_review', None)
if auto_review is None or is_true(auto_review): 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 = get_setting_or_env("GITHUB_ACTION.AUTO_DESCRIBE", None) auto_describe = os.environ.get('github_action.auto_describe', None)
if is_true(auto_describe): if isinstance(auto_describe, str) and auto_describe.lower() == 'true':
await PRDescription(pr_url).run() await PRDescription(pr_url).run()
auto_improve = get_setting_or_env("GITHUB_ACTION.AUTO_IMPROVE", None) auto_improve = os.environ.get('github_action.auto_improve', None)
if is_true(auto_improve): if isinstance(auto_improve, str) and auto_improve.lower() == 'true':
await PRCodeSuggestions(pr_url).run() await PRCodeSuggestions(pr_url).run()
# Handle issue comment event # Handle issue comment event
@ -116,4 +99,4 @@ async def run_action():
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(run_action()) asyncio.run(run_action())

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.get("token_name", secret_dict.get("id", "unknown")) log_context["sender"] = secret_dict["id"]
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,14 +1,12 @@
commands_text = "> **/review**: Request a review of your Pull Request.\n" \ commands_text = "> **/review [-i]**: Request a review of your Pull Request. For an incremental review, which only " \
"> **/describe**: Update the PR title and description based on the contents of the PR.\n" \ "considers changes since the last review, include the '-i' option.\n" \
"> **/improve [--extended]**: Suggest code improvements. Extended mode provides a higher quality feedback.\n" \ "> **/describe**: Modify the PR title and description based on the contents of the PR.\n" \
"> **/ask \\<QUESTION\\>**: Ask a question about the PR.\n" \ "> **/improve [--extended]**: Suggest improvements to the code in the PR. Extended mode employs several calls, and provides a more thorough feedback. \n" \
"> **/update_changelog**: Update the changelog based on the PR's contents.\n" \ "> **/ask \\<QUESTION\\>**: Pose a question about the PR.\n" \
"> **/add_docs**: Generate docstring for new components introduced in the PR.\n" \ "> **/update_changelog**: Update the changelog based on the PR's contents.\n\n" \
"> **/generate_labels**: Generate labels for the PR based on the PR's contents.\n" \ ">To edit any configuration parameter from **configuration.toml**, add --config_path=new_value\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, add a **/config** comment.\n" \ ">To list the possible configuration parameters, use the **/config** command.\n" \
def bot_help_text(user: str): def bot_help_text(user: str):

View File

@ -3,8 +3,10 @@ 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,10 +36,6 @@ 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=0 # 0,1,2 verbosity_level=2 # 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,13 +16,11 @@ 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
@ -30,9 +28,6 @@ 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
@ -57,7 +52,6 @@ include_generated_by_header=true
[pr_code_suggestions] # /improve # [pr_code_suggestions] # /improve #
num_code_suggestions=4 num_code_suggestions=4
summarize = false
extra_instructions = "" extra_instructions = ""
rank_suggestions = false rank_suggestions = false
# params for '/improve --extended' mode # params for '/improve --extended' mode
@ -80,7 +74,6 @@ 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

@ -3,16 +3,16 @@ enable_custom_labels=false
## template for custom labels ## template for custom labels
#[custom_labels."Bug fix"] #[custom_labels."Bug fix"]
#description = """Fixes a bug in the code""" #description = "Fixes a bug in the code"
#[custom_labels."Tests"] #[custom_labels."Tests"]
#description = """Adds or modifies tests""" #description = "Adds or modifies tests"
#[custom_labels."Bug fix with tests"] #[custom_labels."Bug fix with tests"]
#description = """Fixes a bug in the code and adds or modifies tests""" #description = "Fixes a bug in the code and adds or modifies tests"
#[custom_labels."Refactoring"] #[custom_labels."Refactoring"]
#description = """Code refactoring without changing functionality""" #description = "Code refactoring without changing functionality"
#[custom_labels."Enhancement"] #[custom_labels."Enhancement"]
#description = """Adds new features or functionality""" #description = "Adds new features or functionality"
#[custom_labels."Documentation"] #[custom_labels."Documentation"]
#description = """Adds or modifies documentation""" #description = "Adds or modifies documentation"
#[custom_labels."Other"] #[custom_labels."Other"]
#description = """Other changes that do not fit in any of the above categories""" #description = "Other changes that do not fit in any of the above categories"

View File

@ -1,6 +1,6 @@
[pr_add_docs_prompt] [pr_add_docs_prompt]
system="""You are a language model called PR-Code-Documentation Agent, that specializes in generating documentation for code. system="""You are a language model called PR-Code-Documentation Agent, that specializes in generating documentation for code.
Your task is to generate meaningfull {{ docs_for_language }} to a PR (lines starting with '+'). Your task is to generate meaningfull {{ docs_for_language }} to a PR (the '+' lines).
Example for a PR Diff input: Example for a PR Diff input:
' '
@ -103,7 +103,7 @@ Description: '{{description}}'
{%- if language %} {%- if language %}
Main PR language: '{{language}}' Main language: {{language}}
{%- endif %} {%- endif %}

View File

@ -1,6 +1,6 @@
[pr_code_suggestions_prompt] [pr_code_suggestions_prompt]
system="""You are PR-Reviewer, a language model that specializes in suggesting code improvements for a Pull Request (PR). system="""You are a language model called PR-Code-Reviewer, that specializes in suggesting code improvements for Pull Request (PR).
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR diff (lines starting with '+'). Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR (the '+' lines in the diff).
Example for a PR Diff input: Example for a PR Diff input:
' '
@ -90,19 +90,16 @@ 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: |- relevant lines start: 12
12 relevant lines end: 12
relevant lines end: |- improved code: |-
12 ...
improved code: |-
...
...
``` ```
@ -120,7 +117,7 @@ Description: '{{description}}'
{%- if language %} {%- if language %}
Main PR language: '{{ language }}' Main language: {{language}}
{%- endif %} {%- endif %}

View File

@ -1,10 +1,8 @@
[pr_custom_labels_prompt] [pr_custom_labels_prompt]
system="""You are PR-Reviewer, a language model designed to review a git Pull Request (PR). system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
Your task is to provide labels that describe the PR content. Your task is to label the type of the PR content.
{%- if enable_custom_labels %} - Make sure not to focus the new PR code (the '+' lines).
Thoroughly read the labels name and the provided description, and decide whether the label is relevant to the PR. - If needed, each YAML output should be in block scalar format ('|-')
{%- endif %}
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
@ -13,56 +11,52 @@ Extra instructions from the user:
' '
{% endif %} {% endif %}
You must use the following YAML schema to format your answer:
The output must be a YAML object equivalent to type $Labels, according to the following Pydantic definitions: ```yaml
' PR Type:
type: array
{%- if enable_custom_labels %} {%- if enable_custom_labels %}
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.
{{ custom_labels_class }} {%- endif %}
items:
{%- else %} type: string
class Label(str, Enum): enum:
bug_fix = "Bug fix" {%- if enable_custom_labels %}
tests = "Tests" {{ custom_labels }}
refactoring = "Refactoring" {%- else %}
enhancement = "Enhancement" - Bug fix
documentation = "Documentation" - Tests
other = "Other" - Refactoring
- Enhancement
- Documentation
- Other
{%- endif %} {%- endif %}
class Labels(BaseModel):
labels: List[Label] = Field(min_items=0, description="custom labels that describe the PR. Return the label value, not the name.")
'
Example output: Example output:
```yaml ```yaml
labels: PR Type:
- ... {%- if enable_custom_labels %}
- ... {{ custom_labels_examples }}
{%- else %}
- Bug fix
{%- endif %}
``` ```
Answer should be a valid YAML, and nothing else. Make sure to output a valid YAML. Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
""" """
user="""PR Info: user="""PR Info:
Previous title: '{{title}}' Previous title: '{{title}}'
Previous description: '{{description}}'
Branch: '{{ branch }}' Branch: '{{branch}}'
Description: '{{ description }}'
{%- if language %} {%- if language %}
Main PR language: '{{ language }}' Main language: {{language}}
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
' {{commit_messages_str}}
{{ commit_messages_str }}
'
{%- endif %} {%- endif %}

View File

@ -1,9 +1,9 @@
[pr_description_prompt] [pr_description_prompt]
system="""You are PR-Reviewer, a language model designed to review a git Pull Request (PR). system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
Your task is to provide a full description for the PR content. Your task is to provide full description of a Pull Request (PR) content.
- Make sure to focus on the new PR code (lines starting with '+'). - Make sure to focus on the new PR code (the '+' lines).
- Keep in mind that the 'Previous title', 'Previous description' and 'Commit messages' sections may be partial, simplistic, non-informative or out of date. Hence, compare them to the PR diff code, and use them only as a reference. - Notice that the 'Previous title', 'Previous description' and 'Commit messages' sections may be partial, simplistic, non-informative or not up-to-date. Hence, compare them to the PR diff code, and use them only as a reference.
- Prioritize the most significant PR changes first, followed by the minor ones. - Emphasize first the most important changes, and then the less important ones.
- If needed, each YAML output should be in block scalar format ('|-') - If needed, each YAML output should be in block scalar format ('|-')
{%- if extra_instructions %} {%- if extra_instructions %}
@ -13,83 +13,81 @@ Extra instructions from the user:
' '
{% endif %} {% endif %}
You must use the following YAML schema to format your answer:
The output must be a YAML object equivalent to type $PRDescription, according to the following Pydantic definitions: ```yaml
' PR Title:
class PRType(str, Enum): type: string
bug_fix = "Bug fix" description: an informative title for the PR, describing its main theme
tests = "Tests" PR Type:
refactoring = "Refactoring" type: string
enhancement = "Enhancement" enum:
documentation = "Documentation" - Bug fix
other = "Other" - Tests
- Refactoring
- Enhancement
- Documentation
- Other
{%- if enable_custom_labels %} {%- if enable_custom_labels %}
PR Labels:
{{ custom_labels_class }} type: array
description: Labels that are applicable to the Pull Request. Don't output the description in the parentheses. If none of the labels is relevant to the PR, output an empty array.
items:
type: string
enum:
{{ custom_labels }}
{%- endif %} {%- endif %}
PR Description:
class FileWalkthrough(BaseModel): type: string
filename: str = Field(description="the relevant file full path") description: an informative and concise description of the PR.
changes_in_file: str = Field(description="minimal and concise description of the changes in the relevant file") {%- if use_bullet_points %} Use bullet points. {% endif %}
PR Main Files Walkthrough:
Class PRDescription(BaseModel): type: array
title: str = Field(description="an informative title for the PR, describing its main theme") maxItems: 10
type: List[PRType] = Field(description="one or more types that describe the PR type. . Return the label value, not the name.") description: |-
description: str = Field(description="an informative and concise description of the PR. {%- if use_bullet_points %} Use bullet points. {% endif %}") a walkthrough of the PR changes. Review main files, and shortly describe the changes in each file (up to 10 most important files).
{%- if enable_custom_labels %} items:
labels: List[Label] = Field(min_items=0, description="custom labels that describe the PR. Return the label value, not the name.") filename:
{%- endif %} type: string
main_files_walkthrough: List[FileWalkthrough] = Field(max_items=10) description: the relevant file full path
' changes in file:
type: string
description: minimal and concise description of the changes in the relevant file
```
Example output: Example output:
```yaml ```yaml
title: |- PR Title: |-
...
PR Type:
... ...
type:
- ...
- ...
{%- if enable_custom_labels %} {%- if enable_custom_labels %}
labels: PR Labels:
- ... - ...
- ... - ...
{%- endif %} {%- endif %}
description: |- PR Description: |-
... ...
main_files_walkthrough: PR Main Files Walkthrough:
- ... - ...
- ... - ...
``` ```
Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|-') Make sure to output a valid YAML. Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
""" """
user="""PR Info: user="""PR Info:
Previous title: '{{title}}' Previous title: '{{title}}'
Previous description: '{{description}}'
{%- if description %}
Previous description:
'
{{ description }}
'
{%- endif %}
Branch: '{{branch}}' Branch: '{{branch}}'
{%- if language %} {%- if language %}
Main PR language: '{{ language }}' Main language: {{language}}
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
' {{commit_messages_str}}
{{ commit_messages_str }}
'
{%- endif %} {%- endif %}
@ -97,8 +95,6 @@ The PR Git Diff:
``` ```
{{diff}} {{diff}}
``` ```
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions, and ' ' (a space) for unchanged lines. Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions, and ' ' (a space) for unchanged lines.
Response (should be a valid YAML, and nothing else): Response (should be a valid YAML, and nothing else):

View File

@ -1,5 +1,5 @@
[pr_information_from_user_prompt] [pr_information_from_user_prompt]
system="""You are PR-Reviewer, a language model designed to review a git Pull Request (PR). system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
Given the PR Info and the PR Git Diff, generate 3 short questions about the PR code for the PR author. Given the PR Info and the PR Git Diff, generate 3 short questions about the PR code for the PR author.
The goal of the questions is to help the language model understand the PR better, so the questions should be insightful, informative, non-trivial, and relevant to the PR. The goal of the questions is to help the language model understand the PR better, so the questions should be insightful, informative, non-trivial, and relevant to the PR.
You should prefer asking yes\\no questions, or multiple choice questions. Also add at least one open-ended question, but make sure they are not too difficult, and can be answered in a sentence or two. You should prefer asking yes\\no questions, or multiple choice questions. Also add at least one open-ended question, but make sure they are not too difficult, and can be answered in a sentence or two.
@ -16,21 +16,15 @@ Questions to better understand the PR:
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}' Description: '{{description}}'
{%- if language %} {%- if language %}
Main language: {{language}}
Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
'
{{commit_messages_str}} {{commit_messages_str}}
'
{%- endif %} {%- endif %}

View File

@ -1,29 +1,22 @@
[pr_questions_prompt] [pr_questions_prompt]
system="""You are PR-Reviewer, a language model designed to review a git Pull Request (PR). system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
Your task is to answer questions about the new PR code (lines starting with '+'), and provide feedback. Your task is to answer questions about the new PR code (the '+' lines), and provide feedback.
Be informative, constructive, and give examples. Try to be as specific as possible. Be informative, constructive, and give examples. Try to be as specific as possible.
Don't avoid answering the questions. You must answer the questions, as best as you can, without adding unrelated content. Don't avoid answering the questions. You must answer the questions, as best as you can, without adding unrelated content.
Make sure not to repeat modifications already implemented in the new PR code (the '+' lines). Make sure not to repeat modifications already implemented in the new PR code (the '+' lines).
""" """
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}' Description: '{{description}}'
{%- if language %} {%- if language %}
Main language: {{language}}
Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
' {{commit_messages_str}}
{{ commit_messages_str }}
'
{%- endif %} {%- endif %}

View File

@ -1,7 +1,6 @@
[pr_review_prompt] [pr_review_prompt]
system="""You are PR-Reviewer, a language model designed to review a git Pull Request (PR). system="""You are PR-Reviewer, a language model designed to review git pull requests.
Your task is to provide constructive and concise feedback for the PR, and also provide meaningful code suggestions. Your task is to provide constructive and concise feedback for the PR, and also provide meaningful code suggestions.
The review should focus on new code added in the PR diff (lines starting with '+')
Example PR Diff input: Example PR Diff input:
' '
@ -23,14 +22,14 @@ code line that already existed in the file....
... ...
' '
{%- if num_code_suggestions > 0 %} The review should focus on new code added in the PR (lines starting with '+'), and not on code that already existed in the file (lines starting with '-', or without prefix).
Code suggestions guidelines: {%- if num_code_suggestions > 0 %}
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions. - Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices. - Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code. - Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
- Don't suggest to add docstring, type hints, or comments. - Don't suggest to add docstring, type hints, or comments.
- Suggestions should focus on the new code added in the PR diff (lines starting with '+') - Suggestions should focus on improving the new code added in the PR (lines starting with '+')
{%- endif %} {%- endif %}
{%- if extra_instructions %} {%- if extra_instructions %}
@ -94,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). Use the format: '1, because ...' Explain your answer shortly (1-2 sentences).
{%- endif %} {%- endif %}
PR Feedback: PR Feedback:
General suggestions: General suggestions:
@ -131,8 +130,7 @@ PR Feedback:
Security concerns: Security concerns:
type: string type: string
description: >- description: >-
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. 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.
Answer 'Yes, because ...' if there are security concerns or issues. Explain your answer shortly.
{%- endif %} {%- endif %}
``` ```
@ -180,29 +178,16 @@ Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'desc
""" """
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}'
{%- if description %}
Description:
'
{{description}}
'
{%- endif %}
{%- if language %} {%- if language %}
Main language: {{language}}
Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
'
{{commit_messages_str}} {{commit_messages_str}}
'
{%- endif %} {%- endif %}
{%- if question_str %} {%- if question_str %}
@ -222,7 +207,7 @@ The PR Git Diff:
``` ```
{{diff}} {{diff}}
``` ```
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions. Focus on the '+' lines.
Response (should be a valid YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml

View File

@ -2,10 +2,10 @@
system=""" system="""
""" """
user="""You are given a list of code suggestions to improve a git Pull Request (PR): user="""You are given a list of code suggestions to improve a PR:
'
{{ suggestion_str|trim }} {{ suggestion_str|trim }}
'
Your task is to sort the code suggestions by their order of importance, and return a list with sorting order. Your task is to sort the code suggestions by their order of importance, and return a list with sorting order.
The sorting order is a list of pairs, where each pair contains the index of the suggestion in the original list. The sorting order is a list of pairs, where each pair contains the index of the suggestion in the original list.

View File

@ -15,23 +15,16 @@ Extra instructions from the user:
""" """
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}' Description: '{{description}}'
{%- if language %} {%- if language %}
Main language: {{language}}
Main PR language: '{{ language }}'
{%- endif %} {%- endif %}
{%- if commit_messages_str %} {%- if commit_messages_str %}
Commit messages: Commit messages:
' {{commit_messages_str}}
{{ commit_messages_str }}
'
{%- endif %} {%- endif %}

View File

@ -1,6 +1,7 @@
import copy import copy
import textwrap import textwrap
from typing import Dict, List from typing import Dict, List
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handler import AiHandler
@ -54,9 +55,9 @@ class PRCodeSuggestions:
try: try:
get_logger().info('Generating code suggestions for PR...') get_logger().info('Generating code suggestions for PR...')
if get_settings().config.publish_output: if get_settings().config.publish_output:
self.git_provider.publish_comment("Preparing suggestions...", is_temporary=True) self.git_provider.publish_comment("Preparing review...", is_temporary=True)
get_logger().info('Preparing PR code suggestions...') get_logger().info('Preparing PR review...')
if not self.is_extended: if not self.is_extended:
await retry_with_fallback_models(self._prepare_prediction) await retry_with_fallback_models(self._prepare_prediction)
data = self._prepare_pr_code_suggestions() data = self._prepare_pr_code_suggestions()
@ -72,14 +73,10 @@ class PRCodeSuggestions:
data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions']) data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions'])
if get_settings().config.publish_output: if get_settings().config.publish_output:
get_logger().info('Pushing PR code suggestions...') get_logger().info('Pushing PR review...')
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
if get_settings().pr_code_suggestions.summarize: get_logger().info('Pushing inline code suggestions...')
get_logger().info('Pushing summarize code suggestions...') self.push_inline_code_suggestions(data)
self.publish_summarizes_suggestions(data)
else:
get_logger().info('Pushing inline code suggestions...')
self.push_inline_code_suggestions(data)
except Exception as e: except Exception as e:
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}") get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
@ -119,7 +116,6 @@ class PRCodeSuggestions:
code_suggestions = [] code_suggestions = []
if not data['Code suggestions']: if not data['Code suggestions']:
get_logger().info('No suggestions found to improve this PR.')
return self.git_provider.publish_comment('No suggestions found to improve this PR.') return self.git_provider.publish_comment('No suggestions found to improve this PR.')
for d in data['Code suggestions']: for d in data['Code suggestions']:
@ -248,27 +244,4 @@ class PRCodeSuggestions:
return data_sorted return data_sorted
def publish_summarizes_suggestions(self, data: Dict):
try:
data_markdown = "## PR Code Suggestions\n\n"
for s in data['Code suggestions']:
code_snippet_link = self.git_provider.get_line_link(s['relevant file'], s['relevant lines start'],
s['relevant lines end'])
data_markdown += f"\n💡 Suggestion:\n\n**{s['suggestion content']}**\n\n"
if code_snippet_link:
data_markdown += f" File: [{s['relevant file']} ({s['relevant lines start']}-{s['relevant lines end']})]({code_snippet_link})\n\n"
else:
data_markdown += f"File: {s['relevant file']} ({s['relevant lines start']}-{s['relevant lines end']})\n\n"
if self.git_provider.is_supported("gfm_markdown"):
data_markdown += "<details> <summary> Example code:</summary>\n\n"
data_markdown += f"___\n\n"
data_markdown += f"Existing code:\n```{self.main_language}\n{s['existing code']}\n```\n"
data_markdown += f"Improved code:\n```{self.main_language}\n{s['improved code']}\n```\n"
if self.git_provider.is_supported("gfm_markdown"):
data_markdown += "</details>\n"
data_markdown += "\n___\n\n"
self.git_provider.publish_comment(data_markdown)
except Exception as e:
get_logger().info(f"Failed to publish summarized code suggestions, error: {e}")

View File

@ -44,7 +44,8 @@ class PRDescription:
"extra_instructions": get_settings().pr_description.extra_instructions, "extra_instructions": get_settings().pr_description.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(), "commit_messages_str": self.git_provider.get_commit_messages(),
"enable_custom_labels": get_settings().config.enable_custom_labels, "enable_custom_labels": get_settings().config.enable_custom_labels,
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function "custom_labels": "",
"custom_labels_examples": "",
} }
self.user_description = self.git_provider.get_user_description() self.user_description = self.git_provider.get_user_description()
@ -157,9 +158,6 @@ 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):
@ -174,16 +172,16 @@ class PRDescription:
pr_types = [] pr_types = []
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types' # If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'labels' in self.data: if 'PR Labels' in self.data:
if type(self.data['labels']) == list: if type(self.data['PR Labels']) == list:
pr_types = self.data['labels'] pr_types = self.data['PR Labels']
elif type(self.data['labels']) == str: elif type(self.data['PR Labels']) == str:
pr_types = self.data['labels'].split(',') pr_types = self.data['PR Labels'].split(',')
elif 'type' in self.data: elif 'PR Type' in self.data:
if type(self.data['type']) == list: if type(self.data['PR Type']) == list:
pr_types = self.data['type'] pr_types = self.data['PR Type']
elif type(self.data['type']) == str: elif type(self.data['PR Type']) == str:
pr_types = self.data['type'].split(',') pr_types = self.data['PR Type'].split(',')
return pr_types return pr_types
def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]: def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]:
@ -195,12 +193,12 @@ class PRDescription:
else: else:
ai_header = "" ai_header = ""
ai_type = self.data.get('type') ai_type = self.data.get('PR Type')
if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body): if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body):
pr_type = f"{ai_header}{ai_type}" pr_type = f"{ai_header}{ai_type}"
body = body.replace('pr_agent:type', pr_type) body = body.replace('pr_agent:type', pr_type)
ai_summary = self.data.get('description') ai_summary = self.data.get('PR Description')
if ai_summary and not re.search(r'<!--\s*pr_agent:summary\s*-->', body): if ai_summary and not re.search(r'<!--\s*pr_agent:summary\s*-->', body):
summary = f"{ai_header}{ai_summary}" summary = f"{ai_header}{ai_summary}"
body = body.replace('pr_agent:summary', summary) body = body.replace('pr_agent:summary', summary)
@ -230,16 +228,16 @@ class PRDescription:
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format # Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
markdown_text = "" markdown_text = ""
# Don't display 'PR Labels' # Don't display 'PR Labels'
if 'labels' in self.data and self.git_provider.is_supported("get_labels"): if 'PR Labels' in self.data and self.git_provider.is_supported("get_labels"):
self.data.pop('labels') self.data.pop('PR Labels')
if not get_settings().pr_description.enable_pr_type: if not get_settings().pr_description.enable_pr_type:
self.data.pop('type') self.data.pop('PR Type')
for key, value in self.data.items(): for key, value in self.data.items():
markdown_text += f"## {key}\n\n" markdown_text += f"## {key}\n\n"
markdown_text += f"{value}\n\n" markdown_text += f"{value}\n\n"
# Remove the 'PR Title' key from the dictionary # Remove the 'PR Title' key from the dictionary
ai_title = self.data.pop('title', self.vars["title"]) ai_title = self.data.pop('PR Title', self.vars["title"])
if get_settings().pr_description.keep_original_user_title: if get_settings().pr_description.keep_original_user_title:
# Assign the original PR title to the 'title' variable # Assign the original PR title to the 'title' variable
title = self.vars["title"] title = self.vars["title"]
@ -258,7 +256,7 @@ class PRDescription:
pr_body += "<details> <summary>files:</summary>\n\n" pr_body += "<details> <summary>files:</summary>\n\n"
for file in value: for file in value:
filename = file['filename'].replace("'", "`") filename = file['filename'].replace("'", "`")
description = file['changes_in_file'] description = file['changes in file']
pr_body += f'- `{filename}`: {description}\n' pr_body += f'- `{filename}`: {description}\n'
if self.git_provider.is_supported("gfm_markdown"): if self.git_provider.is_supported("gfm_markdown"):
pr_body +="</details>\n" pr_body +="</details>\n"

View File

@ -43,8 +43,9 @@ class PRGenerateLabels:
"use_bullet_points": get_settings().pr_description.use_bullet_points, "use_bullet_points": get_settings().pr_description.use_bullet_points,
"extra_instructions": get_settings().pr_description.extra_instructions, "extra_instructions": get_settings().pr_description.extra_instructions,
"commit_messages_str": self.git_provider.get_commit_messages(), "commit_messages_str": self.git_provider.get_commit_messages(),
"custom_labels": "",
"custom_labels_examples": "",
"enable_custom_labels": get_settings().config.enable_custom_labels, "enable_custom_labels": get_settings().config.enable_custom_labels,
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function
} }
# Initialize the token handler # Initialize the token handler
@ -147,9 +148,6 @@ class PRGenerateLabels:
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):
@ -161,11 +159,11 @@ class PRGenerateLabels:
def _prepare_labels(self) -> List[str]: def _prepare_labels(self) -> List[str]:
pr_types = [] pr_types = []
# If the 'labels' key is present in the dictionary, split its value by comma and assign it to 'pr_types' # If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'labels' in self.data: if 'PR Type' in self.data:
if type(self.data['labels']) == list: if type(self.data['PR Type']) == list:
pr_types = self.data['labels'] pr_types = self.data['PR Type']
elif type(self.data['labels']) == str: elif type(self.data['PR Type']) == str:
pr_types = self.data['labels'].split(',') pr_types = self.data['PR Type'].split(',')
return pr_types return pr_types

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, get_user_labels from pr_agent.algo.utils import convert_to_markdown, load_yaml, try_fix_yaml, set_custom_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_header="## PR Analysis", initial_text="## PR Analysis",
update_header=True) updated_text="## PR Analysis (updated)")
else: else:
self.git_provider.publish_comment(pr_comment) self.git_provider.publish_comment(pr_comment)
@ -178,9 +178,6 @@ 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:
@ -249,18 +246,11 @@ 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\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:
@ -278,7 +268,14 @@ class PRReviewer:
if get_settings().pr_reviewer.num_code_suggestions == 0: if get_settings().pr_reviewer.num_code_suggestions == 0:
return return
data = load_yaml(self.prediction.strip()) review_text = 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()
@ -375,29 +372,3 @@ 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')
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')]
if current_labels or review_labels:
get_logger().info(f"Setting review labels: {review_labels + current_labels_filtered}")
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,7 +8,6 @@ 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.12.5 litellm~=0.1.574
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,4 +22,3 @@ 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

@ -1,4 +1,3 @@
from pr_agent.git_providers import BitbucketServerProvider
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
@ -9,10 +8,3 @@ class TestBitbucketProvider:
assert workspace_slug == "WORKSPACE_XYZ" assert workspace_slug == "WORKSPACE_XYZ"
assert repo_slug == "MY_TEST_REPO" assert repo_slug == "MY_TEST_REPO"
assert pr_number == 321 assert pr_number == 321
def test_bitbucket_server_pr_url(self):
url = "https://git.onpreminstance.com/projects/AAA/repos/my-repo/pull-requests/1"
workspace_slug, repo_slug, pr_number = BitbucketServerProvider._parse_pr_url(url)
assert workspace_slug == "AAA"
assert repo_slug == "my-repo"
assert pr_number == 1

View File

@ -1,19 +0,0 @@
# Generated by CodiumAI
import pytest
from pr_agent.algo.utils import clip_tokens
class TestClipTokens:
def test_clip(self):
text = "line1\nline2\nline3\nline4\nline5\nline6"
max_tokens = 25
result = clip_tokens(text, max_tokens)
assert result == text
max_tokens = 10
result = clip_tokens(text, max_tokens)
expected_results = 'line1\nline2\nline3\nli...(truncated)'
assert result == expected_results

View File

@ -71,7 +71,7 @@ class TestConvertToMarkdown:
- 📌 **Type of PR:** Test type\n\ - 📌 **Type of PR:** Test type\n\
- 🧪 **Relevant tests added:** no\n\ - 🧪 **Relevant tests added:** no\n\
- ✨ **Focused PR:** Yes\n\ - ✨ **Focused PR:** Yes\n\
- **General PR suggestions:** general suggestion...\n\n\n- <details><summary> 🤖 Code feedback:</summary>\n\n - **Code example:**\n - **Before:**\n ```\n Code before\n ```\n - **After:**\n ```\n Code after\n ```\n\n - **Code example:**\n - **Before:**\n ```\n Code before 2\n ```\n - **After:**\n ```\n Code after 2\n ```\n\n</details>\ - **General PR suggestions:** general suggestion...\n\n\n- **<details><summary> 🤖 Code feedback:**</summary>\n\n - **Code example:**\n - **Before:**\n ```\n Code before\n ```\n - **After:**\n ```\n Code after\n ```\n\n - **Code example:**\n - **Before:**\n ```\n Code before 2\n ```\n - **After:**\n ```\n Code after 2\n ```\n\n</details>\
""" """
assert convert_to_markdown(input_data).strip() == expected_output.strip() assert convert_to_markdown(input_data).strip() == expected_output.strip()

View File

@ -2,9 +2,6 @@
# 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
@ -15,7 +12,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_invalid_yaml1(self): def test_load_complicated_yaml(self):
yaml_str = \ yaml_str = \
'''\ '''\
PR Analysis: PR Analysis:
@ -29,23 +26,7 @@ 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: aaa relevant line: 'user="""PR Info:'
Security concerns: No''' Security concerns: No'''
with pytest.raises(ScannerError): 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}}
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

View File

@ -61,7 +61,7 @@ class TestParseCodeSuggestion:
'before': 'Before 1', 'before': 'Before 1',
'after': 'After 1' 'after': 'After 1'
} }
expected_output = ' **suggestion:** Suggestion 1 \n **description:** Description 1 \n **before:** Before 1 \n **after:** After 1 \n\n' # noqa: E501 expected_output = " **suggestion:** Suggestion 1\n **description:** Description 1\n **before:** Before 1\n **after:** After 1\n\n" # noqa: E501
assert parse_code_suggestion(code_suggestions) == expected_output assert parse_code_suggestion(code_suggestions) == expected_output
# Tests that function returns correct output when input dictionary has 'code example' key # Tests that function returns correct output when input dictionary has 'code example' key
@ -74,5 +74,5 @@ class TestParseCodeSuggestion:
'after': 'After 2' 'after': 'After 2'
} }
} }
expected_output = ' **suggestion:** Suggestion 2 \n **description:** Description 2 \n - **code example:**\n - **before:**\n ```\n Before 2\n ```\n - **after:**\n ```\n After 2\n ```\n\n' # noqa: E501 expected_output = " **suggestion:** Suggestion 2\n **description:** Description 2\n - **code example:**\n - **before:**\n ```\n Before 2\n ```\n - **after:**\n ```\n After 2\n ```\n\n" # noqa: E501
assert parse_code_suggestion(code_suggestions) == expected_output assert parse_code_suggestion(code_suggestions) == expected_output