Compare commits

..

89 Commits

Author SHA1 Message Date
90f97b0226 Lint fixes 2023-07-18 11:34:57 +03:00
9e0f5f0ccc Merge pull request #78 from Codium-ai/tr/agent_logic
Enhancement of PR Agent with User Interaction
2023-07-18 10:37:08 +03:00
87ea0176b9 Update README.md 2023-07-18 10:36:05 +03:00
62f08f4ec4 removed an unneeded file 2023-07-18 10:35:05 +03:00
fe0058f25f Merge branch 'tr/agent_logic' of github.com:Codium-ai/pr-agent into tr/agent_logic 2023-07-18 10:34:40 +03:00
6d2673f39d Merge remote-tracking branch 'origin/tr/agent_logic' into tr/agent_logic 2023-07-18 10:32:43 +03:00
b3a1d456b2 if settings.pr_reviewer.num_code_suggestions 2023-07-18 10:32:36 +03:00
f77a5f6929 Call PRAgent from github_action_runner.py 2023-07-18 10:31:24 +03:00
fdeae9c209 Update pr_agent/agent/pr_agent.py 2023-07-18 10:20:52 +03:00
a994ec1427 Call PRAgent from github_action_runner.py 2023-07-18 10:19:32 +03:00
e5259e2f5c Small refactor 2023-07-18 10:17:09 +03:00
6f1b418b25 Merge pull request #79 from patryk-kowalski-ds/deepsense.ai/gitlab-provider-file-creation-handling
Fixes 404 error on gitlab file provider happening in case a MR introduced a new file.
2023-07-18 08:27:59 +03:00
51e08c3c2b reflect and review + protections 2023-07-18 08:22:25 +03:00
4c29ff2db1 Merge remote-tracking branch 'origin/tr/agent_logic' into tr/agent_logic
# Conflicts:
#	pr_agent/tools/pr_description.py
2023-07-18 08:06:47 +03:00
5fbaa4366f publish_output instead publish_review 2023-07-18 08:05:42 +03:00
aee08ebbfe Merge branch 'main' into tr/agent_logic 2023-07-18 08:04:47 +03:00
6ad8df6be7 Merge pull request #80 from Codium-ai/ok/remove_pics
Remove most pics from repo
2023-07-17 23:51:24 +03:00
539edcad3c works 2023-07-17 16:53:38 +03:00
b7172df700 Remove most pics from repo 2023-07-17 16:52:23 +03:00
768bd40ad8 Remove most pics from repo 2023-07-17 16:50:27 +03:00
ea27c63f13 Insights from user's answers 2023-07-17 15:59:57 +03:00
c866288b0a Merge remote-tracking branch 'origin/main' into tr/agent_logic 2023-07-17 15:59:37 +03:00
8ae3c60670 In case of new file creation by the MR there is a 404 error on file retrieval by gitlab provider.
It was handled by catching the error and replacing the file string with an empty string.
Type checking was added before byte decoding - necessary in case of the empty string.
2023-07-17 14:53:23 +02:00
f8f415eb75 stable 2023-07-17 15:49:29 +03:00
fa421fd169 Merge pull request #75 from Codium-ai/bugfix/rename_get_description
get_description was removed
2023-07-17 10:32:01 +03:00
e0ae5c945e get_description was removed 2023-07-17 10:30:44 +03:00
865888e4e8 Merge pull request #74 from Codium-ai/update-gifs
Update GIFs
2023-07-17 09:35:06 +03:00
3b7cfe7bc5 Merge pull request #73 from Codium-ai/hl/clean_comments
Clean comments
2023-07-17 09:33:49 +03:00
262f9dddbc Merge pull request #72 from Codium-ai/tr/minor_fixes
Minor fixes
2023-07-17 09:33:18 +03:00
fa706b6e96 update gifs 2023-07-17 09:30:45 +03:00
ff51ab0946 Add files via upload 2023-07-17 09:27:41 +03:00
7884aa2348 Clean 2023-07-17 09:25:38 +03:00
8f3520807c minor fixes
minor fixes
2023-07-17 08:42:18 +03:00
fa90b242e3 pr_information_from_user_prompts 2023-07-17 08:09:56 +03:00
2dfd34bd61 Merge pull request #71 from Codium-ai/Minor-spelling-fix
Minor Spelling Fix
2023-07-17 08:08:45 +03:00
48f569bef0 Update README.md 2023-07-17 02:39:58 +03:00
a20fb9cc0c Merge pull request #70 from Codium-ai/hl/gitlab_code_suggestion
GitLab Code Suggestions Integration
2023-07-17 02:11:30 +03:00
c58e1f90e7 Merge branch 'main' into hl/gitlab_code_suggestion 2023-07-17 02:10:15 +03:00
d363f148f0 Merge pull request #65 from Codium-ai/tr/agent_logic
pr_information_from_user_prompts
2023-07-17 02:08:56 +03:00
cbf96a2e67 Merge pull request #68 from ilchemla/feature/remove-duplicate-function
Removal of Duplicate Function Across Git Providers
2023-07-17 02:07:09 +03:00
4d87c3ec6a Merge pull request #69 from Codium-ai/readme-update-demo
Update README with new demo instructions and gif
2023-07-17 02:04:50 +03:00
c13c52d733 Merge pull request #66 from Codium-ai/hl/refactor_install_md
Refactor Installation Instructions
2023-07-17 02:02:42 +03:00
dbf8142fe0 Merge with README changes 2023-07-17 02:01:49 +03:00
bacf6c96c2 Merge remote-tracking branch 'origin/hl/refactor_install_md' into hl/refactor_install_md
# Conflicts:
#	README.md
2023-07-17 01:59:46 +03:00
c9d49da8f7 Merge with README changes 2023-07-17 01:54:30 +03:00
7b22edac60 Merge branch 'main' into hl/refactor_install_md
# Conflicts:
#	README.md
2023-07-17 01:50:29 +03:00
fc309f69b9 Support Code Suggestion in Gitlab 2023-07-17 01:44:40 +03:00
7efb5cf74e add demo gif 2023-07-16 22:06:54 +03:00
8e200197c5 update demo description 2023-07-16 22:05:09 +03:00
fe98f67e08 Merge commit '0b1edd9716160bf57eecf307db72439b5443704d' into hl/refactor_install_md
# Conflicts:
#	README.md
2023-07-16 22:02:34 +03:00
0b1edd9716 A small typo 2023-07-16 21:47:50 +03:00
e638dc075c Remove git_provider.get_description() which is a duplicate of git_provider.get_pr_description() 2023-07-16 21:47:48 +03:00
559b160886 Merge pull request #67 from Codium-ai/update-readme-gifs
Update README with new GIFs and minor text corrections
2023-07-16 21:29:23 +03:00
571b8769ac add improve gif 2023-07-16 21:20:11 +03:00
e4bd2148ce update readme 2023-07-16 21:19:40 +03:00
1637bd8774 add improve gif 2023-07-16 21:13:26 +03:00
ce33582d3d Change styling 2023-07-16 21:12:36 +03:00
bc6b592fd9 small fix 2023-07-16 19:58:34 +03:00
24ae6b966f Refactor Install section to a separate MD file 2023-07-16 19:56:58 +03:00
f4de3d2899 pr_information_from_user_prompts 2023-07-16 19:36:20 +03:00
4cacb07ec2 add review gif 2023-07-16 19:27:53 +03:00
2371a9b041 Change titles style 2023-07-16 19:25:10 +03:00
5b7403ae80 Fix typos 2023-07-16 19:15:02 +03:00
e979b8643d Merge pull request #63 from Codium-ai/enhancement/phrasing_update
Enhancement: Update Phrasing and Documentation
2023-07-16 18:18:22 +03:00
05b4f167a3 Fix requirements.txt 2023-07-16 18:05:30 +03:00
2c4245e023 Add github action to README.md 2023-07-16 17:56:32 +03:00
d54ee252ee Improve help text 2023-07-16 17:41:23 +03:00
85eec0b98c Merge pull request #62 from Codium-ai/readme-update-gifs
Readme update gifs
2023-07-16 17:35:02 +03:00
41a988d99a Update readme with gifs 2023-07-16 17:32:48 +03:00
448da3d481 added gifs for ask and describe 2023-07-16 17:23:26 +03:00
b030299547 Merge pull request #51 from ilchemla/feature/new-bitbucket-handler
Adding Bitbucket Provider Support
2023-07-16 17:04:06 +03:00
5bdbfda1e2 Merge pull request #61 from zmeir/zmeir-minor_typos_in_prompt
Minor Typos Fix in Prompt Files
2023-07-16 16:59:49 +03:00
047cfb21f3 Merge pull request #31 from Codium-ai/case-update
update the pr-agent name to title case
2023-07-16 16:50:26 +03:00
35a2497a38 Merge branch 'main' into case-update 2023-07-16 16:49:47 +03:00
99630f83c2 Update README.md 2023-07-16 16:38:38 +03:00
1757f2707c Update README.md 2023-07-16 16:38:32 +03:00
66c44d715c Update README.md 2023-07-16 16:38:25 +03:00
8f7855013a Update README.md 2023-07-16 16:38:15 +03:00
e200be4e57 Update README.md 2023-07-16 16:38:05 +03:00
d0b734bc91 Update README.md 2023-07-16 16:37:57 +03:00
399d5c5c5d Fixed minor typos in promts 2023-07-16 16:37:19 +03:00
1b88049cb0 Update README.md 2023-07-16 16:37:15 +03:00
0304bf05c1 Update README.md 2023-07-16 16:36:11 +03:00
94173cbb06 Merge pull request #58 from Codium-ai/tr/readme_update2
update readme
2023-07-16 16:12:26 +03:00
75447280e4 updated main pic 2023-07-16 16:11:55 +03:00
5edff8b7e4 update readme
update readme
2023-07-16 16:04:06 +03:00
487351d343 Merge pull request #60 from Codium-ai/enhancement/github_app
Enhancement of PR Agent with new commands
2023-07-16 15:59:01 +03:00
0a4a604c28 Adding handler for Bitbucket Cloud users 2023-07-15 21:29:36 +03:00
973cb2de1c update the pr-agent name to title case 2023-07-10 21:08:14 +03:00
45 changed files with 776 additions and 377 deletions

