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
102 changed files with 1174 additions and 3475 deletions

View File

@ -5,9 +5,8 @@
name: PR-Agent name: PR-Agent
on: on:
# pull_request: pull_request:
# issue_comment: issue_comment:
workflow_dispatch:
permissions: permissions:
issues: write issues: write
@ -27,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)
@ -46,7 +45,7 @@ docker run --rm -it -e OPENAI.KEY=<your key> -e CONFIG.GIT_PROVIDER=gitlab -e GI
Note: If you have a dedicated GitLab instance, you need to specify the custom url as variable: Note: If you have a dedicated GitLab instance, you need to specify the custom url as variable:
``` ```
docker run --rm -it -e OPENAI.KEY=<your key> -e CONFIG.GIT_PROVIDER=gitlab -e GITLAB.PERSONAL_ACCESS_TOKEN=<your token> -e GITLAB.URL=<your gitlab instance url> codiumai/pr-agent:latest --pr_url <pr_url> review docker run --rm -it -e OPENAI.KEY=<your key> -e CONFIG.GIT_PROVIDER=gitlab -e GITLAB.PERSONAL_ACCESS_TOKEN=<your token> GITLAB.URL=<your gitlab instance url> codiumai/pr-agent:latest --pr_url <pr_url> review
``` ```
- For BitBucket: - For BitBucket:
@ -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
... ...
``` ```
@ -157,11 +155,10 @@ jobs:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }} OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
``` ```
2. Add the following secret to your repository under `Settings > Secrets and variables > Actions > New repository secret > Add secret`: 2. Add the following secret to your repository under `Settings > Secrets`:
``` ```
Name = OPENAI_KEY OPENAI_KEY: <your key>
Secret = <your key>
``` ```
The GITHUB_TOKEN secret is automatically created by GitHub. The GITHUB_TOKEN secret is automatically created by GitHub.
@ -206,7 +203,6 @@ Allowing you to automate the review process on your private or public repositori
- Set the following events: - Set the following events:
- Issue comment - Issue comment
- Pull request - Pull request
- Push (if you need to enable triggering on PR update)
2. Generate a random secret for your app, and save it for later. For example, you can use: 2. Generate a random secret for your app, and save it for later. For example, you can use:
@ -293,8 +289,7 @@ docker push codiumai/pr-agent:github_app # Push to your Docker repository
``` ```
4. Create a lambda function that uses the uploaded image. Set the lambda timeout to be at least 3m. 4. Create a lambda function that uses the uploaded image. Set the lambda timeout to be at least 3m.
5. Configure the lambda function to have a Function URL. 5. Configure the lambda function to have a Function URL.
6. In the environment variables of the Lambda function, specify `AZURE_DEVOPS_CACHE_DIR` to a writable location such as /tmp. (see [link](https://github.com/Codium-ai/pr-agent/pull/450#issuecomment-1840242269)) 6. Go back to steps 8-9 of [Method 5](#run-as-a-github-app) with the function url as your Webhook URL.
7. Go back to steps 8-9 of [Method 5](#run-as-a-github-app) with the function url as your Webhook URL.
The Webhook URL would look like `https://<LAMBDA_FUNCTION_URL>/api/v1/github_webhooks` The Webhook URL would look like `https://<LAMBDA_FUNCTION_URL>/api/v1/github_webhooks`
--- ---
@ -414,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 end with `/webhook`, for example: https://domain.com/webhook
======= =======

View File

@ -39,4 +39,4 @@ We use [tiktoken](https://github.com/openai/tiktoken) to tokenize the patches af
4. If we haven't reached the max token length, add the `deleted files` to the prompt until the prompt reaches the max token length (hard stop), skip the rest of the patches. 4. If we haven't reached the max token length, add the `deleted files` to the prompt until the prompt reaches the max token length (hard stop), skip the rest of the patches.
### Example ### Example
<kbd><img src=https://codium.ai/images/git_patch_logic.png width="768"></kbd> ![](https://codium.ai/images/git_patch_logic.png)

187
README.md
View File

@ -2,13 +2,8 @@
<div align="center"> <div align="center">
<img src="./pics/logo-dark.png#gh-dark-mode-only" width="330"/>
<picture> <img src="./pics/logo-light.png#gh-light-mode-only" width="330"/><br/>
<source media="(prefers-color-scheme: dark)" srcset="https://codium.ai/images/pr_agent/logo-dark.png" width="330">
<source media="(prefers-color-scheme: light)" srcset="https://codium.ai/images/pr_agent/logo-light.png" width="330">
<img alt="logo">
</picture>
<br/>
Making pull requests less painful with an AI agent Making pull requests less painful with an AI agent
</div> </div>
@ -21,7 +16,7 @@ Making pull requests less painful with an AI agent
</div> </div>
<div style="text-align:left;"> <div style="text-align:left;">
CodiumAI PR-Agent is an open-source tool to help efficiently review and handle pull requests. It automatically analyzes the pull request and can provide several types of commands: CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull requests faster and more efficiently. It automatically analyzes the pull request and can provide several types of commands:
**Auto Description ([`/describe`](./docs/DESCRIBE.md))**: Automatically generating PR description - title, type, summary, code walkthrough and labels. **Auto Description ([`/describe`](./docs/DESCRIBE.md))**: Automatically generating PR description - title, type, summary, code walkthrough and labels.
\ \
@ -33,54 +28,31 @@ CodiumAI PR-Agent is an open-source tool to help efficiently review and handle p
\ \
**Update Changelog ([`/update_changelog`](./docs/UPDATE_CHANGELOG.md))**: Automatically updating the CHANGELOG.md file with the PR changes. **Update Changelog ([`/update_changelog`](./docs/UPDATE_CHANGELOG.md))**: Automatically updating the CHANGELOG.md file with the PR changes.
\ \
**Find Similar Issue ([`/similar_issue`](./docs/SIMILAR_ISSUE.md))**: Automatically retrieves and presents similar issues. **Find Similar Issue ([`/similar_issue`](./docs/SIMILAR_ISSUE.md))**: Automatically retrieves and presents similar issues
\ \
**Add Documentation 💎 ([`/add_docs`](./docs/ADD_DOCUMENTATION.md))**: Automatically adds documentation to methods/functions/classes that changed in the PR. **Add Documentation ([`/add_docs`](./docs/ADD_DOCUMENTATION.md))**: Automatically adds documentation to un-documented functions/classes in the PR.
\ \
**Generate Custom Labels 💎 ([`/generate_labels`](./docs/GENERATE_CUSTOM_LABELS.md))**: Automatically suggests custom labels based on the PR code changes. **Generate Custom Labels ([`/generate_labels`](./docs/GENERATE_CUSTOM_LABELS.md))**: Automatically suggests custom labels based on the PR code changes.
\
**Analyze 💎 ([`/analyze`](./docs/Analyze.md))**: Automatically analyzes the PR, and presents changes walkthrough for each component.
See the [Installation Guide](./INSTALL.md) for instructions how to install and run the tool on different platforms.
See the [Installation Guide](./INSTALL.md) for instructions on installing and running the tool on different git platforms. See the [Usage Guide](./Usage.md) for instructions how to run the different tools from _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened.
See the [Usage Guide](./Usage.md) for running the PR-Agent commands via different interfaces, including _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened. See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the different tools.
See the [Tools Guide](./docs/TOOLS_GUIDE.md) for a detailed description of the different tools (tools are run via the commands). <h3>Example results:</h3>
## Table of Contents
- [Example results](#example-results)
- [Features overview](#features-overview)
- [Try it now](#try-it-now)
- [Installation](#installation)
- [PR-Agent Pro 💎](#pr-agent-pro-)
- [How it works](#how-it-works)
- [Why use PR-Agent?](#why-use-pr-agent)
## Example results
</div> </div>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/530">/describe</a></h4> <h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1687561986">/describe:</a></h4>
<div align="center"> <div align="center">
<p float="center"> <p float="center">
<img src="https://www.codium.ai/images/pr_agent/describe_short_main.png" width="800"> <img src="https://www.codium.ai/images/describe-2.gif" width="800">
</p> </p>
</div> </div>
<hr>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/472#discussion_r1435819374">/improve</a></h4>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901">/review:</a></h4>
<div align="center"> <div align="center">
<p float="center"> <p float="center">
<kbd> <img src="https://www.codium.ai/images/review-2.gif" width="800">
<img src="https://www.codium.ai/images/pr_agent/improve_short_main.png" width="768">
</kbd>
</p>
</div>
<hr>
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/530">/generate_labels</a></h4>
<div align="center">
<p float="center">
<kbd><img src="https://www.codium.ai/images/pr_agent/geneare_custom_labels_main_short.png" width="300"></kbd>
</p> </p>
</div> </div>
@ -121,48 +93,49 @@ See the [Tools Guide](./docs/TOOLS_GUIDE.md) for a detailed description of the d
[//]: # (</div>) [//]: # (</div>)
<div align="left"> <div align="left">
## Table of Contents
- [Overview](#overview)
- [Try it now](#try-it-now)
- [Installation](#installation)
- [How it works](#how-it-works)
- [Why use PR-Agent?](#why-use-pr-agent)
- [Roadmap](#roadmap)
</div> </div>
<hr>
## Features overview
## Overview
`PR-Agent` offers extensive pull request functionalities across various git providers: `PR-Agent` offers extensive pull request functionalities across various git providers:
| | | GitHub | Gitlab | Bitbucket | | | | GitHub | Gitlab | Bitbucket | CodeCommit | Azure DevOps | Gerrit |
|-------|---------------------------------------------|:------:|:------:|:---------:| |-------|---------------------------------------------|:------:|:------:|:---------:|:----------:|:----------:|:----------:|
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Incremental | :white_check_mark: | | | | | ⮑ Incremental | :white_check_mark: | | | | | |
| | ⮑ [SOC2 Compliance](https://github.com/Codium-ai/pr-agent/blob/main/docs/REVIEW.md#soc2-ticket-compliance-) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Auto-Description | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Describe | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Improve Code | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | Improve | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: |
| | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Find similar issue | :white_check_mark: | | | | | |
| | Find Similar Issue | :white_check_mark: | | | | | Add Documentation | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | [Add PR Documentation](https://github.com/Codium-ai/pr-agent/blob/main/docs/ADD_DOCUMENTATION.md) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Generate Labels | :white_check_mark: | :white_check_mark: | | | | |
| | [Generate Custom Labels](https://github.com/Codium-ai/pr-agent/blob/main/docs/DESCRIBE.md#handle-custom-labels-from-the-repos-labels-page-gem) 💎 | :white_check_mark: | :white_check_mark: | | | | | | | | | |
| | [Analyze PR Components](https://github.com/Codium-ai/pr-agent/blob/main/docs/Analyze.md) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | | | | | | | App / webhook | :white_check_mark: | :white_check_mark: | | | |
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Tagging bot | :white_check_mark: | | | | |
| | App / webhook | :white_check_mark: | :white_check_mark: | | | | Actions | :white_check_mark: | | | | |
| | Tagging bot | :white_check_mark: | | | | | Web server | | | | | | :white_check_mark: |
| | Actions | :white_check_mark: | | | | | | | | | | |
| | | | | | | CORE | PR compression | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| CORE | PR compression | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Repo language prioritization | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Repo language prioritization | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Adaptive and token-aware<br />file patch fitting | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Adaptive and token-aware<br />file patch fitting | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Multiple models support | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Multiple models support | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | Incremental PR Review | :white_check_mark: | | | | | |
| | Incremental PR review | :white_check_mark: | | |
| | [Static code analysis](https://github.com/Codium-ai/pr-agent/blob/main/docs/Analyze.md) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | [Global configuration](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#global-configuration-file-) 💎 | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Review the [usage guide](./Usage.md) section for detailed instructions how to use the different tools, select the relevant git provider (GitHub, Gitlab, Bitbucket,...), and adjust the configuration file to your needs.
- 💎 means this feature is available only in [PR-Agent Pro](https://www.codium.ai/pricing/)
- Support for additional git providers is described in [here](./docs/Full_enviroments.md)
## Try it now ## Try it now
Try the GPT-4 powered PR-Agent instantly on _your public GitHub repository_. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command. You can try GPT-4 powered PR-Agent, on your public GitHub repository, instantly. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command.
For example, add a comment to any pull request with the following text: For example, add a comment to any pull request with the following text:
``` ```
@CodiumAI-Agent /review @CodiumAI-Agent /review
@ -173,12 +146,12 @@ and the agent will respond with a review of your PR
To set up your own PR-Agent, see the [Installation](#installation) section below. To set up your own PR-Agent, see the [Installation](#installation) section below.
Note that when you set your own PR-Agent or use CodiumAI hosted PR-Agent, there is no need to mention `@CodiumAI-Agent ...`. Instead, directly start with the command, e.g., `/ask ...`.
--- ---
## Installation ## Installation
To use your own version of PR-Agent, you first need to acquire two tokens:
To get started with PR-Agent quickly, you first need to acquire two tokens:
1. An OpenAI key from [here](https://platform.openai.com/), with access to GPT-4. 1. An OpenAI key from [here](https://platform.openai.com/), with access to GPT-4.
2. A GitHub personal access token (classic) with the repo scope. 2. A GitHub personal access token (classic) with the repo scope.
@ -197,49 +170,59 @@ There are several ways to use PR-Agent:
- [Method 8: Run a GitLab webhook server](INSTALL.md#method-8---run-a-gitlab-webhook-server) - [Method 8: Run a GitLab webhook server](INSTALL.md#method-8---run-a-gitlab-webhook-server)
- [Method 9: Run as a Bitbucket Pipeline](INSTALL.md#method-9-run-as-a-bitbucket-pipeline) - [Method 9: Run as a Bitbucket Pipeline](INSTALL.md#method-9-run-as-a-bitbucket-pipeline)
## PR-Agent Pro 💎
[PR-Agent Pro](https://www.codium.ai/pricing/) is a hosted version of PR-Agent, provided by CodiumAI. It is available for a monthly fee, and provides the following benefits:
1. **Fully managed** - We take care of everything for you - hosting, models, regular updates, and more. Installation is as simple as signing up and adding the PR-Agent app to your GitHub\BitBucket repo.
2. **Improved privacy** - No data will be stored or used to train models. PR-Agent Pro will employ zero data retention, and will use an OpenAI account with zero data retention.
3. **Improved support** - PR-Agent Pro users will receive priority support, and will be able to request new features and capabilities.
4. **Extra features** -In addition to the benefits listed above, PR-Agent Pro will emphasize more customization, and the usage of static code analysis, in addition to LLM logic, to improve results. It has the following additional features:
- [**SOC2 compliance check**](https://github.com/Codium-ai/pr-agent/blob/main/docs/REVIEW.md#soc2-ticket-compliance-)
- [**PR documentation**](https://github.com/Codium-ai/pr-agent/blob/main/docs/ADD_DOCUMENTATION.md)
- [**Custom labels**](https://github.com/Codium-ai/pr-agent/blob/main/docs/DESCRIBE.md#handle-custom-labels-from-the-repos-labels-page-gem)
- [**Global configuration**](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#global-configuration-file-)
- [**Analyze PR components**](https://github.com/Codium-ai/pr-agent/blob/main/docs/Analyze.md)
- **Custom Code Suggestions** [WIP]
- **Chat on Specific Code Lines** [WIP]
## How it works ## How it works
The following diagram illustrates PR-Agent tools and their flow: The following diagram illustrates PR-Agent tools and their flow:
![PR-Agent Tools](https://codium.ai/images/pr_agent/diagram-v0.9.png) ![PR-Agent Tools](https://www.codium.ai/wp-content/uploads/2023/10/codiumai-diagram-v5.png)
Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more details on how we convert a code diff to a manageable LLM prompt Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more details on how we convert a code diff to a manageable LLM prompt
## Why use PR-Agent? ## Why use PR-Agent?
A reasonable question that can be asked is: `"Why use PR-Agent? What makes it stand out from existing tools?"` A reasonable question that can be asked is: `"Why use PR-Agent? What make it stand out from existing tools?"`
Here are some advantages of PR-Agent: Here are some advantages of PR-Agent:
- We emphasize **real-life practical usage**. Each tool (review, improve, ask, ...) has a single GPT-4 call, no more. We feel that this is critical for realistic team usage - obtaining an answer quickly (~30 seconds) and affordably. - We emphasize **real-life practical usage**. Each tool (review, improve, ask, ...) has a single GPT-4 call, no more. We feel that this is critical for realistic team usage - obtaining an answer quickly (~30 seconds) and affordably.
- Our [PR Compression strategy](./PR_COMPRESSION.md) is a core ability that enables to effectively tackle both short and long PRs. - Our [PR Compression strategy](./PR_COMPRESSION.md) is a core ability that enables to effectively tackle both short and long PRs.
- Our JSON prompting strategy enables to have **modular, customizable tools**. For example, the '/review' tool categories can be controlled via the [configuration](pr_agent/settings/configuration.toml) file. Adding additional categories is easy and accessible. - Our JSON prompting strategy enables to have **modular, customizable tools**. For example, the '/review' tool categories can be controlled via the [configuration](pr_agent/settings/configuration.toml) file. Adding additional categories is easy and accessible.
- We support **multiple git providers** (GitHub, Gitlab, Bitbucket), **multiple ways** to use the tool (CLI, GitHub Action, GitHub App, Docker, ...), and **multiple models** (GPT-4, GPT-3.5, Anthropic, Cohere, Llama2). - We support **multiple git providers** (GitHub, Gitlab, Bitbucket, CodeCommit), **multiple ways** to use the tool (CLI, GitHub Action, GitHub App, Docker, ...), and **multiple models** (GPT-4, GPT-3.5, Anthropic, Cohere, Llama2).
- We are open-source, and welcome contributions from the community.
## Data privacy ## Roadmap
If you host PR-Agent with your OpenAI API key, it is between you and OpenAI. You can read their API data privacy policy here: - [x] Support additional models, as a replacement for OpenAI (see [here](https://github.com/Codium-ai/pr-agent/pull/172))
- [x] Develop additional logic for handling large PRs (see [here](https://github.com/Codium-ai/pr-agent/pull/229))
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
- [x] PR-Agent for issues
- [ ] Adding more tools. Possible directions:
- [x] PR description
- [x] Inline code suggestions
- [x] Reflect and review
- [x] Rank the PR (see [here](https://github.com/Codium-ai/pr-agent/pull/89))
- [ ] Enforcing CONTRIBUTING.md guidelines
- [ ] Performance (are there any performance issues)
- [x] Documentation (is the PR properly documented)
- [ ] ...
See the [Release notes](./RELEASE_NOTES.md) for updates on the latest changes.
## Similar Projects
- [CodiumAI - Meaningful tests for busy devs](https://github.com/Codium-ai/codiumai-vscode-release) (although various capabilities are much more advanced in the CodiumAI IDE plugins)
- [Aider - GPT powered coding in your terminal](https://github.com/paul-gauthier/aider)
- [openai-pr-reviewer](https://github.com/coderabbitai/openai-pr-reviewer)
- [CodeReview BOT](https://github.com/anc95/ChatGPT-CodeReview)
- [AI-Maintainer](https://github.com/merwanehamadi/AI-Maintainer)
## Data Privacy
If you use self-host PR-Agent, e.g. via CLI running on your computer, with your OpenAI API key, it is between you and OpenAI. You can read their API data privacy policy here:
https://openai.com/enterprise-privacy https://openai.com/enterprise-privacy
When using PR-Agent Pro 💎, hosted by CodiumAI, we will not store any of your data, nor will we use it for training.
You will also benefit from an OpenAI account with zero data retention.
## Links ## Links
[![Join our Discord community](https://raw.githubusercontent.com/Codium-ai/codiumai-vscode-release/main/media/docs/Joincommunity.png)](https://discord.gg/kG35uSHDBc) [![Join our Discord community](https://raw.githubusercontent.com/Codium-ai/codiumai-vscode-release/main/media/docs/Joincommunity.png)](https://discord.gg/kG35uSHDBc)

View File

@ -1,42 +1,5 @@
## [Version 0.11] - 2023-12-07 ## Unreleased
- codiumai/pr-agent:0.11 - review tool now posts persistent comments by default
- codiumai/pr-agent:0.11-github_app
- codiumai/pr-agent:0.11-bitbucket-app
- codiumai/pr-agent:0.11-gitlab_webhook
- codiumai/pr-agent:0.11-github_polling
- codiumai/pr-agent:0.11-github_action
### Added::Algo
- New section in `/describe` tool - [PR changes walkthrough](https://github.com/Codium-ai/pr-agent/pull/509)
- Improving PR Agent [prompts](https://github.com/Codium-ai/pr-agent/pull/501)
- Persistent tools (`/review`, `/describe`) now send an [update message](https://github.com/Codium-ai/pr-agent/pull/499) after finishing
- Add Amazon Bedrock [support](https://github.com/Codium-ai/pr-agent/pull/483)
### Fixed
- Update [dependencies](https://github.com/Codium-ai/pr-agent/pull/503) in requirements.txt for Python 3.12
## [Version 0.10] - 2023-11-15
- codiumai/pr-agent:0.10
- codiumai/pr-agent:0.10-github_app
- codiumai/pr-agent:0.10-bitbucket-app
- codiumai/pr-agent:0.10-gitlab_webhook
- codiumai/pr-agent:0.10-github_polling
- codiumai/pr-agent:0.10-github_action
### Added::Algo
- Review tool now works with [persistent comments](https://github.com/Codium-ai/pr-agent/pull/451) by default
- Bitbucket now publishes review suggestions with [code links](https://github.com/Codium-ai/pr-agent/pull/428)
- Enabling to limit [max number of tokens](https://github.com/Codium-ai/pr-agent/pull/437/files)
- Support ['gpt-4-1106-preview'](https://github.com/Codium-ai/pr-agent/pull/437/files) model
- Support for Google's [Vertex AI](https://github.com/Codium-ai/pr-agent/pull/436)
- Implementing [thresholds](https://github.com/Codium-ai/pr-agent/pull/423) for incremental PR reviews
- Decoupled custom labels from [PR type](https://github.com/Codium-ai/pr-agent/pull/431)
### Fixed
- Fixed bug in [parsing quotes](https://github.com/Codium-ai/pr-agent/pull/446) in CLI
- Preserve [user-added labels](https://github.com/Codium-ai/pr-agent/pull/433) in pull requests
- Bug fixes in GitLab and BitBucket
## [Version 0.9] - 2023-10-29 ## [Version 0.9] - 2023-10-29
- codiumai/pr-agent:0.9 - codiumai/pr-agent:0.9

209
Usage.md
View File

@ -2,12 +2,13 @@
### Table of Contents ### Table of Contents
- [Introduction](#introduction) - [Introduction](#introduction)
- [Local Repo (CLI)](#working-from-a-local-repo-cli) - [Working from a local repo (CLI)](#working-from-a-local-repo-cli)
- [Online Usage](#online-usage) - [Online usage](#online-usage)
- [GitHub App](#working-with-github-app) - [Working with GitHub App](#working-with-github-app)
- [GitHub Action](#working-with-github-action) - [Working with GitHub Action](#working-with-github-action)
- [BitBucket App](#working-with-bitbucket-self-hosted-app) - [Changing a model](#changing-a-model)
- [Additional Configurations Walkthrough](#appendix---additional-configurations-walkthrough) - [Working with large PRs](#working-with-large-prs)
- [Appendix - additional configurations walkthrough](#appendix---additional-configurations-walkthrough)
### Introduction ### Introduction
@ -23,53 +24,20 @@ GitHub App and GitHub Action also enable to run PR-Agent specific tool automatic
#### The configuration file #### The configuration file
- The different tools and sub-tools used by CodiumAI PR-Agent are adjustable via the **[configuration file](pr_agent/settings/configuration.toml)**. The different tools and sub-tools used by CodiumAI PR-Agent are adjustable via the **[configuration file](pr_agent/settings/configuration.toml)**.
In addition to general configuration options, each tool has its own configurations. For example, the `review` tool will use parameters from the [pr_reviewer](/pr_agent/settings/configuration.toml#L16) section in the configuration file. In addition to general configuration options, each tool has its own configurations. For example, the `review` tool will use parameters from the [pr_reviewer](/pr_agent/settings/configuration.toml#L16) section in the configuration file.
- The [Tools Guide](./docs/TOOLS_GUIDE.md) provides a detailed description of the different tools and their configurations. The [Tools Guide](./docs/TOOLS_GUIDE.md) provides a detailed description of the different tools and their configurations.
- 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.
For example, if you set in `.pr_agent.toml`:
```
[pr_reviewer]
extra_instructions="""\
- instruction a
- instruction b
...
"""
```
Then you can give a list of extra instructions to the `review` tool.
#### Global configuration file 💎
If you create a repo called `pr-agent-settings` in your **organization**, it's configuration file `.pr_agent.toml` will be used as a global configuration file for any other repo that belongs to the same organization.
Parameters from a local `.pr_agent.toml` file, in a specific repo, will override the global configuration parameters.
For example, in the GitHub organization `Codium-ai`:
- The repo [`https://github.com/Codium-ai/pr-agent-settings`](https://github.com/Codium-ai/pr-agent-settings/blob/main/.pr_agent.toml) contains a `.pr_agent.toml` file that serves as a global configuration file for all the repos in the GitHub organization `Codium-ai`.
- The repo [`https://github.com/Codium-ai/pr-agent`](https://github.com/Codium-ai/pr-agent/blob/main/.pr_agent.toml) inherits the global configuration file from `pr-agent-settings`.
#### 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:
@ -77,9 +45,21 @@ The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configu
"github", "gitlab", "azure", "codecommit", "local", "gerrit" "github", "gitlab", "azure", "codecommit", "local", "gerrit"
` `
[//]: # (** online usage:**)
[//]: # (Options that are available in the configuration file can be specified at run time when calling actions. Two examples:)
[//]: # (```)
[//]: # (- /review --pr_reviewer.extra_instructions="focus on the file: ...")
[//]: # (- /describe --pr_description.add_original_user_description=false -pr_description.extra_instructions="make sure to mention: ...")
[//]: # (```)
### 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`
@ -103,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
@ -120,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`:
@ -139,11 +119,11 @@ 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.
In this section, you can define configurations to control the conditions for which tools will **run automatically**. In this section you can define configurations to control the conditions for which tools will **run automatically**.
##### GitHub app automatic tools for PR actions ##### GitHub app automatic tools for PR actions
The GitHub app can respond to the following actions on a PR: The GitHub app can respond to the following actions on a PR:
@ -153,17 +133,17 @@ 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']
pr_commands = [ pr_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true", "/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/review", "/auto_review",
] ]
``` ```
This means that when a new PR is opened/reopened or marked as ready for review, PR-Agent will run the `describe` and `review` tools. This means that when a new PR is opened/reopened or marked as ready for review, PR-Agent will run the `describe` and `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.
You can override the default tool parameters by uploading a local configuration file called `.pr_agent.toml` to the root of your repo. You can override the default tool parameters by uploading a local configuration file called `.pr_agent.toml` to the root of your repo.
For example, if your local `.pr_agent.toml` file contains: For example, if your local `.pr_agent.toml` file contains:
@ -190,14 +170,14 @@ The configuration parameter `push_commands` defines the list of tools that will
handle_push_trigger = true handle_push_trigger = true
push_commands = [ push_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true", "/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/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 `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 `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.
@ -238,65 +218,15 @@ For example, you can set an environment variable: `pr_description.add_original_u
add_original_user_description = false add_original_user_description = false
``` ```
### Working with BitBucket Self-Hosted App
Similar to GitHub app, when running PR-Agent from BitBucket 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. ### Changing a model
For example, if your local `.pr_agent.toml` file contains:
```
[pr_reviewer]
inline_code_comments = true
```
Each time you invoke a `/review` tool, it will use inline code comments.
#### BitBucket Self-Hosted App automatic tools
You can configure in your local `.pr_agent.toml` file which tools will **run automatically** when a new PR is opened.
Specifically, set the following values:
```yaml
[bitbucket_app]
auto_review = true # set as config var in .pr_agent.toml
auto_describe = true # set as config var in .pr_agent.toml
auto_improve = true # set as config var in .pr_agent.toml
```
`bitbucket_app.auto_review`, `bitbucket_app.auto_describe` and `bitbucket_app.auto_improve` are used to enable/disable automatic tools.
If not set, the default option is that only the `review` tool will run automatically when a new PR is opened.
Note that due to limitations of the bitbucket platform, the `auto_describe` tool will be able to publish a PR description only as a comment.
In addition, some subsections like `PR changes walkthrough` will not appear, since they require the usage of collapsible sections, which are not supported by bitbucket.
### Appendix - additional configurations walkthrough
#### Extra instructions
All PR-Agent tools have a parameter called `extra_instructions`, that enables to add free-text extra instructions. Example usage:
```
/update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..."
```
#### 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.
This mode provide a very good speed-quality-cost tradeoff, and can handle most PRs successfully.
When the PR is above the token limit, it employs a [PR Compression strategy](./PR_COMPRESSION.md).
However, for very large PRs, or in case you want to emphasize quality over speed and cost, there are 2 possible solutions:
1) [Use a model](#changing-a-model) with larger context, like GPT-32K, or claude-100K. This solution will be applicable for all the tools.
2) For the `/improve` tool, there is an ['extended' mode](./docs/IMPROVE.md) (`/improve --extended`),
which divides the PR to chunks, and process each chunk separately. With this mode, regardless of the model, no compression will be done (but for large PRs, multiple model calls may occur)
#### Changing a model
See [here](pr_agent/algo/__init__.py) for the list of available models. See [here](pr_agent/algo/__init__.py) for the list of available models.
To use a different model than the default (GPT-4), you need to edit [configuration file](pr_agent/settings/configuration.toml#L2). To use a different model than the default (GPT-4), you need to edit [configuration file](pr_agent/settings/configuration.toml#L2).
For models and environments not from OPENAI, you might need to provide additional keys and other parameters. See below for instructions. For models and environments not from OPENAI, you might need to provide additional keys and other parameters. See below for instructions.
##### Azure #### Azure
To use Azure, set in your `.secrets.toml` (working from CLI), or in the GitHub `Settings > Secrets and variables` (working from GitHub App or GitHub Action): To use Azure, set in your .secrets.toml:
``` ```
api_key = "" # your azure api key api_key = "" # your azure api key
api_type = "azure" api_type = "azure"
@ -305,13 +235,14 @@ api_base = "" # The base URL for your Azure OpenAI resource. e.g. "https://<you
openai.deployment_id = "" # The deployment name you chose when you deployed the engine openai.deployment_id = "" # The deployment name you chose when you deployed the engine
``` ```
and set in your configuration file: and
``` ```
[config] [config]
model="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo) model="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
``` ```
in the configuration.toml
##### Huggingface #### Huggingface
**Local** **Local**
You can run Huggingface models locally through either [VLLM](https://docs.litellm.ai/docs/providers/vllm) or [Ollama](https://docs.litellm.ai/docs/providers/ollama) You can run Huggingface models locally through either [VLLM](https://docs.litellm.ai/docs/providers/vllm) or [Ollama](https://docs.litellm.ai/docs/providers/ollama)
@ -325,7 +256,7 @@ MAX_TOKENS = {
e.g. e.g.
MAX_TOKENS={ MAX_TOKENS={
..., ...,
"ollama/llama2": 4096 "llama2": 4096
} }
@ -334,8 +265,6 @@ model = "ollama/llama2"
[ollama] # in .secrets.toml [ollama] # in .secrets.toml
api_base = ... # the base url for your huggingface inference endpoint api_base = ... # the base url for your huggingface inference endpoint
# e.g. if running Ollama locally, you may use:
api_base = "http://localhost:11434/"
``` ```
**Inference Endpoints** **Inference Endpoints**
@ -360,7 +289,7 @@ api_base = ... # the base url for your huggingface inference endpoint
``` ```
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api)) (you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
##### Replicate #### Replicate
To use Llama2 model with Replicate, for example, set: To use Llama2 model with Replicate, for example, set:
``` ```
@ -374,42 +303,26 @@ 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 ### Working with large PRs
To use Google's Vertex AI platform and its associated models (chat-bison/codechat-bison) set: The default mode of CodiumAI is to have a single call per tool, using GPT-4, which has a token limit of 8000 tokens.
This mode provide a very good speed-quality-cost tradeoff, and can handle most PRs successfully.
When the PR is above the token limit, it employs a [PR Compression strategy](./PR_COMPRESSION.md).
``` However, for very large PRs, or in case you want to emphasize quality over speed and cost, there are 2 possible solutions:
[config] # in configuration.toml 1) [Use a model](#changing-a-model) with larger context, like GPT-32K, or claude-100K. This solution will be applicable for all the tools.
model = "vertex_ai/codechat-bison" 2) For the `/improve` tool, there is an ['extended' mode](./docs/IMPROVE.md) (`/improve --extended`),
fallback_models="vertex_ai/codechat-bison" which divides the PR to chunks, and process each chunk separately. With this mode, regardless of the model, no compression will be done (but for large PRs, multiple model calls may occur)
[vertexai] # in .secrets.toml ### Appendix - additional configurations walkthrough
vertex_project = "my-google-cloud-project"
vertex_location = ""
#### Extra instructions
All PR-Agent tools have a parameter called `extra_instructions`, that enables to add free-text extra instructions. Example usage:
``` ```
/update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..."
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.
##### Amazon Bedrock
To use Amazon Bedrock and its foundational models, add the below configuration:
```
[config] # in configuration.toml
model = "anthropic.claude-v2"
fallback_models="anthropic.claude-instant-v1"
[aws] # in .secrets.toml
bedrock_region = "us-east-1"
``` ```
Note that you have to add access to foundational models before using them. Please refer to [this document](https://docs.aws.amazon.com/bedrock/latest/userguide/setting-up.html) for more details.
AWS session is automatically authenticated from your environment, but you can also explicitly set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables.
#### Patch Extra Lines #### Patch Extra Lines
By default, around any change in your PR, git patch provides 3 lines of context above and below the change. By default, around any change in your PR, git patch provides 3 lines of context above and below the change.
``` ```

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,5 +1,5 @@
# Add Documentation Tool 💎 # Add Documentation Tool
The `add_docs` tool scans the PR code changes, and automatically suggests documentation for any code components that changed in the PR (functions, classes, etc.). The `add_docs` tool scans the PR code changes, and automatically suggests documentation for the undocumented code components (functions, classes, etc.).
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```
@ -7,18 +7,9 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=https://codium.ai/images/pr_agent/docs_command.png width="768"></kbd> <kbd><img src=./../pics/add_docs_comment.png width="768"></kbd>
___ <kbd><img src=./../pics/add_docs.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/docs_components.png width="768"></kbd>
___
<kbd><img src=https://codium.ai/images/pr_agent/docs_single_component.png width="768"></kbd>
### Configuration options ### Configuration options
- `docs_style`: The exact style of the documentation (for python docstring). you can choose between: `google`, `numpy`, `sphinx`, `restructuredtext`, `plain`. Default is `sphinx`. - `docs_style`: The exact style of the documentation (for python docstring). you can choose between: `google`, `numpy`, `sphinx`, `restructuredtext`, `plain`. Default is `sphinx`.
- `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 ...".
Notes
- Language that are currently fully supported: Python, Java, C++, JavaScript, TypeScript.
- For languages that are not fully supported, the tool will suggest documentation only for new components in the PR.
- A previous version of the tool, that offered support only for new components, was deprecated.

View File

@ -7,5 +7,5 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=https://codium.ai/images/pr_agent/ask_comment.png width="768"></kbd> <kbd><img src=./../pics/ask_comment.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/ask.png width="768"></kbd> <kbd><img src=./../pics/ask.png width="768"></kbd>

View File

@ -1,21 +0,0 @@
# Analyze Tool 💎
The `analyze` tool combines static code analysis with LLM capabilities to provide a comprehensive analysis of the PR code changes.
The tool scans the PR code changes, find the code components (methods, functions, classes) that changed, and summarizes the changes in each component.
It can be invoked manually by commenting on any PR:
```
/analyze
```
An example [result](https://github.com/Codium-ai/pr-agent/pull/546#issuecomment-1868524805):
<kbd><img src=https://codium.ai/images/pr_agent/analyze_1.png width="768"></kbd>
___
<kbd><img src=https://codium.ai/images/pr_agent/analyze_2.png width="768"></kbd>
___
<kbd><img src=https://codium.ai/images/pr_agent/analyze_3.png width="768"></kbd>
Notes
- Language that are currently supported: Python, Java, C++, JavaScript, TypeScript.

View File

@ -1,34 +1,22 @@
# Describe Tool # Describe Tool
## Table of Contents
- [Overview](#overview)
- [Configuration options](#configuration-options)
- [Handle custom labels from the Repo's labels page :gem:](#handle-custom-labels-from-the-repos-labels-page-gem)
- [Markers template](#markers-template)
- [Usage Tips](#usage-tips)
- [Automation](#automation)
- [Custom labels](#custom-labels)
## Overview The `describe` tool scans the PR code changes, and automatically generates PR description - title, type, summary, code walkthrough and labels.
The `describe` tool scans the PR code changes, and generates a description for the PR - title, type, summary, walkthrough and labels. It can be invoked manually by commenting on any PR:
The tool can be triggered automatically every time a new PR is [opened](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools), or it can be invoked manually by commenting on any PR:
``` ```
/describe /describe
``` ```
For example: For example:
___
<kbd><img src=https://codium.ai/images/pr_agent/describe_comment.png width="768"></kbd>
___
<kbd><img src=https://codium.ai/images/pr_agent/describe.png width="768"></kbd>
___
### Configuration options
To edit [configurations](./../pr_agent/settings/configuration.toml#L46) related to the describe tool (`pr_description` section), use the following template:
```
/describe --pr_description.some_config1=... --pr_description.some_config2=...
```
**Possible configurations:** <kbd><img src=./../pics/describe_comment.png width="768"></kbd>
<kbd><img src=./../pics/describe.png width="768"></kbd>
The `describe` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
### Configuration options
Under the section 'pr_description', the [configuration file](./../pr_agent/settings/configuration.toml#L28) contains options to customize the 'describe' tool:
- `publish_labels`: if set to true, the tool will publish the labels to the PR. Default is true. - `publish_labels`: if set to true, the tool will publish the labels to the PR. Default is true.
- `publish_description_as_comment`: if set to true, the tool will publish the description as a comment to the PR. If false, it will overwrite the origianl description. Default is false. - `publish_description_as_comment`: if set to true, the tool will publish the description as a comment to the PR. If false, it will overwrite the origianl description. Default is false.
@ -38,43 +26,17 @@ To edit [configurations](./../pr_agent/settings/configuration.toml#L46) related
- `keep_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is false. - `keep_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is false.
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...". - `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
- To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes) - To enable `custom labels`, apply the configuration changes described [here](./GENERATE_CUSTOM_LABELS.md#configuration-changes)
- `enable_pr_type`: if set to false, it will not show the `PR type` as a text value in the description content. Default is true. - `enable_pr_type`: if set to false, it will not show the `PR type` as a text value in the description content. Default is true.
- `final_update_message`: if set to true, it will add a comment message [`PR Description updated to latest commit...`](https://github.com/Codium-ai/pr-agent/pull/499#issuecomment-1837412176) after finishing calling `/describe`. Default is true.
- `enable_semantic_files_types`: if set to true, "Changes walkthrough" section will be generated. Default is true.
- `collapsible_file_list`: if set to true, the file list in the "Changes walkthrough" section will be collapsible. If set to "adaptive", the file list will be collapsible only if there are more than 8 files. Default is "adaptive".
### Handle custom labels from the Repo's labels page :gem:
> This feature is available only in PR-Agent Pro
You can control the custom labels that will be suggested by the `describe` tool, from the repo's labels page:
* GitHub : go to `https://github.com/{owner}/{repo}/labels` (or click on the "Labels" tab in the issues or PRs page)
* GitLab : go to `https://gitlab.com/{owner}/{repo}/-/labels` (or click on "Manage" -> "Labels" on the left menu)
Now add/edit the custom labels. they should be formatted as follows:
* Label name: The name of the custom label.
* Description: Start the description of with prefix `pr_agent:`, for example: `pr_agent: Description of when AI should suggest this label`.<br>
The description should be comprehensive and detailed, indicating when to add the desired label. For example:
<kbd><img src=https://codium.ai/images/pr_agent/add_native_custom_labels.png width="880"></kbd>
### Markers template ### Markers template
To enable markers, set `pr_description.use_description_markers=true`.
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism. markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
For example, if the PR original description was: For example, if the PR original description was:
``` ```
User content... User content...
## PR Type:
pr_agent:type
## PR Description: ## PR Description:
pr_agent:summary pr_agent:summary
@ -82,60 +44,21 @@ pr_agent:summary
## PR Walkthrough: ## PR Walkthrough:
pr_agent:walkthrough pr_agent:walkthrough
``` ```
The marker `pr_agent:type` will be replaced with the PR type, `pr_agent:summary` will be replaced with the PR summary, and `pr_agent:walkthrough` will be replaced with the PR walkthrough. The marker `pr_agent:summary` will be replaced with the PR summary, and `pr_agent:walkthrough` will be replaced with the PR walkthrough.
<kbd><img src=https://codium.ai/images/pr_agent/describe_markers_before.png width="768"></kbd> ##### Example:
```
env:
pr_description.use_description_markers: 'true'
```
<kbd><img src=./../pics/describe_markers_before.png width="768"></kbd>
==> ==>
<kbd><img src=https://codium.ai/images/pr_agent/describe_markers_after.png width="768"></kbd> <kbd><img src=./../pics/describe_markers_after.png width="768"></kbd>
**Configuration params:** ##### Configuration params:
- `use_description_markers`: if set to true, the tool will use markers template. It replaces every marker of the form `pr_agent:marker_name` with the relevant content. Default is false. - `use_description_markers`: if set to true, the tool will use markers template. It replaces every marker of the form `pr_agent:marker_name` with the relevant content. Default is false.
- `include_generated_by_header`: if set to true, the tool will add a dedicated header: 'Generated by PR Agent at ...' to any automatic content. Default is true. - `include_generated_by_header`: if set to true, the tool will add a dedicated header: 'Generated by PR Agent at ...' to any automatic content. Default is true.
## Usage Tips
1) [Automation](#automation)
2) [Custom labels](#custom-labels)
### Automation
- When you first install the app, the [default mode](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) for the describe tool is:
```
pr_commands = ["/describe --pr_description.add_original_user_description=true"
"--pr_description.keep_original_user_title=true", ...]
```
meaning the `describe` tool will run automatically on every PR, will keep the original title, and will add the original user description above the generated description.
<br> This default settings aim to strike a good balance between automation and control:
If you want more automation, just give the PR a title, and the tool will auto-write a full description; If you want more control, you can add a detailed description, and the tool will add the complementary description below it.
- For maximal automation, you can change the default mode to:
```
pr_commands = ["/describe --pr_description.add_original_user_description=false"
"--pr_description.keep_original_user_title=true", ...]
```
so the title will be auto-generated as well.
- Markers are an alternative way to control the generated description, to give maximal control to the user. If you set:
```
pr_commands = ["/describe --pr_description.use_description_markers=true", ...]
```
the tool will replace every marker of the form `pr_agent:marker_name` in the PR description with the relevant content, where `marker_name` is one of the following:
- `type`: the PR type.
- `summary`: the PR summary.
- `walkthrough`: the PR walkthrough.
Note that when markers are enabled, if the original PR description does not contain any markers, the tool will not alter the description at all.
### Custom labels
The default labels of the describe tool are quite generic, since they are meant to be used in any repo: [`Bug fix`, `Tests`, `Enhancement`, `Documentation`, `Other`].
If you specify [custom labels](#handle-custom-labels-from-the-repos-labels-page-gem) in the repo's labels page, you can get tailored labels for your use cases.
Examples for custom labels:
- `Main topic:performence` - pr_agent:The main topic of this PR is performance
- `New endpoint` - pr_agent:A new endpoint was added in this PR
- `SQL query` - pr_agent:A new SQL query was added in this PR
- `Dockerfile changes` - pr_agent:The PR contains changes in the Dockerfile
- ...
The list above is eclectic, and aims to give an idea of different possibilities. Define custom labels that are relevant for your repo and use cases.
Note that Labels are not mutually exclusive, so you can add multiple label categories.
<br>Make sure to provide proper title, and detailed and well-phrased description for each label, so the tool will know when to suggest it.

View File

@ -1,27 +0,0 @@
## Overview
`PR-Agent` offers extensive pull request functionalities across various git providers:
| | | GitHub | Gitlab | Bitbucket | CodeCommit | Azure DevOps | Gerrit |
|-------|---------------------------------------------|:------:|:------:|:---------:|:----------:|:----------:|:----------:|
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Incremental | :white_check_mark: | | | | | |
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Auto-Description | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Improve Code | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: |
| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
| | Find similar issue | :white_check_mark: | | | | | |
| | Add Documentation | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
| | Generate Custom Labels 💎 | :white_check_mark: | :white_check_mark: | | | | |
| | | | | | | |
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | App / webhook | :white_check_mark: | :white_check_mark: | | | |
| | Tagging bot | :white_check_mark: | | | | |
| | Actions | :white_check_mark: | | | | |
| | Web server | | | | | | :white_check_mark: |
| | | | | | | |
| CORE | PR compression | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Repo language prioritization | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Adaptive and token-aware<br />file patch fitting | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Multiple models support | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Incremental PR Review | :white_check_mark: | | | | | |

View File

@ -1,4 +1,4 @@
# Generate Custom Labels 💎 # Generate Custom Labels
The `generate_labels` tool scans the PR code changes, and given a list of labels and their descriptions, it automatically suggests labels that match the PR code changes. The `generate_labels` tool scans the PR code changes, and given a list of labels and their descriptions, it automatically suggests labels that match the PR code changes.
It can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
@ -9,37 +9,21 @@ For example:
If we wish to add detect changes to SQL queries in a given PR, we can add the following custom label along with its description: If we wish to add detect changes to SQL queries in a given PR, we can add the following custom label along with its description:
<kbd><img src=https://codium.ai/images/pr_agent/custom_labels_list.png width="768"></kbd> <kbd><img src=./../pics/custom_labels_list.png width="768"></kbd>
When running the `generate_labels` tool on a PR that includes changes in SQL queries, it will automatically suggest the custom label: When running the `generate_labels` tool on a PR that includes changes in SQL queries, it will automatically suggest the custom label:
<kbd><img src=https://codium.ai/images/pr_agent/custom_label_published.png width="768"></kbd> <kbd><img src=./../pics/custom_label_published.png width="768"></kbd>
Note that in addition to the dedicated tool `generate_labels`, the custom labels will also be used by the `describe` tool.
### How to enable custom labels ### How to enable custom labels
There are 3 ways to enable custom labels:
#### 1. CLI (local configuration file) Note that in addition to the dedicated tool `generate_labels`, the custom labels will also be used by the `review` and `describe` tools.
When working from CLI, you need to apply the [configuration changes](#configuration-changes) to the [custom_labels file](./../pr_agent/settings/custom_labels.toml):
#### 2. Repo configuration file #### CLI
To enable custom labels, you need to apply the [configuration changes](#configuration-changes) to the [custom_labels file](./../pr_agent/settings/custom_labels.toml):
#### GitHub Action and GitHub App
To enable custom labels, you need to apply the [configuration changes](#configuration-changes) to the local `.pr_agent.toml` file in you repository. To enable custom labels, you need to apply the [configuration changes](#configuration-changes) to the local `.pr_agent.toml` file in you repository.
#### 3. Handle custom labels from the Repo's labels page
> This feature is available only in PR-Agent Pro
* GitHub : `https://github.com/{owner}/{repo}/labels`, or click on the "Labels" tab in the issues or PRs page.
* GitLab : `https://gitlab.com/{owner}/{repo}/-/labels`, or click on "Manage" -> "Labels" on the left menu.
b. Add/edit the custom labels. It should be formatted as follows:
* Label name: The name of the custom label.
* Description: Start the description of with prefix `pr_agent:`, for example: `pr_agent: Description of when AI should suggest this label`.<br>
The description should be comprehensive and detailed, indicating when to add the desired label.
<kbd><img src=https://codium.ai/images/pr_agent/add_native_custom_labels.png width="880"></kbd>
c. Now the custom labels will be included in the `generate_labels` tool.
*This feature is supported in GitHub and GitLab.
#### Configuration changes #### Configuration changes
- Change `enable_custom_labels` to True: This will turn off the default labels and enable the custom labels provided in the custom_labels.toml file. - Change `enable_custom_labels` to True: This will turn off the default labels and enable the custom labels provided in the custom_labels.toml file.
- Add the custom labels. It should be formatted as follows: - Add the custom labels. It should be formatted as follows:

View File

@ -1,100 +1,45 @@
# Improve Tool # Improve Tool
## Table of Contents The `improve` tool scans the PR code changes, and automatically generate committable suggestions for improving the PR code.
- [Overview](#overview) It can be invoked manually by commenting on any PR:
- [Configuration options](#configuration-options)
- [Summarize mode](#summarize-mode)
- [Usage Tips](#usage-tips)
- [Extra instructions](#extra-instructions)
- [PR footprint - regular vs summarize mode](#pr-footprint---regular-vs-summarize-mode)
- [A note on code suggestions quality](#a-note-on-code-suggestions-quality)
## Overview
The `improve` tool scans the PR code changes, and automatically generates committable suggestions for improving the PR code.
The tool can be triggered automatically every time a new PR is [opened](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools), or it can be invoked manually by commenting on any PR:
``` ```
/improve /improve
``` ```
For example: For example:
<kbd><img src=https://codium.ai/images/pr_agent/improve_comment.png width="768"></kbd> <kbd><img src=./../pics/improve_comment.png width="768"></kbd>
<kbd><img src=./../pics/improve.png width="768"></kbd>
--- The `improve` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
<kbd><img src=https://codium.ai/images/pr_agent/improve.png width="768"></kbd>
---
An extended mode, which does not involve PR Compression and provides more comprehensive suggestions, can be invoked by commenting on any PR: An extended mode, which does not involve PR Compression and provides more comprehensive suggestions, can be invoked by commenting on any PR:
``` ```
/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 (might use multiple calls to GPT-4 for large PRs). 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
To edit [configurations](./../pr_agent/settings/configuration.toml#L66) related to the improve tool (`pr_code_suggestions` section), use the following template: Under the section 'pr_code_suggestions', the [configuration file](./../pr_agent/settings/configuration.toml#L40) contains options to customize the 'improve' tool:
```
/improve --pr_code_suggestions.some_config1=... --pr_code_suggestions.some_config2=...
```
#### General options
- `num_code_suggestions`: number of code suggestions provided by the 'improve' tool. Default is 4. - `num_code_suggestions`: number of code suggestions provided by the 'improve' tool. Default is 4.
- `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 ...".
- `rank_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is false. - `rank_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is false.
- `include_improved_code`: if set to true, the tool will include an improved code implementation in the suggestion. Default is true.
#### params for '/improve --extended' mode #### params for '/improve --extended' mode
- `auto_extended_mode`: enable extended mode automatically (no need for the `--extended` option). Default is false.
- `num_code_suggestions_per_chunk`: number of code suggestions provided by the 'improve' tool, per chunk. Default is 8. - `num_code_suggestions_per_chunk`: number of code suggestions provided by the 'improve' tool, per chunk. Default is 8.
- `rank_extended_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is true. - `rank_extended_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is true.
- `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
In this mode, instead of presenting committable suggestions, the different suggestions will be combined into a single compact comment, with significantly smaller PR footprint.
To invoke the summarize mode, use the following command: #### A note on code suggestions quality
```
/improve --pr_code_suggestions.summarize=true
```
For example: - 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.
<kbd><img src=https://codium.ai/images/pr_agent/improved_summerize_open.png width="768"></kbd>
___
## Usage Tips
### Extra instructions
Extra instructions are very important for the `imrpove` tool, since they enable you to guide the model to suggestions that are more relevant to the specific needs of the project.
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Specify relevant aspects that you want the model to focus on.
Examples for extra instructions:
```
[pr_code_suggestions] # /improve #
extra_instructions="""
Emphasize the following aspects:
- Does the code logic covers relevant edge cases?
- Is the code logic clear and easy to understand?
- Is the code logic efficient?
...
"""
```
Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.
### PR footprint - regular vs summarize mode
The default mode of the `improve` tool provides committable suggestions. This mode as a high PR footprint, since each suggestion is a separate comment you need to resolve.
If you prefer something more compact, use the [`summarize`](#summarize-mode) mode, which combines all the suggestions into a single comment.
### A note on code suggestions quality
- While the current AI for code is getting better and better (GPT-4), it's not flawless. 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.
- Best quality will be obtained by using 'improve --extended' mode.
- 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.

View File

@ -1,67 +1,43 @@
# Review Tool # Review Tool
## Table of Contents
- [Overview](#overview)
- [Configuration options](#configuration-options)
- [Incremental Mode](#incremental-mode)
- [PR Reflection](#pr-reflection)
- [Usage Tips](#usage-tips)
- [General guidelines](#general-guidelines)
- [Code suggestions](#code-suggestions)
- [Automation](#automation)
- [Auto-labels](#auto-labels)
- [Extra instructions](#extra-instructions)
## Overview
The `review` tool scans the PR code changes, and automatically generates a PR review. The `review` tool scans the PR code changes, and automatically generates a PR review.
The tool can be triggered automatically every time a new PR is [opened](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools), or can be invoked manually by commenting on any PR: It can be invoked manually by commenting on any PR:
``` ```
/review /review
``` ```
For example: For example:
___
<kbd><img src=https://codium.ai/images/pr_agent/review_comment.png width="768"></kbd> <kbd><img src=./../pics/review_comment.png width="768"></kbd>
___ <kbd><img src=./../pics/describe.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/review.png width="768"></kbd>
___ The `review` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
### Configuration options ### Configuration options
To edit [configurations](./../pr_agent/settings/configuration.toml#L19) related to the review tool (`pr_reviewer` section), use the following template: Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review' tool:
```
/review --pr_reviewer.some_config1=... --pr_reviewer.some_config2=...
```
#### General options
- `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.
- `persistent_comment`: if set to true, the review comment will be persistent, meaning that every new review request will edit the previous one. Default is true.
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
#### 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.
#### SOC2 ticket compliance 💎 - `num_code_suggestions`: number of code suggestions provided by the 'review' tool. Default is 4.
This sub-tool checks if the PR description properly contains a ticket to a project management system (e.g., Jira, Asana, Trello, etc.), as required by SOC2 compliance. If not, it will add a label to the PR: "Missing SOC2 ticket". - `inline_code_comments`: if set to true, the tool will publish the code suggestions as comments on the code diff. Default is false.
- `require_soc2_review`: If set to true, the SOC2 ticket checker sub-tool will be enabled. Default is false. - `automatic_review`: if set to false, no automatic reviews will be done. Default is true.
- `soc2_ticket_prompt`: The prompt for the SOC2 ticket review. Default is: `Does the PR description include a link to ticket in a project management system (e.g., Jira, Asana, Trello, etc.) ?`. Edit this field if your compliance requirements are different. - `remove_previous_review_comment`: if set to true, the tool will remove the previous review comment before adding a new one. Default is false.
#### Adding PR labels - `persistent_comment`: if set to true, the review comment will be persistent. Default is true.
- `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. - `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
- `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)
#### 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:
Incremental review 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.
For invoking the incremental mode, the following command can be used:
``` ```
/review -i /review -i
``` ```
Note that the incremental mode is only available for GitHub. Note that the incremental mode is only available for GitHub.
<kbd><img src=https://codium.ai/images/pr_agent/incremental_review.png width="768"></kbd> <kbd><img src=./../pics/incremental_review.png width="768"></kbd>
Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L19) contains options to customize the 'review -i' tool. Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review -i' tool.
These configurations can be used to control the rate at which the incremental review tool will create new review comments when invoked automatically, to prevent making too much noise in the PR. These configurations can be used to control the rate at which the incremental review tool will create new review comments when invoked automatically, to prevent making too much noise in the PR.
- `minimal_commits_for_incremental_review`: Minimal number of commits since the last review that are required to create incremental review. - `minimal_commits_for_incremental_review`: Minimal number of commits since the last review that are required to create incremental review.
If there are less than the specified number of commits since the last review, the tool will not perform any action. If there are less than the specified number of commits since the last review, the tool will not perform any action.
@ -74,75 +50,25 @@ For example, if `minimal_commits_for_incremental_review=2` and `minimal_minutes_
When `require_all_thresholds_for_incremental_review=true` the incremental review __will not__ run, because only 1 out of 2 conditions were met (we have enough commits but the last review is too recent), When `require_all_thresholds_for_incremental_review=true` the incremental review __will not__ run, because only 1 out of 2 conditions were met (we have enough commits but the last review is too recent),
but when `require_all_thresholds_for_incremental_review=false` the incremental review __will__ run, because one condition is enough (we have 3 commits which is more than the configured 2). but when `require_all_thresholds_for_incremental_review=false` the incremental review __will__ run, because one condition is enough (we have 3 commits which is more than the configured 2).
Default is false - the tool will run as long as at least once conditions is met. Default is false - the tool will run as long as at least once conditions is met.
- `remove_previous_review_comment`: if set to true, the tool will remove the previous review comment before adding a new one. Default is false.
### PR Reflection #### PR Reflection
By invoking: By invoking:
``` ```
/reflect_and_review /reflect_and_review
``` ```
The tool will first ask the author questions about the PR, and will guide the review based on their answers. The tool will first ask the author questions about the PR, and will guide the review based on his answers.
<kbd><img src=https://codium.ai/images/pr_agent/reflection_questions.png width="768"></kbd> <kbd><img src=./../pics/reflection_questions.png width="768"></kbd>
___ <kbd><img src=./../pics/reflection_answers.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/reflection_answers.png width="768"></kbd> <kbd><img src=./../pics/reflection_insights.png width="768"></kbd>
___
<kbd><img src=https://codium.ai/images/pr_agent/reflection_insights.png width="768"></kbd>
___
## Usage Tips #### A note on code suggestions quality
1) [General guidelines](#general-guidelines)
2) [Code suggestions](#code-suggestions)
3) [Automation](#automation)
4) [Auto-labels](#auto-labels)
5) [Extra instructions](#extra-instructions)
### General guidelines - 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.
The `review` tool provides a collection of possible feedbacks about a PR.
It is recommended to review the [Configuration options](#configuration-options) section, and choose the relevant options for your use case.
Some of the feature that are disabled by default are quite useful, and should be considered for enabling. For example: - Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_reviewer_prompts.toml#L29). 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.
`require_score_review`, `require_soc2_review`, `enable_review_labels_effort`, and more.
On the other hand, if you find one of the enabled features to be irrelevant for your use case, disable it. No default configuration can fit all use cases. - Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project.
### Code suggestions
The `review` tool provides several type of feedbacks, one of them is code suggestions.
If you are interested **only** in the code suggestions, it is recommended to use the [`improve`](./IMPROVE.md) feature instead, since it dedicated only to code suggestions, and usually gives better results.
Use the `review` tool if you want to get a more comprehensive feedback, which includes code suggestions as well.
### Automation
- When you first install the app, the [default mode](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) for the `review` tool is:
```
pr_commands = ["/review", ...]
```
meaning the `review` tool will run automatically on every PR, with the default configuration.
Edit this field to enable/disable the tool, or to change the used configurations
### Auto-labels
The `review` tool can auto-generate two specific types of labels for a PR:
- a `possible security issue` label if it detects a [security issue](https://github.com/Codium-ai/pr-agent/blob/tr/user_description/pr_agent/settings/pr_reviewer_prompts.toml#L136) (`enable_review_labels_security` flag)
- a `Review effort [1-5]: x` label, where x is the estimated effort to review the PR (`enable_review_labels_effort` flag)
Both modes are useful, and we recommended to enable them.
### Extra instructions
Extra instruction are important.
The `review` tool can be configured with extra instructions, which can be used to guide the model to feedback tailored to the needs of your project.
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Specify the relevant sub-tool, and the relevant aspects of the PR that you want to emphasize.
Examples for extra instructions:
```
[pr_reviewer] # /review #
extra_instructions="""
In the code feedback section, emphasize the following:
- Does the code logic covers relevant edge cases?
- Is the code logic clear and easy to understand?
- Is the code logic efficient?
...
"""
```
Use triple quotes to write multi-line instructions. Use bullet points to make the instructions more readable.
- Unlike the 'review' feature, which does a lot of things, the ['improve --extended'](./IMPROVE.md) feature is dedicated only to suggestions, and usually gives better results.

View File

@ -6,21 +6,13 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=https://codium.ai/images/pr_agent/similar_issue_original_issue.png width="768"></kbd> <kbd><img src=./../pics/similar_issue_original_issue.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/similar_issue_comment.png width="768"></kbd> <kbd><img src=./../pics/similar_issue_comment.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/similar_issue.png width="768"></kbd> <kbd><img src=./../pics/similar_issue.png width="768"></kbd>
Note that to perform retrieval, the `similar_issue` tool indexes all the repo previous issues (once). Note that to perform retrieval, the `similar_issue` tool indexes all the repo previous issues (once).
To enable usage of the '**similar issue**' tool, you need to set the following keys in `.secrets.toml` (or in the relevant environment variables):
**Select VectorDBs** by changing `pr_similar_issue` parameter in `configuration.toml` file
2 VectorDBs are available to switch in
1. LanceDB
2. Pinecone
To enable usage of the '**similar issue**' tool for Pinecone, you need to set the following keys in `.secrets.toml` (or in the relevant environment variables):
``` ```
[pinecone] [pinecone]
api_key = "..." api_key = "..."

View File

@ -7,6 +7,5 @@
- [UPDATE CHANGELOG](./UPDATE_CHANGELOG.md) - [UPDATE CHANGELOG](./UPDATE_CHANGELOG.md)
- [ADD DOCUMENTATION](./ADD_DOCUMENTATION.md) - [ADD DOCUMENTATION](./ADD_DOCUMENTATION.md)
- [GENERATE CUSTOM LABELS](./GENERATE_CUSTOM_LABELS.md) - [GENERATE CUSTOM LABELS](./GENERATE_CUSTOM_LABELS.md)
- [Analyze](./Analyze.md)
See the **[installation guide](/INSTALL.md)** for instructions on how to setup PR-Agent. See the **[installation guide](/INSTALL.md)** for instructions on how to setup PR-Agent.

View File

@ -7,8 +7,8 @@ It can be invoked manually by commenting on any PR:
``` ```
For example: For example:
<kbd><img src=https://codium.ai/images/pr_agent/update_changelog_comment.png width="768"></kbd> <kbd><img src=./../pics/update_changelog_comment.png width="768"></kbd>
<kbd><img src=https://codium.ai/images/pr_agent/update_changelog.png width="768"></kbd> <kbd><img src=./../pics/update_changelog.png width="768"></kbd>
### Configuration options ### Configuration options

BIN
pics/add_docs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

BIN
pics/add_docs_comment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
pics/ask.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

BIN
pics/ask_comment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

BIN
pics/custom_labels_list.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
pics/describe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

BIN
pics/describe_comment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
pics/improve.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

BIN
pics/improve_comment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
pics/incremental_review.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

BIN
pics/logo-dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
pics/logo-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
pics/reflection_answers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
pics/review.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
pics/review_comment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
pics/similar_issue.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
pics/update_changelog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,13 +1,8 @@
import shlex import shlex
from functools import partial
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
from pr_agent.algo.utils import update_settings_from_args from pr_agent.algo.utils import update_settings_from_args
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.log import get_logger
from pr_agent.tools.pr_add_docs import PRAddDocs from pr_agent.tools.pr_add_docs import PRAddDocs
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_config import PRConfig from pr_agent.tools.pr_config import PRConfig
@ -43,8 +38,8 @@ command2class = {
commands = list(command2class.keys()) commands = list(command2class.keys())
class PRAgent: class PRAgent:
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler): def __init__(self):
self.ai_handler = ai_handler # will be initialized in run_action pass
async def handle_request(self, pr_url, request, notify=None) -> bool: async def handle_request(self, pr_url, request, notify=None) -> bool:
# First, apply repo specific settings if exists # First, apply repo specific settings if exists
@ -66,14 +61,13 @@ class PRAgent:
if action == "answer": if action == "answer":
if notify: if notify:
notify() notify()
await PRReviewer(pr_url, is_answer=True, args=args, ai_handler=self.ai_handler).run() await PRReviewer(pr_url, is_answer=True, args=args).run()
elif action == "auto_review": elif action == "auto_review":
await PRReviewer(pr_url, is_auto=True, args=args, ai_handler=self.ai_handler).run() await PRReviewer(pr_url, is_auto=True, args=args).run()
elif action in command2class: elif action in command2class:
if notify: if notify:
notify() notify()
await command2class[action](pr_url, args=args).run()
await command2class[action](pr_url, ai_handler=self.ai_handler, args=args).run()
else: else:
return False return False
return True return True

View File

@ -13,12 +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,
'anthropic.claude-v2': 100000,
'anthropic.claude-instant-v1': 100000,
'anthropic.claude-v1': 100000,
} }

View File

@ -1,19 +1,17 @@
import os import os
import boto3
import litellm import litellm
import openai import openai
from litellm import acompletion from litellm import acompletion
from openai.error import APIError, RateLimitError, Timeout, TryAgain from openai.error import APIError, RateLimitError, Timeout, TryAgain
from retry import retry from retry import retry
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.config_loader import get_settings from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger from pr_agent.log import get_logger
OPENAI_RETRIES = 5 OPENAI_RETRIES = 5
class LiteLLMAIHandler(BaseAiHandler): class AiHandler:
""" """
This class handles interactions with the OpenAI API for chat completions. This class handles interactions with the OpenAI API for chat completions.
It initializes the API key and other settings from a configuration file, It initializes the API key and other settings from a configuration file,
@ -25,50 +23,39 @@ class LiteLLMAIHandler(BaseAiHandler):
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:
self.aws_bedrock_client = None
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
)
if get_settings().get("AWS.BEDROCK_REGION", None):
litellm.AmazonAnthropicConfig.max_tokens_to_sample = 2000
self.aws_bedrock_client = boto3.client(
service_name="bedrock-runtime",
region_name=get_settings().aws.bedrock_region,
)
@property @property
def deployment_id(self): def deployment_id(self):
@ -101,19 +88,21 @@ class LiteLLMAIHandler(BaseAiHandler):
""" """
try: try:
deployment_id = self.deployment_id deployment_id = self.deployment_id
if get_settings().config.verbosity_level >= 2:
get_logger().debug(
f"Generating completion with {model}"
f"{(' from deployment ' + deployment_id) if deployment_id else ''}"
)
if self.azure: if self.azure:
model = 'azure/' + model model = 'azure/' + model
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}] messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
kwargs = { response = await acompletion(
"model": model, model=model,
"deployment_id": deployment_id, deployment_id=deployment_id,
"messages": messages, messages=messages,
"temperature": temperature, temperature=temperature,
"force_timeout": get_settings().config.ai_timeout, force_timeout=get_settings().config.ai_timeout
} )
if self.aws_bedrock_client:
kwargs["aws_bedrock_client"] = self.aws_bedrock_client
response = await acompletion(**kwargs)
except (APIError, Timeout, TryAgain) as e: except (APIError, Timeout, TryAgain) as e:
get_logger().error("Error during OpenAI inference: ", e) get_logger().error("Error during OpenAI inference: ", e)
raise raise
@ -130,4 +119,4 @@ class LiteLLMAIHandler(BaseAiHandler):
usage = response.get("usage") usage = response.get("usage")
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason, get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
model=model, usage=usage) model=model, usage=usage)
return resp, finish_reason return resp, finish_reason

View File

@ -1,28 +0,0 @@
from abc import ABC, abstractmethod
class BaseAiHandler(ABC):
"""
This class defines the interface for an AI handler to be used by the PR Agents.
"""
@abstractmethod
def __init__(self):
pass
@property
@abstractmethod
def deployment_id(self):
pass
@abstractmethod
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
"""
This method should be implemented to return a chat completion from the AI model.
Args:
model (str): the name of the model to use for the chat completion
system (str): the system message string to use for the chat completion
user (str): the user message string to use for the chat completion
temperature (float): the temperature to use for the chat completion
"""
pass

View File

@ -1,67 +0,0 @@
try:
from langchain.chat_models import ChatOpenAI, AzureChatOpenAI
from langchain.schema import SystemMessage, HumanMessage
except: # we don't enforce langchain as a dependency, so if it's not installed, just move on
pass
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
from openai.error import APIError, RateLimitError, Timeout, TryAgain
from retry import retry
import functools
OPENAI_RETRIES = 5
class LangChainOpenAIHandler(BaseAiHandler):
def __init__(self):
# Initialize OpenAIHandler specific attributes here
super().__init__()
self.azure = get_settings().get("OPENAI.API_TYPE", "").lower() == "azure"
try:
if self.azure:
# using a partial function so we can set the deployment_id later to support fallback_deployments
# but still need to access the other settings now so we can raise a proper exception if they're missing
self._chat = functools.partial(
lambda **kwargs: AzureChatOpenAI(**kwargs),
openai_api_key=get_settings().openai.key,
openai_api_base=get_settings().openai.api_base,
openai_api_version=get_settings().openai.api_version,
)
else:
self._chat = ChatOpenAI(openai_api_key=get_settings().openai.key)
except AttributeError as e:
if getattr(e, "name"):
raise ValueError(f"OpenAI {e.name} is required") from e
else:
raise e
@property
def chat(self):
if self.azure:
# we must set the deployment_id only here (instead of the __init__ method) to support fallback_deployments
return self._chat(deployment_name=self.deployment_id)
else:
return self._chat
@property
def deployment_id(self):
"""
Returns the deployment ID for the OpenAI API.
"""
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
try:
messages=[SystemMessage(content=system), HumanMessage(content=user)]
# get a chat completion from the formatted messages
resp = self.chat(messages, model=model, temperature=temperature)
finish_reason="completed"
return resp.content, finish_reason
except (Exception) as e:
get_logger().error("Unknown error during OpenAI inference: ", e)
raise e

View File

@ -1,67 +0,0 @@
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
import openai
from openai.error import APIError, RateLimitError, Timeout, TryAgain
from retry import retry
from pr_agent.config_loader import get_settings
from pr_agent.log import get_logger
OPENAI_RETRIES = 5
class OpenAIHandler(BaseAiHandler):
def __init__(self):
# Initialize OpenAIHandler specific attributes here
try:
super().__init__()
openai.api_key = get_settings().openai.key
if get_settings().get("OPENAI.ORG", None):
openai.organization = get_settings().openai.org
if get_settings().get("OPENAI.API_TYPE", None):
if get_settings().openai.api_type == "azure":
self.azure = True
openai.azure_key = get_settings().openai.key
if get_settings().get("OPENAI.API_VERSION", None):
openai.api_version = get_settings().openai.api_version
if get_settings().get("OPENAI.API_BASE", None):
openai.api_base = get_settings().openai.api_base
except AttributeError as e:
raise ValueError("OpenAI key is required") from e
@property
def deployment_id(self):
"""
Returns the deployment ID for the OpenAI API.
"""
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
try:
deployment_id = self.deployment_id
get_logger().info("System: ", system)
get_logger().info("User: ", user)
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
chat_completion = await openai.ChatCompletion.acreate(
model=model,
deployment_id=deployment_id,
messages=messages,
temperature=temperature,
)
resp = chat_completion["choices"][0]['message']['content']
finish_reason = chat_completion["choices"][0]["finish_reason"]
usage = chat_completion.get("usage")
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
model=model, usage=usage)
return resp, finish_reason
except (APIError, Timeout, TryAgain) as e:
get_logger().error("Error during OpenAI inference: ", e)
raise
except (RateLimitError) as e:
get_logger().error("Rate limit error during OpenAI inference: ", e)
raise
except (Exception) as e:
get_logger().error("Unknown error during OpenAI inference: ", e)
raise TryAgain from e

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
@ -226,11 +226,6 @@ async def retry_with_fallback_models(f: Callable):
# try each (model, deployment_id) pair until one is successful, otherwise raise exception # try each (model, deployment_id) pair until one is successful, otherwise raise exception
for i, (model, deployment_id) in enumerate(zip(all_models, all_deployments)): for i, (model, deployment_id) in enumerate(zip(all_models, all_deployments)):
try: try:
if get_settings().config.verbosity_level >= 2:
get_logger().debug(
f"Generating prediction with {model}"
f"{(' from deployment ' + deployment_id) if deployment_id else ''}"
)
get_settings().set("openai.deployment_id", deployment_id) get_settings().set("openai.deployment_id", deployment_id)
return await f(model) return await f(model)
except Exception as e: except Exception as e:
@ -269,44 +264,51 @@ def _get_all_deployments(all_models: List[str]) -> List[str]:
def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo], def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
relevant_file: str, relevant_file: str,
relevant_line_in_file: str, relevant_line_in_file: str) -> Tuple[int, int]:
absolute_position: int = None) -> Tuple[int, int]: """
Find the line number and absolute position of a relevant line in a file.
Args:
diff_files (List[FilePatchInfo]): A list of FilePatchInfo objects representing the patches of files.
relevant_file (str): The name of the file where the relevant line is located.
relevant_line_in_file (str): The content of the relevant line.
Returns:
Tuple[int, int]: A tuple containing the line number and absolute position of the relevant line in the file.
"""
position = -1 position = -1
if absolute_position is None: absolute_position = -1
absolute_position = -1
re_hunk_header = re.compile( re_hunk_header = re.compile(
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()
# try to find the line in the patch using difflib, with some margin of error
matches_difflib: list[str | Any] = difflib.get_close_matches(relevant_line_in_file,
patch_lines, n=3, cutoff=0.93)
if len(matches_difflib) == 1 and matches_difflib[0].startswith('+'):
relevant_line_in_file = matches_difflib[0]
delta = 0 delta = 0
start1, size1, start2, size2 = 0, 0, 0, 0 start1, size1, start2, size2 = 0, 0, 0, 0
if absolute_position != -1: # matching absolute to relative for i, line in enumerate(patch_lines):
for i, line in enumerate(patch_lines): if line.startswith('@@'):
# new hunk delta = 0
if line.startswith('@@'): match = re_hunk_header.match(line)
delta = 0 start1, size1, start2, size2 = map(int, match.groups()[:4])
match = re_hunk_header.match(line) elif not line.startswith('-'):
start1, size1, start2, size2 = map(int, match.groups()[:4]) delta += 1
elif not line.startswith('-'):
delta += 1
#
absolute_position_curr = start2 + delta - 1
if absolute_position_curr == absolute_position:
position = i
break
else:
# try to find the line in the patch using difflib, with some margin of error
matches_difflib: list[str | Any] = difflib.get_close_matches(relevant_line_in_file,
patch_lines, n=3, cutoff=0.93)
if len(matches_difflib) == 1 and matches_difflib[0].startswith('+'):
relevant_line_in_file = matches_difflib[0]
if relevant_line_in_file in line and line[0] != '-':
position = i
absolute_position = start2 + delta - 1
break
if position == -1 and relevant_line_in_file[0] == '+':
no_plus_line = relevant_line_in_file[1:].lstrip()
for i, line in enumerate(patch_lines): for i, line in enumerate(patch_lines):
if line.startswith('@@'): if line.startswith('@@'):
delta = 0 delta = 0
@ -315,30 +317,44 @@ def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
elif not line.startswith('-'): elif not line.startswith('-'):
delta += 1 delta += 1
if relevant_line_in_file in line and line[0] != '-': if no_plus_line in line and line[0] != '-':
# The model might add a '+' to the beginning of the relevant_line_in_file even if originally
# it's a context line
position = i position = i
absolute_position = start2 + delta - 1 absolute_position = start2 + delta - 1
break break
if position == -1 and relevant_line_in_file[0] == '+':
no_plus_line = relevant_line_in_file[1:].lstrip()
for i, line in enumerate(patch_lines):
if line.startswith('@@'):
delta = 0
match = re_hunk_header.match(line)
start1, size1, start2, size2 = map(int, match.groups()[:4])
elif not line.startswith('-'):
delta += 1
if no_plus_line in line and line[0] != '-':
# The model might add a '+' to the beginning of the relevant_line_in_file even if originally
# it's a context line
position = i
absolute_position = start2 + delta - 1
break
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
@ -49,7 +48,7 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
} }
for key, value in output_data.items(): for key, value in output_data.items():
if value is None or value == '' or value == {} or value == []: if value is None or value == '' or value == {}:
continue continue
if isinstance(value, dict): if isinstance(value, dict):
markdown_text += f"## {key}\n\n" markdown_text += f"## {key}\n\n"
@ -58,15 +57,14 @@ 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> <strong>{ emoji } Code feedback:</strong></summary>"
else: else:
markdown_text += f"\n\n**{emoji} Code feedback:**\n\n" markdown_text += f"\n\n- **{emoji} Code feedback:**\n\n"
else: else:
markdown_text += f"- {emoji} **{key}:**\n\n" markdown_text += f"- {emoji} **{key}:**\n\n"
for i, item in enumerate(value): for item in value:
if isinstance(item, dict) and key.lower() == 'code feedback': if isinstance(item, dict) and key.lower() == 'code feedback':
markdown_text += parse_code_suggestion(item, i, gfm_supported) markdown_text += parse_code_suggestion(item, gfm_supported)
elif item: elif item:
markdown_text += f" - {item}\n" markdown_text += f" - {item}\n"
if key.lower() == 'code feedback': if key.lower() == 'code feedback':
@ -76,17 +74,11 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
markdown_text += "\n\n" markdown_text += "\n\n"
elif value != 'n/a': elif value != 'n/a':
emoji = emojis.get(key, "") emoji = emojis.get(key, "")
if key.lower() == 'general suggestions': markdown_text += f"- {emoji} **{key}:** {value}\n"
if gfm_supported:
markdown_text += f"\n\n<strong>{emoji} General suggestions:</strong> {value}\n"
else:
markdown_text += f"{emoji} **General suggestions:** {value}\n"
else:
markdown_text += f"- {emoji} **{key}:** {value}\n"
return markdown_text return markdown_text
def parse_code_suggestion(code_suggestions: dict, i: int = 0, gfm_supported: bool = True) -> str: def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> str:
""" """
Convert a dictionary of data into markdown format. Convert a dictionary of data into markdown format.
@ -97,53 +89,24 @@ def parse_code_suggestion(code_suggestions: dict, i: int = 0, gfm_supported: boo
str: A string containing the markdown formatted text generated from the input dictionary. str: A string containing the markdown formatted text generated from the input dictionary.
""" """
markdown_text = "" markdown_text = ""
if gfm_supported and 'relevant line' in code_suggestions: for sub_key, sub_value in code_suggestions.items():
if i == 0: if isinstance(sub_value, dict): # "code example"
markdown_text += "<hr>" markdown_text += f" - **{sub_key}:**\n"
markdown_text += '<table>' for code_key, code_value in sub_value.items(): # 'before' and 'after' code
for sub_key, sub_value in code_suggestions.items(): code_str = f"```\n{code_value}\n```"
try: code_str_indented = textwrap.indent(code_str, ' ')
if sub_key.lower() == 'relevant file': markdown_text += f" - **{code_key}:**\n{code_str_indented}\n"
relevant_file = sub_value.strip('`').strip('"').strip("'") else:
markdown_text += f"<tr><td>{sub_key}</td><td>{relevant_file}</td></tr>" if "relevant file" in sub_key.lower():
# continue markdown_text += f"\n - **{sub_key}:** {sub_value}\n"
elif sub_key.lower() == 'suggestion':
markdown_text += (f"<tr><td>{sub_key} &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>"
f"<td><br>\n\n**{sub_value.strip()}**\n<br></td></tr>")
elif sub_key.lower() == 'relevant line':
markdown_text += f"<tr><td>relevant line</td>"
sub_value_list = sub_value.split('](')
relevant_line = sub_value_list[0].lstrip('`').lstrip('[')
if len(sub_value_list) > 1:
link = sub_value_list[1].rstrip(')').strip('`')
markdown_text += f"<td><a href={link}>{relevant_line}</a></td>"
else:
markdown_text += f"<td>{relevant_line}</td>"
markdown_text += "</tr>"
except Exception as e:
get_logger().exception(f"Failed to parse code suggestion: {e}")
pass
markdown_text += '</table>'
markdown_text += "<hr>"
else:
for sub_key, sub_value in code_suggestions.items():
if isinstance(sub_value, dict): # "code example"
markdown_text += f" - **{sub_key}:**\n"
for code_key, code_value in sub_value.items(): # 'before' and 'after' code
code_str = f"```\n{code_value}\n```"
code_str_indented = textwrap.indent(code_str, ' ')
markdown_text += f" - **{code_key}:**\n{code_str_indented}\n"
else: else:
if "relevant file" in sub_key.lower(): markdown_text += f" **{sub_key}:** {sub_value}\n"
markdown_text += f"\n - **{sub_key}:** {sub_value} \n" if not gfm_supported:
else: if "relevant line" not in sub_key.lower(): # nicer presentation
markdown_text += f" **{sub_key}:** {sub_value} \n"
if not gfm_supported:
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
markdown_text = markdown_text.rstrip('\n') + " \n" # works for gitlab and bitbucker markdown_text = markdown_text.rstrip('\n') + " \n" # works for gitlab and bitbucker
markdown_text += "\n" markdown_text += "\n"
return markdown_text return markdown_text
@ -319,96 +282,67 @@ 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, keys_fix_yaml: List[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, keys_fix_yaml=keys_fix_yaml) data = try_fix_yaml(review_text)
return data
def try_fix_yaml(review_text: str) -> dict:
review_text_lines = review_text.split('\n')
# first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...'
review_text_lines_copy = review_text_lines.copy()
for i in range(0, len(review_text_lines_copy)):
if 'relevant line:' in review_text_lines_copy[i] and not '|-' in review_text_lines_copy[i]:
review_text_lines_copy[i] = review_text_lines_copy[i].replace('relevant line: ',
'relevant line: |-\n ')
try:
data = yaml.load('\n'.join(review_text_lines_copy), Loader=yaml.SafeLoader)
get_logger().info(f"Successfully parsed AI prediction after adding |-\n to relevant line")
return data
except:
get_logger().debug(f"Failed to parse AI prediction after adding |-\n to relevant line")
# second fallback - try to remove last lines
data = {}
for i in range(1, len(review_text_lines)):
review_text_lines_tmp = '\n'.join(review_text_lines[:-i])
try:
data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader)
get_logger().info(f"Successfully parsed AI prediction after removing {i} lines")
break
except:
pass
return data return data
def try_fix_yaml(response_text: str, keys_fix_yaml: List[str] = []) -> dict: def set_custom_labels(variables):
response_text_lines = response_text.split('\n')
keys = ['relevant line:', 'suggestion content:', 'relevant file:', 'existing code:', 'improved code:']
keys = keys + keys_fix_yaml
# first fallback - try to convert 'relevant line: ...' to relevant line: |-\n ...'
response_text_lines_copy = response_text_lines.copy()
for i in range(0, len(response_text_lines_copy)):
for key in keys:
if key in response_text_lines_copy[i] and not '|-' in response_text_lines_copy[i]:
response_text_lines_copy[i] = response_text_lines_copy[i].replace(f'{key}',
f'{key} |-\n ')
try:
data = yaml.safe_load('\n'.join(response_text_lines_copy))
get_logger().info(f"Successfully parsed AI prediction after adding |-\n")
return data
except:
get_logger().info(f"Failed to parse AI prediction after adding |-\n")
# second fallback - try to extract only range from first ```yaml to ````
snippet_pattern = r'```(yaml)?[\s\S]*?```'
snippet = re.search(snippet_pattern, '\n'.join(response_text_lines_copy))
if snippet:
snippet_text = snippet.group()
try:
data = yaml.safe_load(snippet_text.removeprefix('```yaml').rstrip('`'))
get_logger().info(f"Successfully parsed AI prediction after extracting yaml snippet")
return data
except:
pass
# third fallback - try to remove leading and trailing curly brackets
response_text_copy = response_text.strip().rstrip().removeprefix('{').removesuffix('}')
try:
data = yaml.safe_load(response_text_copy,)
get_logger().info(f"Successfully parsed AI prediction after removing curly brackets")
return data
except:
pass
# fourth fallback - try to remove last lines
data = {}
for i in range(1, len(response_text_lines)):
response_text_lines_tmp = '\n'.join(response_text_lines[:-i])
try:
data = yaml.safe_load(response_text_lines_tmp,)
get_logger().info(f"Successfully parsed AI prediction after removing {i} lines")
return data
except:
pass
def set_custom_labels(variables, git_provider=None):
if not get_settings().config.enable_custom_labels: if not get_settings().config.enable_custom_labels:
return return
labels = get_settings().custom_labels labels = get_settings().custom_labels
if not labels: if not labels:
# set default labels # set default labels
labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other'] labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Refactoring', 'Enhancement', 'Documentation', 'Other']
labels_list = "\n - ".join(labels) if labels else "" labels_list = "\n - ".join(labels) if labels else ""
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 = ""
# Set custom labels
variables["custom_labels_class"] = "class Label(str, Enum):"
counter = 0
labels_minimal_to_labels_dict = {}
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_class"] += f"\n {k.lower().replace(' ', '_')} = {description}" variables["custom_labels_examples"] = f" - {list(labels.keys())[0]}"
labels_minimal_to_labels_dict[k.lower().replace(' ', '_')] = k
counter += 1
variables["labels_minimal_to_labels_dict"] = labels_minimal_to_labels_dict
def get_user_labels(current_labels: List[str] = None): def get_user_labels(current_labels: List[str] = None):
""" """
@ -419,7 +353,7 @@ def get_user_labels(current_labels: List[str] = None):
current_labels = [] current_labels = []
user_labels = [] user_labels = []
for label in current_labels: for label in current_labels:
if label.lower() in ['bug fix', 'tests', 'enhancement', 'documentation', 'other']: if label.lower() in ['bug fix', 'tests', 'refactoring', 'enhancement', 'documentation', 'other']:
continue continue
if get_settings().config.enable_custom_labels: if get_settings().config.enable_custom_labels:
if label in get_settings().custom_labels: if label in get_settings().custom_labels:
@ -435,43 +369,8 @@ def get_user_labels(current_labels: List[str] = None):
def get_max_tokens(model): def get_max_tokens(model):
settings = get_settings() settings = get_settings()
if model in MAX_TOKENS: max_tokens_model = MAX_TOKENS[model]
max_tokens_model = MAX_TOKENS[model]
else:
raise Exception(f"MAX_TOKENS must be set for model {model} in ./pr_agent/algo/__init__.py")
if settings.config.max_model_tokens: if settings.config.max_model_tokens:
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

@ -1,147 +1,30 @@
import os import json
from typing import Optional, Tuple from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
import os
from ..log import get_logger from ..log import get_logger
AZURE_DEVOPS_AVAILABLE = True AZURE_DEVOPS_AVAILABLE = True
try: try:
from msrest.authentication import BasicAuthentication from msrest.authentication import BasicAuthentication
from azure.devops.connection import Connection from azure.devops.connection import Connection
from azure.devops.v7_1.git.models import ( from azure.devops.v7_1.git.models import Comment, CommentThread, GitVersionDescriptor, GitPullRequest
Comment, except ImportError:
CommentThread,
GitVersionDescriptor,
GitPullRequest,
)
except ImportError as e:
AZURE_DEVOPS_AVAILABLE = False AZURE_DEVOPS_AVAILABLE = False
from ..algo.language_handler import is_valid_file from ..algo.pr_processing import clip_tokens
from ..algo.utils import clip_tokens, load_large_diff
from ..config_loader import get_settings from ..config_loader import get_settings
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider from ..algo.utils import load_large_diff
from ..algo.language_handler import is_valid_file
from .git_provider import EDIT_TYPE, FilePatchInfo
class AzureDevopsProvider(GitProvider): class AzureDevopsProvider:
def publish_code_suggestions(self, code_suggestions: list) -> bool: def __init__(self, pr_url: Optional[str] = None, incremental: Optional[bool] = False):
"""
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:
for post_parameters in post_parameters_list:
comment = Comment(content=post_parameters["body"], comment_type=1)
thread = CommentThread(comments=[comment],
thread_context={
"filePath": post_parameters["path"],
"rightFileStart": {
"line": post_parameters["start_line"],
"offset": 1,
},
"rightFileEnd": {
"line": post_parameters["line"],
"offset": 1,
},
})
self.azure_devops_client.create_thread(
comment_thread=thread,
project=self.workspace_slug,
repository_id=self.repo_slug,
pull_request_id=self.pr_num
)
if get_settings().config.verbosity_level >= 2:
get_logger().info(
f"Published code suggestion on {self.pr_num} at {post_parameters['path']}"
)
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 get_pr_description_full(self) -> str:
pass
def remove_comment(self, comment):
try:
self.azure_devops_client.delete_comment(
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
thread_id=comment["thread_id"],
comment_id=comment["comment_id"],
project=self.workspace_slug,
)
except Exception as e:
get_logger().exception(f"Failed to remove comment, error: {e}")
def publish_labels(self, pr_types):
try:
for pr_type in pr_types:
self.azure_devops_client.create_pull_request_label(
label={"name": pr_type},
project=self.workspace_slug,
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
)
except Exception as e:
get_logger().exception(f"Failed to publish labels, error: {e}")
def get_pr_labels(self):
try:
labels = self.azure_devops_client.get_pull_request_labels(
project=self.workspace_slug,
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
)
return [label.name for label in labels]
except Exception as e:
get_logger().exception(f"Failed to get labels, error: {e}")
return []
def __init__(
self, pr_url: Optional[str] = None, incremental: Optional[bool] = False
):
if not AZURE_DEVOPS_AVAILABLE: if not AZURE_DEVOPS_AVAILABLE:
raise ImportError( raise ImportError("Azure DevOps provider is not available. Please install the required dependencies.")
"Azure DevOps provider is not available. Please install the required dependencies."
)
self.azure_devops_client = self._get_azure_devops_client() self.azure_devops_client = self._get_azure_devops_client()
@ -156,11 +39,8 @@ class AzureDevopsProvider(GitProvider):
self.set_pr(pr_url) self.set_pr(pr_url)
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
if capability in [ if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels',
"get_issue_comments", 'remove_initial_comment', 'gfm_markdown']:
"create_inline_comment",
"publish_inline_comments",
]:
return False return False
return True return True
@ -170,14 +50,10 @@ class AzureDevopsProvider(GitProvider):
def get_repo_settings(self): def get_repo_settings(self):
try: try:
contents = self.azure_devops_client.get_item_content( contents = self.azure_devops_client.get_item_content(repository_id=self.repo_slug,
repository_id=self.repo_slug, project=self.workspace_slug, download=False,
project=self.workspace_slug, include_content_metadata=False, include_content=True,
download=False, path=".pr_agent.toml")
include_content_metadata=False,
include_content=True,
path=".pr_agent.toml",
)
return contents return contents
except Exception as e: except Exception as e:
get_logger().exception("get repo settings error") get_logger().exception("get repo settings error")
@ -185,19 +61,15 @@ class AzureDevopsProvider(GitProvider):
def get_files(self): def get_files(self):
files = [] files = []
for i in self.azure_devops_client.get_pull_request_commits( for i in self.azure_devops_client.get_pull_request_commits(project=self.workspace_slug,
project=self.workspace_slug, repository_id=self.repo_slug,
repository_id=self.repo_slug, pull_request_id=self.pr_num):
pull_request_id=self.pr_num,
): changes_obj = self.azure_devops_client.get_changes(project=self.workspace_slug,
changes_obj = self.azure_devops_client.get_changes( repository_id=self.repo_slug, commit_id=i.commit_id)
project=self.workspace_slug,
repository_id=self.repo_slug,
commit_id=i.commit_id,
)
for c in changes_obj.changes: for c in changes_obj.changes:
files.append(c["item"]["path"]) files.append(c['item']['path'])
return list(set(files)) return list(set(files))
def get_diff_files(self) -> list[FilePatchInfo]: def get_diff_files(self) -> list[FilePatchInfo]:
@ -205,27 +77,22 @@ class AzureDevopsProvider(GitProvider):
base_sha = self.pr.last_merge_target_commit base_sha = self.pr.last_merge_target_commit
head_sha = self.pr.last_merge_source_commit head_sha = self.pr.last_merge_source_commit
commits = self.azure_devops_client.get_pull_request_commits( commits = self.azure_devops_client.get_pull_request_commits(project=self.workspace_slug,
project=self.workspace_slug, repository_id=self.repo_slug,
repository_id=self.repo_slug, pull_request_id=self.pr_num)
pull_request_id=self.pr_num,
)
diff_files = [] diff_files = []
diffs = [] diffs = []
diff_types = {} diff_types = {}
for c in commits: for c in commits:
changes_obj = self.azure_devops_client.get_changes( changes_obj = self.azure_devops_client.get_changes(project=self.workspace_slug,
project=self.workspace_slug, repository_id=self.repo_slug, commit_id=c.commit_id)
repository_id=self.repo_slug,
commit_id=c.commit_id,
)
for i in changes_obj.changes: for i in changes_obj.changes:
if i["item"]["gitObjectType"] == "tree": if(i['item']['gitObjectType'] == 'tree'):
continue continue
diffs.append(i["item"]["path"]) diffs.append(i['item']['path'])
diff_types[i["item"]["path"]] = i["changeType"] diff_types[i['item']['path']] = i['changeType']
diffs = list(set(diffs)) diffs = list(set(diffs))
@ -233,72 +100,47 @@ class AzureDevopsProvider(GitProvider):
if not is_valid_file(file): if not is_valid_file(file):
continue continue
version = GitVersionDescriptor( version = GitVersionDescriptor(version=head_sha.commit_id, version_type='commit')
version=head_sha.commit_id, version_type="commit"
)
try: try:
new_file_content_str = self.azure_devops_client.get_item( new_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug,
repository_id=self.repo_slug, path=file,
path=file, project=self.workspace_slug,
project=self.workspace_slug, version_descriptor=version,
version_descriptor=version, download=False,
download=False, include_content=True)
include_content=True,
)
new_file_content_str = new_file_content_str.content new_file_content_str = new_file_content_str.content
except Exception as error: except Exception as error:
get_logger().error( get_logger().error("Failed to retrieve new file content of %s at version %s. Error: %s", file, version, str(error))
"Failed to retrieve new file content of %s at version %s. Error: %s",
file,
version,
str(error),
)
new_file_content_str = "" new_file_content_str = ""
edit_type = EDIT_TYPE.MODIFIED edit_type = EDIT_TYPE.MODIFIED
if diff_types[file] == "add": if diff_types[file] == 'add':
edit_type = EDIT_TYPE.ADDED edit_type = EDIT_TYPE.ADDED
elif diff_types[file] == "delete": elif diff_types[file] == 'delete':
edit_type = EDIT_TYPE.DELETED edit_type = EDIT_TYPE.DELETED
elif diff_types[file] == "rename": elif diff_types[file] == 'rename':
edit_type = EDIT_TYPE.RENAMED edit_type = EDIT_TYPE.RENAMED
version = GitVersionDescriptor( version = GitVersionDescriptor(version=base_sha.commit_id, version_type='commit')
version=base_sha.commit_id, version_type="commit"
)
try: try:
original_file_content_str = self.azure_devops_client.get_item( original_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug,
repository_id=self.repo_slug, path=file,
path=file, project=self.workspace_slug,
project=self.workspace_slug, version_descriptor=version,
version_descriptor=version, download=False,
download=False, include_content=True)
include_content=True,
)
original_file_content_str = original_file_content_str.content original_file_content_str = original_file_content_str.content
except Exception as error: except Exception as error:
get_logger().error( get_logger().error("Failed to retrieve original file content of %s at version %s. Error: %s", file, version, str(error))
"Failed to retrieve original file content of %s at version %s. Error: %s",
file,
version,
str(error),
)
original_file_content_str = "" original_file_content_str = ""
patch = load_large_diff( patch = load_large_diff(file, new_file_content_str, original_file_content_str)
file, new_file_content_str, original_file_content_str
)
diff_files.append( diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str,
FilePatchInfo( patch=patch,
original_file_content_str, filename=file,
new_file_content_str, edit_type=edit_type))
patch=patch,
filename=file,
edit_type=edit_type,
)
)
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
@ -309,92 +151,67 @@ class AzureDevopsProvider(GitProvider):
def publish_comment(self, pr_comment: str, is_temporary: bool = False): def publish_comment(self, pr_comment: str, is_temporary: bool = False):
comment = Comment(content=pr_comment) comment = Comment(content=pr_comment)
thread = CommentThread(comments=[comment]) thread = CommentThread(comments=[comment])
thread_response = self.azure_devops_client.create_thread( thread_response = self.azure_devops_client.create_thread(comment_thread=thread, project=self.workspace_slug,
comment_thread=thread, repository_id=self.repo_slug,
project=self.workspace_slug, pull_request_id=self.pr_num)
repository_id=self.repo_slug,
pull_request_id=self.pr_num,
)
if is_temporary: if is_temporary:
self.temp_comments.append( self.temp_comments.append({'thread_id': thread_response.id, 'comment_id': comment.id})
{"thread_id": thread_response.id, "comment_id": thread_response.comments[0].id}
)
def publish_description(self, pr_title: str, pr_body: str): def publish_description(self, pr_title: str, pr_body: str):
try: try:
updated_pr = GitPullRequest() updated_pr = GitPullRequest()
updated_pr.title = pr_title updated_pr.title = pr_title
updated_pr.description = pr_body updated_pr.description = pr_body
self.azure_devops_client.update_pull_request( self.azure_devops_client.update_pull_request(project=self.workspace_slug,
project=self.workspace_slug, repository_id=self.repo_slug,
repository_id=self.repo_slug, pull_request_id=self.pr_num,
pull_request_id=self.pr_num, git_pull_request_to_update=updated_pr)
git_pull_request_to_update=updated_pr,
)
except Exception as e: except Exception as e:
get_logger().exception( get_logger().exception(f"Could not update pull request {self.pr_num} description: {e}")
f"Could not update pull request {self.pr_num} description: {e}"
)
def remove_initial_comment(self): def remove_initial_comment(self):
try: return "" # not implemented yet
for comment in self.temp_comments:
self.remove_comment(comment)
except Exception as e:
get_logger().exception(f"Failed to remove temp comments, error: {e}")
def publish_inline_comment( def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
self, body: str, relevant_file: str, relevant_line_in_file: str raise NotImplementedError("Azure DevOps provider does not support publishing inline comment yet")
):
raise NotImplementedError( def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
"Azure DevOps provider does not support publishing inline comment yet" raise NotImplementedError("Azure DevOps provider does not support creating inline comments yet")
)
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
raise NotImplementedError( raise NotImplementedError("Azure DevOps provider does not support publishing inline comments yet")
"Azure DevOps provider does not support publishing inline comments yet"
)
def get_title(self): def get_title(self):
return self.pr.title return self.pr.title
def get_languages(self): def get_languages(self):
languages = [] languages = []
files = self.azure_devops_client.get_items( files = self.azure_devops_client.get_items(project=self.workspace_slug, repository_id=self.repo_slug,
project=self.workspace_slug, recursion_level="Full", include_content_metadata=True,
repository_id=self.repo_slug, include_links=False, download=False)
recursion_level="Full",
include_content_metadata=True,
include_links=False,
download=False,
)
for f in files: for f in files:
if f.git_object_type == "blob": if f.git_object_type == 'blob':
file_name, file_extension = os.path.splitext(f.path) file_name, file_extension = os.path.splitext(f.path)
languages.append(file_extension[1:]) languages.append(file_extension[1:])
extension_counts = {} extension_counts = {}
for ext in languages: for ext in languages:
if ext != "": if ext != '':
extension_counts[ext] = extension_counts.get(ext, 0) + 1 extension_counts[ext] = extension_counts.get(ext, 0) + 1
total_extensions = sum(extension_counts.values()) total_extensions = sum(extension_counts.values())
extension_percentages = { extension_percentages = {ext: (count / total_extensions) * 100 for ext, count in extension_counts.items()}
ext: (count / total_extensions) * 100
for ext, count in extension_counts.items()
}
return extension_percentages return extension_percentages
def get_pr_branch(self): def get_pr_branch(self):
pr_info = self.azure_devops_client.get_pull_request_by_id( pr_info = self.azure_devops_client.get_pull_request_by_id(project=self.workspace_slug,
project=self.workspace_slug, pull_request_id=self.pr_num pull_request_id=self.pr_num)
) source_branch = pr_info.source_ref_name.split('/')[-1]
source_branch = pr_info.source_ref_name.split("/")[-1]
return source_branch return source_branch
def get_pr_description(self, full=False): def get_pr_description(self):
max_tokens = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None) max_tokens = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
if max_tokens: if max_tokens:
return clip_tokens(self.pr.description, max_tokens) return clip_tokens(self.pr.description, max_tokens)
@ -404,9 +221,7 @@ class AzureDevopsProvider(GitProvider):
return 0 return 0
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError( raise NotImplementedError("Azure DevOps provider does not support issue comments yet")
"Azure DevOps provider does not support issue comments yet"
)
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]: def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
return True return True
@ -415,20 +230,16 @@ class AzureDevopsProvider(GitProvider):
return True return True
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError( raise NotImplementedError("Azure DevOps provider does not support issue comments yet")
"Azure DevOps provider does not support issue comments yet"
)
@staticmethod @staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]: def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url) parsed_url = urlparse(pr_url)
path_parts = parsed_url.path.strip("/").split("/") path_parts = parsed_url.path.strip('/').split('/')
if len(path_parts) < 6 or path_parts[4] != "pullrequest": if len(path_parts) < 6 or path_parts[4] != 'pullrequest':
raise ValueError( raise ValueError("The provided URL does not appear to be a Azure DevOps PR URL")
"The provided URL does not appear to be a Azure DevOps PR URL"
)
workspace_slug = path_parts[1] workspace_slug = path_parts[1]
repo_slug = path_parts[3] repo_slug = path_parts[3]
@ -444,9 +255,10 @@ class AzureDevopsProvider(GitProvider):
pat = get_settings().azure_devops.pat pat = get_settings().azure_devops.pat
org = get_settings().azure_devops.org org = get_settings().azure_devops.org
except AttributeError as e: except AttributeError as e:
raise ValueError("Azure DevOps PAT token is required ") from e raise ValueError(
"Azure DevOps PAT token is required ") from e
credentials = BasicAuthentication("", pat) credentials = BasicAuthentication('', pat)
azure_devops_connection = Connection(base_url=org, creds=credentials) azure_devops_connection = Connection(base_url=org, creds=credentials)
azure_devops_client = azure_devops_connection.clients.get_git_client() azure_devops_client = azure_devops_connection.clients.get_git_client()
@ -454,23 +266,13 @@ class AzureDevopsProvider(GitProvider):
def _get_repo(self): def _get_repo(self):
if self.repo is None: if self.repo is None:
self.repo = self.azure_devops_client.get_repository( self.repo = self.azure_devops_client.get_repository(project=self.workspace_slug,
project=self.workspace_slug, repository_id=self.repo_slug repository_id=self.repo_slug)
)
return self.repo return self.repo
def _get_pr(self): def _get_pr(self):
self.pr = self.azure_devops_client.get_pull_request_by_id( self.pr = self.azure_devops_client.get_pull_request_by_id(pull_request_id=self.pr_num, project=self.workspace_slug)
pull_request_id=self.pr_num, project=self.workspace_slug
)
return self.pr return self.pr
def get_commit_messages(self): def get_commit_messages(self):
return "" # not implemented yet return "" # not implemented yet
def get_pr_id(self):
try:
pr_id = f"{self.workspace_slug}/{self.repo_slug}/{self.pr_num}"
return pr_id
except Exception:
return ""

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}")
@ -201,10 +189,8 @@ class BitbucketProvider(GitProvider):
get_logger().exception(f"Failed to remove comment, error: {e}") get_logger().exception(f"Failed to remove comment, error: {e}")
# funtion to create_inline_comment # funtion to create_inline_comment
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, absolute_position: int = None): 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(), position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(), relevant_file.strip('`'), relevant_line_in_file)
relevant_file.strip('`'),
relevant_line_in_file, absolute_position)
if position == -1: if position == -1:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}") get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
@ -230,13 +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:
if relevant_line_start == -1:
link = f"{self.pr_url}/#L{relevant_file}"
else:
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("'")
@ -259,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
@ -356,5 +327,5 @@ class BitbucketProvider(GitProvider):
pass pass
# bitbucket does not support labels # bitbucket does not support labels
def get_pr_labels(self): def get_labels(self):
pass pass

View File

@ -1,354 +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,
absolute_position: int = None):
position, absolute_position = find_line_number_of_relevant_line_in_file(
self.get_diff_files(),
relevant_file.strip('`'),
relevant_line_in_file,
absolute_position
)
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_pr_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
@ -61,7 +61,6 @@ class CodeCommitProvider(GitProvider):
self.pr = None self.pr = None
self.diff_files = None self.diff_files = None
self.git_files = None self.git_files = None
self.pr_url = pr_url
if pr_url: if pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
@ -216,7 +215,7 @@ class CodeCommitProvider(GitProvider):
def publish_labels(self, labels): def publish_labels(self, labels):
return [""] # not implemented yet return [""] # not implemented yet
def get_pr_labels(self): def get_labels(self):
return [""] # not implemented yet return [""] # not implemented yet
def remove_initial_comment(self): def remove_initial_comment(self):
@ -229,6 +228,9 @@ class CodeCommitProvider(GitProvider):
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet") raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
raise NotImplementedError("CodeCommit provider does not support creating inline comments yet")
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet") raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
@ -267,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

@ -192,7 +192,7 @@ class GerritProvider(GitProvider):
) )
self.repo = Repo(self.repo_path) self.repo = Repo(self.repo_path)
assert self.repo assert self.repo
self.pr_url = base_url
self.pr = PullRequestMimic(self.get_pr_title(), self.get_diff_files()) self.pr = PullRequestMimic(self.get_pr_title(), self.get_diff_files())
def get_pr_title(self): def get_pr_title(self):
@ -207,7 +207,7 @@ class GerritProvider(GitProvider):
Comment = namedtuple('Comment', ['body']) Comment = namedtuple('Comment', ['body'])
return Comments([Comment(c['message']) for c in reversed(comments)]) return Comments([Comment(c['message']) for c in reversed(comments)])
def get_pr_labels(self): def get_labels(self):
raise NotImplementedError( raise NotImplementedError(
'Getting labels is not implemented for the gerrit provider') 'Getting labels is not implemented for the gerrit provider')
@ -380,6 +380,11 @@ class GerritProvider(GitProvider):
'Publishing inline comments is not implemented for the gerrit ' 'Publishing inline comments is not implemented for the gerrit '
'provider') 'provider')
def create_inline_comment(self, body: str, relevant_file: str,
relevant_line_in_file: str):
raise NotImplementedError(
'Creating inline comments is not implemented for the gerrit '
'provider')
def publish_labels(self, labels): def publish_labels(self, labels):
# Not applicable to the local git provider, # Not applicable to the local git provider,

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
@ -26,8 +25,6 @@ class FilePatchInfo:
tokens: int = -1 tokens: int = -1
edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN edit_type: EDIT_TYPE = EDIT_TYPE.UNKNOWN
old_filename: str = None old_filename: str = None
num_plus_lines: int = -1
num_minus_lines: int = -1
class GitProvider(ABC): class GitProvider(ABC):
@ -43,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
@ -65,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:
@ -74,103 +106,22 @@ class GitProvider(ABC):
def get_user_description(self) -> str: def get_user_description(self) -> str:
description = (self.get_pr_description_full() or "").strip() description = (self.get_pr_description_full() or "").strip()
description_lowercase = description.lower()
# if the existing description wasn't generated by the pr-agent, just return it as-is # if the existing description wasn't generated by the pr-agent, just return it as-is
if not self._is_generated_by_pr_agent(description_lowercase): if not any(description.startswith(header) for header in ("## PR Type", "## PR Description")):
return description return description
# if the existing description was generated by the pr-agent, but it doesn't contain the user description,
# if the existing description was generated by the pr-agent, but it doesn't contain a user description,
# return nothing (empty string) because it means there is no user description # return nothing (empty string) because it means there is no user description
user_description_header = "## user description" if "## User Description:" not in description:
if user_description_header not in description_lowercase:
return "" return ""
# 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
# user_description_start_position = description_lowercase.find(user_description_header) + len(user_description_header) return description.split("## User Description:", 1)[1].strip()
# return description[user_description_start_position:].split("\n", 1)[-1].strip()
# the 'user description' is in the beginning. extract and return it
possible_headers = self._possible_headers()
start_position = description_lowercase.find(user_description_header) + len(user_description_header)
end_position = len(description)
for header in possible_headers: # try to clip at the next header
if header != user_description_header and header in description_lowercase:
end_position = min(end_position, description_lowercase.find(header))
if end_position != len(description) and end_position > start_position:
original_user_description = description[start_position:end_position].strip()
if original_user_description.endswith("___"):
original_user_description = original_user_description[:-3].strip()
else:
original_user_description = description.split("___")[0].strip()
if original_user_description.lower().startswith(user_description_header):
original_user_description = original_user_description[len(user_description_header):].strip()
return original_user_description
def _possible_headers(self):
return ("## user description", "## pr type", "## pr description", "## pr labels", "## type", "## description",
"## labels", "### 🤖 generated by pr agent")
def _is_generated_by_pr_agent(self, description_lowercase: str) -> bool:
possible_headers = self._possible_headers()
return any(description_lowercase.startswith(header) for header in possible_headers)
@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
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str,
absolute_position: int = None):
raise NotImplementedError("This git provider does not support creating inline comments yet")
@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_pr_labels(self):
pass
def get_repo_labels(self):
pass pass
@abstractmethod @abstractmethod
@ -181,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:
@ -214,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
@ -34,7 +34,6 @@ class GithubProvider(GitProvider):
if pr_url and 'pull' in pr_url: if pr_url and 'pull' in pr_url:
self.set_pr(pr_url) self.set_pr(pr_url)
self.last_commit_id = list(self.pr.get_commits())[-1] self.last_commit_id = list(self.pr.get_commits())[-1]
self.pr_url = self.get_pr_url() # pr_url for github actions can be as api.github.com, so we need to get the url from the pr object
def is_supported(self, capability: str) -> bool: def is_supported(self, capability: str) -> bool:
return True return True
@ -61,8 +60,6 @@ class GithubProvider(GitProvider):
get_logger().info(f"Skipping merge commit {commit.commit.message}") get_logger().info(f"Skipping merge commit {commit.commit.message}")
continue continue
self.file_set.update({file.filename: file for file in commit.files}) self.file_set.update({file.filename: file for file in commit.files})
else:
raise ValueError("No previous review found")
def get_commit_range(self): def get_commit_range(self):
last_review_time = self.previous_review.created_at last_review_time = self.previous_review.created_at
@ -143,15 +140,8 @@ class GithubProvider(GitProvider):
else: else:
get_logger().error(f"Unknown edit type: {file.status}") get_logger().error(f"Unknown edit type: {file.status}")
edit_type = EDIT_TYPE.UNKNOWN edit_type = EDIT_TYPE.UNKNOWN
# count number of lines added and removed
patch_lines = patch.splitlines(keepends=True)
num_plus_lines = len([line for line in patch_lines if line.startswith('+')])
num_minus_lines = len([line for line in patch_lines if line.startswith('-')])
file_patch_canonical_structure = FilePatchInfo(original_file_content_str, new_file_content_str, patch, file_patch_canonical_structure = FilePatchInfo(original_file_content_str, new_file_content_str, patch,
file.filename, edit_type=edit_type, file.filename, edit_type=edit_type)
num_plus_lines=num_plus_lines,
num_minus_lines=num_minus_lines,)
diff_files.append(file_patch_canonical_structure) diff_files.append(file_patch_canonical_structure)
self.diff_files = diff_files self.diff_files = diff_files
@ -164,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)
@ -206,12 +184,8 @@ class GithubProvider(GitProvider):
self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)]) self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)])
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
absolute_position: int = None): position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file)
position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files,
relevant_file.strip('`'),
relevant_line_in_file,
absolute_position)
if position == -1: if position == -1:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}") get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
@ -419,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:
@ -428,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 \
@ -453,7 +427,7 @@ class GithubProvider(GitProvider):
def publish_labels(self, pr_types): def publish_labels(self, pr_types):
try: try:
label_color_map = {"Bug fix": "1d76db", "Tests": "e99695", "Bug fix with tests": "c5def5", label_color_map = {"Bug fix": "1d76db", "Tests": "e99695", "Bug fix with tests": "c5def5",
"Enhancement": "bfd4f2", "Documentation": "d4c5f9", "Refactoring": "bfdadc", "Enhancement": "bfd4f2", "Documentation": "d4c5f9",
"Other": "d1bcf9"} "Other": "d1bcf9"}
post_parameters = [] post_parameters = []
for p in pr_types: for p in pr_types:
@ -465,17 +439,13 @@ class GithubProvider(GitProvider):
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to publish labels, error: {e}") get_logger().exception(f"Failed to publish labels, error: {e}")
def get_pr_labels(self): def get_labels(self):
try: try:
return [label.name for label in self.pr.labels] return [label.name for label in self.pr.labels]
except Exception as e: except Exception as e:
get_logger().exception(f"Failed to get labels, error: {e}") get_logger().exception(f"Failed to get labels, error: {e}")
return [] return []
def get_repo_labels(self):
labels = self.repo_obj.get_labels()
return [label for label in labels]
def get_commit_messages(self): def get_commit_messages(self):
""" """
Retrieves the commit messages of a pull request. Retrieves the commit messages of a pull request.
@ -519,17 +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_start == -1:
link = f"https://github.com/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}"
elif 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
@ -37,14 +37,13 @@ class GitLabProvider(GitProvider):
self.diff_files = None self.diff_files = None
self.git_files = None self.git_files = None
self.temp_comments = [] self.temp_comments = []
self.pr_url = merge_request_url
self._set_merge_request(merge_request_url) self._set_merge_request(merge_request_url)
self.RE_HUNK_HEADER = re.compile( self.RE_HUNK_HEADER = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
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
@ -115,20 +114,12 @@ class GitLabProvider(GitProvider):
if not patch: if not patch:
patch = load_large_diff(filename, new_file_content_str, original_file_content_str) patch = load_large_diff(filename, new_file_content_str, original_file_content_str)
# count number of lines added and removed
patch_lines = patch.splitlines(keepends=True)
num_plus_lines = len([line for line in patch_lines if line.startswith('+')])
num_minus_lines = len([line for line in patch_lines if line.startswith('-')])
diff_files.append( diff_files.append(
FilePatchInfo(original_file_content_str, new_file_content_str, FilePatchInfo(original_file_content_str, new_file_content_str,
patch=patch, patch=patch,
filename=filename, filename=filename,
edit_type=edit_type, edit_type=edit_type,
old_filename=None if diff['old_path'] == diff['new_path'] else diff['old_path'], old_filename=None if diff['old_path'] == diff['new_path'] else diff['old_path']))
num_plus_lines=num_plus_lines,
num_minus_lines=num_minus_lines, ))
self.diff_files = diff_files self.diff_files = diff_files
return diff_files return diff_files
@ -145,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}")
@ -183,7 +162,7 @@ class GitLabProvider(GitProvider):
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no, self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
target_file, target_line_no) target_file, target_line_no)
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str, absolute_position: int = None): def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
raise NotImplementedError("Gitlab provider does not support creating inline comments yet") raise NotImplementedError("Gitlab provider does not support creating inline comments yet")
def create_inline_comments(self, comments: list[dict]): def create_inline_comments(self, comments: list[dict]):
@ -211,11 +190,7 @@ class GitLabProvider(GitProvider):
pos_obj['new_line'] = target_line_no - 1 pos_obj['new_line'] = target_line_no - 1
pos_obj['old_line'] = source_line_no - 1 pos_obj['old_line'] = source_line_no - 1
get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}") get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}")
try: self.mr.discussions.create({'body': body, 'position': pos_obj})
self.mr.discussions.create({'body': body, 'position': pos_obj})
except Exception as e:
get_logger().debug(
f"Failed to create comment in {self.id_mr} with position {pos_obj} (probably not a '+' line)")
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]: def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]:
changes = self.mr.changes() # Retrieve the changes for the merge request once changes = self.mr.changes() # Retrieve the changes for the merge request once
@ -408,12 +383,9 @@ class GitLabProvider(GitProvider):
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
pass pass
def get_pr_labels(self): def get_labels(self):
return self.mr.labels return self.mr.labels
def get_repo_labels(self):
return self.gl.projects.get(self.id_project).labels.list()
def get_commit_messages(self): def get_commit_messages(self):
""" """
Retrieves the commit messages of a pull request. Retrieves the commit messages of a pull request.
@ -438,16 +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_start == -1:
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads"
elif 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

@ -121,6 +121,9 @@ class LocalGitProvider(GitProvider):
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
raise NotImplementedError('Publishing inline comments is not implemented for the local git provider') raise NotImplementedError('Publishing inline comments is not implemented for the local git provider')
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
raise NotImplementedError('Creating inline comments is not implemented for the local git provider')
def publish_inline_comments(self, comments: list[dict]): def publish_inline_comments(self, comments: list[dict]):
raise NotImplementedError('Publishing inline comments is not implemented for the local git provider') raise NotImplementedError('Publishing inline comments is not implemented for the local git provider')
@ -175,5 +178,5 @@ class LocalGitProvider(GitProvider):
def get_issue_comments(self): def get_issue_comments(self):
raise NotImplementedError('Getting issue comments is not implemented for the local git provider') raise NotImplementedError('Getting issue comments is not implemented for the local git provider')
def get_pr_labels(self): def get_labels(self):
raise NotImplementedError('Getting labels is not implemented for the local git provider') raise NotImplementedError('Getting labels is not implemented for the local git provider')

View File

@ -16,13 +16,8 @@ from starlette_context.middleware import RawContextMiddleware
from pr_agent.agent.pr_agent import PRAgent from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings, global_settings from pr_agent.config_loader import get_settings, global_settings
from pr_agent.git_providers.utils import apply_repo_settings
from pr_agent.log import LoggingFormat, get_logger, setup_logger from pr_agent.log import LoggingFormat, get_logger, setup_logger
from pr_agent.secret_providers import get_secret_provider from pr_agent.secret_providers import get_secret_provider
from pr_agent.servers.github_action_runner import get_setting_or_env, is_true
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_reviewer import PRReviewer
setup_logger(fmt=LoggingFormat.JSON) setup_logger(fmt=LoggingFormat.JSON)
router = APIRouter() router = APIRouter()
@ -94,20 +89,8 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"] pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
log_context["api_url"] = pr_url log_context["api_url"] = pr_url
log_context["event"] = "pull_request" log_context["event"] = "pull_request"
if pr_url: with get_logger().contextualize(**log_context):
with get_logger().contextualize(**log_context): await agent.handle_request(pr_url, "review")
apply_repo_settings(pr_url)
auto_review = get_setting_or_env("BITBUCKET_APP.AUTO_REVIEW", None)
if auto_review is None or is_true(auto_review): # by default, auto review is enabled
await PRReviewer(pr_url).run()
auto_improve = get_setting_or_env("BITBUCKET_APP.AUTO_IMPROVE", None)
if is_true(auto_improve): # by default, auto improve is disabled
await PRCodeSuggestions(pr_url).run()
auto_describe = get_setting_or_env("BITBUCKET_APP.AUTO_DESCRIBE", None)
if is_true(auto_describe): # by default, auto describe is disabled
await PRDescription(pr_url).run()
# with get_logger().contextualize(**log_context):
# await agent.handle_request(pr_url, "review")
elif event == "pullrequest:comment_created": elif event == "pullrequest:comment_created":
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"] pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
log_context["api_url"] = pr_url log_context["api_url"] = pr_url

View File

@ -1,80 +0,0 @@
import json
import os
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
from pr_agent.servers.utils import verify_signature
router = APIRouter()
def handle_request(
background_tasks: BackgroundTasks, url: str, body: str, log_context: dict
):
log_context["action"] = body
log_context["api_url"] = url
with get_logger().contextualize(**log_context):
background_tasks.add_task(PRAgent().handle_request, url, body)
@router.post("/")
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))
webhook_secret = get_settings().get("BITBUCKET_SERVER.WEBHOOK_SECRET", None)
if webhook_secret:
body_bytes = await request.body()
signature_header = request.headers.get("x-hub-signature", None)
verify_signature(body_bytes, webhook_secret, signature_header)
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"
if data["eventKey"] == "pr:opened":
body = "review"
elif data["eventKey"] == "pr:comment:added":
body = data["comment"]["text"]
else:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=json.dumps({"message": "Unsupported event"}),
)
handle_request(background_tasks, pr_url, body, 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():
app = FastAPI(middleware=[Middleware(RawContextMiddleware)])
app.include_router(router)
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("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

@ -125,15 +125,11 @@ async def handle_request(body: Dict[str, Any], event: str):
await _perform_commands("pr_commands", agent, body, api_url, log_context) await _perform_commands("pr_commands", agent, body, api_url, log_context)
# handle pull_request event with synchronize action - "push trigger" for new commits # handle pull_request event with synchronize action - "push trigger" for new commits
elif event == 'pull_request' and action == 'synchronize': elif event == 'pull_request' and action == 'synchronize' and get_settings().github_app.handle_push_trigger:
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user) pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
if not (pull_request and api_url): if not (pull_request and api_url):
return {} return {}
apply_repo_settings(api_url)
if not get_settings().github_app.handle_push_trigger:
return {}
# TODO: do we still want to get the list of commits to filter bot/merge commits? # TODO: do we still want to get the list of commits to filter bot/merge commits?
before_sha = body.get("before") before_sha = body.get("before")
after_sha = body.get("after") after_sha = body.get("after")

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,20 +1,17 @@
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" \
"> - **/analyze** 💎: Automatically analyzes the PR, and presents changes walkthrough for each component. \n\n" \ ">For example: /review --pr_reviewer.extra_instructions=\"focus on the file: ...\" \n" \
">See the [tools guide](https://github.com/Codium-ai/pr-agent/blob/main/docs/TOOLS_GUIDE.md) for more details.\n" \ ">To list the possible configuration parameters, use the **/config** command.\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" \
">To list the possible configuration parameters, add a **/config** comment. \n" \
def bot_help_text(user: str): def bot_help_text(user: str):
return f"> Tag me in a comment '@{user}' and add one of the following commands: \n" + commands_text return f"> Tag me in a comment '@{user}' and add one of the following commands:\n" + commands_text
actions_help_text = "> To invoke the PR-Agent, add a comment using one of the following commands: \n" + \ actions_help_text = "> To invoke the PR-Agent, add a comment using one of the following commands:\n" + \
commands_text commands_text

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,13 +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
[aws]
bedrock_region = "" # the AWS region to call Bedrock APIs
[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.
@ -65,11 +58,6 @@ personal_access_token = ""
# For Bitbucket personal/repository bearer token # For Bitbucket personal/repository bearer token
bearer_token = "" bearer_token = ""
[bitbucket_server]
# For Bitbucket Server bearer token
auth_token = ""
webhook_secret = ""
# For Bitbucket app # For Bitbucket app
app_key = "" app_key = ""
base_url = "" base_url = ""

View File

@ -4,10 +4,9 @@ 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
use_global_settings_file=true
ai_timeout=180 ai_timeout=180
max_description_tokens = 500 max_description_tokens = 500
max_commits_tokens = 500 max_commits_tokens = 500
@ -17,31 +16,22 @@ 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
# soc2
require_soc2_ticket=false
soc2_ticket_prompt="Does the PR description include a link to ticket in a project management system (e.g., Jira, Asana, Trello, etc.) ?"
# general options
num_code_suggestions=4 num_code_suggestions=4
inline_code_comments = false inline_code_comments = false
ask_and_reflect=false ask_and_reflect=false
#automatic_review=true automatic_review=true
remove_previous_review_comment=false remove_previous_review_comment=false
persistent_comment=true 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
minimal_minutes_for_incremental_review=0 minimal_minutes_for_incremental_review=0
enable_help_text=true # Determines whether to include help text in the PR review. Enabled by default.
[pr_description] # /describe # [pr_description] # /describe #
publish_labels=true publish_labels=true
@ -51,26 +41,20 @@ keep_original_user_title=false
use_bullet_points=true use_bullet_points=true
extra_instructions = "" extra_instructions = ""
enable_pr_type=true enable_pr_type=true
final_update_message = true
## changes walkthrough section
enable_semantic_files_types=true
collapsible_file_list='adaptive' # true, false, 'adaptive'
# markers # markers
use_description_markers=false use_description_markers=false
include_generated_by_header=true include_generated_by_header=true
#custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other'] #custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Refactoring', 'Enhancement', 'Documentation', 'Other']
[pr_questions] # /ask # [pr_questions] # /ask #
[pr_code_suggestions] # /improve # [pr_code_suggestions] # /improve #
num_code_suggestions=4 num_code_suggestions=4
summarize = false
include_improved_code = true
extra_instructions = "" extra_instructions = ""
rank_suggestions = false rank_suggestions = false
# params for '/improve --extended' mode # params for '/improve --extended' mode
auto_extended_mode=false
num_code_suggestions_per_chunk=8 num_code_suggestions_per_chunk=8
rank_extended_suggestions = true rank_extended_suggestions = true
max_number_of_calls = 5 max_number_of_calls = 5
@ -84,15 +68,12 @@ docs_style = "Sphinx Style" # "Google Style with Args, Returns, Attributes...etc
push_changelog_changes=false push_changelog_changes=false
extra_instructions = "" extra_instructions = ""
[pr_analyze] # /analyze #
[pr_config] # /config # [pr_config] # /config #
[github] [github]
# 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
@ -111,7 +92,7 @@ duplicate_requests_cache_ttl = 60 # in seconds
handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested'] handle_pr_actions = ['opened', 'reopened', 'ready_for_review', 'review_requested']
pr_commands = [ pr_commands = [
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true", "/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
"/review", "/auto_review",
] ]
# settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits # settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits
handle_push_trigger = false handle_push_trigger = false
@ -151,12 +132,6 @@ magic_word = "AutoReview"
# Polling interval # Polling interval
polling_interval_seconds = 30 polling_interval_seconds = 30
[bitbucket_app]
#auto_review = true # set as config var in .pr_agent.toml
#auto_describe = true # set as config var in .pr_agent.toml
#auto_improve = true # set as config var in .pr_agent.toml
[local] [local]
# LocalGitProvider settings - uncomment to use paths other than default # LocalGitProvider settings - uncomment to use paths other than default
# description_path= "path/to/description.md" # description_path= "path/to/description.md"
@ -172,11 +147,6 @@ polling_interval_seconds = 30
# token to authenticate in the patch server # token to authenticate in the patch server
# patch_server_token = "" # patch_server_token = ""
[bitbucket_server]
# URL to the BitBucket Server instance
# url = "https://git.bitbucket.com"
url = ""
[litellm] [litellm]
#use_client = false #use_client = false
@ -184,12 +154,8 @@ url = ""
skip_comments = false skip_comments = false
force_update_dataset = false force_update_dataset = false
max_issues_to_scan = 500 max_issues_to_scan = 500
vectordb = "pinecone"
[pinecone] [pinecone]
# fill and place in .secrets.toml # fill and place in .secrets.toml
#api_key = ... #api_key = ...
# environment = "gcp-starter" # environment = "gcp-starter"
[lancedb]
uri = "./lancedb"

View File

@ -3,14 +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"]
#description = "Code refactoring without changing functionality"
#[custom_labels."Enhancement"] #[custom_labels."Enhancement"]
#description = """Adds new features or modifies existing ones""" #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,22 +1,22 @@
[pr_add_docs_prompt] [pr_add_docs_prompt]
system="""You are PR-Doc, a language model that specializes in generating documentation for code components in a Pull Request (PR). system="""You are a language model called PR-Code-Documentation Agent, that specializes in generating documentation for code.
Your task is to generate {{ docs_for_language }} for code components in the PR Diff. Your task is to generate meaningfull {{ docs_for_language }} to a PR (the '+' lines).
Example for a PR Diff input:
Example for the PR Diff format: '
======
## src/file1.py ## src/file1.py
@@ -12,3 +12,4 @@ def func1(): @@ -12,3 +12,5 @@ def func1():
__new hunk__ __new hunk__
12 code line1 that remained unchanged in the PR 12 code line that already existed in the file...
13 code line that already existed in the file....
14 +new code line1 added in the PR 14 +new code line1 added in the PR
15 +new code line2 added in the PR 15 +new code line2 added in the PR
16 code line2 that remained unchanged in the PR 16 code line that already existed in the file...
__old hunk__ __old hunk__
code line1 that remained unchanged in the PR code line that already existed in the file...
-code line that was removed in the PR -code line that was removed in the PR
code line2 that remained unchanged in the PR code line that already existed in the file...
@@ ... @@ def func2(): @@ ... @@ def func2():
@ -28,14 +28,13 @@ __old hunk__
## src/file2.py ## src/file2.py
... ...
====== '
Specific instructions: Specific instructions:
- Try to identify edited/added code components (classes/functions/methods...) that are undocumented, and generate {{ docs_for_language }} for each one. - Try to identify edited/added code components (classes/functions/methods...) that are undocumented. and generate {{ docs_for_language }} for each one.
- If there are documented (any type of {{ language }} documentation) code components in the PR, Don't generate {{ docs_for_language }} for them. - If there are documented (any type of {{ language }} documentation) code components in the PR, Don't generate {{ docs_for_language }} for them.
- Ignore code components that don't appear fully in the '__new hunk__' section. For example, you must see the component header and body. - Ignore code components that don't appear fully in the '__new hunk__' section. For example. you must see the component header and body,
- Make sure the {{ docs_for_language }} starts and ends with standard {{ language }} {{ docs_for_language }} signs. - Make sure the {{ docs_for_language }} starts and ends with standart {{ language }} {{ docs_for_language }} signs.
- The {{ docs_for_language }} should be in standard format. - The {{ docs_for_language }} should be in standard format.
- Provide the exact line number (inclusive) where the {{ docs_for_language }} should be added. - Provide the exact line number (inclusive) where the {{ docs_for_language }} should be added.
@ -43,12 +42,11 @@ Specific instructions:
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
====== '
{{ extra_instructions }} {{ extra_instructions }}
====== '
{%- endif %} {%- endif %}
You must use the following YAML schema to format your answer: You must use the following YAML schema to format your answer:
```yaml ```yaml
Code Documentation: Code Documentation:
@ -69,7 +67,6 @@ Code Documentation:
- after - after
description: |- description: |-
The {{ docs_for_language }} placement relative to the relevant line (code component). The {{ docs_for_language }} placement relative to the relevant line (code component).
For example, in Python the docs are placed after the function signature, but in Java they are placed before.
documentation: documentation:
type: string type: string
description: |- description: |-
@ -102,25 +99,18 @@ Title: '{{ title }}'
Branch: '{{ branch }}' Branch: '{{ branch }}'
{%- if description %} Description: '{{description}}'
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main PR language: '{{language}}' Main language: {{language}}
{%- endif %} {%- endif %}
The PR Diff: The PR Diff:
====== ```
{{ diff|trim }} {{- diff|trim }}
====== ```
Response (should be a valid YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml

View File

@ -1,21 +1,22 @@
[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 the PR Diff format: Example for a PR Diff input:
====== '
## src/file1.py ## src/file1.py
@@ -12,3 +12,4 @@ def func1(): @@ -12,3 +12,5 @@ def func1():
__new hunk__ __new hunk__
12 code line1 that remained unchanged in the PR 12 code line that already existed in the file...
13 code line that already existed in the file....
14 +new code line1 added in the PR 14 +new code line1 added in the PR
15 +new code line2 added in the PR 15 +new code line2 added in the PR
16 code line2 that remained unchanged in the PR 16 code line that already existed in the file...
__old hunk__ __old hunk__
code line1 that remained unchanged in the PR code line that already existed in the file...
-code line that was removed in the PR -code line that was removed in the PR
code line2 that remained unchanged in the PR code line that already existed in the file...
@@ ... @@ def func2(): @@ ... @@ def func2():
@ -27,63 +28,83 @@ __old hunk__
## src/file2.py ## src/file2.py
... ...
====== '
Specific instructions: Specific instructions:
- 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.
- Prioritize suggestions that address major problems, issues and bugs in the code. As a second priority, suggestions should focus on enhancement, best practice, performance, maintainability, and other aspects. - Prioritize suggestions that address major problems, issues and bugs in the code.
As a second priority, suggestions should focus on best practices, code readability, maintainability, enhancments, performance, and other aspects.
- Don't suggest to add docstring, type hints, or comments. - Don't suggest to add docstring, type hints, or comments.
- Suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+'). - Suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+').
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the '__new hunk__' code. - Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the '__new hunk__' code.
- Provide the exact line numbers range (inclusive) for each suggestion. - For each suggestion, make sure to take into consideration also the context, meaning the lines before and after the relevant code.
- Provide the exact line numbers range (inclusive) for each issue.
- Assume there is additional relevant code, that is not included in the diff. - Assume there is additional relevant code, that is not included in the diff.
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
====== '
{{ extra_instructions }} {{ extra_instructions }}
====== '
{%- endif %} {%- endif %}
The output must be a YAML object equivalent to type PRCodeSuggestions, according to the following Pydantic definitions: You must use the following YAML schema to format your answer:
===== ```yaml
class CodeSuggestion(BaseModel): Code suggestions:
relevant_file: str = Field(description="the relevant file full path") type: array
suggestion_content: str = Field(description="an actionable suggestion for meaningfully improving the new code introduced in the PR") minItems: 1
existing_code: str = Field(description="a code snippet, showing the relevant code lines from a '__new hunk__' section. It must be contiguous, correctly formatted and indented, and without line numbers") maxItems: {{ num_code_suggestions }}
relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above") uniqueItems: true
relevant_lines_end: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion ends (inclusive). Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above") items:
improved_code: str = Field(description="a new code snippet, that can be used to replace the relevant lines in '__new hunk__' code. Replacement suggestions should be complete, correctly formatted and indented, and without line numbers") relevant file:
label: str = Field(description="a single label for the suggestion, to help the user understand the suggestion type. For example: 'security', 'bug', 'performance', 'enhancement', 'possible issue', 'best practice', 'maintainability', etc. Other labels are also allowed") type: string
description: the relevant file full path
class PRCodeSuggestions(BaseModel): suggestion content:
code_suggestions: List[CodeSuggestion] type: string
===== description: |-
a concrete suggestion for meaningfully improving the new PR code.
existing code:
type: string
description: |-
a code snippet showing the relevant code lines from a '__new hunk__' section.
It must be contiguous, correctly formatted and indented, and without line numbers.
relevant lines start:
type: integer
description: |-
The relevant line number from a '__new hunk__' section where the suggestion starts (inclusive).
Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.
relevant lines end:
type: integer
description: |-
The relevant line number from a '__new hunk__' section where the suggestion ends (inclusive).
Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.
improved code:
type: string
description: |-
a new code snippet that can be used to replace the relevant lines in '__new hunk__' code.
Replacement suggestions should be complete, correctly formatted and indented, and without line numbers.
```
Example output: Example output:
```yaml ```yaml
code_suggestions: Code suggestions:
- relevant_file: |- - relevant file: |-
src/file1.py src/file1.py
suggestion_content: |- suggestion content: |-
Add a docstring to func1() Add a docstring to func1()
existing_code: |- existing code: |-
def func1(): def func1():
relevant_lines_start: 12 relevant lines start: 12
relevant_lines_end: 12 relevant lines end: 12
improved_code: |- improved code: |-
... ...
label: |-
...
``` ```
Each YAML output MUST be after a newline, indented, with block scalar indicator ('|-'). Each YAML output MUST be after a newline, indented, with block scalar indicator ('|-').
Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
""" """
user="""PR Info: user="""PR Info:
@ -92,25 +113,18 @@ Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
{%- if description %} Description: '{{description}}'
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main PR language: '{{ language }}' Main language: {{language}}
{%- endif %} {%- endif %}
The PR Diff: The PR Diff:
====== ```
{{ diff|trim }} {{- diff|trim }}
====== ```
Response (should be a valid YAML, and nothing else): Response (should be a valid YAML, and nothing else):
```yaml ```yaml

View File

@ -1,86 +1,71 @@
[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:
====== '
{{ extra_instructions }} {{ extra_instructions }}
====== '
{% 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:
type: string
enum:
{%- if enable_custom_labels %}
{{ custom_labels }}
{%- else %} {%- else %}
class Label(str, Enum): - Bug fix
bug_fix = "Bug fix" - Tests
tests = "Tests" - Refactoring
enhancement = "Enhancement" - Enhancement
documentation = "Documentation" - Documentation
other = "Other" - Other
{%- endif %} {%- endif %}
class Labels(BaseModel):
labels: List[Label] = Field(min_items=0, description="choose the relevant custom labels that describe the PR content, and return their keys. Use the value field of the Label object to better understand the label meaning.")
======
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}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- 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|trim }}
======
{%- endif %} {%- endif %}
The PR Git Diff: The PR Git Diff:
====== ```
{{ diff|trim }} {{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):
```yaml ```yaml
""" """

View File

@ -1,127 +1,102 @@
[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.
{%- if enable_custom_labels %} Your task is to provide full description of a Pull Request (PR) content.
Your task is to provide a full description for the PR content - files walkthrough, title, type, description and labels. - Make sure to focus on the new PR code (the '+' lines).
{%- else %} - 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.
Your task is to provide a full description for the PR content - files walkthrough, title, type, and description. - Emphasize first the most important changes, and then the less important ones.
{%- endif %} - If needed, each YAML output should be in block scalar format ('|-')
- Focus on the new PR code (lines starting with '+').
- 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.
- The generated title and description should prioritize the most significant changes.
- If needed, each YAML output should be in block scalar indicator ('|-')
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
===== '
{{ extra_instructions }} {{ extra_instructions }}
===== '
{% 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:
enhancement = "Enhancement" type: string
documentation = "Documentation" enum:
other = "Other" - Bug fix
- 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:
{%- if enable_semantic_files_types %} type: string
description: an informative and concise description of the PR.
Class FileDescription(BaseModel): {%- if use_bullet_points %} Use bullet points. {% endif %}
filename: str = Field(description="the relevant file full path") PR Main Files Walkthrough:
changes_summary: str = Field(description="minimal and concise summary of the changes in the relevant file") type: array
label: str = Field(description="a single semantic label that represents a type of code changes that occurred in the File. Possible values (partial list): 'bug fix', 'tests', 'enhancement', 'documentation', 'error handling', 'configuration changes', 'dependencies', 'formatting', 'miscellaneous', ...") maxItems: 10
{%- endif %} description: |-
a walkthrough of the PR changes. Review main files, and shortly describe the changes in each file (up to 10 most important files).
Class PRDescription(BaseModel): items:
type: List[PRType] = Field(description="one or more types that describe the PR content. Return the label member value (e.g. 'Bug fix', not 'bug_fix')") filename:
{%- if enable_semantic_files_types %} type: string
pr_files[List[FileDescription]] = Field(max_items=15, description="a list of the files in the PR, and their changes summary.") description: the relevant file full path
{%- endif %} changes in file:
description: str = Field(description="an informative and concise description of the PR. Use bullet points. Display first the most significant changes.") type: string
title: str = Field(description="an informative title for the PR, describing its main theme") description: minimal and concise description of the changes in the relevant file
{%- if enable_custom_labels %} ```
labels: List[Label] = Field(min_items=0, description="choose the relevant custom labels that describe the PR content, and return their keys. Use the value field of the Label object to better understand the label meaning.")
{%- endif %}
=====
Example output: Example output:
```yaml ```yaml
type: PR Title: |-
- ...
- ...
{%- if enable_semantic_files_types %}
pr_files:
- filename: |
...
changes_summary: |
...
label: |
...
...
{%- endif %}
description: |-
... ...
title: |- PR Type:
... ...
{%- if enable_custom_labels %} {%- if enable_custom_labels %}
labels: PR Labels:
- | - ...
... - ...
- |
...
{%- endif %} {%- endif %}
PR Description: |-
...
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|trim }}
=====
{%- 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|trim }}
=====
{%- endif %} {%- endif %}
The PR Diff: The PR Git Diff:
===== ```
{{ diff|trim }} {{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):
```yaml ```yaml
""" """

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,36 +16,22 @@ Questions to better understand the PR:
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- 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|trim }}
======
{%- endif %} {%- endif %}
The PR Git Diff: The PR Git Diff:
====== ```
{{ diff|trim }} {{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

View File

@ -1,42 +1,36 @@
[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 (the '+' lines), and provide feedback.
Your goal is to answer questions\\tasks about the new PR code (lines starting with '+'), 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 any 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).
""" """
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- endif %}
{%- if language %} {%- if language %}
Main language: {{language}}
{%- endif %}
{%- if commit_messages_str %}
Main PR language: '{{ language }}' Commit messages:
{{commit_messages_str}}
{%- endif %} {%- endif %}
The PR Git Diff: The PR Git Diff:
====== ```
{{ diff|trim }} {{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
The PR Questions: The PR Questions:
====== ```
{{ questions|trim }} {{ questions }}
====== ```
Response to the PR Questions: Response:
""" """

View File

@ -1,19 +1,18 @@
[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: Example PR Diff input:
====== '
## src/file1.py ## src/file1.py
@@ -12,5 +12,5 @@ def func1(): @@ -12,5 +12,5 @@ def func1():
code line 1 that remained unchanged in the PR code line that already existed in the file...
code line 2 that remained unchanged in the PR code line that already existed in the file....
-code line that was removed in the PR -code line that was removed in the PR
+code line added in the PR +new code line added in the PR
code line 3 that remained unchanged in the PR code line that already existed in the file...
code line that already existed in the file...
@@ ... @@ def func2(): @@ ... @@ def func2():
... ...
@ -21,29 +20,26 @@ code line 3 that remained unchanged in the PR
## src/file2.py ## src/file2.py
... ...
====== '
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).
{%- if num_code_suggestions > 0 %} {%- if num_code_suggestions > 0 %}
Code suggestions guidelines:
- 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 '+')
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
{%- endif %} {%- endif %}
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
====== '
{{ extra_instructions }} {{ extra_instructions }}
====== '
{% endif %} {% endif %}
You must use the following YAML schema to format your answer: You must use the following YAML schema to format your answer:
```yaml ```yaml
PR Analysis: PR Analysis:
@ -58,6 +54,7 @@ PR Analysis:
enum: enum:
- Bug fix - Bug fix
- Tests - Tests
- Refactoring
- Enhancement - Enhancement
- Documentation - Documentation
- Other - Other
@ -96,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:
@ -133,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 %}
``` ```
@ -182,50 +178,36 @@ 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|trim }}
======
{%- 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 %}
===== ######
Here are questions to better understand the PR. Use the answers to provide better feedback. Here are questions to better understand the PR. Use the answers to provide better feedback.
{{ question_str|trim }} {{question_str|trim}}
User answers: User answers:
' '
{{ answer_str|trim }} {{answer_str|trim}}
' '
===== ######
{%- endif %} {%- endif %}
The PR Git Diff:
The PR Diff: ```
====== {{diff}}
{{ diff|trim }} ```
====== 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

@ -1,5 +1,5 @@
[pr_update_changelog_prompt] [pr_update_changelog_prompt]
system="""You are a language model called PR-Changelog-Updater. system="""You are a language model called CodiumAI-PR-Changlog-summarizer.
Your task is to update the CHANGELOG.md file of the project, to shortly summarize important changes introduced in this PR (the '+' lines). Your task is to update the CHANGELOG.md file of the project, to shortly summarize important changes introduced in this PR (the '+' lines).
- The output should match the existing CHANGELOG.md format, style and conventions, so it will look like a natural part of the file. For example, if previous changes were summarized in a single line, you should do the same. - The output should match the existing CHANGELOG.md format, style and conventions, so it will look like a natural part of the file. For example, if previous changes were summarized in a single line, you should do the same.
- Don't repeat previous changes. Generate only new content, that is not already in the CHANGELOG.md file. - Don't repeat previous changes. Generate only new content, that is not already in the CHANGELOG.md file.
@ -8,44 +8,30 @@ Your task is to update the CHANGELOG.md file of the project, to shortly summariz
{%- if extra_instructions %} {%- if extra_instructions %}
Extra instructions from the user: Extra instructions from the user:
====== '
{{ extra_instructions|trim }} {{ extra_instructions }}
====== '
{%- endif %} {%- endif %}
""" """
user="""PR Info: user="""PR Info:
Title: '{{title}}' Title: '{{title}}'
Branch: '{{branch}}' Branch: '{{branch}}'
Description: '{{description}}'
{%- if description %}
Description:
======
{{ description|trim }}
======
{%- 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|trim }}
======
{%- endif %} {%- endif %}
The PR Git Diff: The PR Diff:
====== ```
{{ diff|trim }} {{diff}}
====== ```
Current date: Current date:
``` ```
@ -53,10 +39,9 @@ Current date:
``` ```
The current CHANGELOG.md: The current CHANGELOG.md:
====== ```
{{ changelog_file_str }} {{ changelog_file_str }}
====== ```
Response: Response:
""" """

View File

@ -1,12 +1,10 @@
import copy import copy
import textwrap import textwrap
from functools import partial
from typing import Dict from typing import Dict
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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 load_yaml from pr_agent.algo.utils import load_yaml
@ -17,15 +15,14 @@ from pr_agent.log import get_logger
class PRAddDocs: class PRAddDocs:
def __init__(self, pr_url: str, cli_mode=False, args: list = None, def __init__(self, pr_url: str, cli_mode=False, args: list = None):
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
self.git_provider = get_git_provider()(pr_url) self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language( self.main_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files() self.git_provider.get_languages(), self.git_provider.get_files()
) )
self.ai_handler = ai_handler() self.ai_handler = AiHandler()
self.patches_diff = None self.patches_diff = None
self.prediction = None self.prediction = None
self.cli_mode = cli_mode self.cli_mode = cli_mode

View File

@ -1,11 +1,10 @@
import copy import copy
import textwrap import textwrap
from functools import partial
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_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models
from pr_agent.algo.token_handler import TokenHandler from pr_agent.algo.token_handler import TokenHandler
from pr_agent.algo.utils import load_yaml from pr_agent.algo.utils import load_yaml
@ -16,8 +15,7 @@ from pr_agent.log import get_logger
class PRCodeSuggestions: class PRCodeSuggestions:
def __init__(self, pr_url: str, cli_mode=False, args: list = None, def __init__(self, pr_url: str, cli_mode=False, args: list = None):
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
self.git_provider = get_git_provider()(pr_url) self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language( self.main_language = get_main_pr_language(
@ -26,7 +24,7 @@ class PRCodeSuggestions:
# extended mode # extended mode
try: try:
self.is_extended = self._get_is_extended(args or []) self.is_extended = any(["extended" in arg for arg in args])
except: except:
self.is_extended = False self.is_extended = False
if self.is_extended: if self.is_extended:
@ -34,7 +32,7 @@ class PRCodeSuggestions:
else: else:
num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions
self.ai_handler = ai_handler() self.ai_handler = AiHandler()
self.patches_diff = None self.patches_diff = None
self.prediction = None self.prediction = None
self.cli_mode = cli_mode self.cli_mode = cli_mode
@ -57,32 +55,28 @@ 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()
else: else:
data = await retry_with_fallback_models(self._prepare_prediction_extended) data = await retry_with_fallback_models(self._prepare_prediction_extended)
if (not data) or (not 'code_suggestions' in data): if (not data) or (not 'Code suggestions' in data):
get_logger().info('No code suggestions found for PR.') get_logger().info('No code suggestions found for PR.')
return return
if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \ if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \
(self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions): (self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions):
get_logger().info('Ranking Suggestions...') get_logger().info('Ranking Suggestions...')
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}")
@ -109,80 +103,47 @@ class PRCodeSuggestions:
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2, response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
system=system_prompt, user=user_prompt) system=system_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_code_suggestions(self) -> Dict: def _prepare_pr_code_suggestions(self) -> Dict:
review = self.prediction.strip() review = self.prediction.strip()
data = load_yaml(review, data = load_yaml(review)
keys_fix_yaml=["relevant_file", "suggestion_content", "existing_code", "improved_code"])
if isinstance(data, list): if isinstance(data, list):
data = {'code_suggestions': data} data = {'Code suggestions': data}
# remove invalid suggestions
suggestion_list = []
for i, suggestion in enumerate(data['code_suggestions']):
if suggestion['existing_code'] != suggestion['improved_code']:
suggestion_list.append(suggestion)
else:
get_logger().debug(
f"Skipping suggestion {i + 1}, because existing code is equal to improved code {suggestion['existing_code']}")
data['code_suggestions'] = suggestion_list
return data return data
def push_inline_code_suggestions(self, data): def push_inline_code_suggestions(self, data):
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']:
try: try:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().info(f"suggestion: {d}") get_logger().info(f"suggestion: {d}")
relevant_file = d['relevant_file'].strip() relevant_file = d['relevant file'].strip()
relevant_lines_start = int(d['relevant_lines_start']) # absolute position relevant_lines_start = int(d['relevant lines start']) # absolute position
relevant_lines_end = int(d['relevant_lines_end']) relevant_lines_end = int(d['relevant lines end'])
content = d['suggestion_content'].rstrip() content = d['suggestion content']
new_code_snippet = d['improved_code'].rstrip() new_code_snippet = d['improved code']
label = d['label'].strip()
if new_code_snippet: if new_code_snippet:
new_code_snippet = self.dedent_code(relevant_file, relevant_lines_start, new_code_snippet) new_code_snippet = self.dedent_code(relevant_file, relevant_lines_start, new_code_snippet)
if get_settings().pr_code_suggestions.include_improved_code: body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```"
body = f"**Suggestion:** {content} [{label}]\n```suggestion\n" + new_code_snippet + "\n```" code_suggestions.append({'body': body, 'relevant_file': relevant_file,
code_suggestions.append({'body': body, 'relevant_file': relevant_file, 'relevant_lines_start': relevant_lines_start,
'relevant_lines_start': relevant_lines_start, 'relevant_lines_end': relevant_lines_end})
'relevant_lines_end': relevant_lines_end})
else:
if self.git_provider.is_supported("create_inline_comment"):
body = f"**Suggestion:** {content} [{label}]"
comment = self.git_provider.create_inline_comment(body, relevant_file, "",
absolute_position=relevant_lines_end)
if comment:
code_suggestions.append(comment)
else:
get_logger().error("Inline comments are not supported by the git provider")
except Exception: except Exception:
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().info(f"Could not parse suggestion: {d}") get_logger().info(f"Could not parse suggestion: {d}")
if get_settings().pr_code_suggestions.include_improved_code: is_successful = self.git_provider.publish_code_suggestions(code_suggestions)
is_successful = self.git_provider.publish_code_suggestions(code_suggestions)
else:
is_successful = self.git_provider.publish_inline_comments(code_suggestions)
if not is_successful: if not is_successful:
get_logger().info("Failed to publish code suggestions, trying to publish each suggestion separately") get_logger().info("Failed to publish code suggestions, trying to publish each suggestion separately")
for code_suggestion in code_suggestions: for code_suggestion in code_suggestions:
if get_settings().pr_code_suggestions.include_improved_code: self.git_provider.publish_code_suggestions([code_suggestion])
self.git_provider.publish_code_suggestions([code_suggestion])
else:
self.git_provider.publish_inline_comments([code_suggestion])
def dedent_code(self, relevant_file, relevant_lines_start, new_code_snippet): def dedent_code(self, relevant_file, relevant_lines_start, new_code_snippet):
try: # dedent code snippet try: # dedent code snippet
@ -206,16 +167,6 @@ class PRCodeSuggestions:
return new_code_snippet return new_code_snippet
def _get_is_extended(self, args: list[str]) -> bool:
"""Check if extended mode should be enabled by the `--extended` flag or automatically according to the configuration"""
if any(["extended" in arg for arg in args]):
get_logger().info("Extended mode is enabled by the `--extended` flag")
return True
if get_settings().pr_code_suggestions.auto_extended_mode:
get_logger().info("Extended mode is enabled automatically based on the configuration toggle")
return True
return False
async def _prepare_prediction_extended(self, model: str) -> dict: async def _prepare_prediction_extended(self, model: str) -> dict:
get_logger().info('Getting PR diff...') get_logger().info('Getting PR diff...')
patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model, patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model,
@ -234,8 +185,8 @@ class PRCodeSuggestions:
for prediction in prediction_list: for prediction in prediction_list:
self.prediction = prediction self.prediction = prediction
data_per_chunk = self._prepare_pr_code_suggestions() data_per_chunk = self._prepare_pr_code_suggestions()
if "code_suggestions" in data: if "Code suggestions" in data:
data["code_suggestions"].extend(data_per_chunk["code_suggestions"]) data["Code suggestions"].extend(data_per_chunk["Code suggestions"])
else: else:
data.update(data_per_chunk) data.update(data_per_chunk)
self.data = data self.data = data
@ -253,8 +204,11 @@ class PRCodeSuggestions:
""" """
suggestion_list = [] suggestion_list = []
for suggestion in data: # remove invalid suggestions
suggestion_list.append(suggestion) for i, suggestion in enumerate(data):
if suggestion['existing code'] != suggestion['improved code']:
suggestion_list.append(suggestion)
data_sorted = [[]] * len(suggestion_list) data_sorted = [[]] * len(suggestion_list)
try: try:
@ -281,14 +235,8 @@ class PRCodeSuggestions:
data_sorted[importance_order - 1] = suggestion_list[suggestion_number - 1] data_sorted[importance_order - 1] = suggestion_list[suggestion_number - 1]
if get_settings().pr_code_suggestions.final_clip_factor != 1: if get_settings().pr_code_suggestions.final_clip_factor != 1:
max_len = max( new_len = int(0.5 + len(data_sorted) * get_settings().pr_code_suggestions.final_clip_factor)
len(data_sorted), data_sorted = data_sorted[:new_len]
get_settings().pr_code_suggestions.num_code_suggestions,
get_settings().pr_code_suggestions.num_code_suggestions_per_chunk,
)
new_len = int(0.5 + max_len * get_settings().pr_code_suggestions.final_clip_factor)
if new_len < len(data_sorted):
data_sorted = data_sorted[:new_len]
except Exception as e: except Exception as e:
if get_settings().config.verbosity_level >= 1: if get_settings().config.verbosity_level >= 1:
get_logger().info(f"Could not sort suggestions, error: {e}") get_logger().info(f"Could not sort suggestions, error: {e}")
@ -296,40 +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"
language_extension_map_org = get_settings().language_extension_map_org
extension_to_language = {}
for language, extensions in language_extension_map_org.items():
for ext in extensions:
extension_to_language[ext] = language
for s in data['code_suggestions']:
try:
extension_s = s['relevant_file'].rsplit('.')[-1]
code_snippet_link = self.git_provider.get_line_link(s['relevant_file'], s['relevant_lines_start'],
s['relevant_lines_end'])
label = s['label'].strip()
data_markdown += f"\n💡 [{label}]\n\n**{s['suggestion_content'].rstrip().rstrip()}**\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"
language_name = "python"
if extension_s and (extension_s in extension_to_language):
language_name = extension_to_language[extension_s]
data_markdown += f"Existing code:\n```{language_name}\n{s['existing_code'].rstrip()}\n```\n"
data_markdown += f"Improved code:\n```{language_name}\n{s['improved_code'].rstrip()}\n```\n"
if self.git_provider.is_supported("gfm_markdown"):
data_markdown += "</details>\n"
data_markdown += "\n___\n\n"
except Exception as e:
get_logger().error(f"Could not parse suggestion: {s}, error: {e}")
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

@ -7,7 +7,7 @@ class PRConfig:
""" """
The PRConfig class is responsible for listing all configuration options available for the user. The PRConfig class is responsible for listing all configuration options available for the user.
""" """
def __init__(self, pr_url: str, args=None, ai_handler=None): def __init__(self, pr_url: str, args=None):
""" """
Initialize the PRConfig object with the necessary attributes and objects to comment on a pull request. Initialize the PRConfig object with the necessary attributes and objects to comment on a pull request.

View File

@ -1,12 +1,10 @@
import copy import copy
import re import re
from functools import partial
from typing import List, Tuple from typing import List, Tuple
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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 load_yaml, set_custom_labels, get_user_labels from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels
@ -17,8 +15,7 @@ from pr_agent.log import get_logger
class PRDescription: class PRDescription:
def __init__(self, pr_url: str, args: list = None, def __init__(self, pr_url: str, args: list = None):
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
""" """
Initialize the PRDescription object with the necessary attributes and objects for generating a PR description Initialize the PRDescription object with the necessary attributes and objects for generating a PR description
using an AI model. using an AI model.
@ -33,13 +30,8 @@ class PRDescription:
) )
self.pr_id = self.git_provider.get_pr_id() self.pr_id = self.git_provider.get_pr_id()
if get_settings().pr_description.enable_semantic_files_types and not self.git_provider.is_supported(
"gfm_markdown"):
get_logger().debug(f"Disabling semantic files types for {self.pr_id}")
get_settings().pr_description.enable_semantic_files_types = False
# Initialize the AI handler # Initialize the AI handler
self.ai_handler = ai_handler() self.ai_handler = AiHandler()
# Initialize the variables dictionary # Initialize the variables dictionary
self.vars = { self.vars = {
@ -48,11 +40,12 @@ class PRDescription:
"description": self.git_provider.get_pr_description(full=False), "description": self.git_provider.get_pr_description(full=False),
"language": self.main_pr_language, "language": self.main_pr_language,
"diff": "", # empty diff for initial calculation "diff": "", # empty diff for initial calculation
"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(),
"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": "",
"enable_semantic_files_types": get_settings().pr_description.enable_semantic_files_types, "custom_labels_examples": "",
} }
self.user_description = self.git_provider.get_user_description() self.user_description = self.git_provider.get_user_description()
@ -87,9 +80,6 @@ class PRDescription:
else: else:
return None return None
if get_settings().pr_description.enable_semantic_files_types:
self._prepare_file_labels()
pr_labels = [] pr_labels = []
if get_settings().pr_description.publish_labels: if get_settings().pr_description.publish_labels:
pr_labels = self._prepare_labels() pr_labels = self._prepare_labels()
@ -103,21 +93,14 @@ class PRDescription:
if get_settings().config.publish_output: if get_settings().config.publish_output:
get_logger().info(f"Pushing answer {self.pr_id}") get_logger().info(f"Pushing answer {self.pr_id}")
if get_settings().pr_description.publish_description_as_comment: if get_settings().pr_description.publish_description_as_comment:
get_logger().info(f"Publishing answer as comment")
self.git_provider.publish_comment(full_markdown_description) self.git_provider.publish_comment(full_markdown_description)
else: else:
self.git_provider.publish_description(pr_title, pr_body) self.git_provider.publish_description(pr_title, pr_body)
if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"): if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"):
current_labels = self.git_provider.get_pr_labels() current_labels = self.git_provider.get_labels()
user_labels = get_user_labels(current_labels) user_labels = get_user_labels(current_labels)
self.git_provider.publish_labels(pr_labels + user_labels)
if (get_settings().pr_description.final_update_message and self.git_provider.publish_labels(pr_labels + user_labels)
hasattr(self.git_provider, 'pr_url') and self.git_provider.pr_url):
latest_commit_url = self.git_provider.get_latest_commit_url()
if latest_commit_url:
self.git_provider.publish_comment(
f"**[PR Description]({self.git_provider.pr_url})** updated to latest commit ({latest_commit_url})")
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
except Exception as e: except Exception as e:
get_logger().error(f"Error generating PR description {self.pr_id}: {e}") get_logger().error(f"Error generating PR description {self.pr_id}: {e}")
@ -160,8 +143,7 @@ class PRDescription:
variables["diff"] = self.patches_diff # update diff variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined) environment = Environment(undefined=StrictUndefined)
set_custom_labels(variables, self.git_provider) set_custom_labels(variables)
self.variables = variables
system_prompt = environment.from_string(get_settings().pr_description_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_description_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_description_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_description_prompt.user).render(variables)
@ -176,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):
@ -188,47 +167,21 @@ class PRDescription:
if get_settings().pr_description.add_original_user_description and self.user_description: if get_settings().pr_description.add_original_user_description and self.user_description:
self.data["User Description"] = self.user_description self.data["User Description"] = self.user_description
# re-order keys
if 'User Description' in self.data:
self.data['User Description'] = self.data.pop('User Description')
if 'title' in self.data:
self.data['title'] = self.data.pop('title')
if 'type' in self.data:
self.data['type'] = self.data.pop('type')
if 'labels' in self.data:
self.data['labels'] = self.data.pop('labels')
if 'description' in self.data:
self.data['description'] = self.data.pop('description')
if 'pr_files' in self.data:
self.data['pr_files'] = self.data.pop('pr_files')
def _prepare_labels(self) -> List[str]: def _prepare_labels(self) -> List[str]:
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(',')
# convert lowercase labels to original case
try:
if "labels_minimal_to_labels_dict" in self.variables:
d: dict = self.variables["labels_minimal_to_labels_dict"]
for i, label_i in enumerate(pr_types):
if label_i in d:
pr_types[i] = d[label_i]
except Exception as e:
get_logger().error(f"Error converting labels to original case {self.pr_id}: {e}")
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]:
@ -240,25 +193,26 @@ 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)
ai_walkthrough = self.data.get('pr_files') if not re.search(r'<!--\s*pr_agent:walkthrough\s*-->', body):
if ai_walkthrough and not re.search(r'<!--\s*pr_agent:walkthrough\s*-->', body): ai_walkthrough = self.data.get('PR Main Files Walkthrough')
try: if ai_walkthrough:
walkthrough_gfm = "" walkthrough = str(ai_header)
walkthrough_gfm = self.process_pr_files_prediction(walkthrough_gfm, self.file_label_dict) for file in ai_walkthrough:
body = body.replace('pr_agent:walkthrough', walkthrough_gfm) filename = file['filename'].replace("'", "`")
except Exception as e: description = file['changes in file'].replace("'", "`")
get_logger().error(f"Failing to process walkthrough {self.pr_id}: {e}") walkthrough += f'- `{filename}`: {description}\n'
body = body.replace('pr_agent:walkthrough', "")
body = body.replace('pr_agent:walkthrough', walkthrough)
return title, body return title, body
@ -274,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"]
@ -295,147 +249,26 @@ class PRDescription:
# except for the items containing the word 'walkthrough' # except for the items containing the word 'walkthrough'
pr_body = "" pr_body = ""
for idx, (key, value) in enumerate(self.data.items()): for idx, (key, value) in enumerate(self.data.items()):
if key == 'pr_files': pr_body += f"## {key}:\n"
value = self.file_label_dict
key_publish = "Changes walkthrough"
else:
key_publish = key.rstrip(':').replace("_", " ").capitalize()
pr_body += f"## {key_publish}\n"
if 'walkthrough' in key.lower(): if 'walkthrough' in key.lower():
# for filename, description in value.items():
if self.git_provider.is_supported("gfm_markdown"): if self.git_provider.is_supported("gfm_markdown"):
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"
elif 'pr_files' in key.lower():
pr_body = self.process_pr_files_prediction(pr_body, value)
else: else:
# if the value is a list, join its items by comma # if the value is a list, join its items by comma
if isinstance(value, list): if type(value) == list:
value = ', '.join(v for v in value) value = ', '.join(v for v in value)
pr_body += f"{value}\n" pr_body += f"{value}\n"
if idx < len(self.data) - 1: if idx < len(self.data) - 1:
pr_body += "\n\n___\n\n" pr_body += "\n___\n"
if get_settings().config.verbosity_level >= 2: if get_settings().config.verbosity_level >= 2:
get_logger().info(f"title:\n{title}\n{pr_body}") get_logger().info(f"title:\n{title}\n{pr_body}")
return title, pr_body return title, pr_body
def _prepare_file_labels(self):
self.file_label_dict = {}
for file in self.data['pr_files']:
try:
filename = file['filename'].replace("'", "`").replace('"', '`')
changes_summary = file['changes_summary']
label = file.get('label')
if label not in self.file_label_dict:
self.file_label_dict[label] = []
self.file_label_dict[label].append((filename, changes_summary))
except Exception as e:
get_logger().error(f"Error preparing file label dict {self.pr_id}: {e}")
pass
def process_pr_files_prediction(self, pr_body, value):
# logic for using collapsible file list
use_collapsible_file_list = get_settings().pr_description.collapsible_file_list
num_files = 0
if value:
for semantic_label in value.keys():
num_files += len(value[semantic_label])
if use_collapsible_file_list == "adaptive":
use_collapsible_file_list = num_files > 8
if not self.git_provider.is_supported("gfm_markdown"):
get_logger().info(f"Disabling semantic files types for {self.pr_id} since gfm_markdown is not supported")
return pr_body
try:
pr_body += "<table>"
header = f"Relevant files"
delta = 65
header += "&nbsp; " * delta
pr_body += f"""<thead><tr><th></th><th>{header}</th></tr></thead>"""
pr_body += """<tbody>"""
for semantic_label in value.keys():
s_label = semantic_label.strip("'").strip('"')
pr_body += f"""<tr><td><strong>{s_label.capitalize()}</strong></td>"""
list_tuples = value[semantic_label]
if use_collapsible_file_list:
pr_body += f"""<td><details><summary>{len(list_tuples)} files</summary><table>"""
else:
pr_body += f"""<td><table>"""
for filename, file_change_description in list_tuples:
filename = filename.replace("'", "`")
filename_publish = filename.split("/")[-1]
filename_publish = f"{filename_publish}"
if len(filename_publish) < (delta - 5):
filename_publish += "&nbsp; " * ((delta - 5) - len(filename_publish))
diff_plus_minus = ""
diff_files = self.git_provider.diff_files
for f in diff_files:
if f.filename.lower() == filename.lower():
num_plus_lines = f.num_plus_lines
num_minus_lines = f.num_minus_lines
diff_plus_minus += f"+{num_plus_lines}/-{num_minus_lines}"
break
# try to add line numbers link to code suggestions
link = ""
if hasattr(self.git_provider, 'get_line_link'):
filename = filename.strip()
link = self.git_provider.get_line_link(filename, relevant_line_start=-1)
file_change_description = self._insert_br_after_x_chars(file_change_description, x=(delta - 5))
pr_body += f"""
<tr>
<td>
<details>
<summary><strong>{filename_publish}</strong></summary>
<ul>
{filename}<br><br>
**{file_change_description}**
</ul>
</details>
</td>
<td><a href="{link}"> {diff_plus_minus}</a></td>
</tr>
"""
if use_collapsible_file_list:
pr_body += """</table></details></td></tr>"""
else:
pr_body += """</table></td></tr>"""
pr_body += """</tr></tbody></table>"""
except Exception as e:
get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {e}")
pass
return pr_body
def _insert_br_after_x_chars(self, text, x=70):
"""
Insert <br> into a string after a word that increases its length above x characters.
"""
if len(text) < x:
return text
words = text.split(' ')
new_text = ""
current_length = 0
for word in words:
# Check if adding this word exceeds x characters
if current_length + len(word) > x:
new_text += "<br>" # Insert line break
current_length = 0 # Reset counter
# Add the word to the new text
new_text += word + " "
current_length += len(word) + 1 # Add 1 for the space
return new_text.strip() # Remove trailing space

View File

@ -1,12 +1,10 @@
import copy import copy
import re import re
from functools import partial
from typing import List, Tuple from typing import List, Tuple
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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 load_yaml, set_custom_labels, get_user_labels from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels
@ -17,8 +15,7 @@ from pr_agent.log import get_logger
class PRGenerateLabels: class PRGenerateLabels:
def __init__(self, pr_url: str, args: list = None, def __init__(self, pr_url: str, args: list = None):
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
""" """
Initialize the PRGenerateLabels object with the necessary attributes and objects for generating labels Initialize the PRGenerateLabels object with the necessary attributes and objects for generating labels
corresponding to the PR using an AI model. corresponding to the PR using an AI model.
@ -34,7 +31,7 @@ class PRGenerateLabels:
self.pr_id = self.git_provider.get_pr_id() self.pr_id = self.git_provider.get_pr_id()
# Initialize the AI handler # Initialize the AI handler
self.ai_handler = ai_handler() self.ai_handler = AiHandler()
# Initialize the variables dictionary # Initialize the variables dictionary
self.vars = { self.vars = {
@ -43,10 +40,12 @@ class PRGenerateLabels:
"description": self.git_provider.get_pr_description(full=False), "description": self.git_provider.get_pr_description(full=False),
"language": self.main_pr_language, "language": self.main_pr_language,
"diff": "", # empty diff for initial calculation "diff": "", # empty diff for initial calculation
"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
@ -84,7 +83,7 @@ class PRGenerateLabels:
if get_settings().config.publish_output: if get_settings().config.publish_output:
get_logger().info(f"Pushing labels {self.pr_id}") get_logger().info(f"Pushing labels {self.pr_id}")
current_labels = self.git_provider.get_pr_labels() current_labels = self.git_provider.get_labels()
user_labels = get_user_labels(current_labels) user_labels = get_user_labels(current_labels)
pr_labels = pr_labels + user_labels pr_labels = pr_labels + user_labels
@ -134,8 +133,7 @@ class PRGenerateLabels:
variables["diff"] = self.patches_diff # update diff variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined) environment = Environment(undefined=StrictUndefined)
set_custom_labels(variables, self.git_provider) set_custom_labels(variables)
self.variables = variables
system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables) system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables)
user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables) user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables)
@ -150,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):
@ -164,21 +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(',')
# convert lowercase labels to original case
try:
if "labels_minimal_to_labels_dict" in self.variables:
d: dict = self.variables["labels_minimal_to_labels_dict"]
for i, label_i in enumerate(pr_types):
if label_i in d:
pr_types[i] = d[label_i]
except Exception as e:
get_logger().error(f"Error converting labels to original case {self.pr_id}: {e}")
return pr_types return pr_types

View File

@ -1,10 +1,8 @@
import copy import copy
from functools import partial
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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.config_loader import get_settings from pr_agent.config_loader import get_settings
@ -14,13 +12,12 @@ from pr_agent.log import get_logger
class PRInformationFromUser: class PRInformationFromUser:
def __init__(self, pr_url: str, args: list = None, def __init__(self, pr_url: str, args: list = None):
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
self.git_provider = get_git_provider()(pr_url) self.git_provider = get_git_provider()(pr_url)
self.main_pr_language = get_main_pr_language( self.main_pr_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files() self.git_provider.get_languages(), self.git_provider.get_files()
) )
self.ai_handler = ai_handler() self.ai_handler = AiHandler()
self.vars = { self.vars = {
"title": self.git_provider.pr.title, "title": self.git_provider.pr.title,
"branch": self.git_provider.get_pr_branch(), "branch": self.git_provider.get_pr_branch(),

View File

@ -1,10 +1,8 @@
import copy import copy
from functools import partial
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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.config_loader import get_settings from pr_agent.config_loader import get_settings
@ -14,13 +12,13 @@ from pr_agent.log import get_logger
class PRQuestions: class PRQuestions:
def __init__(self, pr_url: str, args=None, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler): def __init__(self, pr_url: str, args=None):
question_str = self.parse_args(args) question_str = self.parse_args(args)
self.git_provider = get_git_provider()(pr_url) self.git_provider = get_git_provider()(pr_url)
self.main_pr_language = get_main_pr_language( self.main_pr_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files() self.git_provider.get_languages(), self.git_provider.get_files()
) )
self.ai_handler = ai_handler() self.ai_handler = AiHandler()
self.question_str = question_str self.question_str = question_str
self.vars = { self.vars = {
"title": self.git_provider.pr.title, "title": self.git_provider.pr.title,

View File

@ -1,18 +1,16 @@
import copy import copy
import datetime import datetime
from collections import OrderedDict from collections import OrderedDict
from functools import partial
from typing import List, Tuple from typing import List, Tuple
import yaml import yaml
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from yaml import SafeLoader from yaml import SafeLoader
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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
@ -24,16 +22,13 @@ class PRReviewer:
""" """
The PRReviewer class is responsible for reviewing a pull request and generating feedback using an AI model. The PRReviewer class is responsible for reviewing a pull request and generating feedback using an AI model.
""" """
def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False, args: list = None, def __init__(self, pr_url: str, is_answer: bool = False, is_auto: bool = False, args: list = None):
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
""" """
Initialize the PRReviewer object with the necessary attributes and objects to review a pull request. Initialize the PRReviewer object with the necessary attributes and objects to review a pull request.
Args: Args:
pr_url (str): The URL of the pull request to be reviewed. pr_url (str): The URL of the pull request to be reviewed.
is_answer (bool, optional): Indicates whether the review is being done in answer mode. Defaults to False. is_answer (bool, optional): Indicates whether the review is being done in answer mode. Defaults to False.
is_auto (bool, optional): Indicates whether the review is being done in automatic mode. Defaults to False.
ai_handler (BaseAiHandler): The AI handler to be used for the review. Defaults to None.
args (list, optional): List of arguments passed to the PRReviewer class. Defaults to None. args (list, optional): List of arguments passed to the PRReviewer class. Defaults to None.
""" """
self.parse_args(args) # -i command self.parse_args(args) # -i command
@ -48,7 +43,7 @@ class PRReviewer:
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"): if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
raise Exception(f"Answer mode is not supported for {get_settings().config.git_provider} for now") raise Exception(f"Answer mode is not supported for {get_settings().config.git_provider} for now")
self.ai_handler = ai_handler() self.ai_handler = AiHandler()
self.patches_diff = None self.patches_diff = None
self.prediction = None self.prediction = None
@ -103,9 +98,9 @@ class PRReviewer:
""" """
try: try:
# if self.is_auto and not get_settings().pr_reviewer.automatic_review: if self.is_auto and not get_settings().pr_reviewer.automatic_review:
# get_logger().info(f'Automatic review is disabled {self.pr_url}') get_logger().info(f'Automatic review is disabled {self.pr_url}')
# return None return None
if self.incremental.is_incremental and not self._can_run_incremental_review(): if self.incremental.is_incremental and not self._can_run_incremental_review():
return None return None
@ -126,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)
@ -183,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:
@ -251,18 +243,14 @@ class PRReviewer:
markdown_text = convert_to_markdown(data, self.git_provider.is_supported("gfm_markdown")) markdown_text = convert_to_markdown(data, self.git_provider.is_supported("gfm_markdown"))
user = self.git_provider.get_user_id() user = self.git_provider.get_user_id()
# Add help text if gfm_markdown is supported # Add help text if not in CLI mode
if self.git_provider.is_supported("gfm_markdown") and get_settings().pr_reviewer.enable_help_text: if not get_settings().get("CONFIG.CLI_MODE", False):
markdown_text += "\n\n<details> <summary><strong>✨ Usage tips:</strong></summary><hr> \n\n" markdown_text += "\n### How to use\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 and not get_settings().get("CONFIG.CLI_MODE", False): 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
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:
@ -280,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()
@ -377,32 +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_pr_labels()
if current_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')]
else:
current_labels_filtered = []
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

@ -5,11 +5,9 @@ from typing import List
import openai import openai
import pandas as pd import pandas as pd
import pinecone import pinecone
import lancedb
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
@ -20,7 +18,7 @@ MODEL = "text-embedding-ada-002"
class PRSimilarIssue: class PRSimilarIssue:
def __init__(self, issue_url: str, ai_handler, args: list = None): def __init__(self, issue_url: str, args: list = None):
if get_settings().config.git_provider != "github": if get_settings().config.git_provider != "github":
raise Exception("Only github is supported for similar issue tool") raise Exception("Only github is supported for similar issue tool")
@ -36,138 +34,75 @@ class PRSimilarIssue:
repo_name_for_index = self.repo_name_for_index = repo_obj.full_name.lower().replace('/', '-').replace('_/', '-') repo_name_for_index = self.repo_name_for_index = repo_obj.full_name.lower().replace('/', '-').replace('_/', '-')
index_name = self.index_name = "codium-ai-pr-agent-issues" index_name = self.index_name = "codium-ai-pr-agent-issues"
if get_settings().pr_similar_issue.vectordb == "pinecone": # assuming pinecone api key and environment are set in secrets file
# assuming pinecone api key and environment are set in secrets file try:
try: api_key = get_settings().pinecone.api_key
api_key = get_settings().pinecone.api_key environment = get_settings().pinecone.environment
environment = get_settings().pinecone.environment except Exception:
except Exception: if not self.cli_mode:
if not self.cli_mode: repo_name, original_issue_number = self.git_provider._parse_issue_url(self.issue_url.split('=')[-1])
repo_name, original_issue_number = self.git_provider._parse_issue_url(self.issue_url.split('=')[-1]) issue_main = self.git_provider.repo_obj.get_issue(original_issue_number)
issue_main = self.git_provider.repo_obj.get_issue(original_issue_number) issue_main.create_comment("Please set pinecone api key and environment in secrets file")
issue_main.create_comment("Please set pinecone api key and environment in secrets file") raise Exception("Please set pinecone api key and environment in secrets file")
raise Exception("Please set pinecone api key and environment in secrets file")
# check if index exists, and if repo is already indexed # check if index exists, and if repo is already indexed
run_from_scratch = False run_from_scratch = False
if run_from_scratch: # for debugging if run_from_scratch: # for debugging
pinecone.init(api_key=api_key, environment=environment)
if index_name in pinecone.list_indexes():
get_logger().info('Removing index...')
pinecone.delete_index(index_name)
get_logger().info('Done')
upsert = True
pinecone.init(api_key=api_key, environment=environment) pinecone.init(api_key=api_key, environment=environment)
if not index_name in pinecone.list_indexes(): if index_name in pinecone.list_indexes():
run_from_scratch = True get_logger().info('Removing index...')
upsert = False pinecone.delete_index(index_name)
else:
if get_settings().pr_similar_issue.force_update_dataset:
upsert = True
else:
pinecone_index = pinecone.Index(index_name=index_name)
res = pinecone_index.fetch([f"example_issue_{repo_name_for_index}"]).to_dict()
if res["vectors"]:
upsert = False
if run_from_scratch or upsert: # index the entire repo
get_logger().info('Indexing the entire repo...')
get_logger().info('Getting issues...')
issues = list(repo_obj.get_issues(state='all'))
get_logger().info('Done') get_logger().info('Done')
self._update_index_with_issues(issues, repo_name_for_index, upsert=upsert)
else: # update index if needed upsert = True
pinecone.init(api_key=api_key, environment=environment)
if not index_name in pinecone.list_indexes():
run_from_scratch = True
upsert = False
else:
if get_settings().pr_similar_issue.force_update_dataset:
upsert = True
else:
pinecone_index = pinecone.Index(index_name=index_name) pinecone_index = pinecone.Index(index_name=index_name)
issues_to_update = [] res = pinecone_index.fetch([f"example_issue_{repo_name_for_index}"]).to_dict()
issues_paginated_list = repo_obj.get_issues(state='all') if res["vectors"]:
counter = 1 upsert = False
for issue in issues_paginated_list:
if issue.pull_request: if run_from_scratch or upsert: # index the entire repo
continue get_logger().info('Indexing the entire repo...')
issue_str, comments, number = self._process_issue(issue)
issue_key = f"issue_{number}" get_logger().info('Getting issues...')
id = issue_key + "." + "issue" issues = list(repo_obj.get_issues(state='all'))
res = pinecone_index.fetch([id]).to_dict() get_logger().info('Done')
is_new_issue = True self._update_index_with_issues(issues, repo_name_for_index, upsert=upsert)
for vector in res["vectors"].values(): else: # update index if needed
if vector['metadata']['repo'] == repo_name_for_index: pinecone_index = pinecone.Index(index_name=index_name)
is_new_issue = False issues_to_update = []
break issues_paginated_list = repo_obj.get_issues(state='all')
if is_new_issue: counter = 1
counter += 1 for issue in issues_paginated_list:
issues_to_update.append(issue) if issue.pull_request:
else: continue
issue_str, comments, number = self._process_issue(issue)
issue_key = f"issue_{number}"
id = issue_key + "." + "issue"
res = pinecone_index.fetch([id]).to_dict()
is_new_issue = True
for vector in res["vectors"].values():
if vector['metadata']['repo'] == repo_name_for_index:
is_new_issue = False
break break
if is_new_issue:
if issues_to_update: counter += 1
get_logger().info(f'Updating index with {counter} new issues...') issues_to_update.append(issue)
self._update_index_with_issues(issues_to_update, repo_name_for_index, upsert=True)
else: else:
get_logger().info('No new issues to update') break
elif get_settings().pr_similar_issue.vectordb == "lancedb":
self.db = lancedb.connect(get_settings().lancedb.uri)
self.table = None
run_from_scratch = False if issues_to_update:
if run_from_scratch: # for debugging get_logger().info(f'Updating index with {counter} new issues...')
if index_name in self.db.table_names(): self._update_index_with_issues(issues_to_update, repo_name_for_index, upsert=True)
get_logger().info('Removing Table...')
self.db.drop_table(index_name)
get_logger().info('Done')
ingest = True
if index_name not in self.db.table_names():
run_from_scratch = True
ingest = False
else: else:
if get_settings().pr_similar_issue.force_update_dataset: get_logger().info('No new issues to update')
ingest = True
else:
self.table = self.db[index_name]
res = self.table.search().limit(len(self.table)).where(f"id='example_issue_{repo_name_for_index}'").to_list()
get_logger().info("result: ", res)
if res[0].get("vector"):
ingest = False
if run_from_scratch or ingest: # indexing the entire repo
get_logger().info('Indexing the entire repo...')
get_logger().info('Getting issues...')
issues = list(repo_obj.get_issues(state='all'))
get_logger().info('Done')
self._update_table_with_issues(issues, repo_name_for_index, ingest=ingest)
else: # update table if needed
issues_to_update = []
issues_paginated_list = repo_obj.get_issues(state='all')
counter = 1
for issue in issues_paginated_list:
if issue.pull_request:
continue
issue_str, comments, number = self._process_issue(issue)
issue_key = f"issue_{number}"
issue_id = issue_key + "." + "issue"
res = self.table.search().limit(len(self.table)).where(f"id='{issue_id}'").to_list()
is_new_issue = True
for r in res:
if r['metadata']['repo'] == repo_name_for_index:
is_new_issue = False
break
if is_new_issue:
counter += 1
issues_to_update.append(issue)
else:
break
if issues_to_update:
get_logger().info(f'Updating index with {counter} new issues...')
self._update_table_with_issues(issues_to_update, repo_name_for_index, ingest=True)
else:
get_logger().info('No new issues to update')
async def run(self): async def run(self):
get_logger().info('Getting issue...') get_logger().info('Getting issue...')
@ -180,69 +115,38 @@ class PRSimilarIssue:
get_logger().info('Querying...') get_logger().info('Querying...')
res = openai.Embedding.create(input=[issue_str], engine=MODEL) res = openai.Embedding.create(input=[issue_str], engine=MODEL)
embeds = [record['embedding'] for record in res['data']] embeds = [record['embedding'] for record in res['data']]
pinecone_index = pinecone.Index(index_name=self.index_name)
res = pinecone_index.query(embeds[0],
top_k=5,
filter={"repo": self.repo_name_for_index},
include_metadata=True).to_dict()
relevant_issues_number_list = [] relevant_issues_number_list = []
relevant_comment_number_list = [] relevant_comment_number_list = []
score_list = [] score_list = []
for r in res['matches']:
if get_settings().pr_similar_issue.vectordb == "pinecone": # skip example issue
pinecone_index = pinecone.Index(index_name=self.index_name) if 'example_issue_' in r["id"]:
res = pinecone_index.query(embeds[0], continue
top_k=5,
filter={"repo": self.repo_name_for_index},
include_metadata=True).to_dict()
for r in res['matches']:
# skip example issue
if 'example_issue_' in r["id"]:
continue
try: try:
issue_number = int(r["id"].split('.')[0].split('_')[-1]) issue_number = int(r["id"].split('.')[0].split('_')[-1])
except: except:
get_logger().debug(f"Failed to parse issue number from {r['id']}") get_logger().debug(f"Failed to parse issue number from {r['id']}")
continue continue
if original_issue_number == issue_number: if original_issue_number == issue_number:
continue continue
if issue_number not in relevant_issues_number_list: if issue_number not in relevant_issues_number_list:
relevant_issues_number_list.append(issue_number) relevant_issues_number_list.append(issue_number)
if 'comment' in r["id"]: if 'comment' in r["id"]:
relevant_comment_number_list.append(int(r["id"].split('.')[1].split('_')[-1])) relevant_comment_number_list.append(int(r["id"].split('.')[1].split('_')[-1]))
else: else:
relevant_comment_number_list.append(-1) relevant_comment_number_list.append(-1)
score_list.append(str("{:.2f}".format(r['score']))) score_list.append(str("{:.2f}".format(r['score'])))
get_logger().info('Done') get_logger().info('Done')
elif get_settings().pr_similar_issue.vectordb == "lancedb":
res = self.table.search(embeds[0]).where(f"metadata.repo='{self.repo_name_for_index}'", prefilter=True).to_list()
for r in res:
# skip example issue
if 'example_issue_' in r["id"]:
continue
try:
issue_number = int(r["id"].split('.')[0].split('_')[-1])
except:
get_logger().debug(f"Failed to parse issue number from {r['id']}")
continue
if original_issue_number == issue_number:
continue
if issue_number not in relevant_issues_number_list:
relevant_issues_number_list.append(issue_number)
if 'comment' in r["id"]:
relevant_comment_number_list.append(int(r["id"].split('.')[1].split('_')[-1]))
else:
relevant_comment_number_list.append(-1)
score_list.append(str("{:.2f}".format(1-r['_distance'])))
get_logger().info('Done')
get_logger().info('Publishing response...') get_logger().info('Publishing response...')
similar_issues_str = "### Similar Issues\n___\n\n" similar_issues_str = "### Similar Issues\n___\n\n"
for i, issue_number_similar in enumerate(relevant_issues_number_list): for i, issue_number_similar in enumerate(relevant_issues_number_list):
issue = self.git_provider.repo_obj.get_issue(issue_number_similar) issue = self.git_provider.repo_obj.get_issue(issue_number_similar)
title = issue.title title = issue.title
@ -361,96 +265,6 @@ class PRSimilarIssue:
time.sleep(5) # wait for pinecone to finalize upserting before querying time.sleep(5) # wait for pinecone to finalize upserting before querying
get_logger().info('Done') get_logger().info('Done')
def _update_table_with_issues(self, issues_list, repo_name_for_index, ingest=False):
get_logger().info('Processing issues...')
corpus = Corpus()
example_issue_record = Record(
id=f"example_issue_{repo_name_for_index}",
text="example_issue",
metadata=Metadata(repo=repo_name_for_index)
)
corpus.append(example_issue_record)
counter = 0
for issue in issues_list:
if issue.pull_request:
continue
counter += 1
if counter % 100 == 0:
get_logger().info(f"Scanned {counter} issues")
if counter >= self.max_issues_to_scan:
get_logger().info(f"Scanned {self.max_issues_to_scan} issues, stopping")
break
issue_str, comments, number = self._process_issue(issue)
issue_key = f"issue_{number}"
username = issue.user.login
created_at = str(issue.created_at)
if len(issue_str) < 8000 or \
self.token_handler.count_tokens(issue_str) < get_max_tokens(MODEL): # fast reject first
issue_record = Record(
id=issue_key + "." + "issue",
text=issue_str,
metadata=Metadata(repo=repo_name_for_index,
username=username,
created_at=created_at,
level=IssueLevel.ISSUE)
)
corpus.append(issue_record)
if comments:
for j, comment in enumerate(comments):
comment_body = comment.body
num_words_comment = len(comment_body.split())
if num_words_comment < 10 or not isinstance(comment_body, str):
continue
if len(comment_body) < 8000 or \
self.token_handler.count_tokens(comment_body) < MAX_TOKENS[MODEL]:
comment_record = Record(
id=issue_key + ".comment_" + str(j + 1),
text=comment_body,
metadata=Metadata(repo=repo_name_for_index,
username=username, # use issue username for all comments
created_at=created_at,
level=IssueLevel.COMMENT)
)
corpus.append(comment_record)
df = pd.DataFrame(corpus.dict()["documents"])
get_logger().info('Done')
get_logger().info('Embedding...')
openai.api_key = get_settings().openai.key
list_to_encode = list(df["text"].values)
try:
res = openai.Embedding.create(input=list_to_encode, engine=MODEL)
embeds = [record['embedding'] for record in res['data']]
except:
embeds = []
get_logger().error('Failed to embed entire list, embedding one by one...')
for i, text in enumerate(list_to_encode):
try:
res = openai.Embedding.create(input=[text], engine=MODEL)
embeds.append(res['data'][0]['embedding'])
except:
embeds.append([0] * 1536)
df["vector"] = embeds
get_logger().info('Done')
if not ingest:
get_logger().info('Creating table from scratch...')
self.table = self.db.create_table(self.index_name, data=df, mode="overwrite")
time.sleep(15)
else:
get_logger().info('Ingesting in Table...')
if self.index_name not in self.db.table_names():
self.table.add(df)
else:
get_logger().info(f"Table {self.index_name} doesn't exists!")
time.sleep(5)
get_logger().info('Done')
class IssueLevel(str, Enum): class IssueLevel(str, Enum):
ISSUE = "issue" ISSUE = "issue"

View File

@ -1,13 +1,11 @@
import copy import copy
from datetime import date from datetime import date
from functools import partial
from time import sleep from time import sleep
from typing import Tuple from typing import Tuple
from jinja2 import Environment, StrictUndefined from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
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.config_loader import get_settings from pr_agent.config_loader import get_settings
@ -19,7 +17,7 @@ CHANGELOG_LINES = 50
class PRUpdateChangelog: class PRUpdateChangelog:
def __init__(self, pr_url: str, cli_mode=False, args=None, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler): def __init__(self, pr_url: str, cli_mode=False, args=None):
self.git_provider = get_git_provider()(pr_url) self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language( self.main_language = get_main_pr_language(
@ -27,7 +25,7 @@ class PRUpdateChangelog:
) )
self.commit_changelog = get_settings().pr_update_changelog.push_changelog_changes self.commit_changelog = get_settings().pr_update_changelog.push_changelog_changes
self._get_changlog_file() # self.changelog_file_str self._get_changlog_file() # self.changelog_file_str
self.ai_handler = ai_handler() self.ai_handler = AiHandler()
self.patches_diff = None self.patches_diff = None
self.prediction = None self.prediction = None
self.cli_mode = cli_mode self.cli_mode = cli_mode

View File

@ -17,7 +17,7 @@ maintainers = [
] ]
description = "CodiumAI PR-Agent is an open-source tool to automatically analyze a pull request and provide several types of feedback" description = "CodiumAI PR-Agent is an open-source tool to automatically analyze a pull request and provide several types of feedback"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.9"
keywords = ["ai", "tool", "developer", "review", "agent"] keywords = ["ai", "tool", "developer", "review", "agent"]
license = {file = "LICENSE", name = "Apache 2.0 License"} license = {file = "LICENSE", name = "Apache 2.0 License"}
classifiers = [ classifiers = [

View File

@ -1,27 +1,24 @@
aiohttp==3.9.1 dynaconf==3.1.12
atlassian-python-api==3.39.0
azure-devops==7.1.0b3
boto3==1.33.6
dynaconf==3.2.4
fastapi==0.99.0 fastapi==0.99.0
GitPython==3.1.32 PyGithub==1.59.*
google-cloud-aiplatform==1.35.0 retry==0.9.2
google-cloud-storage==2.10.0
Jinja2==3.1.2
litellm==0.12.5
loguru==0.7.2
msrest==0.7.1
openai==0.27.8 openai==0.27.8
Jinja2==3.1.2
tiktoken==0.4.0
uvicorn==0.22.0
python-gitlab==3.15.0
pytest==7.4.0
aiohttp==3.8.4
atlassian-python-api==3.39.0
GitPython==3.1.32
PyYAML==6.0
starlette-context==0.3.6
litellm~=0.1.574
boto3==1.28.25
google-cloud-storage==2.10.0
ujson==5.8.0
azure-devops==7.1.0b3
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
lancedb==0.3.4 loguru==0.7.2
pytest==7.4.0
PyGithub==1.59.*
PyYAML==6.0.1
python-gitlab==3.15.0
retry==0.9.2
starlette-context==0.3.6
tiktoken==0.5.2
ujson==5.8.0
uvicorn==0.22.0
# langchain==0.0.349 # uncomment this to support language LangChainOpenAIHandler

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> <strong>🤖 Code feedback:</strong></summary> - **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

Some files were not shown because too many files have changed in this diff Show More