View File

@ -1,2 +1,3 @@
venv/ venv/
pr_agent/settings/.secrets.toml pr_agent/settings/.secrets.toml
pics/

View File

@ -11,6 +11,6 @@ jobs:
uses: Codium-ai/pr-agent@main uses: Codium-ai/pr-agent@main
env: env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }} OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
OPENAI_ORG: ${{ secrets.OPENAI_ORG }} OPENAI_ORG: ${{ secrets.OPENAI_ORG }} # optional
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

18
CONFIGURATION.md Normal file
View File

@ -0,0 +1,18 @@
## Configuration
The different tools and sub-tools used by CodiumAI pr-agent are easily configurable via the configuration file: `/pr-agent/settings/configuration.toml`.
##### Git Provider:
You can select your git_provider with the flag `git_provider` in the `config` section
##### PR Reviewer:
You can enable/disable the different PR Reviewer abilities with the following flags (`pr_reviewer` section):
```
require_focused_review=true
require_tests_review=true
require_security_review=true
```
You can contol the number of suggestions returned by the PR Reviewer with the following flag:
```inline_code_comments=3```
And enable/disable the inline code suggestions with the following flag:
```inline_code_comments=true```

180
INSTALL.md Normal file
View File

@ -0,0 +1,180 @@
## Installation
---
#### Method 1: Use Docker image (no installation required)
To request a review for a PR, or ask a question about a PR, you can run directly from the Docker image. Here's how:
1. To request a review for a PR, run the following command:
```
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> review
```
2. To ask a question about a PR, run the following command:
```
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> ask "<your question>"
```
Possible questions you can ask include:
- What is the main theme of this PR?
- Is the PR ready for merge?
- What are the main changes in this PR?
- Should this PR be split into smaller parts?
- Can you compose a rhymed song about this PR?
---
#### Method 2: Run as a GitHub Action
You can use our pre-built Github Action Docker image to run PR-Agent as a Github Action.
1. Add the following file to your repository under `.github/workflows/pr_agent.yml`:
```yaml
on:
pull_request:
issue_comment:
jobs:
pr_agent_job:
runs-on: ubuntu-latest
name: Run pr agent on every pull request, respond to user comments
steps:
- name: PR Agent action step
id: pragent
uses: Codium-ai/pr-agent@main
env:
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```
2. Add the following secret to your repository under `Settings > Secrets`:
```
OPENAI_KEY: <your key>
```
The GITHUB_TOKEN secret is automatically created by GitHub.
3. Merge this change to your main branch.
When you open your next PR, you should see a comment from `github-actions` bot with a review of your PR, and instructions on how to use the rest of the tools.
4. You may configure PR-Agent by adding environment variables under the env section corresponding to any configurable property in the [configuration](./CONFIGURATION.md) file. Some examples:
```yaml
env:
# ... previous environment values
OPENAI.ORG: "<Your organization name under your OpenAI account>"
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false" # Disable tests review
PR_CODE_SUGGESTIONS.NUM_CODE_SUGGESTIONS: 6 # Increase number of code suggestions
```
---
#### Method 3: Run from source
1. Clone this repository:
```
git clone https://github.com/Codium-ai/pr-agent.git
```
2. Install the requirements in your favorite virtual environment:
```
pip install -r requirements.txt
```
3. Copy the secrets template file and fill in your OpenAI key and your GitHub user token:
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
4. Run the appropriate Python scripts from the scripts folder:
```
python pr_agent/cli.py --pr_url <pr_url> review
python pr_agent/cli.py --pr_url <pr_url> ask <your question>
python pr_agent/cli.py --pr_url <pr_url> describe
python pr_agent/cli.py --pr_url <pr_url> improve
```
---
#### Method 4: Run as a polling server
Request reviews by tagging your Github user on a PR
Follow steps 1-3 of method 2.
Run the following command to start the server:
```
python pr_agent/servers/github_polling.py
```
---
#### Method 5: Run as a GitHub App
Allowing you to automate the review process on your private or public repositories.
1. Create a GitHub App from the [Github Developer Portal](https://docs.github.com/en/developers/apps/creating-a-github-app).
- Set the following permissions:
- Pull requests: Read & write
- Issue comment: Read & write
- Metadata: Read-only
- Set the following events:
- Issue comment
- Pull request
2. Generate a random secret for your app, and save it for later. For example, you can use:
```
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
```
3. Acquire the following pieces of information from your app's settings page:
- App private key (click "Generate a private key" and save the file)
- App ID
4. Clone this repository:
```
git clone https://github.com/Codium-ai/pr-agent.git
```
5. Copy the secrets template file and fill in the following:
- Your OpenAI key.
- Set deployment_type to 'app'
- Copy your app's private key to the private_key field.
- Copy your app's ID to the app_id field.
- Copy your app's webhook secret to the webhook_secret field.
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
6. Build a Docker image for the app and optionally push it to a Docker repository. We'll use Dockerhub as an example:
```
docker build . -t codiumai/pr-agent:github_app --target github_app -f docker/Dockerfile
docker push codiumai/pr-agent:github_app # Push to your Docker repository
```
7. Host the app using a server, serverless function, or container environment. Alternatively, for development and
debugging, you may use tools like smee.io to forward webhooks to your local machine.
8. Go back to your app's settings, and set the following:
- Webhook URL: The URL of your app's server or the URL of the smee.io channel.
- Webhook secret: The secret you generated earlier.
9. Install the app by navigating to the "Install App" tab and selecting your desired repositories.
---

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
![](./pics/git_patch_logic.png) ![](https://codium.ai/images/git_patch_logic.png)

306
README.md
View File

@ -10,26 +10,42 @@
[![GitHub license](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE) [![GitHub license](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
[![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.com/channels/1057273017547378788/1126104260430528613) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label&color=purple)](https://discord.com/channels/1057273017547378788/1126104260430528613)
</div> </div>
<div align="left"> <div style="text-align:left;">
CodiumAI `pr-agent` is an open-source tool aiming to help developers review PRs faster and more efficiently. It automatically analyzes the PR and can provide several types of feedback: CodiumAI `PR-Agent` is an open-source tool aiming to help developers review PRs faster and more efficiently. It automatically analyzes the PR and can provide several types of feedback:
**Auto-Description**: Automatically generating PR description - name, type, summary, and code walkthrough. **Auto-Description**: Automatically generating PR description - name, type, summary, and code walkthrough.
\ \
**PR Review**: Feedback about the PR main theme, type, relevant tests, security issues, focused, and various suggestions for the PR content. **PR Review**: Feedback about the PR main theme, type, relevant tests, security issues, focused PR, and various suggestions for the PR content.
\ \
**Question Answering**: Answering free-text questions about the PR. **Question Answering**: Answering free-text questions about the PR.
\ \
**Code Suggestion**: Committable code suggestions for improving the PR. **Code Suggestion**: Committable code suggestions for improving the PR.
Example results: <h3>Example results:</h2>
</div> </div>
<h4>Describe:</h4>
<div align="center"> <div align="center">
<p float="center"> <p float="center">
<img src="./pics/pr_reviewer_1.png" width="800"> <img src="https://codium.ai/images/describe.gif" width="800">
</p> </p>
</div>
<h4>Review:</h4>
<div align="center">
<p float="center"> <p float="center">
<img src="./pics/pr_code_suggestions.png" width="800"> <img src="https://codium.ai/images/review.gif" width="800">
</p>
</div>
<h4>Ask:</h4>
<div align="center">
<p float="center">
<img src="https://codium.ai/images/ask.gif" width="800">
</p>
</div>
<h4>Improve:</h4>
<div align="center">
<p float="center">
<img src="https://codium.ai/images/improve.gif" width="800">
</p> </p>
</div> </div>
<div align="left"> <div align="left">
@ -38,7 +54,7 @@ Example results:
- [Overview](#overview) - [Overview](#overview)
- [Quickstart](#quickstart) - [Quickstart](#quickstart)
- [Usage and tools](#usage-and-tools) - [Usage and tools](#usage-and-tools)
- [Configuration](#configuration) - [Configuration](./CONFIGURATION.md)
- [How it works](#how-it-works) - [How it works](#how-it-works)
- [Roadmap](#roadmap) - [Roadmap](#roadmap)
- [Similar projects](#similar-projects) - [Similar projects](#similar-projects)
@ -46,265 +62,91 @@ Example results:
## Live demo ## Live demo
Experience GPT-4 powered PR review on your public GitHub repository with our hosted pr-agent. To try it, just mention `@CodiumAI-Agent` in any PR comment! The agent will generate a PR review in response. Experience GPT-4 powered PR review on your public GitHub repository with our hosted PR-Agent. To try it, just mention `@CodiumAI-Agent` and add the desired command in any PR comment! The agent will generate a response based on your command.
![Review generation process](./pics/pr-agent-review-process1.gif) ![Review generation process](https://codium.ai/images/demo.gif)
To set up your own pr-agent, see the [Quickstart](#Quickstart) section To set up your own PR-Agent, see the [Quickstart](#Quickstart) section
--- ---
## 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 |
|-------|---------------------------------------------|--------|--------|-----------| |-------|---------------------------------------------|:------:|:------:|:---------:|
| TOOLS | Review | | | | | TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | ⮑ Inline review | | | | | | ⮑ Inline review | :white_check_mark: | :white_check_mark: | |
| | Ask | | | | | | Ask | :white_check_mark: | :white_check_mark: | |
| | Auto-Description | | | | | | Auto-Description | :white_check_mark: | | |
| | Improve Code | | | | | | Improve Code | :white_check_mark: | :white_check_mark: | |
| | Reflect and Review | :white_check_mark: | | |
| | | | | | | | | | | |
| USAGE | CLI | | | | | USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Tagging bot | | | | | | Tagging bot | :white_check_mark: | :white_check_mark: | |
| | Actions | | | | | | Actions | :white_check_mark: | | |
| | | | | | | | | | | |
| CORE | PR compression | | | | | CORE | PR compression | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Repo language prioritization | | | | | | Repo language prioritization | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Adaptive and token-aware<br />file patch fitting | | | | | | Adaptive and token-aware<br />file patch fitting | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Examples for invoking the different tools via the [CLI](#quickstart):
- **Review**: python cli.py --pr-url=<pr_url> review
- **Describe**: python cli.py --pr-url=<pr_url> describe
- **Improve**: python cli.py --pr-url=<pr_url> improve
- **Ask**: python cli.py --pr-url=<pr_url> ask "Write me a poem about this PR"
- **Reflect**: python cli.py --pr-url=<pr_url> reflect
"<pr_url>" is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).
In the [configuration](./CONFIGURATION.md) file you can select your git provider (GitHub, Gitlab, Bitbucket), and further configure the different tools.
## Quickstart ## Quickstart
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:
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.
There are several ways to use pr-agent. Let's start with the simplest one: There are several ways to use PR-Agent. Let's start with the simplest one:
---
#### Method 1: Use Docker image (no installation required) ## Install
Here are several ways to install and run PR-Agent:
To request a review for a PR, or ask a question about a PR, you can run directly from the Docker image. Here's how: - [Method 1: Use Docker image (no installation required)](INSTALL.md#method-1-use-docker-image-no-installation-required)
- [Method 2: Run as a GitHub Action](INSTALL.md#method-2-run-as-a-github-action)
1. To request a review for a PR, run the following command: - [Method 3: Run from source](INSTALL.md#method-3-run-from-source)
- [Method 4: Run as a polling server](INSTALL.md#method-4-run-as-a-polling-server)
``` - Request reviews by tagging your GitHub user on a PR
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr url> - [Method 5: Run as a GitHub App](INSTALL.md#method-5-run-as-a-github-app)
``` - Allowing you to automate the review process on your private or public repositories
2. To ask a question about a PR, run the following command:
```
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr url> --question "<your question>"
```
Possible questions you can ask include:
- What is the main theme of this PR?
- Is the PR ready for merge?
- What are the main changes in this PR?
- Should this PR be split into smaller parts?
- Can you compose a rhymed song about this PR.
---
#### Method 2: Run from source
1. Clone this repository:
```
git clone https://github.com/Codium-ai/pr-agent.git
```
2. Install the requirements in your favorite virtual environment:
```
pip install -r requirements.txt
```
3. Copy the secrets template file and fill in your OpenAI key and your GitHub user token:
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
4. Run the appropriate Python scripts from the scripts folder:
```
python pr_agent/cli.py --pr_url <pr url>
python pr_agent/cli.py --pr_url <pr url> --question "<your question>"
```
---
#### Method 3: Method 3: Run as a polling server; request reviews by tagging your Github user on a PR
Follow steps 1-3 of method 2.
Run the following command to start the server:
```
python pr_agent/servers/github_polling.py
```
---
#### Method 4: Run as a Github App, allowing you to automate the review process on your private or public repositories.
1. Create a GitHub App from the [Github Developer Portal](https://docs.github.com/en/developers/apps/creating-a-github-app).
- Set the following permissions:
- Pull requests: Read & write
- Issue comment: Read & write
- Metadata: Read-only
- Set the following events:
- Issue comment
- Pull request
2. Generate a random secret for your app, and save it for later. For example, you can use:
```
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
```
3. Acquire the following pieces of information from your app's settings page:
- App private key (click "Generate a private key", and save the file)
- App ID
4. Clone this repository:
```
git clone https://github.com/Codium-ai/pr-agent.git
```
5. Copy the secrets template file and fill in the following:
- Your OpenAI key.
- Set deployment_type to 'app'
- Copy your app's private key to the private_key field.
- Copy your app's ID to the app_id field.
- Copy your app's webhook secret to the webhook_secret field.
```
cp pr_agent/settings/.secrets_template.toml pr_agent/settings/.secrets.toml
# Edit .secrets.toml file
```
6. Build a Docker image for the app and optionally push it to a Docker repository. We'll use Dockerhub as an example:
```
docker build . -t codiumai/pr-agent:github_app --target github_app -f docker/Dockerfile
docker push codiumai/pr-agent:github_app # Push to your Docker repository
```
7. Host the app using a server, serverless function, or container environment. Alternatively, for development and
debugging, you may use tools like smee.io to forward webhooks to your local machine.
8. Go back to your app's settings, set the following:
- Webhook URL: The URL of your app's server, or the URL of the smee.io channel.
- Webhook secret: The secret you generated earlier.
9. Install the app by navigating to the "Install App" tab, and selecting your desired repositories.
---
## Usage and Tools ## Usage and Tools
CodiumAI pr-agent provides two types of interactions ("tools"): `"PR Reviewer"` and `"PR Q&A"`. **PR-Agent** provides five types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"`, `"PR Code Sueggestions"` and `"PR Reflect and Review"`.
- The "PR Reviewer" tool automatically analyzes PRs, and provides different types of feedbacks. - The "PR Reviewer" tool automatically analyzes PRs, and provides various types of feedback.
- The "PR Q&A" tool answers free-text questions about the PR. - The "PR Q&A" tool answers free-text questions about the PR.
- The "PR Description" tool automatically sets the PR Title and body.
### PR Reviewer - The "PR Code Suggestion" tool provide inline code suggestions for the PR that can be applied and committed.
- The "PR Reflect and Review" tool first initiates a dialog with the user and asks them to reflect on the PR, and then provides a review.
Here is a quick overview of the different sub-tools of PR Reviewer:
- PR Analysis
- Summarize main theme
- PR type classification
- Is the PR covered by relevant tests
- Is this a focused PR
- Are there security concerns
- PR Feedback
- General PR suggestions
- Code suggestions
This is how a typical output of the PR Reviewer looks like:
---
#### PR Analysis
- 🎯 **Main theme:** Adding language extension handler and token handler
- 📌 **Type of PR:** Enhancement
- 🧪 **Relevant tests added:** No
-**Focused PR:** Yes, the PR is focused on adding two new handlers for language extension and token counting.
- 🔒 **Security concerns:** No, the PR does not introduce possible security concerns or issues.
#### PR Feedback
- 💡 **General PR suggestions:** The PR is generally well-structured and the code is clean. However, it would be beneficial to add some tests to ensure the new handlers work as expected. Also, consider adding docstrings to the new functions and classes to improve code readability and maintainability.
- 🤖 **Code suggestions:**
- **relevant file:** pr_agent/algo/language_handler.py
**suggestion content:** Consider using a set instead of a list for 'bad_extensions' as checking membership in a set is faster than in a list. [medium]
- **relevant file:** pr_agent/algo/language_handler.py
**suggestion content:** In the 'filter_bad_extensions' function, you are splitting the filename on '.' and taking the last element to get the extension. This might not work as expected if the filename contains multiple '.' characters. Consider using 'os.path.splitext' to get the file extension more reliably. [important]
---
### PR Q&A
This tool answers free-text questions about the PR. This is how a typical output of the PR Q&A looks like:
**Question**: summarize for me the PR in 4 bullet points
**Answer**:
- The PR introduces a new feature to sort files by their main languages. It uses a mapping of programming languages to their file extensions to achieve this.
- It also introduces a filter to exclude files with certain extensions, deemed as 'bad extensions', from the sorting process.
- The PR modifies the `get_pr_diff` function in `pr_processing.py` to use the new sorting function. It also refactors the code to move the PR pruning logic into a separate function.
- A new `TokenHandler` class is introduced in `token_handler.py` to handle token counting operations. This class is initialized with a PR, variables, system, and user, and provides methods to get system and user tokens and to count tokens in a patch.
---
## Configuration
The different tools and sub-tools used by CodiumAI pr-agent are easily configurable via the configuration file: `/settings/configuration.toml`.
#### Enabling/disabling sub-tools:
You can enable/disable the different PR Reviewer sub-sections with the following flags:
```
require_focused_review=true
require_tests_review=true
require_security_review=true
```
## How it works ## How it works
![PR-Agent Tools](./pics/pr_agent_overview.png) ![PR-Agent Tools](https://codium.ai/images/pr_agent_overview.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
## Roadmap ## Roadmap
- [ ] Support open-source models, as a replacement for openai models. Note that a minimal requirement for each open-source model is to have 8k+ context, and good support for generating json as an output - [ ] Support open-source models, as a replacement for openai models. (Note - a minimal requirement for each open-source model is to have 8k+ context, and good support for generating json as an output)
- [ ] Support other Git providers, such as Gitlab and Bitbucket. - [x] Support other Git providers, such as Gitlab and Bitbucket.
- [ ] Develop additional logics for handling large PRs, and compressing git patches - [ ] Develop additional logics for handling large PRs, and compressing git patches
- [ ] Dedicated tools and sub-tools for specific programming languages (Python, Javascript, Java, C++, etc)
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags) - [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
- [ ] Adding more tools. Possible directions: - [ ] Adding more tools. Possible directions:
- [ ] Code Quality - [x] PR description
- [ ] Coding Style - [x] Inline code suggestions
- [x] Reflect and review
- [ ] Enforcing CONTRIBUTING.md guidelines
- [ ] Performance (are there any performance issues) - [ ] Performance (are there any performance issues)
- [ ] Documentation (is the PR properly documented) - [ ] Documentation (is the PR properly documented)
- [ ] Rank the PR importance - [ ] Rank the PR importance
@ -314,6 +156,6 @@ Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more detai
- [CodiumAI - Meaningful tests for busy devs](https://github.com/Codium-ai/codiumai-vscode-release) - [CodiumAI - Meaningful tests for busy devs](https://github.com/Codium-ai/codiumai-vscode-release)
- [Aider - GPT powered coding in your terminal](https://github.com/paul-gauthier/aider) - [Aider - GPT powered coding in your terminal](https://github.com/paul-gauthier/aider)
- [GPT-Engineer](https://github.com/AntonOsika/gpt-engineer) - [openai-pr-reviewer](https://github.com/coderabbitai/openai-pr-reviewer)
- [CodeReview BOT](https://github.com/anc95/ChatGPT-CodeReview) - [CodeReview BOT](https://github.com/anc95/ChatGPT-CodeReview)
- [AI-Maintainer](https://github.com/merwanehamadi/AI-Maintainer) - [AI-Maintainer](https://github.com/merwanehamadi/AI-Maintainer)

BIN
pics/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

View File

@ -1,7 +1,9 @@
import re import re
from pr_agent.config_loader import settings
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
from pr_agent.tools.pr_questions import PRQuestions from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer from pr_agent.tools.pr_reviewer import PRReviewer
@ -11,8 +13,13 @@ class PRAgent:
pass pass
async def handle_request(self, pr_url, request) -> bool: async def handle_request(self, pr_url, request) -> bool:
if any(cmd in request for cmd in ["/review", "/review_pr"]): if any(cmd in request for cmd in ["/answer"]):
await PRReviewer(pr_url).review() await PRReviewer(pr_url, is_answer=True).review()
elif any(cmd in request for cmd in ["/review", "/review_pr", "/reflect_and_review"]):
if settings.pr_reviewer.ask_and_reflect or "/reflect_and_review" in request:
await PRInformationFromUser(pr_url).generate_questions()
else:
await PRReviewer(pr_url).review()
elif any(cmd in request for cmd in ["/describe", "/describe_pr"]): elif any(cmd in request for cmd in ["/describe", "/describe_pr"]):
await PRDescription(pr_url).describe() await PRDescription(pr_url).describe()
elif any(cmd in request for cmd in ["/improve", "/improve_code"]): elif any(cmd in request for cmd in ["/improve", "/improve_code"]):

View File

@ -158,7 +158,7 @@ def convert_to_hunks_with_lines_numbers(patch: str, file) -> str:
patch_with_lines_str += f"{start2 + i} {line_new}\n" patch_with_lines_str += f"{start2 + i} {line_new}\n"
if old_content_lines: if old_content_lines:
patch_with_lines_str += '--old hunk--\n' patch_with_lines_str += '--old hunk--\n'
for i, line_old in enumerate(old_content_lines): for line_old in old_content_lines:
patch_with_lines_str += f"{line_old}\n" patch_with_lines_str += f"{line_old}\n"
new_content_lines = [] new_content_lines = []
old_content_lines = [] old_content_lines = []
@ -179,7 +179,7 @@ def convert_to_hunks_with_lines_numbers(patch: str, file) -> str:
patch_with_lines_str += f"{start2 + i} {line_new}\n" patch_with_lines_str += f"{start2 + i} {line_new}\n"
if old_content_lines: if old_content_lines:
patch_with_lines_str += '\n--old hunk--\n' patch_with_lines_str += '\n--old hunk--\n'
for i, line_old in enumerate(old_content_lines): for line_old in old_content_lines:
patch_with_lines_str += f"{line_old}\n" patch_with_lines_str += f"{line_old}\n"
return patch_with_lines_str.strip() return patch_with_lines_str.strip()

View File

@ -4,8 +4,7 @@ import difflib
import logging import logging
from typing import Any, Tuple, Union from typing import Any, Tuple, Union
from pr_agent.algo.git_patch_processing import extend_patch, handle_patch_deletions, \ from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions
convert_to_hunks_with_lines_numbers
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.token_handler import TokenHandler from pr_agent.algo.token_handler import TokenHandler
from pr_agent.config_loader import settings from pr_agent.config_loader import settings

View File

@ -61,18 +61,24 @@ def parse_code_suggestion(code_suggestions: dict) -> str:
return markdown_text return markdown_text
def try_fix_json(review, max_iter=10): def try_fix_json(review, max_iter=10, code_suggestions=False):
if review.endswith("}"):
return fix_json_escape_char(review)
# Try to fix JSON if it is broken/incomplete: parse until the last valid code suggestion # Try to fix JSON if it is broken/incomplete: parse until the last valid code suggestion
data = {} data = {}
if code_suggestions:
closing_bracket = "]}"
else:
closing_bracket = "]}}"
if review.rfind("'Code suggestions': [") > 0 or review.rfind('"Code suggestions": [') > 0: if review.rfind("'Code suggestions': [") > 0 or review.rfind('"Code suggestions": [') > 0:
last_code_suggestion_ind = [m.end() for m in re.finditer(r"\}\s*,", review)][-1] - 1 last_code_suggestion_ind = [m.end() for m in re.finditer(r"\}\s*,", review)][-1] - 1
valid_json = False valid_json = False
iter_count = 0 iter_count = 0
while last_code_suggestion_ind > 0 and not valid_json and iter_count < max_iter: while last_code_suggestion_ind > 0 and not valid_json and iter_count < max_iter:
try: try:
data = json.loads(review[:last_code_suggestion_ind] + "]}}") data = json.loads(review[:last_code_suggestion_ind] + closing_bracket)
valid_json = True valid_json = True
review = review[:last_code_suggestion_ind].strip() + "]}}" review = review[:last_code_suggestion_ind].strip() + closing_bracket
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
review = review[:last_code_suggestion_ind] review = review[:last_code_suggestion_ind]
# Use regular expression to find the last occurrence of "}," with any number of whitespaces or newlines # Use regular expression to find the last occurrence of "}," with any number of whitespaces or newlines
@ -82,3 +88,17 @@ def try_fix_json(review, max_iter=10):
logging.error("Unable to decode JSON response from AI") logging.error("Unable to decode JSON response from AI")
data = {} data = {}
return data return data
def fix_json_escape_char(json_message=None):
try:
result = json.loads(json_message)
except Exception as e:
# Find the offending character index:
idx_to_replace = int(str(e).split(' ')[-1].replace(')', ''))
# Remove the offending character:
json_message = list(json_message)
json_message[idx_to_replace] = ' '
new_message = ''.join(json_message)
return fix_json_escape_char(json_message=new_message)
return result

View File

@ -5,6 +5,7 @@ import os
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
from pr_agent.tools.pr_questions import PRQuestions from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer from pr_agent.tools.pr_reviewer import PRReviewer
@ -12,18 +13,27 @@ from pr_agent.tools.pr_reviewer import PRReviewer
def run(): def run():
parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage="""\ parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage="""\
Usage: cli.py --pr-url <URL on supported git hosting service> <command> [<args>]. Usage: cli.py --pr-url <URL on supported git hosting service> <command> [<args>].
For example:
- cli.py --pr-url=... review
- cli.py --pr-url=... describe
- cli.py --pr-url=... improve
- cli.py --pr-url=... ask "write me a poem about this PR"
- cli.py --pr-url=... reflect
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.
reflect - Ask the PR author questions about the PR.
""") """)
parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True) parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True)
parser.add_argument('command', type=str, help='The', choices=['review', 'review_pr', parser.add_argument('command', type=str, help='The', choices=['review', 'review_pr',
'ask', 'ask_question', 'ask', 'ask_question',
'describe', 'describe_pr', 'describe', 'describe_pr',
'improve', 'improve_code'], default='review') 'improve', 'improve_code',
'reflect', 'review_after_reflect'],
default='review')
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[]) parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
@ -49,6 +59,14 @@ improve / improve_code - Suggest improvements to the code in the PR as pull requ
print(f"Reviewing PR: {args.pr_url}") print(f"Reviewing PR: {args.pr_url}")
reviewer = PRReviewer(args.pr_url, cli_mode=True) reviewer = PRReviewer(args.pr_url, cli_mode=True)
asyncio.run(reviewer.review()) asyncio.run(reviewer.review())
elif command in ['reflect']:
print(f"Asking the PR author questions: {args.pr_url}")
reviewer = PRInformationFromUser(args.pr_url)
asyncio.run(reviewer.generate_questions())
elif command in ['review_after_reflect']:
print(f"Processing author's answers and sending review: {args.pr_url}")
reviewer = PRReviewer(args.pr_url, cli_mode=True, is_answer=True)
asyncio.run(reviewer.review())
else: else:
print(f"Unknown command: {command}") print(f"Unknown command: {command}")
parser.print_help() parser.print_help()

View File

@ -13,6 +13,7 @@ settings = Dynaconf(
"settings/pr_questions_prompts.toml", "settings/pr_questions_prompts.toml",
"settings/pr_description_prompts.toml", "settings/pr_description_prompts.toml",
"settings/pr_code_suggestions_prompts.toml", "settings/pr_code_suggestions_prompts.toml",
"settings/pr_information_from_user_prompts.toml",
"settings_prod/.secrets.toml" "settings_prod/.secrets.toml"
]] ]]
) )

View File

@ -1,10 +1,12 @@
from pr_agent.config_loader import settings from pr_agent.config_loader import settings
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
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
_GIT_PROVIDERS = { _GIT_PROVIDERS = {
'github': GithubProvider, 'github': GithubProvider,
'gitlab': GitLabProvider, 'gitlab': GitLabProvider,
'bitbucket': BitbucketProvider,
} }
def get_git_provider(): def get_git_provider():

View File

@ -0,0 +1,116 @@
import logging
from typing import Optional, Tuple
from urllib.parse import urlparse
import requests
from atlassian.bitbucket import Cloud
from pr_agent.config_loader import settings
from .git_provider import FilePatchInfo
class BitbucketProvider:
def __init__(self, pr_url: Optional[str] = None):
s = requests.Session()
s.headers['Authorization'] = f'Bearer {settings.get("BITBUCKET.BEARER_TOKEN", None)}'
self.bitbucket_client = Cloud(session=s)
self.workspace_slug = None
self.repo_slug = None
self.repo = None
self.pr_num = None
self.pr = None
self.temp_comments = []
if pr_url:
self.set_pr(pr_url)
def is_supported(self, capability: str) -> bool:
if capability == 'get_issue_comments':
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_files(self):
return [diff.new.path for diff in self.pr.diffstat()]
def get_diff_files(self) -> list[FilePatchInfo]:
diffs = self.pr.diffstat()
diff_split = ['diff --git%s' % x for x in self.pr.diff().split('diff --git') if x.strip()]
diff_files = []
for index, diff in enumerate(diffs):
original_file_content_str = self._get_pr_file_content(diff.old.get_data('links'))
new_file_content_str = self._get_pr_file_content(diff.new.get_data('links'))
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str,
diff_split[index], diff.new.path))
return diff_files
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
comment = self.pr.comment(pr_comment)
if is_temporary:
self.temp_comments.append(comment['id'])
def remove_initial_comment(self):
try:
for comment in self.temp_comments:
self.pr.delete(f'comments/{comment}')
except Exception as e:
logging.exception(f"Failed to remove temp comments, error: {e}")
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
pass
def get_title(self):
return self.pr.title
def get_languages(self):
languages = {self._get_repo().get_data('language'): 0}
return languages
def get_pr_branch(self):
return self.pr.source_branch
def get_pr_description(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")
@staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url)
if 'bitbucket.org' not in parsed_url.netloc:
raise ValueError("The provided URL is not a valid GitHub URL")
path_parts = parsed_url.path.strip('/').split('/')
if len(path_parts) < 4 or path_parts[2] != 'pull-requests':
raise ValueError("The provided URL does not appear to be a Bitbucket PR URL")
workspace_slug = path_parts[0]
repo_slug = path_parts[1]
try:
pr_number = int(path_parts[3])
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.workspaces.get(self.workspace_slug).repositories.get(self.repo_slug)
return self.repo
def _get_pr(self):
return self._get_repo().pullrequests.get(self.pr_num)
def _get_pr_file_content(self, remote_link: str):
return ""

View File

@ -3,12 +3,15 @@ from dataclasses import dataclass
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED) # enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
from enum import Enum from enum import Enum
class EDIT_TYPE(Enum): class EDIT_TYPE(Enum):
ADDED = 1 ADDED = 1
DELETED = 2 DELETED = 2
MODIFIED = 3 MODIFIED = 3
RENAMED = 4 RENAMED = 4
@dataclass @dataclass
class FilePatchInfo: class FilePatchInfo:
base_file: str base_file: str
@ -21,6 +24,10 @@ class FilePatchInfo:
class GitProvider(ABC): class GitProvider(ABC):
@abstractmethod
def is_supported(self, capability: str) -> bool:
pass
@abstractmethod @abstractmethod
def get_diff_files(self) -> list[FilePatchInfo]: def get_diff_files(self) -> list[FilePatchInfo]:
pass pass
@ -62,6 +69,10 @@ class GitProvider(ABC):
def get_pr_description(self): def get_pr_description(self):
pass pass
@abstractmethod
def get_issue_comments(self):
pass
def get_main_pr_language(languages, files) -> str: def get_main_pr_language(languages, files) -> str:
""" """

View File

@ -23,6 +23,9 @@ class GithubProvider(GitProvider):
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]
def is_supported(self, capability: str) -> bool:
return True
def set_pr(self, pr_url: str): def set_pr(self, pr_url: str):
self.repo, self.pr_num = self._parse_pr_url(pr_url) self.repo, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr() self.pr = self._get_pr()
@ -134,9 +137,6 @@ class GithubProvider(GitProvider):
def get_title(self): def get_title(self):
return self.pr.title return self.pr.title
def get_description(self):
return self.pr.body
def get_languages(self): def get_languages(self):
languages = self._get_repo().get_languages() languages = self._get_repo().get_languages()
return languages return languages
@ -164,6 +164,9 @@ class GithubProvider(GitProvider):
notifications = self.github_client.get_user().get_notifications(since=since) notifications = self.github_client.get_user().get_notifications(since=since)
return notifications return notifications
def get_issue_comments(self):
return self.pr.get_issue_comments()
@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)

View File

@ -4,10 +4,11 @@ from typing import Optional, Tuple
from urllib.parse import urlparse from urllib.parse import urlparse
import gitlab import gitlab
from gitlab import GitlabGetError
from pr_agent.config_loader import settings from pr_agent.config_loader import settings
from .git_provider import FilePatchInfo, GitProvider, EDIT_TYPE from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
class GitLabProvider(GitProvider): class GitLabProvider(GitProvider):
@ -28,6 +29,13 @@ class GitLabProvider(GitProvider):
self.diff_files = None self.diff_files = None
self.temp_comments = [] self.temp_comments = []
self._set_merge_request(merge_request_url) self._set_merge_request(merge_request_url)
self.RE_HUNK_HEADER = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
def is_supported(self, capability: str) -> bool:
if capability == 'get_issue_comments':
return False
return True
@property @property
def pr(self): def pr(self):
@ -40,7 +48,12 @@ class GitLabProvider(GitProvider):
self.last_diff = self.mr.diffs.list()[-1] self.last_diff = self.mr.diffs.list()[-1]
def _get_pr_file_content(self, file_path: str, branch: str) -> str: def _get_pr_file_content(self, file_path: str, branch: str) -> str:
return self.gl.projects.get(self.id_project).files.get(file_path, branch).decode() try:
return self.gl.projects.get(self.id_project).files.get(file_path, branch).decode()
except GitlabGetError:
# In case of file creation the method returns GitlabGetError (404 file not found).
# In this case we return an empty string for the diff.
return ''
def get_diff_files(self) -> list[FilePatchInfo]: def get_diff_files(self) -> list[FilePatchInfo]:
diffs = self.mr.changes()['changes'] diffs = self.mr.changes()['changes']
@ -56,8 +69,10 @@ class GitLabProvider(GitProvider):
elif diff['renamed_file']: elif diff['renamed_file']:
edit_type = EDIT_TYPE.RENAMED edit_type = EDIT_TYPE.RENAMED
try: try:
original_file_content_str = bytes.decode(original_file_content_str, 'utf-8') if isinstance(original_file_content_str, bytes):
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8') original_file_content_str = bytes.decode(original_file_content_str, 'utf-8')
if isinstance(new_file_content_str, bytes):
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
except UnicodeDecodeError: except UnicodeDecodeError:
logging.warning( logging.warning(
f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}") f"Cannot decode file {diff['old_path']} or {diff['new_path']} in merge request {self.id_mr}")
@ -84,25 +99,26 @@ class GitLabProvider(GitProvider):
self.diff_files = self.diff_files if self.diff_files else self.get_diff_files() self.diff_files = self.diff_files if self.diff_files else self.get_diff_files()
edit_type, found, source_line_no, target_file, target_line_no = self.search_line(relevant_file, edit_type, found, source_line_no, target_file, target_line_no = self.search_line(relevant_file,
relevant_line_in_file) relevant_line_in_file)
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
target_file, target_line_no)
def send_inline_comment(self, body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
target_file, target_line_no):
if not found: if not found:
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
else: else:
if edit_type == 'addition':
position = target_line_no - 1
else:
position = source_line_no - 1
d = self.last_diff d = self.last_diff
pos_obj = {'position_type': 'text', pos_obj = {'position_type': 'text',
'new_path': target_file.filename, 'new_path': target_file.filename,
'old_path': target_file.old_filename if target_file.old_filename else target_file.filename, 'old_path': target_file.old_filename if target_file.old_filename else target_file.filename,
'base_sha': d.base_commit_sha, 'start_sha': d.start_commit_sha, 'head_sha': d.head_commit_sha} 'base_sha': d.base_commit_sha, 'start_sha': d.start_commit_sha, 'head_sha': d.head_commit_sha}
if edit_type == 'deletion': if edit_type == 'deletion':
pos_obj['old_line'] = position pos_obj['old_line'] = source_line_no - 1
elif edit_type == 'addition': elif edit_type == 'addition':
pos_obj['new_line'] = position pos_obj['new_line'] = target_line_no - 1
else: else:
pos_obj['new_line'] = position pos_obj['new_line'] = target_line_no - 1
pos_obj['old_line'] = position pos_obj['old_line'] = source_line_no - 1
self.mr.discussions.create({'body': body, self.mr.discussions.create({'body': body,
'position': pos_obj}) 'position': pos_obj})
@ -110,47 +126,67 @@ class GitLabProvider(GitProvider):
relevant_file: str, relevant_file: str,
relevant_lines_start: int, relevant_lines_start: int,
relevant_lines_end: int): relevant_lines_end: int):
raise "not implemented yet for gitlab" self.diff_files = self.diff_files if self.diff_files else self.get_diff_files()
target_file = None
for file in self.diff_files:
if file.filename == relevant_file:
if file.filename == relevant_file:
target_file = file
break
range = relevant_lines_end - relevant_lines_start + 1
body = body.replace('```suggestion', f'```suggestion:-0+{range}')
lines = target_file.head_file.splitlines()
relevant_line_in_file = lines[relevant_lines_start - 1]
edit_type, found, source_line_no, target_file, target_line_no = self.find_in_file(target_file,
relevant_line_in_file)
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
target_file, target_line_no)
def search_line(self, relevant_file, relevant_line_in_file): def search_line(self, relevant_file, relevant_line_in_file):
RE_HUNK_HEADER = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
target_file = None target_file = None
source_line_no = 0
target_line_no = 0
found = False
edit_type = self.get_edit_type(relevant_line_in_file) edit_type = self.get_edit_type(relevant_line_in_file)
for file in self.diff_files: for file in self.diff_files:
if file.filename == relevant_file: if file.filename == relevant_file:
target_file = file edit_type, found, source_line_no, target_file, target_line_no = self.find_in_file(file,
patch = file.patch relevant_line_in_file)
patch_lines = patch.splitlines() return edit_type, found, source_line_no, target_file, target_line_no
for i, line in enumerate(patch_lines):
if line.startswith('@@'): def find_in_file(self, file, relevant_line_in_file):
match = RE_HUNK_HEADER.match(line) edit_type = 'context'
if not match: source_line_no = 0
continue target_line_no = 0
start_old, size_old, start_new, size_new, _ = match.groups() found = False
source_line_no = int(start_old) target_file = file
target_line_no = int(start_new) patch = file.patch
continue patch_lines = patch.splitlines()
if line.startswith('-'): for line in patch_lines:
source_line_no += 1 if line.startswith('@@'):
elif line.startswith('+'): match = self.RE_HUNK_HEADER.match(line)
target_line_no += 1 if not match:
elif line.startswith(' '): continue
source_line_no += 1 start_old, size_old, start_new, size_new, _ = match.groups()
target_line_no += 1 source_line_no = int(start_old)
if relevant_line_in_file in line: target_line_no = int(start_new)
found = True continue
edit_type = self.get_edit_type(line) if line.startswith('-'):
break source_line_no += 1
elif relevant_line_in_file[0] == '+' and relevant_line_in_file[1:] in line: elif line.startswith('+'):
# The model often adds a '+' to the beginning of the relevant_line_in_file even if originally target_line_no += 1
# it's a context line elif line.startswith(' '):
found = True source_line_no += 1
edit_type = self.get_edit_type(line) target_line_no += 1
break if relevant_line_in_file in line:
found = True
edit_type = self.get_edit_type(line)
break
elif relevant_line_in_file[0] == '+' and relevant_line_in_file[1:] in line:
# The model often adds a '+' to the beginning of the relevant_line_in_file even if originally
# it's a context line
found = True
edit_type = self.get_edit_type(line)
break
return edit_type, found, source_line_no, target_file, target_line_no return edit_type, found, source_line_no, target_file, target_line_no
def get_edit_type(self, relevant_line_in_file): def get_edit_type(self, relevant_line_in_file):
@ -171,9 +207,6 @@ class GitLabProvider(GitProvider):
def get_title(self): def get_title(self):
return self.mr.title return self.mr.title
def get_description(self):
return self.mr.description
def get_languages(self): def get_languages(self):
languages = self.gl.projects.get(self.id_project).languages() languages = self.gl.projects.get(self.id_project).languages()
return languages return languages
@ -184,6 +217,9 @@ class GitLabProvider(GitProvider):
def get_pr_description(self): def get_pr_description(self):
return self.mr.description return self.mr.description
def get_issue_comments(self):
raise NotImplementedError("GitLab provider does not support issue comments yet")
def _parse_merge_request_url(self, merge_request_url: str) -> Tuple[int, int]: def _parse_merge_request_url(self, merge_request_url: str) -> Tuple[int, int]:
parsed_url = urlparse(merge_request_url) parsed_url = urlparse(merge_request_url)

View File

@ -1,12 +1,9 @@
import asyncio import asyncio
import json import json
import os import os
import re
from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import settings from pr_agent.config_loader import settings
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer from pr_agent.tools.pr_reviewer import PRReviewer
@ -53,20 +50,7 @@ async def run_action():
pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None) pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None)
if pr_url: if pr_url:
body = comment_body.strip().lower() body = comment_body.strip().lower()
if any(cmd in body for cmd in ["/review", "/review_pr"]): await PRAgent().handle_request(pr_url, body)
await PRReviewer(pr_url).review()
elif any(cmd in body for cmd in ["/describe", "/describe_pr"]):
await PRDescription(pr_url).describe()
elif any(cmd in body for cmd in ["/improve", "/improve_code"]):
await PRCodeSuggestions(pr_url).suggest()
elif any(cmd in body for cmd in ["/ask", "/ask_question"]):
pattern = r'(/ask|/ask_question)\s*(.*)'
matches = re.findall(pattern, comment_body, re.IGNORECASE)
if matches:
question = matches[0][1]
await PRQuestions(pr_url, question).answer()
else:
print(f"Unknown command: {body}")
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,6 +1,5 @@
import asyncio import asyncio
import logging import logging
import re
import sys import sys
from datetime import datetime, timezone from datetime import datetime, timezone
@ -10,10 +9,6 @@ from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import settings from pr_agent.config_loader import settings
from pr_agent.git_providers import get_git_provider from pr_agent.git_providers import get_git_provider
from pr_agent.servers.help import bot_help_text from pr_agent.servers.help import bot_help_text
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
NOTIFICATION_URL = "https://api.github.com/notifications" NOTIFICATION_URL = "https://api.github.com/notifications"
@ -94,7 +89,7 @@ async def polling_loop():
success = await agent.handle_request(pr_url, rest_of_comment) success = await agent.handle_request(pr_url, rest_of_comment)
if not success: if not success:
git_provider.set_pr(pr_url) git_provider.set_pr(pr_url)
git_provider.publish_comment("### How to user PR-Agent\n" + git_provider.publish_comment("### How to use PR-Agent\n" +
bot_help_text(user_id)) bot_help_text(user_id))
elif response.status != 304: elif response.status != 304:
@ -103,5 +98,6 @@ async def polling_loop():
except Exception as e: except Exception as e:
logging.error(f"Exception during processing of a notification: {e}") logging.error(f"Exception during processing of a notification: {e}")
if __name__ == '__main__': if __name__ == '__main__':
asyncio.run(polling_loop()) asyncio.run(polling_loop())

View File

@ -1,14 +1,13 @@
commands_text = "> /review - Ask for a new review after your update the PR\n" \ commands_text = "> /review - Request a review of the latest update to the PR.\n" \
"> /describe - Modify the PR title and description based " \ "> /describe - Modify the PR title and description based on the contents of the PR.\n" \
"on the PR's contents.\n" \ "> /improve - Suggest improvements to the code in the PR. " \
"> /improve - Suggest improvements to the code in the PR as pull " \ "These will be provided as pull request comments, ready to commit.\n" \
"request comments ready to commit.\n" \ "> /ask <QUESTION> - Pose a question about the PR.\n"
"> /ask <QUESTION> - Ask a question about the PR.\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 = "> Add a comment to to invoke PR-Agent, use 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

@ -32,3 +32,6 @@ webhook_secret = "<WEBHOOK SECRET>" # Optional, may be commented out.
# Gitlab personal access token # Gitlab personal access token
personal_access_token = "" personal_access_token = ""
[bitbucket]
# Bitbucket personal bearer token
bearer_token = ""

View File

@ -1,7 +1,7 @@
[config] [config]
model="gpt-4-0613" model="gpt-4-0613"
git_provider="github" git_provider="github"
publish_review=true publish_output=true
verbosity_level=0 # 0,1,2 verbosity_level=0 # 0,1,2
[pr_reviewer] [pr_reviewer]
@ -10,6 +10,10 @@ require_tests_review=true
require_security_review=true require_security_review=true
num_code_suggestions=3 num_code_suggestions=3
inline_code_comments = true inline_code_comments = true
ask_and_reflect=false
[pr_description]
publish_description_as_comment=false
[pr_questions] [pr_questions]

View File

@ -1,12 +1,12 @@
[pr_code_suggestions_prompt] [pr_code_suggestions_prompt]
system="""You are a language model called CodiumAI-PR-Code-Reviewer. system="""You are a language model called CodiumAI-PR-Code-Reviewer.
Your task is to provide provide meaningfull non-trivial code suggestions to improve the new code in a PR (the '+' lines). Your task is to provide meaningfull non-trivial code suggestions to improve the new code in a PR (the '+' lines).
- Try to give important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningfull code improvements, like performance, vulnerability, modularity, and best practices. - Try to give important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningfull code improvements, like performance, vulnerability, modularity, and best practices.
- Suggestions should refer only to the 'new hunk' code, and focus on improving the new added code lines, with '+'. - Suggestions should refer only to the 'new hunk' code, and focus on improving the new added code lines, with '+'.
- Provide the exact line number range (inclusive) for each issue. - Provide the exact line number range (inclusive) for each issue.
- Assume there is additional code in the relevant file that is not included in the diff. - Assume there is additional code in the relevant file that is not included in the diff.
- Provide up to {{ num_code_suggestions }} code suggestions. - Provide up to {{ num_code_suggestions }} code suggestions.
- Make sure not to provide suggestion repeating modifications already implemented in the new PR code (the '+' lines). - Make sure not to provide suggestions repeating modifications already implemented in the new PR code (the '+' lines).
- Don't output line numbers in the 'improved code' snippets. - Don't output line numbers in the 'improved code' snippets.
You must use the following JSON schema to format your answer: You must use the following JSON schema to format your answer:

View File

@ -0,0 +1,34 @@
[pr_information_from_user_prompt]
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.
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.
Example output:
'
Questions to better understand the PR:
1) ...
2) ...
...
'
"""
user="""PR Info:
Title: '{{title}}'
Branch: '{{branch}}'
Description: '{{description}}'
{%- if language %}
Main language: {{language}}
{%- endif %}
The PR Git Diff:
```
{{diff}}
```
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions, and ' ' (a space) for unchanged lines
Response:
"""

View File

@ -1,9 +1,9 @@
[pr_questions_prompt] [pr_questions_prompt]
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests. 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 task is to answer questions about the new PR code (the '+' lines), and provide feedback.
Be informative, constructive, and give examples. Try to be as specific as possible, and don't avoid answering the questions. Be informative, constructive, and give examples. Try to be as specific as possible.
Don't avoid answering the questions. You must answer the questions, as best as you can, without adding unrelated content.
Make sure not to repeat modifications already implemented in the new PR code (the '+' lines). Make sure not to repeat modifications already implemented in the new PR code (the '+' lines).
Answer only the questions, and don't add unrelated content.
""" """
user="""PR Info: user="""PR Info:

View File

@ -2,8 +2,11 @@
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests. system="""You are CodiumAI-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 meaningfull code suggestions to improve the new PR code (the '+' lines). Your task is to provide constructive and concise feedback for the PR, and also provide meaningfull code suggestions to improve the new PR code (the '+' lines).
- Provide up to {{ num_code_suggestions }} code suggestions. - Provide up to {{ num_code_suggestions }} code suggestions.
{%- if num_code_suggestions > 0 %}
- Try to focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningfull code improvements, like performance, vulnerability, modularity, and best practices. - Try to focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningfull code improvements, like performance, vulnerability, modularity, and best practices.
- Make sure not to provide suggestion repeating modifications already implemented in the new PR code (the '+' lines). - Suggestions should focus on improving the new added code lines.
- Make sure not to provide suggestions repeating modifications already implemented in the new PR code (the '+' lines).
{%- endif %}
You must use the following JSON schema to format your answer: You must use the following JSON schema to format your answer:
```json ```json
@ -23,6 +26,12 @@ You must use the following JSON schema to format your answer:
"description": "yes\\no question: does this PR have relevant tests ?" "description": "yes\\no question: does this PR have relevant tests ?"
}, },
{%- endif %} {%- endif %}
{%- if question_str %}
"Insights from user's answer": {
"type": "string",
"description": "shortly summarize the insights you gained from the user's answers to the questions"
},
{%- endif %}
{%- if require_focused %} {%- if require_focused %}
"Focused PR": { "Focused PR": {
"type": "string", "type": "string",
@ -35,6 +44,7 @@ You must use the following JSON schema to format your answer:
"type": "string", "type": "string",
"description": "General suggestions and feedback for the contributors and maintainers of this PR. May include important suggestions for the overall structure, primary purpose, best practices, critical bugs, and other aspects of the PR. Explain your suggestions." "description": "General suggestions and feedback for the contributors and maintainers of this PR. May include important suggestions for the overall structure, primary purpose, best practices, critical bugs, and other aspects of the PR. Explain your suggestions."
}, },
{%- if num_code_suggestions > 0 %}
"Code suggestions": { "Code suggestions": {
"type": "array", "type": "array",
"maxItems": {{ num_code_suggestions }}, "maxItems": {{ num_code_suggestions }},
@ -54,6 +64,7 @@ You must use the following JSON schema to format your answer:
} }
} }
}, },
{%- endif %}
{%- if require_security %} {%- if require_security %}
"Security concerns": { "Security concerns": {
"type": "string", "type": "string",
@ -82,6 +93,7 @@ Example output:
"PR Feedback": "PR Feedback":
{ {
"General PR suggestions": "..., `xxx`...", "General PR suggestions": "..., `xxx`...",
{%- if num_code_suggestions > 0 %}
"Code suggestions": [ "Code suggestions": [
{ {
"relevant file": "directory/xxx.py", "relevant file": "directory/xxx.py",
@ -90,6 +102,7 @@ Example output:
}, },
... ...
] ]
{%- endif %}
{%- if require_security %}, {%- if require_security %},
"Security concerns": "No, because ..." "Security concerns": "No, because ..."
{%- endif %} {%- endif %}
@ -108,6 +121,16 @@ Description: '{{description}}'
Main language: {{language}} Main language: {{language}}
{%- endif %} {%- endif %}
{%- if question_str %}
######
Here are questions to better understand the PR. Use the answers to provide better feedback.
{{question_str|trim}}
User answers:
{{answer_str|trim}}
######
{%- endif %}
The PR Git Diff: The PR Git Diff:
``` ```

View File

@ -8,9 +8,9 @@ from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff from pr_agent.algo.pr_processing import get_pr_diff
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, try_fix_json from pr_agent.algo.utils import try_fix_json
from pr_agent.config_loader import settings from pr_agent.config_loader import settings
from pr_agent.git_providers import get_git_provider, GithubProvider from pr_agent.git_providers import BitbucketProvider, get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language from pr_agent.git_providers.git_provider import get_main_pr_language
@ -39,10 +39,10 @@ class PRCodeSuggestions:
settings.pr_code_suggestions_prompt.user) settings.pr_code_suggestions_prompt.user)
async def suggest(self): async def suggest(self):
assert type(self.git_provider) == GithubProvider, "Only Github is supported for now" assert type(self.git_provider) != BitbucketProvider, "Bitbucket is not supported for now"
logging.info('Generating code suggestions for PR...') logging.info('Generating code suggestions for PR...')
if settings.config.publish_review: if settings.config.publish_output:
self.git_provider.publish_comment("Preparing review...", is_temporary=True) self.git_provider.publish_comment("Preparing review...", is_temporary=True)
logging.info('Getting PR diff...') logging.info('Getting PR diff...')
@ -56,13 +56,12 @@ class PRCodeSuggestions:
self.prediction = await self._get_prediction() self.prediction = await self._get_prediction()
logging.info('Preparing PR review...') logging.info('Preparing PR review...')
data = self._prepare_pr_code_suggestions() data = self._prepare_pr_code_suggestions()
if settings.config.publish_review: if settings.config.publish_output:
logging.info('Pushing PR review...') logging.info('Pushing PR review...')
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
logging.info('Pushing inline code comments...') logging.info('Pushing inline code comments...')
self.push_inline_code_suggestions(data) self.push_inline_code_suggestions(data)
async def _get_prediction(self): async def _get_prediction(self):
variables = copy.deepcopy(self.vars) variables = copy.deepcopy(self.vars)
variables["diff"] = self.patches_diff # update diff variables["diff"] = self.patches_diff # update diff
@ -86,7 +85,7 @@ class PRCodeSuggestions:
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
if settings.config.verbosity_level >= 2: if settings.config.verbosity_level >= 2:
logging.info(f"Could not parse json response: {review}") logging.info(f"Could not parse json response: {review}")
data = try_fix_json(review) data = try_fix_json(review, code_suggestions=True)
return data return data
def push_inline_code_suggestions(self, data): def push_inline_code_suggestions(self, data):
@ -98,12 +97,12 @@ class PRCodeSuggestions:
relevant_lines_start = int(relevant_lines_str.split('-')[0]) # absolute position relevant_lines_start = int(relevant_lines_str.split('-')[0]) # absolute position
relevant_lines_end = int(relevant_lines_str.split('-')[-1]) relevant_lines_end = int(relevant_lines_str.split('-')[-1])
content = d['suggestion content'] content = d['suggestion content']
existing_code_snippet = d['existing code']
new_code_snippet = d['improved code'] new_code_snippet = d['improved code']
if new_code_snippet: if new_code_snippet:
try: # dedent code snippet try: # dedent code snippet
self.diff_files = self.git_provider.diff_files if self.git_provider.diff_files else self.git_provider.get_diff_files() self.diff_files = self.git_provider.diff_files if self.git_provider.diff_files \
else self.git_provider.get_diff_files()
original_initial_line = None original_initial_line = None
for file in self.diff_files: for file in self.diff_files:
if file.filename.strip() == relevant_file: if file.filename.strip() == relevant_file:
@ -121,7 +120,7 @@ class PRCodeSuggestions:
logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}") logging.info(f"Could not dedent code snippet for file {relevant_file}, error: {e}")
body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```" body = f"**Suggestion:** {content}\n```suggestion\n" + new_code_snippet + "\n```"
success = self.git_provider.publish_code_suggestion(body=body, self.git_provider.publish_code_suggestion(body=body,
relevant_file=relevant_file, 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)

View File

@ -7,7 +7,6 @@ from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff from pr_agent.algo.pr_processing import get_pr_diff
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
from pr_agent.config_loader import settings from pr_agent.config_loader import 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 get_main_pr_language from pr_agent.git_providers.git_provider import get_main_pr_language
@ -23,7 +22,7 @@ class PRDescription:
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(),
"description": self.git_provider.get_description(), "description": self.git_provider.get_pr_description(),
"language": self.main_pr_language, "language": self.main_pr_language,
"diff": "", # empty diff for initial calculation "diff": "", # empty diff for initial calculation
} }
@ -36,17 +35,20 @@ class PRDescription:
async def describe(self): async def describe(self):
logging.info('Generating a PR description...') logging.info('Generating a PR description...')
if settings.config.publish_review: if settings.config.publish_output:
self.git_provider.publish_comment("Preparing pr description...", is_temporary=True) self.git_provider.publish_comment("Preparing pr description...", is_temporary=True)
logging.info('Getting PR diff...') logging.info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
logging.info('Getting AI prediction...') logging.info('Getting AI prediction...')
self.prediction = await self._get_prediction() self.prediction = await self._get_prediction()
logging.info('Preparing answer...') logging.info('Preparing answer...')
pr_title, pr_body = self._prepare_pr_answer() pr_title, pr_body, markdown_text = self._prepare_pr_answer()
if settings.config.publish_review: if settings.config.publish_output:
logging.info('Pushing answer...') logging.info('Pushing answer...')
self.git_provider.publish_description(pr_title, pr_body) if settings.pr_description.publish_description_as_comment:
self.git_provider.publish_comment(markdown_text)
else:
self.git_provider.publish_description(pr_title, pr_body)
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
return "" return ""
@ -66,10 +68,11 @@ class PRDescription:
def _prepare_pr_answer(self): def _prepare_pr_answer(self):
data = json.loads(self.prediction) data = json.loads(self.prediction)
markdown_text = ""
for key, value in data.items():
markdown_text += f"## {key}\n\n"
markdown_text += f"{value}\n\n"
pr_body = "" pr_body = ""
# for key, value in data.items():
# markdown_text += f"## {key}\n\n"
# markdown_text += f"{value}\n\n"
title = data['PR Title'] title = data['PR Title']
del data['PR Title'] del data['PR Title']
for key, value in data.items(): for key, value in data.items():
@ -80,4 +83,4 @@ class PRDescription:
pr_body += f"**{value}**\n\n___\n" pr_body += f"**{value}**\n\n___\n"
if settings.config.verbosity_level >= 2: if settings.config.verbosity_level >= 2:
logging.info(f"title:\n{title}\n{pr_body}") logging.info(f"title:\n{title}\n{pr_body}")
return title, pr_body return title, pr_body, markdown_text

View File

@ -0,0 +1,71 @@
import copy
import logging
from jinja2 import Environment, StrictUndefined
from pr_agent.algo.ai_handler import AiHandler
from pr_agent.algo.pr_processing import get_pr_diff
from pr_agent.algo.token_handler import TokenHandler
from pr_agent.config_loader import settings
from pr_agent.git_providers import get_git_provider
from pr_agent.git_providers.git_provider import get_main_pr_language
class PRInformationFromUser:
def __init__(self, pr_url: str):
self.git_provider = get_git_provider()(pr_url)
self.main_pr_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files()
)
self.ai_handler = AiHandler()
self.vars = {
"title": self.git_provider.pr.title,
"branch": self.git_provider.get_pr_branch(),
"description": self.git_provider.get_pr_description(),
"language": self.main_pr_language,
"diff": "", # empty diff for initial calculation
}
self.token_handler = TokenHandler(self.git_provider.pr,
self.vars,
settings.pr_information_from_user_prompt.system,
settings.pr_information_from_user_prompt.user)
self.patches_diff = None
self.prediction = None
async def generate_questions(self):
logging.info('Generating question to the user...')
if settings.config.publish_output:
self.git_provider.publish_comment("Preparing questions...", is_temporary=True)
logging.info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
logging.info('Getting AI prediction...')
self.prediction = await self._get_prediction()
logging.info('Preparing questions...')
pr_comment = self._prepare_pr_answer()
if settings.config.publish_output:
logging.info('Pushing questions...')
self.git_provider.publish_comment(pr_comment)
self.git_provider.remove_initial_comment()
return ""
async def _get_prediction(self):
variables = copy.deepcopy(self.vars)
variables["diff"] = self.patches_diff # update diff
environment = Environment(undefined=StrictUndefined)
system_prompt = environment.from_string(settings.pr_information_from_user_prompt.system).render(variables)
user_prompt = environment.from_string(settings.pr_information_from_user_prompt.user).render(variables)
if settings.config.verbosity_level >= 2:
logging.info(f"\nSystem prompt:\n{system_prompt}")
logging.info(f"\nUser prompt:\n{user_prompt}")
model = settings.config.model
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
system=system_prompt, user=user_prompt)
return response
def _prepare_pr_answer(self) -> str:
model_output = self.prediction.strip()
if settings.config.verbosity_level >= 2:
logging.info(f"answer_str:\n{model_output}")
answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\
"\n>/answer\n>1) ...\n>2) ...\n>...\n"
return answer_str

View File

@ -22,7 +22,7 @@ class PRQuestions:
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(),
"description": self.git_provider.get_description(), "description": self.git_provider.get_pr_description(),
"language": self.main_pr_language, "language": self.main_pr_language,
"diff": "", # empty diff for initial calculation "diff": "", # empty diff for initial calculation
"questions": self.question_str, "questions": self.question_str,
@ -36,7 +36,7 @@ class PRQuestions:
async def answer(self): async def answer(self):
logging.info('Answering a PR question...') logging.info('Answering a PR question...')
if settings.config.publish_review: if settings.config.publish_output:
self.git_provider.publish_comment("Preparing answer...", is_temporary=True) self.git_provider.publish_comment("Preparing answer...", is_temporary=True)
logging.info('Getting PR diff...') logging.info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
@ -44,7 +44,7 @@ class PRQuestions:
self.prediction = await self._get_prediction() self.prediction = await self._get_prediction()
logging.info('Preparing answer...') logging.info('Preparing answer...')
pr_comment = self._prepare_pr_answer() pr_comment = self._prepare_pr_answer()
if settings.config.publish_review: if settings.config.publish_output:
logging.info('Pushing answer...') logging.info('Pushing answer...')
self.git_provider.publish_comment(pr_comment) self.git_provider.publish_comment(pr_comment)
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()

View File

@ -11,16 +11,20 @@ from pr_agent.algo.utils import convert_to_markdown, try_fix_json
from pr_agent.config_loader import settings from pr_agent.config_loader import 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 get_main_pr_language from pr_agent.git_providers.git_provider import get_main_pr_language
from pr_agent.servers.help import bot_help_text, actions_help_text from pr_agent.servers.help import actions_help_text, bot_help_text
class PRReviewer: class PRReviewer:
def __init__(self, pr_url: str, cli_mode=False): def __init__(self, pr_url: str, cli_mode=False, is_answer: bool = False):
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.is_answer = is_answer
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
raise Exception(f"Answer mode is not supported for {settings.config.git_provider} for now")
answer_str = question_str = self._get_user_answers()
self.ai_handler = AiHandler() self.ai_handler = AiHandler()
self.patches_diff = None self.patches_diff = None
self.prediction = None self.prediction = None
@ -35,6 +39,9 @@ class PRReviewer:
"require_security": settings.pr_reviewer.require_security_review, "require_security": settings.pr_reviewer.require_security_review,
"require_focused": settings.pr_reviewer.require_focused_review, "require_focused": settings.pr_reviewer.require_focused_review,
'num_code_suggestions': settings.pr_reviewer.num_code_suggestions, 'num_code_suggestions': settings.pr_reviewer.num_code_suggestions,
#
'question_str': question_str,
'answer_str': answer_str,
} }
self.token_handler = TokenHandler(self.git_provider.pr, self.token_handler = TokenHandler(self.git_provider.pr,
self.vars, self.vars,
@ -43,7 +50,7 @@ class PRReviewer:
async def review(self): async def review(self):
logging.info('Reviewing PR...') logging.info('Reviewing PR...')
if settings.config.publish_review: if settings.config.publish_output:
self.git_provider.publish_comment("Preparing review...", is_temporary=True) self.git_provider.publish_comment("Preparing review...", is_temporary=True)
logging.info('Getting PR diff...') logging.info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
@ -51,7 +58,7 @@ class PRReviewer:
self.prediction = await self._get_prediction() self.prediction = await self._get_prediction()
logging.info('Preparing PR review...') logging.info('Preparing PR review...')
pr_comment = self._prepare_pr_review() pr_comment = self._prepare_pr_review()
if settings.config.publish_review: if settings.config.publish_output:
logging.info('Pushing PR review...') logging.info('Pushing PR review...')
self.git_provider.publish_comment(pr_comment) self.git_provider.publish_comment(pr_comment)
self.git_provider.remove_initial_comment() self.git_provider.remove_initial_comment()
@ -89,7 +96,9 @@ class PRReviewer:
del data['PR Feedback']['Security concerns'] del data['PR Feedback']['Security concerns']
data['PR Analysis']['Security concerns'] = val data['PR Analysis']['Security concerns'] = val
if settings.config.git_provider == 'github' and settings.pr_reviewer.inline_code_comments: if settings.config.git_provider == 'github' and \
settings.pr_reviewer.inline_code_comments and \
'Code suggestions' in data['PR Feedback']:
del data['PR Feedback']['Code suggestions'] del data['PR Feedback']['Code suggestions']
markdown_text = convert_to_markdown(data) markdown_text = convert_to_markdown(data)
@ -107,15 +116,35 @@ class PRReviewer:
return markdown_text return markdown_text
def _publish_inline_code_comments(self): def _publish_inline_code_comments(self):
if settings.pr_reviewer.num_code_suggestions == 0:
return
review = self.prediction.strip() review = self.prediction.strip()
try: try:
data = json.loads(review) data = json.loads(review)
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
data = try_fix_json(review) data = try_fix_json(review)
for d in data['PR Feedback']['Code suggestions']: if settings.pr_reviewer.num_code_suggestions > 0:
relevant_file = d['relevant file'].strip() try:
relevant_line_in_file = d['relevant line in file'].strip() for d in data['PR Feedback']['Code suggestions']:
content = d['suggestion content'] relevant_file = d['relevant file'].strip()
relevant_line_in_file = d['relevant line in file'].strip()
content = d['suggestion content']
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
except KeyError:
pass
def _get_user_answers(self):
answer_str = question_str = ""
if self.is_answer:
discussion_messages = self.git_provider.get_issue_comments()
for message in discussion_messages.reversed:
if "Questions to better understand the PR:" in message.body:
question_str = message.body
elif '/answer' in message.body:
answer_str = message.body
if answer_str and question_str:
break
return question_str, answer_str

View File

@ -9,3 +9,4 @@ uvicorn==0.22.0
python-gitlab==3.15.0 python-gitlab==3.15.0
pytest~=7.4.0 pytest~=7.4.0
aiohttp~=3.8.4 aiohttp~=3.8.4
atlassian-python-api==3.39.0

View File

@ -1,13 +1,12 @@
# Generated by CodiumAI # Generated by CodiumAI
from pr_agent.algo.utils import try_fix_json from pr_agent.algo.utils import try_fix_json
import pytest
class TestTryFixJson: class TestTryFixJson:
# Tests that JSON with complete 'Code suggestions' section returns expected output # Tests that JSON with complete 'Code suggestions' section returns expected output
def test_incomplete_code_suggestions(self): def test_incomplete_code_suggestions(self):
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...' review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...' # noqa: E501
expected_output = { expected_output = {
'PR Analysis': { 'PR Analysis': {
'Main theme': 'xxx', 'Main theme': 'xxx',
@ -26,7 +25,7 @@ class TestTryFixJson:
assert try_fix_json(review) == expected_output assert try_fix_json(review) == expected_output
def test_incomplete_code_suggestions_new_line(self): def test_incomplete_code_suggestions_new_line(self):
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n\t, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...' review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n\t, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy [incomp...' # noqa: E501
expected_output = { expected_output = {
'PR Analysis': { 'PR Analysis': {
'Main theme': 'xxx', 'Main theme': 'xxx',
@ -45,7 +44,7 @@ class TestTryFixJson:
assert try_fix_json(review) == expected_output assert try_fix_json(review) == expected_output
def test_incomplete_code_suggestions_many_close_brackets(self): def test_incomplete_code_suggestions_many_close_brackets(self):
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy }, [}\n ,incomp.} ,..' review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"} \n, {"suggestion number": 2, "relevant file": "yyy.py", "suggestion content": "yyy }, [}\n ,incomp.} ,..' # noqa: E501
expected_output = { expected_output = {
'PR Analysis': { 'PR Analysis': {
'Main theme': 'xxx', 'Main theme': 'xxx',
@ -64,7 +63,7 @@ class TestTryFixJson:
assert try_fix_json(review) == expected_output assert try_fix_json(review) == expected_output
def test_incomplete_code_suggestions_relevant_file(self): def test_incomplete_code_suggestions_relevant_file(self):
review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.p' review = '{"PR Analysis": {"Main theme": "xxx", "Type of PR": "Bug fix"}, "PR Feedback": {"General PR suggestions": "..., `xxx`...", "Code suggestions": [{"relevant file": "xxx.py", "suggestion content": "xxx [important]"}, {"suggestion number": 2, "relevant file": "yyy.p' # noqa: E501
expected_output = { expected_output = {
'PR Analysis': { 'PR Analysis': {
'Main theme': 'xxx', 'Main theme': 'xxx',