Compare commits
9 Commits
ok/json_lo
...
pre_pr
Author | SHA1 | Date | |
---|---|---|---|
a81cbaa9bd | |||
e4e1eb6d6b | |||
3d4a062251 | |||
6378603cd8 | |||
588eb6e97f | |||
c5d05d53cd | |||
f6a48c4c8b | |||
f619d60a78 | |||
d51e7ee5ad |
3
.github/workflows/pr-agent-review.yaml
vendored
@ -24,7 +24,4 @@ jobs:
|
|||||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||||
OPENAI_ORG: ${{ secrets.OPENAI_ORG }} # optional
|
OPENAI_ORG: ${{ secrets.OPENAI_ORG }} # optional
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PINECONE.API_KEY: ${{ secrets.PINECONE_API_KEY }}
|
|
||||||
PINECONE.ENVIRONMENT: ${{ secrets.PINECONE_ENVIRONMENT }}
|
|
||||||
|
|
||||||
|
|
||||||
|
57
CONFIGURATION.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
## Configuration
|
||||||
|
|
||||||
|
The different tools and sub-tools used by CodiumAI PR-Agent are adjustable via the **[configuration file](pr_agent/settings/configuration.toml)**
|
||||||
|
|
||||||
|
### Working from CLI
|
||||||
|
When running from source (CLI), your local configuration file will be initially used.
|
||||||
|
|
||||||
|
Example for invoking the 'review' tools via the CLI:
|
||||||
|
|
||||||
|
```
|
||||||
|
python cli.py --pr-url=<pr_url> review
|
||||||
|
```
|
||||||
|
In addition to general configurations, the 'review' tool will use parameters from the `[pr_reviewer]` section (every tool has a dedicated section in the configuration file).
|
||||||
|
|
||||||
|
Note that you can print results locally, without publishing them, by setting in `configuration.toml`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[config]
|
||||||
|
publish_output=true
|
||||||
|
verbosity_level=2
|
||||||
|
```
|
||||||
|
This is useful for debugging or experimenting with the different tools.
|
||||||
|
|
||||||
|
### Working from pre-built repo (GitHub Action/GitHub App/Docker)
|
||||||
|
When running PR-Agent from a pre-built repo, the default configuration file will be loaded.
|
||||||
|
|
||||||
|
To edit the configuration, you have two options:
|
||||||
|
1. Place a local configuration file in the root of your local repo. The local file will be used instead of the default one.
|
||||||
|
2. For online usage, just add `--config_path=<value>` to you command, to edit a specific configuration value.
|
||||||
|
For example if you want to edit `pr_reviewer` configurations, you can run:
|
||||||
|
```
|
||||||
|
/review --pr_reviewer.extra_instructions="..." --pr_reviewer.require_score_review=false ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Any configuration value in `configuration.toml` file can be similarly edited.
|
||||||
|
|
||||||
|
### General configuration parameters
|
||||||
|
|
||||||
|
#### Changing a model
|
||||||
|
See [here](pr_agent/algo/__init__.py) for the list of available models.
|
||||||
|
|
||||||
|
To use Llama2 model, for example, set:
|
||||||
|
```
|
||||||
|
[config]
|
||||||
|
model = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
|
||||||
|
[replicate]
|
||||||
|
key = ...
|
||||||
|
```
|
||||||
|
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
|
||||||
|
|
||||||
|
Also review the [AiHandler](pr_agent/algo/ai_handler.py) file for instruction how to set keys for other models.
|
||||||
|
|
||||||
|
#### Extra instructions
|
||||||
|
All PR-Agent tools have a parameter called `extra_instructions`, that enables to add free-text extra instructions. Example usage:
|
||||||
|
```
|
||||||
|
/update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..."
|
||||||
|
```
|
@ -2,8 +2,7 @@ FROM python:3.10 as base
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD pyproject.toml .
|
ADD pyproject.toml .
|
||||||
ADD requirements.txt .
|
RUN pip install . && rm pyproject.toml
|
||||||
RUN pip install . && rm pyproject.toml requirements.txt
|
|
||||||
ENV PYTHONPATH=/app
|
ENV PYTHONPATH=/app
|
||||||
ADD pr_agent pr_agent
|
ADD pr_agent pr_agent
|
||||||
ADD github_action/entrypoint.sh /
|
ADD github_action/entrypoint.sh /
|
||||||
|
304
INSTALL.md
@ -4,69 +4,128 @@
|
|||||||
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\GitLab\BitBucket 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:
|
There are several ways to use PR-Agent:
|
||||||
|
|
||||||
**Locally**
|
- [Method 1: Use Docker image (no installation required)](INSTALL.md#method-1-use-docker-image-no-installation-required)
|
||||||
- [Using Docker image (no installation required)](INSTALL.md#use-docker-image-no-installation-required)
|
- [Method 2: Run as a GitHub Action](INSTALL.md#method-2-run-as-a-github-action)
|
||||||
- [Run from source](INSTALL.md#run-from-source)
|
- [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)
|
||||||
**GitHub specific methods**
|
- [Method 5: Run as a GitHub App](INSTALL.md#method-5-run-as-a-github-app)
|
||||||
- [Run as a GitHub Action](INSTALL.md#run-as-a-github-action)
|
- [Method 6: Deploy as a Lambda Function](INSTALL.md#method-6---deploy-as-a-lambda-function)
|
||||||
- [Run as a polling server](INSTALL.md#run-as-a-polling-server)
|
- [Method 7: AWS CodeCommit](INSTALL.md#method-7---aws-codecommit-setup)
|
||||||
- [Run as a GitHub App](INSTALL.md#run-as-a-github-app)
|
|
||||||
- [Deploy as a Lambda Function](INSTALL.md#deploy-as-a-lambda-function)
|
|
||||||
- [AWS CodeCommit](INSTALL.md#aws-codecommit-setup)
|
|
||||||
|
|
||||||
**GitLab specific methods**
|
|
||||||
- [Run a GitLab webhook server](INSTALL.md#run-a-gitlab-webhook-server)
|
|
||||||
|
|
||||||
**BitBucket specific methods**
|
|
||||||
- [Run as a Bitbucket Pipeline](INSTALL.md#run-as-a-bitbucket-pipeline)
|
|
||||||
- [Run on a hosted app](INSTALL.md#run-on-a-hosted-bitbucket-app)
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Use Docker image (no installation required)
|
### 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:
|
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:
|
||||||
|
|
||||||
For GitHub:
|
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:latest --pr_url <pr_url> review
|
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --pr_url <pr_url> review
|
||||||
```
|
|
||||||
For GitLab:
|
|
||||||
```
|
|
||||||
docker run --rm -it -e OPENAI.KEY=<your key> -e CONFIG.GIT_PROVIDER=gitlab -e GITLAB.PERSONAL_ACCESS_TOKEN=<your token> codiumai/pr-agent:latest --pr_url <pr_url> review
|
|
||||||
```
|
|
||||||
For BitBucket:
|
|
||||||
```
|
|
||||||
docker run --rm -it -e CONFIG.GIT_PROVIDER=bitbucket -e OPENAI.KEY=$OPENAI_API_KEY -e BITBUCKET.BEARER_TOKEN=$BITBUCKET_BEARER_TOKEN codiumai/pr-agent:latest --pr_url=<pr_url> review
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For other git providers, update CONFIG.GIT_PROVIDER accordingly, and check the `pr_agent/settings/.secrets_template.toml` file for the environment variables expected names and values.
|
2. To ask a question about a PR, run the following command:
|
||||||
|
|
||||||
|
|
||||||
Similarly, 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>"
|
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>"
|
||||||
```
|
```
|
||||||
|
Note: If you want to ensure you're running a specific version of the Docker image, consider using the image's digest.
|
||||||
|
The digest is a unique identifier for a specific version of an image. You can pull and run an image using its digest by referencing it like so: repository@sha256:digest. Always ensure you're using the correct and trusted digest for your operations.
|
||||||
|
|
||||||
A list of the relevant tools can be found in the [tools guide](./docs/TOOLS_GUIDE.md).
|
1. To request a review for a PR using a specific digest, run the following command:
|
||||||
|
|
||||||
|
|
||||||
Note: If you want to ensure you're running a specific version of the Docker image, consider using the image's digest:
|
|
||||||
```bash
|
```bash
|
||||||
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> review
|
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> review
|
||||||
```
|
```
|
||||||
in addition, you can run a [specific released versions](./RELEASE_NOTES.md) of pr-agent, for example:
|
|
||||||
|
2. To ask a question about a PR using the same digest, run the following command:
|
||||||
|
```bash
|
||||||
|
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent@sha256:71b5ee15df59c745d352d84752d01561ba64b6d51327f97d46152f0c58a5f678 --pr_url <pr_url> ask "<your question>"
|
||||||
```
|
```
|
||||||
codiumai/pr-agent@v0.8
|
|
||||||
|
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
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
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 }}
|
||||||
|
```
|
||||||
|
** if you want to pin your action to a specific commit for stability reasons
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
issue_comment:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr_agent_job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
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@<commit_sha>
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Run from source
|
### Method 3: Run from source
|
||||||
|
|
||||||
1. Clone this repository:
|
1. Clone this repository:
|
||||||
|
|
||||||
@ -92,92 +151,18 @@ chmod 600 pr_agent/settings/.secrets.toml
|
|||||||
|
|
||||||
```
|
```
|
||||||
export PYTHONPATH=[$PYTHONPATH:]<PATH to pr_agent folder>
|
export PYTHONPATH=[$PYTHONPATH:]<PATH to pr_agent folder>
|
||||||
python3 -m pr_agent.cli --pr_url <pr_url> review
|
python pr_agent/cli.py --pr_url <pr_url> review
|
||||||
python3 -m pr_agent.cli --pr_url <pr_url> ask <your question>
|
python pr_agent/cli.py --pr_url <pr_url> ask <your question>
|
||||||
python3 -m pr_agent.cli --pr_url <pr_url> describe
|
python pr_agent/cli.py --pr_url <pr_url> describe
|
||||||
python3 -m pr_agent.cli --pr_url <pr_url> improve
|
python pr_agent/cli.py --pr_url <pr_url> improve
|
||||||
python3 -m pr_agent.cli --pr_url <pr_url> add_docs
|
|
||||||
python3 -m pr_agent.cli --issue_url <issue_url> similar_issue
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Run as a GitHub Action
|
### Method 4: Run as a polling server
|
||||||
|
Request reviews by tagging your Github user on a PR
|
||||||
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
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
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 }}
|
|
||||||
```
|
|
||||||
** if you want to pin your action to a specific release (v0.7 for example) for stability reasons, use:
|
|
||||||
```yaml
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
issue_comment:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pr_agent_job:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
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@v0.7
|
|
||||||
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](pr_agent/settings/configuration.toml) 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
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Run as a polling server
|
|
||||||
Request reviews by tagging your GitHub user on a PR
|
|
||||||
|
|
||||||
Follow [steps 1-3](#run-as-a-github-action) of the GitHub Action setup.
|
|
||||||
|
|
||||||
|
Follow steps 1-3 of method 2.
|
||||||
Run the following command to start the server:
|
Run the following command to start the server:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -186,7 +171,7 @@ python pr_agent/servers/github_polling.py
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Run as a GitHub App
|
### Method 5: Run as a GitHub App
|
||||||
Allowing you to automate the review process on your private or public repositories.
|
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).
|
1. Create a GitHub App from the [Github Developer Portal](https://docs.github.com/en/developers/apps/creating-a-github-app).
|
||||||
@ -228,12 +213,12 @@ git clone https://github.com/Codium-ai/pr-agent.git
|
|||||||
- Copy your app's webhook secret to the webhook_secret field.
|
- Copy your app's webhook secret to the webhook_secret field.
|
||||||
- Set deployment_type to 'app' in [configuration.toml](./pr_agent/settings/configuration.toml)
|
- Set deployment_type to 'app' in [configuration.toml](./pr_agent/settings/configuration.toml)
|
||||||
|
|
||||||
> The .secrets.toml file is not copied to the Docker image by default, and is only used for local development.
|
> The .secrets.toml file is not copied to the Docker image by default, and is only used for local development.
|
||||||
> If you want to use the .secrets.toml file in your Docker image, you can add remove it from the .dockerignore file.
|
> If you want to use the .secrets.toml file in your Docker image, you can add remove it from the .dockerignore file.
|
||||||
> In most production environments, you would inject the secrets file as environment variables or as mounted volumes.
|
> In most production environments, you would inject the secrets file as environment variables or as mounted volumes.
|
||||||
> For example, in order to inject a secrets file as a volume in a Kubernetes environment you can update your pod spec to include the following,
|
> For example, in order to inject a secrets file as a volume in a Kubernetes environment you can update your pod spec to include the following,
|
||||||
> assuming you have a secret named `pr-agent-settings` with a key named `.secrets.toml`:
|
> assuming you have a secret named `pr-agent-settings` with a key named `.secrets.toml`:
|
||||||
```
|
```
|
||||||
volumes:
|
volumes:
|
||||||
- name: settings-volume
|
- name: settings-volume
|
||||||
secret:
|
secret:
|
||||||
@ -266,14 +251,11 @@ docker push codiumai/pr-agent:github_app # Push to your Docker repository
|
|||||||
|
|
||||||
9. Install the app by navigating to the "Install App" tab and selecting your desired repositories.
|
9. Install the app by navigating to the "Install App" tab and selecting your desired repositories.
|
||||||
|
|
||||||
> **Note:** When running PR-Agent from GitHub App, the default configuration file (configuration.toml) will be loaded.<br>
|
|
||||||
> However, you can override the default tool parameters by uploading a local configuration file `.pr_agent.toml`<br>
|
|
||||||
> For more information please check out the [USAGE GUIDE](./Usage.md#working-with-github-app)
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Deploy as a Lambda Function
|
### Method 6 - Deploy as a Lambda Function
|
||||||
|
|
||||||
1. Follow steps 1-5 of [Method 5](#run-as-a-github-app).
|
1. Follow steps 1-5 of [Method 5](#method-5-run-as-a-github-app).
|
||||||
2. Build a docker image that can be used as a lambda function
|
2. Build a docker image that can be used as a lambda function
|
||||||
```shell
|
```shell
|
||||||
docker buildx build --platform=linux/amd64 . -t codiumai/pr-agent:serverless -f docker/Dockerfile.lambda
|
docker buildx build --platform=linux/amd64 . -t codiumai/pr-agent:serverless -f docker/Dockerfile.lambda
|
||||||
@ -285,12 +267,12 @@ docker push codiumai/pr-agent:github_app # Push to your Docker repository
|
|||||||
```
|
```
|
||||||
4. Create a lambda function that uses the uploaded image. Set the lambda timeout to be at least 3m.
|
4. Create a lambda function that uses the uploaded image. Set the lambda timeout to be at least 3m.
|
||||||
5. Configure the lambda function to have a Function URL.
|
5. Configure the lambda function to have a Function URL.
|
||||||
6. Go back to steps 8-9 of [Method 5](#run-as-a-github-app) with the function url as your Webhook URL.
|
6. Go back to steps 8-9 of [Method 5](#method-5-run-as-a-github-app) with the function url as your Webhook URL.
|
||||||
The Webhook URL would look like `https://<LAMBDA_FUNCTION_URL>/api/v1/github_webhooks`
|
The Webhook URL would look like `https://<LAMBDA_FUNCTION_URL>/api/v1/github_webhooks`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### AWS CodeCommit Setup
|
### Method 7 - AWS CodeCommit Setup
|
||||||
|
|
||||||
Not all features have been added to CodeCommit yet. As of right now, CodeCommit has been implemented to run the pr-agent CLI on the command line, using AWS credentials stored in environment variables. (More features will be added in the future.) The following is a set of instructions to have pr-agent do a review of your CodeCommit pull request from the command line:
|
Not all features have been added to CodeCommit yet. As of right now, CodeCommit has been implemented to run the pr-agent CLI on the command line, using AWS credentials stored in environment variables. (More features will be added in the future.) The following is a set of instructions to have pr-agent do a review of your CodeCommit pull request from the command line:
|
||||||
|
|
||||||
@ -327,9 +309,7 @@ Example IAM permissions to that user to allow access to CodeCommit:
|
|||||||
"codecommit:Get*",
|
"codecommit:Get*",
|
||||||
"codecommit:List*",
|
"codecommit:List*",
|
||||||
"codecommit:PostComment*",
|
"codecommit:PostComment*",
|
||||||
"codecommit:PutCommentReaction",
|
"codecommit:PutCommentReaction"
|
||||||
"codecommit:UpdatePullRequestDescription",
|
|
||||||
"codecommit:UpdatePullRequestTitle"
|
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
}
|
}
|
||||||
@ -358,57 +338,9 @@ PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
|
|||||||
review
|
review
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### Appendix - **Debugging LLM API Calls**
|
||||||
|
If you're testing your codium/pr-agent server, and need to see if calls were made successfully + the exact call logs, you can use the [LiteLLM Debugger tool](https://docs.litellm.ai/docs/debugging/hosted_debugging).
|
||||||
|
|
||||||
### Run a GitLab webhook server
|
You can do this by setting `litellm_debugger=true` in configuration.toml. Your Logs will be viewable in real-time @ `admin.litellm.ai/<your_email>`. Set your email in the `.secrets.toml` under 'user_email'.
|
||||||
|
|
||||||
1. From the GitLab workspace or group, create an access token. Enable the "api" scope only.
|
<img src="./pics/debugger.png" width="800"/>
|
||||||
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. Follow the instructions to build the Docker image, setup a secrets file and deploy on your own server from [Method 5](#method-5-run-as-a-github-app) steps 4-7.
|
|
||||||
4. In the secrets file, fill in the following:
|
|
||||||
- Your OpenAI key.
|
|
||||||
- In the [gitlab] section, fill in personal_access_token and shared_secret. The access token can be a personal access token, or a group or project access token.
|
|
||||||
- Set deployment_type to 'gitlab' in [configuration.toml](./pr_agent/settings/configuration.toml)
|
|
||||||
5. Create a webhook in GitLab. Set the URL to the URL of your app's server. Set the secret token to the generated secret from step 2.
|
|
||||||
In the "Trigger" section, check the ‘comments’ and ‘merge request events’ boxes.
|
|
||||||
6. Test your installation by opening a merge request or commenting or a merge request using one of CodiumAI's commands.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Run as a Bitbucket Pipeline
|
|
||||||
|
|
||||||
|
|
||||||
You can use the Bitbucket Pipeline system to run PR-Agent on every pull request open or update.
|
|
||||||
|
|
||||||
1. Add the following file in your repository bitbucket_pipelines.yml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
pipelines:
|
|
||||||
pull-requests:
|
|
||||||
'**':
|
|
||||||
- step:
|
|
||||||
name: PR Agent Review
|
|
||||||
image: python:3.10
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
script:
|
|
||||||
- docker run -e CONFIG.GIT_PROVIDER=bitbucket -e OPENAI.KEY=$OPENAI_API_KEY -e BITBUCKET.BEARER_TOKEN=$BITBUCKET_BEARER_TOKEN codiumai/pr-agent:latest --pr_url=https://bitbucket.org/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pull-requests/$BITBUCKET_PR_ID review
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Add the following secure variables to your repository under Repository settings > Pipelines > Repository variables.
|
|
||||||
OPENAI_API_KEY: <your key>
|
|
||||||
BITBUCKET_BEARER_TOKEN: <your token>
|
|
||||||
|
|
||||||
You can get a Bitbucket token for your repository by following Repository Settings -> Security -> Access Tokens.
|
|
||||||
|
|
||||||
|
|
||||||
### Run on a hosted Bitbucket app
|
|
||||||
|
|
||||||
Please contact <support@codium.ai> if you're interested in a hosted BitBucket app solution that provides full functionality including PR reviews and comment handling. It's based on the [bitbucket_app.py](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/git_providers/bitbucket_provider.py) implmentation.
|
|
||||||
|
|
||||||
|
|
||||||
=======
|
|
1
MANIFEST.in
Normal file
@ -0,0 +1 @@
|
|||||||
|
recursive-include pr_agent/settings/ *.toml
|
@ -1,4 +1,4 @@
|
|||||||
# PR Compression Strategy
|
# Git Patch Logic
|
||||||
There are two scenarios:
|
There are two scenarios:
|
||||||
1. The PR is small enough to fit in a single prompt (including system and user prompt)
|
1. The PR is small enough to fit in a single prompt (including system and user prompt)
|
||||||
2. The PR is too large to fit in a single prompt (including system and user prompt)
|
2. The PR is too large to fit in a single prompt (including system and user prompt)
|
||||||
@ -16,7 +16,7 @@ We prioritize the languages of the repo based on the following criteria:
|
|||||||
## Small PR
|
## Small PR
|
||||||
In this case, we can fit the entire PR in a single prompt:
|
In this case, we can fit the entire PR in a single prompt:
|
||||||
1. Exclude binary files and non code files (e.g. images, pdfs, etc)
|
1. Exclude binary files and non code files (e.g. images, pdfs, etc)
|
||||||
2. We Expand the surrounding context of each patch to 3 lines above and below the patch
|
2. We Expand the surrounding context of each patch to 6 lines above and below the patch
|
||||||
## Large PR
|
## Large PR
|
||||||
|
|
||||||
### Motivation
|
### Motivation
|
||||||
@ -25,7 +25,7 @@ We want to be able to pack as much information as possible in a single LMM promp
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Compression strategy
|
#### PR compression strategy
|
||||||
We prioritize additions over deletions:
|
We prioritize additions over deletions:
|
||||||
- Combine all deleted files into a single list (`deleted files`)
|
- Combine all deleted files into a single list (`deleted files`)
|
||||||
- File patches are a list of hunks, remove all hunks of type deletion-only from the hunks in the file patch
|
- File patches are a list of hunks, remove all hunks of type deletion-only from the hunks in the file patch
|
||||||
|
187
README.md
@ -9,36 +9,25 @@ Making pull requests less painful with an AI agent
|
|||||||
|
|
||||||
[](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
|
[](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
|
||||||
[](https://discord.com/channels/1057273017547378788/1126104260430528613)
|
[](https://discord.com/channels/1057273017547378788/1126104260430528613)
|
||||||
[](https://twitter.com/codiumai)
|
|
||||||
<a href="https://github.com/Codium-ai/pr-agent/commits/main">
|
<a href="https://github.com/Codium-ai/pr-agent/commits/main">
|
||||||
<img alt="GitHub" src="https://img.shields.io/github/last-commit/Codium-ai/pr-agent/main?style=for-the-badge" height="20">
|
<img alt="GitHub" src="https://img.shields.io/github/last-commit/Codium-ai/pr-agent/main?style=for-the-badge" height="20">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:left;">
|
<div style="text-align:left;">
|
||||||
|
|
||||||
CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull requests faster and more efficiently. It automatically analyzes the pull request and can provide several types of commands:
|
CodiumAI `PR-Agent` is an open-source tool aiming to help developers review pull requests faster and more efficiently. It automatically analyzes the pull request and can provide several types of PR feedback:
|
||||||
|
|
||||||
‣ **Auto Description ([`/describe`](./docs/DESCRIBE.md))**: Automatically generating PR description - title, type, summary, code walkthrough and labels.
|
**Auto-Description**: Automatically generating [PR description](https://github.com/Codium-ai/pr-agent/pull/229#issue-1860711415) - title, type, summary, code walkthrough and labels.
|
||||||
\
|
\
|
||||||
‣ **Auto Review ([`/review`](./docs/REVIEW.md))**: Adjustable feedback about the PR main theme, type, relevant tests, security issues, score, and various suggestions for the PR content.
|
**Auto Review**: [Adjustable feedback](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695022908) about the PR main theme, type, relevant tests, security issues, score, and various suggestions for the PR content.
|
||||||
\
|
\
|
||||||
‣ **Question Answering ([`/ask ...`](./docs/ASK.md))**: Answering free-text questions about the PR.
|
**Question Answering**: Answering [free-text questions](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021332) about the PR.
|
||||||
\
|
\
|
||||||
‣ **Code Suggestions ([`/improve`](./docs/IMPROVE.md))**: Committable code suggestions for improving the PR.
|
**Code Suggestions**: [Committable code suggestions](https://github.com/Codium-ai/pr-agent/pull/229#discussion_r1306919276) for improving the PR.
|
||||||
\
|
\
|
||||||
‣ **Update Changelog ([`/update_changelog`](./docs/UPDATE_CHANGELOG.md))**: Automatically updating the CHANGELOG.md file with the PR changes.
|
**Update Changelog**: Automatically updating the CHANGELOG.md file with the [PR changes](https://github.com/Codium-ai/pr-agent/pull/168#discussion_r1282077645).
|
||||||
\
|
|
||||||
‣ **Find Similar Issue ([`/similar_issue`](./docs/SIMILAR_ISSUE.md))**: Automatically retrieves and presents similar issues
|
|
||||||
\
|
|
||||||
‣ **Add Documentation ([`/add_docs`](./docs/ADD_DOCUMENTATION.md))**: Automatically adds documentation to un-documented functions/classes in the PR.
|
|
||||||
|
|
||||||
See the [Installation Guide](./INSTALL.md) for instructions how to install and run the tool on different platforms.
|
<h3>Example results:</h2>
|
||||||
|
|
||||||
See the [Usage Guide](./Usage.md) for instructions how to run the different tools from _CLI_, _online usage_, or by _automatically triggering_ them when a new PR is opened.
|
|
||||||
|
|
||||||
See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the different tools.
|
|
||||||
|
|
||||||
<h3>Example results:</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1687561986">/describe:</a></h4>
|
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1687561986">/describe:</a></h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
@ -46,103 +35,87 @@ See the [Tools Guide](./docs/TOOLS_GUIDE.md) for detailed description of the dif
|
|||||||
<img src="https://www.codium.ai/images/describe-2.gif" width="800">
|
<img src="https://www.codium.ai/images/describe-2.gif" width="800">
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901">/review:</a></h4>
|
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901">/review:</a></h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img src="https://www.codium.ai/images/review-2.gif" width="800">
|
<img src="https://www.codium.ai/images/review-2.gif" width="800">
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/78#issuecomment-1639739496">/reflect_and_review:</a></h4>
|
||||||
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/78#issuecomment-1639739496">/reflect_and_review:</a></h4>)
|
<div align="center">
|
||||||
|
<p float="center">
|
||||||
[//]: # (<div align="center">)
|
<img src="https://www.codium.ai/images/reflect_and_review.gif" width="800">
|
||||||
|
</p>
|
||||||
[//]: # (<p float="center">)
|
</div>
|
||||||
|
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695020538">/ask:</a></h4>
|
||||||
[//]: # (<img src="https://www.codium.ai/images/reflect_and_review.gif" width="800">)
|
<div align="center">
|
||||||
|
<p float="center">
|
||||||
[//]: # (</p>)
|
<img src="https://www.codium.ai/images/ask-2.gif" width="800">
|
||||||
|
</p>
|
||||||
[//]: # (</div>)
|
</div>
|
||||||
|
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695024952">/improve:</a></h4>
|
||||||
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695020538">/ask:</a></h4>)
|
<div align="center">
|
||||||
|
<p float="center">
|
||||||
[//]: # (<div align="center">)
|
<img src="https://www.codium.ai/images/improve-2.gif" width="800">
|
||||||
|
</p>
|
||||||
[//]: # (<p float="center">)
|
</div>
|
||||||
|
|
||||||
[//]: # (<img src="https://www.codium.ai/images/ask-2.gif" width="800">)
|
|
||||||
|
|
||||||
[//]: # (</p>)
|
|
||||||
|
|
||||||
[//]: # (</div>)
|
|
||||||
|
|
||||||
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695024952">/improve:</a></h4>)
|
|
||||||
|
|
||||||
[//]: # (<div align="center">)
|
|
||||||
|
|
||||||
[//]: # (<p float="center">)
|
|
||||||
|
|
||||||
[//]: # (<img src="https://www.codium.ai/images/improve-2.gif" width="800">)
|
|
||||||
|
|
||||||
[//]: # (</p>)
|
|
||||||
|
|
||||||
[//]: # (</div>)
|
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Try it now](#try-it-now)
|
- [Try it now](#try-it-now)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
|
- [Configuration](./CONFIGURATION.md)
|
||||||
- [How it works](#how-it-works)
|
- [How it works](#how-it-works)
|
||||||
- [Why use PR-Agent?](#why-use-pr-agent)
|
- [Why use PR-Agent](#why-use-pr-agent)
|
||||||
- [Roadmap](#roadmap)
|
- [Roadmap](#roadmap)
|
||||||
|
- [Similar projects](#similar-projects)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
## 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 | CodeCommit | Azure DevOps | Gerrit |
|
| | | GitHub | Gitlab | Bitbucket | CodeCommit |
|
||||||
|-------|---------------------------------------------|:------:|:------:|:---------:|:----------:|:----------:|:----------:|
|
|-------|---------------------------------------------|:------:|:------:|:---------:|:----------:|
|
||||||
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| TOOLS | Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| | ⮑ Incremental | :white_check_mark: | | | | | |
|
| | ⮑ Inline review | :white_check_mark: | :white_check_mark: | | |
|
||||||
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| | Ask | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| | Auto-Description | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| | Auto-Description | :white_check_mark: | :white_check_mark: | | |
|
||||||
| | Improve Code | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
|
| | Improve Code | :white_check_mark: | :white_check_mark: | | |
|
||||||
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
|
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | | |
|
||||||
| | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: |
|
| | Reflect and Review | :white_check_mark: | | | |
|
||||||
| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | |
|
| | Update CHANGELOG.md | :white_check_mark: | | | |
|
||||||
| | Find similar issue | :white_check_mark: | | | | | |
|
| | | | | | |
|
||||||
| | Add Documentation | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
|
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| | | | | | | |
|
| | App / webhook | :white_check_mark: | :white_check_mark: | | |
|
||||||
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| | Tagging bot | :white_check_mark: | | | |
|
||||||
| | App / webhook | :white_check_mark: | :white_check_mark: | | | |
|
| | Actions | :white_check_mark: | | | |
|
||||||
| | Tagging bot | :white_check_mark: | | | | |
|
| | | | | | |
|
||||||
| | Actions | :white_check_mark: | | | | |
|
| CORE | PR compression | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
|
||||||
| | Web server | | | | | | :white_check_mark: |
|
| | Repo language prioritization | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
|
||||||
| | | | | | | |
|
| | Adaptive and token-aware<br />file patch fitting | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
|
||||||
| CORE | PR compression | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| | Multiple models support | :white_check_mark: | :white_check_mark: | :white_check_mark: | |
|
||||||
| | Repo language prioritization | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| | Incremental PR Review | :white_check_mark: | | | |
|
||||||
| | Adaptive and token-aware<br />file patch fitting | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
||||||
| | Multiple models support | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
||||||
| | Incremental PR Review | :white_check_mark: | | | | | |
|
|
||||||
|
|
||||||
Review the [usage guide](./Usage.md) section for detailed instructions how to use the different tools, select the relevant git provider (GitHub, Gitlab, Bitbucket,...), and adjust the configuration file to your needs.
|
Examples for invoking the different tools via the CLI:
|
||||||
|
- **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
|
||||||
|
- **Update Changelog**: python cli.py --pr_url=<pr_url> update_changelog
|
||||||
|
|
||||||
|
"<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.
|
||||||
|
|
||||||
## Try it now
|
## Try it now
|
||||||
|
|
||||||
You can try GPT-4 powered PR-Agent, on your public GitHub repository, instantly. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment. The agent will generate a response based on your command.
|
Try GPT-4 powered PR-Agent on your public GitHub repository for free. Just mention `@CodiumAI-Agent` and add the desired command in any PR comment! The agent will generate a response based on your command.
|
||||||
For example, add a comment to any pull request with the following text:
|
|
||||||
```
|
|
||||||
@CodiumAI-Agent /review
|
|
||||||
```
|
|
||||||
and the agent will respond with a review of your PR
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
To set up your own PR-Agent, see the [Installation](#installation) section
|
||||||
To set up your own PR-Agent, see the [Installation](#installation) section below.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -156,22 +129,20 @@ To get started with PR-Agent quickly, you first need to acquire two tokens:
|
|||||||
There are several ways to use PR-Agent:
|
There are several ways to use PR-Agent:
|
||||||
|
|
||||||
- [Method 1: Use Docker image (no installation required)](INSTALL.md#method-1-use-docker-image-no-installation-required)
|
- [Method 1: Use Docker image (no installation required)](INSTALL.md#method-1-use-docker-image-no-installation-required)
|
||||||
- [Method 2: Run from source](INSTALL.md#method-2-run-from-source)
|
- [Method 2: Run as a GitHub Action](INSTALL.md#method-2-run-as-a-github-action)
|
||||||
- [Method 3: Run as a GitHub Action](INSTALL.md#method-3-run-as-a-github-action)
|
- [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)
|
- [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
|
- Request reviews by tagging your GitHub user on a PR
|
||||||
- [Method 5: Run as a GitHub App](INSTALL.md#method-5-run-as-a-github-app)
|
- [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
|
- Allowing you to automate the review process on your private or public repositories
|
||||||
- [Method 6: Deploy as a Lambda Function](INSTALL.md#method-6---deploy-as-a-lambda-function)
|
- [Method 6: Deploy as a Lambda Function](INSTALL.md#method-6---deploy-as-a-lambda-function)
|
||||||
- [Method 7: AWS CodeCommit](INSTALL.md#method-7---aws-codecommit-setup)
|
- [Method 7: AWS CodeCommit](INSTALL.md#method-7---aws-codecommit-setup)
|
||||||
- [Method 8: Run a GitLab webhook server](INSTALL.md#method-8---run-a-gitlab-webhook-server)
|
|
||||||
- [Method 9: Run as a Bitbucket Pipeline](INSTALL.md#method-9-run-as-a-bitbucket-pipeline)
|
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
The following diagram illustrates PR-Agent tools and their flow:
|
The following diagram illustrates PR-Agent tools and their flow:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@ -183,7 +154,7 @@ Here are some advantages of PR-Agent:
|
|||||||
|
|
||||||
- We emphasize **real-life practical usage**. Each tool (review, improve, ask, ...) has a single GPT-4 call, no more. We feel that this is critical for realistic team usage - obtaining an answer quickly (~30 seconds) and affordably.
|
- We emphasize **real-life practical usage**. Each tool (review, improve, ask, ...) has a single GPT-4 call, no more. We feel that this is critical for realistic team usage - obtaining an answer quickly (~30 seconds) and affordably.
|
||||||
- Our [PR Compression strategy](./PR_COMPRESSION.md) is a core ability that enables to effectively tackle both short and long PRs.
|
- Our [PR Compression strategy](./PR_COMPRESSION.md) is a core ability that enables to effectively tackle both short and long PRs.
|
||||||
- Our JSON prompting strategy enables to have **modular, customizable tools**. For example, the '/review' tool categories can be controlled via the [configuration](pr_agent/settings/configuration.toml) file. Adding additional categories is easy and accessible.
|
- Our JSON prompting strategy enables to have **modular, customizable tools**. For example, the '/review' tool categories can be controlled via the [configuration](./CONFIGURATION.md) file. Adding additional categories is easy and accessible.
|
||||||
- We support **multiple git providers** (GitHub, Gitlab, Bitbucket, CodeCommit), **multiple ways** to use the tool (CLI, GitHub Action, GitHub App, Docker, ...), and **multiple models** (GPT-4, GPT-3.5, Anthropic, Cohere, Llama2).
|
- We support **multiple git providers** (GitHub, Gitlab, Bitbucket, CodeCommit), **multiple ways** to use the tool (CLI, GitHub Action, GitHub App, Docker, ...), and **multiple models** (GPT-4, GPT-3.5, Anthropic, Cohere, Llama2).
|
||||||
- We are open-source, and welcome contributions from the community.
|
- We are open-source, and welcome contributions from the community.
|
||||||
|
|
||||||
@ -193,7 +164,7 @@ Here are some advantages of PR-Agent:
|
|||||||
- [x] Support additional models, as a replacement for OpenAI (see [here](https://github.com/Codium-ai/pr-agent/pull/172))
|
- [x] Support additional models, as a replacement for OpenAI (see [here](https://github.com/Codium-ai/pr-agent/pull/172))
|
||||||
- [x] Develop additional logic for handling large PRs (see [here](https://github.com/Codium-ai/pr-agent/pull/229))
|
- [x] Develop additional logic for handling large PRs (see [here](https://github.com/Codium-ai/pr-agent/pull/229))
|
||||||
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
|
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
|
||||||
- [x] PR-Agent for issues
|
- [ ] PR-Agent for issues, and just for pull requests
|
||||||
- [ ] Adding more tools. Possible directions:
|
- [ ] Adding more tools. Possible directions:
|
||||||
- [x] PR description
|
- [x] PR description
|
||||||
- [x] Inline code suggestions
|
- [x] Inline code suggestions
|
||||||
@ -201,31 +172,13 @@ Here are some advantages of PR-Agent:
|
|||||||
- [x] Rank the PR (see [here](https://github.com/Codium-ai/pr-agent/pull/89))
|
- [x] Rank the PR (see [here](https://github.com/Codium-ai/pr-agent/pull/89))
|
||||||
- [ ] Enforcing CONTRIBUTING.md guidelines
|
- [ ] Enforcing CONTRIBUTING.md guidelines
|
||||||
- [ ] Performance (are there any performance issues)
|
- [ ] Performance (are there any performance issues)
|
||||||
- [x] Documentation (is the PR properly documented)
|
- [ ] Documentation (is the PR properly documented)
|
||||||
- [ ] ...
|
- [ ] ...
|
||||||
|
|
||||||
See the [Release notes](./RELEASE_NOTES.md) for updates on the latest changes.
|
|
||||||
|
|
||||||
|
|
||||||
## Similar Projects
|
## Similar Projects
|
||||||
|
|
||||||
- [CodiumAI - Meaningful tests for busy devs](https://github.com/Codium-ai/codiumai-vscode-release) (although various capabilities are much more advanced in the CodiumAI IDE plugins)
|
- [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)
|
||||||
- [openai-pr-reviewer](https://github.com/coderabbitai/openai-pr-reviewer)
|
- [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)
|
||||||
|
|
||||||
## Data Privacy
|
|
||||||
|
|
||||||
If you use self-host PR-Agent, e.g. via CLI running on your computer, with your OpenAI API key, it is between you and OpenAI. You can read their API data privacy policy here:
|
|
||||||
https://openai.com/enterprise-privacy
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
[](https://discord.gg/kG35uSHDBc)
|
|
||||||
|
|
||||||
- Discord community: https://discord.gg/kG35uSHDBc
|
|
||||||
- CodiumAI site: https://codium.ai
|
|
||||||
- Blog: https://www.codium.ai/blog/
|
|
||||||
- Troubleshooting: https://www.codium.ai/blog/technical-faq-and-troubleshooting/
|
|
||||||
- Support: support@codium.ai
|
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
## [Version 0.8] - 2023-09-27
|
|
||||||
- codiumai/pr-agent:0.8
|
|
||||||
- codiumai/pr-agent:0.8-github_app
|
|
||||||
- codiumai/pr-agent:0.8-bitbucket-app
|
|
||||||
- codiumai/pr-agent:0.8-gitlab_webhook
|
|
||||||
- codiumai/pr-agent:0.8-github_polling
|
|
||||||
- codiumai/pr-agent:0.8-github_action
|
|
||||||
|
|
||||||
### Added::Algo
|
|
||||||
- GitHub Action: Can control which tools will run automatically when a new PR is created. (see usage guide: https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
|
|
||||||
- Code suggestion tool: Will try to avoid an 'add comments' suggestion (see https://github.com/Codium-ai/pr-agent/pull/327)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Gitlab: Fixed a bug of improper usage of pr_id
|
|
||||||
|
|
||||||
|
|
||||||
## [Version 0.7] - 2023-09-20
|
|
||||||
|
|
||||||
### Docker Tags
|
|
||||||
- codiumai/pr-agent:0.7
|
|
||||||
- codiumai/pr-agent:0.7-github_app
|
|
||||||
- codiumai/pr-agent:0.7-bitbucket-app
|
|
||||||
- codiumai/pr-agent:0.7-gitlab_webhook
|
|
||||||
- codiumai/pr-agent:0.7-github_polling
|
|
||||||
- codiumai/pr-agent:0.7-github_action
|
|
||||||
|
|
||||||
### Added::Algo
|
|
||||||
- New tool /similar_issue - Currently on GitHub app and CLI: indexes the issues in the repo, find the most similar issues to the target issue.
|
|
||||||
- Describe markers: Empower the /describe tool with a templating capability (see more details in https://github.com/Codium-ai/pr-agent/pull/273).
|
|
||||||
- New feature in the /review tool - added an estimated effort estimation to the review (https://github.com/Codium-ai/pr-agent/pull/306).
|
|
||||||
|
|
||||||
### Added::Infrastructure
|
|
||||||
- Implementation of a GitLab webhook.
|
|
||||||
- Implementation of a BitBucket app.
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
- Protection against no code suggestions generated.
|
|
||||||
- Resilience to repositories where the languages cannot be automatically detected.
|
|
318
Usage.md
@ -1,318 +0,0 @@
|
|||||||
## Usage Guide
|
|
||||||
|
|
||||||
### Table of Contents
|
|
||||||
- [Introduction](#introduction)
|
|
||||||
- [Working from a local repo (CLI)](#working-from-a-local-repo-cli)
|
|
||||||
- [Online usage](#online-usage)
|
|
||||||
- [Working with GitHub App](#working-with-github-app)
|
|
||||||
- [Working with GitHub Action](#working-with-github-action)
|
|
||||||
- [Changing a model](#changing-a-model)
|
|
||||||
- [Working with large PRs](#working-with-large-prs)
|
|
||||||
- [Appendix - additional configurations walkthrough](#appendix---additional-configurations-walkthrough)
|
|
||||||
|
|
||||||
### Introduction
|
|
||||||
|
|
||||||
After [installation](/INSTALL.md), there are three basic ways to invoke CodiumAI PR-Agent:
|
|
||||||
1. Locally running a CLI command
|
|
||||||
2. Online usage - by [commenting](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901) on a PR
|
|
||||||
3. Enabling PR-Agent tools to run automatically when a new PR is opened
|
|
||||||
|
|
||||||
|
|
||||||
Specifically, CLI commands can be issued by invoking a pre-built [docker image](/INSTALL.md#running-from-source), or by invoking a [locally cloned repo](INSTALL.md#method-2-run-from-source).
|
|
||||||
For online usage, you will need to setup either a [GitHub App](INSTALL.md#method-5-run-as-a-github-app), or a [GitHub Action](INSTALL.md#method-3-run-as-a-github-action).
|
|
||||||
GitHub App and GitHub Action also enable to run PR-Agent specific tool automatically when a new PR is opened.
|
|
||||||
|
|
||||||
|
|
||||||
#### The configuration file
|
|
||||||
The different tools and sub-tools used by CodiumAI PR-Agent are adjustable via the **[configuration file](pr_agent/settings/configuration.toml)**.
|
|
||||||
In addition to general configuration options, each tool has its own configurations. For example, the `review` tool will use parameters from the [pr_reviewer](/pr_agent/settings/configuration.toml#L16) section in the configuration file.
|
|
||||||
|
|
||||||
The [Tools Guide](./docs/TOOLS_GUIDE.md) provides a detailed description of the different tools and their configurations.
|
|
||||||
|
|
||||||
#### Ignoring files from analysis
|
|
||||||
In some cases, you may want to exclude specific files or directories from the analysis performed by CodiumAI PR-Agent. This can be useful, for example, when you have files that are generated automatically or files that shouldn't be reviewed, like vendored code.
|
|
||||||
|
|
||||||
To ignore files or directories, edit the **[ignore.toml](/pr_agent/settings/ignore.toml)** configuration file. This setting is also exposed the following environment variables:
|
|
||||||
|
|
||||||
- `IGNORE.GLOB`
|
|
||||||
- `IGNORE.REGEX`
|
|
||||||
|
|
||||||
See [dynaconf envvars documentation](https://www.dynaconf.com/envvars/).
|
|
||||||
|
|
||||||
#### git provider
|
|
||||||
The [git_provider](pr_agent/settings/configuration.toml#L4) field in the configuration file determines the GIT provider that will be used by PR-Agent. Currently, the following providers are supported:
|
|
||||||
`
|
|
||||||
"github", "gitlab", "azure", "codecommit", "local", "gerrit"
|
|
||||||
`
|
|
||||||
|
|
||||||
[//]: # (** online usage:**)
|
|
||||||
|
|
||||||
[//]: # (Options that are available in the configuration file can be specified at run time when calling actions. Two examples:)
|
|
||||||
|
|
||||||
[//]: # (```)
|
|
||||||
|
|
||||||
[//]: # (- /review --pr_reviewer.extra_instructions="focus on the file: ...")
|
|
||||||
|
|
||||||
[//]: # (- /describe --pr_description.add_original_user_description=false -pr_description.extra_instructions="make sure to mention: ...")
|
|
||||||
|
|
||||||
[//]: # (```)
|
|
||||||
|
|
||||||
### Working from a local repo (CLI)
|
|
||||||
When running from your local repo (CLI), your local configuration file will be used.
|
|
||||||
Examples for invoking the different tools via the CLI:
|
|
||||||
|
|
||||||
- **Review**: `python -m pr_agent.cli --pr_url=<pr_url> review`
|
|
||||||
- **Describe**: `python -m pr_agent.cli --pr_url=<pr_url> describe`
|
|
||||||
- **Improve**: `python -m pr_agent.cli --pr_url=<pr_url> improve`
|
|
||||||
- **Ask**: `python -m pr_agent.cli --pr_url=<pr_url> ask "Write me a poem about this PR"`
|
|
||||||
- **Reflect**: `python -m pr_agent.cli --pr_url=<pr_url> reflect`
|
|
||||||
- **Update Changelog**: `python -m pr_agent.cli --pr_url=<pr_url> update_changelog`
|
|
||||||
|
|
||||||
`<pr_url>` is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).
|
|
||||||
|
|
||||||
**Notes:**
|
|
||||||
|
|
||||||
(1) in addition to editing your local configuration file, you can also change any configuration value by adding it to the command line:
|
|
||||||
```
|
|
||||||
python -m pr_agent.cli --pr_url=<pr_url> /review --pr_reviewer.extra_instructions="focus on the file: ..."
|
|
||||||
```
|
|
||||||
|
|
||||||
(2) You can print results locally, without publishing them, by setting in `configuration.toml`:
|
|
||||||
```
|
|
||||||
[config]
|
|
||||||
publish_output=true
|
|
||||||
verbosity_level=2
|
|
||||||
```
|
|
||||||
This is useful for debugging or experimenting with the different tools.
|
|
||||||
|
|
||||||
|
|
||||||
### Online usage
|
|
||||||
|
|
||||||
Online usage means invoking PR-Agent tools by [comments](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901) on a PR.
|
|
||||||
Commands for invoking the different tools via comments:
|
|
||||||
|
|
||||||
- **Review**: `/review`
|
|
||||||
- **Describe**: `/describe`
|
|
||||||
- **Improve**: `/improve`
|
|
||||||
- **Ask**: `/ask "..."`
|
|
||||||
- **Reflect**: `/reflect`
|
|
||||||
- **Update Changelog**: `/update_changelog`
|
|
||||||
|
|
||||||
|
|
||||||
To edit a specific configuration value, just add `--config_path=<value>` to any command.
|
|
||||||
For example if you want to edit the `review` tool configurations, you can run:
|
|
||||||
```
|
|
||||||
/review --pr_reviewer.extra_instructions="..." --pr_reviewer.require_score_review=false
|
|
||||||
```
|
|
||||||
Any configuration value in [configuration file](pr_agent/settings/configuration.toml) file can be similarly edited. comment `/config` to see the list of available configurations.
|
|
||||||
|
|
||||||
|
|
||||||
### Working with GitHub App
|
|
||||||
When running PR-Agent from [GitHub App](INSTALL.md#method-5-run-as-a-github-app), the default configurations from a pre-built docker will be initially loaded.
|
|
||||||
|
|
||||||
#### GitHub app automatic tools
|
|
||||||
The [github_app](pr_agent/settings/configuration.toml#L56) section defines GitHub app specific configurations.
|
|
||||||
An important parameter is `pr_commands`, which is a list of tools that will be **run automatically** when a new PR is opened:
|
|
||||||
```
|
|
||||||
[github_app]
|
|
||||||
pr_commands = [
|
|
||||||
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
|
||||||
"/auto_review",
|
|
||||||
]
|
|
||||||
```
|
|
||||||
This means that when a new PR is opened, PR-Agent will run the `describe` and `auto_review` tools.
|
|
||||||
For the describe tool, the `add_original_user_description` and `keep_original_user_title` parameters will be set to true.
|
|
||||||
|
|
||||||
You can override the default tool parameters by uploading a local configuration file called `.pr_agent.toml` to the root of your repo.
|
|
||||||
For example, if your local `.pr_agent.toml` file contains:
|
|
||||||
```
|
|
||||||
[pr_description]
|
|
||||||
add_original_user_description = false
|
|
||||||
keep_original_user_title = false
|
|
||||||
```
|
|
||||||
When a new PR is opened, PR-Agent will run the `describe` tool with the above parameters.
|
|
||||||
|
|
||||||
To cancel the automatic run of all the tools, set:
|
|
||||||
```
|
|
||||||
[github_app]
|
|
||||||
pr_commands = ""
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Note that a local `.pr_agent.toml` file enables you to edit and customize the default parameters of any tool, not just the ones that are run automatically.
|
|
||||||
|
|
||||||
#### Editing the prompts
|
|
||||||
The prompts for the various PR-Agent tools are defined in the `pr_agent/settings` folder.
|
|
||||||
In practice, the prompts are loaded and stored as a standard setting object.
|
|
||||||
Hence, editing them is similar to editing any other configuration value - just place the relevant key in `.pr_agent.toml`file, and override the default value.
|
|
||||||
|
|
||||||
For example, if you want to edit the prompts of the [describe](./pr_agent/settings/pr_description_prompts.toml) tool, you can add the following to your `.pr_agent.toml` file:
|
|
||||||
```
|
|
||||||
[pr_description_prompt]
|
|
||||||
system="""
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
user="""
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
Note that the new prompt will need to generate an output compatible with the relevant [post-process function](./pr_agent/tools/pr_description.py#L137).
|
|
||||||
|
|
||||||
### Working with GitHub Action
|
|
||||||
You can configure settings in GitHub action by adding environment variables under the env section in `.github/workflows/pr_agent.yml` 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
|
|
||||||
github_action.auto_review: "true" # Enable auto review
|
|
||||||
github_action.auto_describe: "true" # Enable auto describe
|
|
||||||
github_action.auto_improve: "false" # Disable auto improve
|
|
||||||
```
|
|
||||||
specifically, `github_action.auto_review`, `github_action.auto_describe` and `github_action.auto_improve` are used to enable/disable automatic tools that run when a new PR is opened.
|
|
||||||
|
|
||||||
If not set, the default option is that only the `review` tool will run automatically when a new PR is opened.
|
|
||||||
|
|
||||||
### Changing a model
|
|
||||||
|
|
||||||
See [here](pr_agent/algo/__init__.py) for the list of available models.
|
|
||||||
To use a different model than the default (GPT-4), you need to edit [configuration file](pr_agent/settings/configuration.toml#L2).
|
|
||||||
For models and environments not from OPENAI, you might need to provide additional keys and other parameters. See below for instructions.
|
|
||||||
|
|
||||||
#### Azure
|
|
||||||
To use Azure, set in your .secrets.toml:
|
|
||||||
```
|
|
||||||
api_key = "" # your azure api key
|
|
||||||
api_type = "azure"
|
|
||||||
api_version = '2023-05-15' # Check Azure documentation for the current API version
|
|
||||||
api_base = "" # The base URL for your Azure OpenAI resource. e.g. "https://<your resource name>.openai.azure.com"
|
|
||||||
openai.deployment_id = "" # The deployment name you chose when you deployed the engine
|
|
||||||
```
|
|
||||||
|
|
||||||
and
|
|
||||||
```
|
|
||||||
[config]
|
|
||||||
model="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
|
|
||||||
```
|
|
||||||
in the configuration.toml
|
|
||||||
|
|
||||||
#### Huggingface
|
|
||||||
|
|
||||||
**Local**
|
|
||||||
You can run Huggingface models locally through either [VLLM](https://docs.litellm.ai/docs/providers/vllm) or [Ollama](https://docs.litellm.ai/docs/providers/ollama)
|
|
||||||
|
|
||||||
E.g. to use a new Huggingface model locally via Ollama, set:
|
|
||||||
```
|
|
||||||
[__init__.py]
|
|
||||||
MAX_TOKENS = {
|
|
||||||
"model-name-on-ollama": <max_tokens>
|
|
||||||
}
|
|
||||||
e.g.
|
|
||||||
MAX_TOKENS={
|
|
||||||
...,
|
|
||||||
"llama2": 4096
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[config] # in configuration.toml
|
|
||||||
model = "ollama/llama2"
|
|
||||||
|
|
||||||
[ollama] # in .secrets.toml
|
|
||||||
api_base = ... # the base url for your huggingface inference endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
**Inference Endpoints**
|
|
||||||
|
|
||||||
To use a new model with Huggingface Inference Endpoints, for example, set:
|
|
||||||
```
|
|
||||||
[__init__.py]
|
|
||||||
MAX_TOKENS = {
|
|
||||||
"model-name-on-huggingface": <max_tokens>
|
|
||||||
}
|
|
||||||
e.g.
|
|
||||||
MAX_TOKENS={
|
|
||||||
...,
|
|
||||||
"meta-llama/Llama-2-7b-chat-hf": 4096
|
|
||||||
}
|
|
||||||
[config] # in configuration.toml
|
|
||||||
model = "huggingface/meta-llama/Llama-2-7b-chat-hf"
|
|
||||||
|
|
||||||
[huggingface] # in .secrets.toml
|
|
||||||
key = ... # your huggingface api key
|
|
||||||
api_base = ... # the base url for your huggingface inference endpoint
|
|
||||||
```
|
|
||||||
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
|
|
||||||
|
|
||||||
#### Replicate
|
|
||||||
|
|
||||||
To use Llama2 model with Replicate, for example, set:
|
|
||||||
```
|
|
||||||
[config] # in configuration.toml
|
|
||||||
model = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
|
|
||||||
[replicate] # in .secrets.toml
|
|
||||||
key = ...
|
|
||||||
```
|
|
||||||
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
|
|
||||||
|
|
||||||
|
|
||||||
Also review the [AiHandler](pr_agent/algo/ai_handler.py) file for instruction how to set keys for other models.
|
|
||||||
|
|
||||||
### Working with large PRs
|
|
||||||
|
|
||||||
The default mode of CodiumAI is to have a single call per tool, using GPT-4, which has a token limit of 8000 tokens.
|
|
||||||
This mode provide a very good speed-quality-cost tradeoff, and can handle most PRs successfully.
|
|
||||||
When the PR is above the token limit, it employs a [PR Compression strategy](./PR_COMPRESSION.md).
|
|
||||||
|
|
||||||
However, for very large PRs, or in case you want to emphasize quality over speed and cost, there are 2 possible solutions:
|
|
||||||
1) [Use a model](#changing-a-model) with larger context, like GPT-32K, or claude-100K. This solution will be applicable for all the tools.
|
|
||||||
2) For the `/improve` tool, there is an ['extended' mode](./docs/IMPROVE.md) (`/improve --extended`),
|
|
||||||
which divides the PR to chunks, and process each chunk separately. With this mode, regardless of the model, no compression will be done (but for large PRs, multiple model calls may occur)
|
|
||||||
|
|
||||||
### Appendix - additional configurations walkthrough
|
|
||||||
|
|
||||||
|
|
||||||
#### Extra instructions
|
|
||||||
All PR-Agent tools have a parameter called `extra_instructions`, that enables to add free-text extra instructions. Example usage:
|
|
||||||
```
|
|
||||||
/update_changelog --pr_update_changelog.extra_instructions="Make sure to update also the version ..."
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Patch Extra Lines
|
|
||||||
By default, around any change in your PR, git patch provides 3 lines of context above and below the change.
|
|
||||||
```
|
|
||||||
@@ -12,5 +12,5 @@ def func1():
|
|
||||||
code line that already existed in the file...
|
|
||||||
code line that already existed in the file...
|
|
||||||
code line that already existed in the file....
|
|
||||||
-code line that was removed in the PR
|
|
||||||
+new code line added in the PR
|
|
||||||
code line that already existed in the file...
|
|
||||||
code line that already existed in the file...
|
|
||||||
code line that already existed in the file...
|
|
||||||
```
|
|
||||||
|
|
||||||
For the `review`, `describe`, `ask` and `add_docs` tools, if the token budget allows, PR-Agent tries to increase the number of lines of context, via the parameter:
|
|
||||||
```
|
|
||||||
[config]
|
|
||||||
patch_extra_lines=3
|
|
||||||
```
|
|
||||||
|
|
||||||
Increasing this number provides more context to the model, but will also increase the token budget.
|
|
||||||
If the PR is too large (see [PR Compression strategy](./PR_COMPRESSION.md)), PR-Agent automatically sets this number to 0, using the original git patch.
|
|
||||||
|
|
||||||
|
|
||||||
#### Azure DevOps provider
|
|
||||||
To use Azure DevOps provider use the following settings in configuration.toml:
|
|
||||||
```
|
|
||||||
[config]
|
|
||||||
git_provider="azure"
|
|
||||||
use_repo_settings_file=false
|
|
||||||
```
|
|
||||||
|
|
||||||
And use the following settings (you have to replace the values) in .secrets.toml:
|
|
||||||
```
|
|
||||||
[azure_devops]
|
|
||||||
org = "https://dev.azure.com/YOUR_ORGANIZATION/"
|
|
||||||
pat = "YOUR_PAT_TOKEN"
|
|
||||||
```
|
|
@ -18,10 +18,6 @@ FROM base as github_polling
|
|||||||
ADD pr_agent pr_agent
|
ADD pr_agent pr_agent
|
||||||
CMD ["python", "pr_agent/servers/github_polling.py"]
|
CMD ["python", "pr_agent/servers/github_polling.py"]
|
||||||
|
|
||||||
FROM base as gitlab_webhook
|
|
||||||
ADD pr_agent pr_agent
|
|
||||||
CMD ["python", "pr_agent/servers/gitlab_webhook.py"]
|
|
||||||
|
|
||||||
FROM base as test
|
FROM base as test
|
||||||
ADD requirements-dev.txt .
|
ADD requirements-dev.txt .
|
||||||
RUN pip install -r requirements-dev.txt && rm requirements-dev.txt
|
RUN pip install -r requirements-dev.txt && rm requirements-dev.txt
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
# Add Documentation Tool
|
|
||||||
The `add_docs` tool scans the PR code changes, and automatically suggests documentation for the undocumented code components (functions, classes, etc.).
|
|
||||||
|
|
||||||
It can be invoked manually by commenting on any PR:
|
|
||||||
```
|
|
||||||
/add_docs
|
|
||||||
```
|
|
||||||
For example:
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/add_docs_comment.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/add_docs.png width="768"></kbd>
|
|
||||||
|
|
||||||
### Configuration options
|
|
||||||
- `docs_style`: The exact style of the documentation (for python docstring). you can choose between: `google`, `numpy`, `sphinx`, `restructuredtext`, `plain`. Default is `sphinx`.
|
|
||||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
|
11
docs/ASK.md
@ -1,11 +0,0 @@
|
|||||||
# ASK Tool
|
|
||||||
|
|
||||||
The `ask` tool answers questions about the PR, based on the PR code changes.
|
|
||||||
It can be invoked manually by commenting on any PR:
|
|
||||||
```
|
|
||||||
/ask "..."
|
|
||||||
```
|
|
||||||
For example:
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/ask_comment.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/ask.png width="768"></kbd>
|
|
@ -1,62 +0,0 @@
|
|||||||
# Describe Tool
|
|
||||||
|
|
||||||
The `describe` tool scans the PR code changes, and automatically generates PR description - title, type, summary, code walkthrough and labels.
|
|
||||||
It can be invoked manually by commenting on any PR:
|
|
||||||
```
|
|
||||||
/describe
|
|
||||||
```
|
|
||||||
For example:
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/describe_comment.png width="768"></kbd>
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/describe.png width="768"></kbd>
|
|
||||||
|
|
||||||
The `describe` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
|
|
||||||
|
|
||||||
### Configuration options
|
|
||||||
|
|
||||||
Under the section 'pr_description', the [configuration file](./../pr_agent/settings/configuration.toml#L28) contains options to customize the 'describe' tool:
|
|
||||||
|
|
||||||
- `publish_labels`: if set to true, the tool will publish the labels to the PR. Default is true.
|
|
||||||
|
|
||||||
- `publish_description_as_comment`: if set to true, the tool will publish the description as a comment to the PR. If false, it will overwrite the origianl description. Default is false.
|
|
||||||
|
|
||||||
- `add_original_user_description`: if set to true, the tool will add the original user description to the generated description. Default is false.
|
|
||||||
|
|
||||||
- `keep_original_user_title`: if set to true, the tool will keep the original PR title, and won't change it. Default is false.
|
|
||||||
|
|
||||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
|
||||||
|
|
||||||
### Markers template
|
|
||||||
|
|
||||||
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
|
|
||||||
|
|
||||||
For example, if the PR original description was:
|
|
||||||
```
|
|
||||||
User content...
|
|
||||||
|
|
||||||
|
|
||||||
## PR Description:
|
|
||||||
pr_agent:summary
|
|
||||||
|
|
||||||
## PR Walkthrough:
|
|
||||||
pr_agent:walkthrough
|
|
||||||
```
|
|
||||||
The marker `pr_agent:summary` will be replaced with the PR summary, and `pr_agent:walkthrough` will be replaced with the PR walkthrough.
|
|
||||||
|
|
||||||
##### Example:
|
|
||||||
```
|
|
||||||
env:
|
|
||||||
pr_description.use_description_markers: 'true'
|
|
||||||
```
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/describe_markers_before.png width="768"></kbd>
|
|
||||||
|
|
||||||
==>
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/describe_markers_after.png width="768"></kbd>
|
|
||||||
|
|
||||||
##### Configuration params:
|
|
||||||
|
|
||||||
- `use_description_markers`: if set to true, the tool will use markers template. It replaces every marker of the form `pr_agent:marker_name` with the relevant content. Default is false.
|
|
||||||
- `include_generated_by_header`: if set to true, the tool will add a dedicated header: 'Generated by PR Agent at ...' to any automatic content. Default is true.
|
|
@ -1,35 +0,0 @@
|
|||||||
# Generate Custom Labels
|
|
||||||
The `generte_labels` tool scans the PR code changes, and given a list of labels and their descriptions, it automatically suggests labels that match the PR code changes.
|
|
||||||
|
|
||||||
It can be invoked manually by commenting on any PR:
|
|
||||||
```
|
|
||||||
/generate_labels
|
|
||||||
```
|
|
||||||
For example:
|
|
||||||
|
|
||||||
If we wish to add detect changes to SQL queries in a given PR, we can add the following custom label along with its description:
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/custom_labels_list.png width="768"></kbd>
|
|
||||||
When running the `generte_labels` tool on a PR that includes changes in SQL queries, it will automatically suggest the custom label:
|
|
||||||
<kbd><img src=./../pics/custom_label_published.png width="768"></kbd>
|
|
||||||
|
|
||||||
### Configuration options
|
|
||||||
To enable custom labels, you need to add the following configuration to the [custom_labels file](./../pr_agent/settings/custom_labels.toml):
|
|
||||||
- Change `enable_custom_labels` to True: This will turn off the default labels and enable the custom labels provided in the custom_labels.toml file.
|
|
||||||
- Add the custom labels to the custom_labels.toml file. It should be formatted as follows:
|
|
||||||
```
|
|
||||||
[custom_labels."Custom Label Name"]
|
|
||||||
description = "Description of when AI should suggest this label"
|
|
||||||
```
|
|
||||||
- You can add modify the list to include all the custom labels you wish to use in your repository.
|
|
||||||
|
|
||||||
#### Github Action
|
|
||||||
To use the `generte_labels` tool with Github Action:
|
|
||||||
|
|
||||||
- Add the following file to your repository under `env` section in `.github/workflows/pr_agent.yml`
|
|
||||||
- Comma separated list of custom labels and their descriptions
|
|
||||||
- The number of labels and descriptions should be the same and in the same order (empty descriptions are allowed):
|
|
||||||
```
|
|
||||||
CUSTOM_LABELS: "label1, label2, ..."
|
|
||||||
CUSTOM_LABELS_DESCRIPTION: "label1 description, label2 description, ..."
|
|
||||||
```
|
|
@ -1,45 +0,0 @@
|
|||||||
# Improve Tool
|
|
||||||
|
|
||||||
The `improve` tool scans the PR code changes, and automatically generate committable suggestions for improving the PR code.
|
|
||||||
It can be invoked manually by commenting on any PR:
|
|
||||||
```
|
|
||||||
/improve
|
|
||||||
```
|
|
||||||
For example:
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/improve_comment.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/improve.png width="768"></kbd>
|
|
||||||
|
|
||||||
The `improve` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
|
|
||||||
|
|
||||||
An extended mode, which does not involve PR Compression and provides more comprehensive suggestions, can be invoked by commenting on any PR:
|
|
||||||
```
|
|
||||||
/improve --extended
|
|
||||||
```
|
|
||||||
Note that the extended mode divides the PR code changes into chunks, up to the token limits, where each chunk is handled separately (multiple calls to GPT-4).
|
|
||||||
Hence, the total number of suggestions is proportional to the number of chunks, i.e. the size of the PR.
|
|
||||||
|
|
||||||
### Configuration options
|
|
||||||
|
|
||||||
Under the section 'pr_code_suggestions', the [configuration file](./../pr_agent/settings/configuration.toml#L40) contains options to customize the 'improve' tool:
|
|
||||||
|
|
||||||
- `num_code_suggestions`: number of code suggestions provided by the 'improve' tool. Default is 4.
|
|
||||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
|
||||||
- `rank_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is false.
|
|
||||||
|
|
||||||
#### params for '/improve --extended' mode
|
|
||||||
- `num_code_suggestions_per_chunk`: number of code suggestions provided by the 'improve' tool, per chunk. Default is 8.
|
|
||||||
- `rank_extended_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is true.
|
|
||||||
- `max_number_of_calls`: maximum number of chunks. Default is 5.
|
|
||||||
- `final_clip_factor`: factor to remove suggestions with low confidence. Default is 0.9.
|
|
||||||
|
|
||||||
|
|
||||||
#### A note on code suggestions quality
|
|
||||||
|
|
||||||
- With current level of AI for code (GPT-4), mistakes can happen. Not all the suggestions will be perfect, and a user should not accept all of them automatically.
|
|
||||||
|
|
||||||
- Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_code_suggestions_prompts.toml#L34). Instead, they aim to give deep feedback and raise questions, ideas and thoughts to the user, who can then use his judgment, experience, and understanding of the code base.
|
|
||||||
|
|
||||||
- Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project.
|
|
||||||
|
|
||||||
- Best quality will be obtained by using 'improve --extended' mode.
|
|
@ -1,57 +0,0 @@
|
|||||||
# Review Tool
|
|
||||||
|
|
||||||
The `review` tool scans the PR code changes, and automatically generates a PR review.
|
|
||||||
It can be invoked manually by commenting on any PR:
|
|
||||||
```
|
|
||||||
/review
|
|
||||||
```
|
|
||||||
For example:
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/review_comment.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/describe.png width="768"></kbd>
|
|
||||||
|
|
||||||
The `review` tool can also be triggered automatically every time a new PR is opened. See examples for automatic triggers for [GitHub App](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#github-app-automatic-tools) and [GitHub Action](https://github.com/Codium-ai/pr-agent/blob/main/Usage.md#working-with-github-action)
|
|
||||||
|
|
||||||
### Configuration options
|
|
||||||
|
|
||||||
Under the section 'pr_reviewer', the [configuration file](./../pr_agent/settings/configuration.toml#L16) contains options to customize the 'review' tool:
|
|
||||||
|
|
||||||
- `require_focused_review`: if set to true, the tool will add a section - 'is the PR a focused one'. Default is false.
|
|
||||||
- `require_score_review`: if set to true, the tool will add a section that scores the PR. Default is false.
|
|
||||||
- `require_tests_review`: if set to true, the tool will add a section that checks if the PR contains tests. Default is true.
|
|
||||||
- `require_security_review`: if set to true, the tool will add a section that checks if the PR contains security issues. Default is true.
|
|
||||||
- `require_estimate_effort_to_review`: if set to true, the tool will add a section that estimates thed effort needed to review the PR. Default is true.
|
|
||||||
- `num_code_suggestions`: number of code suggestions provided by the 'review' tool. Default is 4.
|
|
||||||
- `inline_code_comments`: if set to true, the tool will publish the code suggestions as comments on the code diff. Default is false.
|
|
||||||
- `automatic_review`: if set to false, no automatic reviews will be done. Default is true.
|
|
||||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
|
||||||
#### Incremental Mode
|
|
||||||
For an incremental review, which only considers changes since the last PR-Agent review, this can be useful when working on the PR in an iterative manner, and you want to focus on the changes since the last review instead of reviewing the entire PR again, the following command can be used:
|
|
||||||
```
|
|
||||||
/improve -i
|
|
||||||
```
|
|
||||||
Note that the incremental mode is only available for GitHub.
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/incremental_review.png width="768"></kbd>
|
|
||||||
|
|
||||||
#### PR Reflection
|
|
||||||
By invoking:
|
|
||||||
```
|
|
||||||
/reflect_and_review
|
|
||||||
```
|
|
||||||
The tool will first ask the author questions about the PR, and will guide the review based on his answers.
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/reflection_questions.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/reflection_answers.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/reflection_insights.png width="768"></kbd>
|
|
||||||
|
|
||||||
|
|
||||||
#### A note on code suggestions quality
|
|
||||||
|
|
||||||
- With current level of AI for code (GPT-4), mistakes can happen. Not all the suggestions will be perfect, and a user should not accept all of them automatically.
|
|
||||||
|
|
||||||
- Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_reviewer_prompts.toml#L29). Instead, they aim to give deep feedback and raise questions, ideas and thoughts to the user, who can then use his judgment, experience, and understanding of the code base.
|
|
||||||
|
|
||||||
- Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project.
|
|
||||||
|
|
||||||
- Unlike the 'review' feature, which does a lot of things, the ['improve --extended'](./IMPROVE.md) feature is dedicated only to suggestions, and usually gives better results.
|
|
@ -1,31 +0,0 @@
|
|||||||
# Similar Issue Tool
|
|
||||||
The similar issue tool retrieves the most similar issues to the current issue.
|
|
||||||
It can be invoked manually by commenting on any PR:
|
|
||||||
```
|
|
||||||
/similar_issue
|
|
||||||
```
|
|
||||||
For example:
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/similar_issue_original_issue.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/similar_issue_comment.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/similar_issue.png width="768"></kbd>
|
|
||||||
|
|
||||||
Note that to perform retrieval, the `similar_issue` tool indexes all the repo previous issues (once).
|
|
||||||
|
|
||||||
To enable usage of the '**similar issue**' tool, you need to set the following keys in `.secrets.toml` (or in the relevant environment variables):
|
|
||||||
```
|
|
||||||
[pinecone]
|
|
||||||
api_key = "..."
|
|
||||||
environment = "..."
|
|
||||||
```
|
|
||||||
These parameters can be obtained by registering to [Pinecone](https://app.pinecone.io/?sessionType=signup/).
|
|
||||||
|
|
||||||
|
|
||||||
### How to use:
|
|
||||||
- To invoke the 'similar issue' tool from **CLI**, run:
|
|
||||||
`python3 cli.py --issue_url=... similar_issue`
|
|
||||||
|
|
||||||
- To invoke the 'similar' issue tool via online usage, [comment](https://github.com/Codium-ai/pr-agent/issues/178#issuecomment-1716934893) on a PR:
|
|
||||||
`/similar_issue`
|
|
||||||
|
|
||||||
- You can also enable the 'similar issue' tool to run automatically when a new issue is opened, by adding it to the [pr_commands list in the github_app section](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L66)
|
|
@ -1,10 +0,0 @@
|
|||||||
## Tools Guide
|
|
||||||
- [DESCRIBE](./DESCRIBE.md)
|
|
||||||
- [REVIEW](./REVIEW.md)
|
|
||||||
- [IMPROVE](./IMPROVE.md)
|
|
||||||
- [ASK](./ASK.md)
|
|
||||||
- [SIMILAR_ISSUE](./SIMILAR_ISSUE.md)
|
|
||||||
- [UPDATE CHANGELOG](./UPDATE_CHANGELOG.md)
|
|
||||||
- [ADD DOCUMENTATION](./ADD_DOCUMENTATION.md)
|
|
||||||
|
|
||||||
See the **[installation guide](/INSTALL.md)** for instructions on how to setup PR-Agent.
|
|
@ -1,19 +0,0 @@
|
|||||||
# Update Changelog Tool
|
|
||||||
|
|
||||||
The `update_changelog` tool automatically updates the CHANGELOG.md file with the PR changes.
|
|
||||||
It can be invoked manually by commenting on any PR:
|
|
||||||
```
|
|
||||||
/update_changelog
|
|
||||||
```
|
|
||||||
For example:
|
|
||||||
|
|
||||||
<kbd><img src=./../pics/update_changelog_comment.png width="768"></kbd>
|
|
||||||
<kbd><img src=./../pics/update_changelog.png width="768"></kbd>
|
|
||||||
|
|
||||||
|
|
||||||
### Configuration options
|
|
||||||
|
|
||||||
Under the section 'pr_update_changelog', the [configuration file](./../pr_agent/settings/configuration.toml#L50) contains options to customize the 'update changelog' tool:
|
|
||||||
|
|
||||||
- `push_changelog_changes`: whether to push the changes to CHANGELOG.md, or just print them. Default is false (print only).
|
|
||||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...
|
|
Before Width: | Height: | Size: 325 KiB |
Before Width: | Height: | Size: 51 KiB |
BIN
pics/ask.png
Before Width: | Height: | Size: 308 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 253 KiB |
Before Width: | Height: | Size: 84 KiB |
BIN
pics/debugger.png
Normal file
After Width: | Height: | Size: 534 KiB |
Before Width: | Height: | Size: 244 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 30 KiB |
BIN
pics/improve.png
Before Width: | Height: | Size: 234 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 286 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 217 KiB |
Before Width: | Height: | Size: 86 KiB |
BIN
pics/review.png
Before Width: | Height: | Size: 190 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 66 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 138 KiB |
Before Width: | Height: | Size: 122 KiB |
Before Width: | Height: | Size: 25 KiB |
@ -1,18 +1,18 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from pr_agent.algo.utils import update_settings_from_args
|
from pr_agent.algo.utils import update_settings_from_args
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.utils import apply_repo_settings
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.tools.pr_add_docs import PRAddDocs
|
|
||||||
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
||||||
from pr_agent.tools.pr_config import PRConfig
|
|
||||||
from pr_agent.tools.pr_description import PRDescription
|
from pr_agent.tools.pr_description import PRDescription
|
||||||
from pr_agent.tools.pr_generate_labels import PRGenerateLabels
|
|
||||||
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
|
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
|
||||||
from pr_agent.tools.pr_similar_issue import PRSimilarIssue
|
|
||||||
from pr_agent.tools.pr_update_changelog import PRUpdateChangelog
|
from pr_agent.tools.pr_update_changelog import PRUpdateChangelog
|
||||||
|
from pr_agent.tools.pr_config import PRConfig
|
||||||
|
|
||||||
command2class = {
|
command2class = {
|
||||||
"auto_review": PRReviewer,
|
"auto_review": PRReviewer,
|
||||||
@ -30,9 +30,6 @@ command2class = {
|
|||||||
"update_changelog": PRUpdateChangelog,
|
"update_changelog": PRUpdateChangelog,
|
||||||
"config": PRConfig,
|
"config": PRConfig,
|
||||||
"settings": PRConfig,
|
"settings": PRConfig,
|
||||||
"similar_issue": PRSimilarIssue,
|
|
||||||
"add_docs": PRAddDocs,
|
|
||||||
"generate_labels": PRGenerateLabels,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commands = list(command2class.keys())
|
commands = list(command2class.keys())
|
||||||
@ -43,7 +40,22 @@ class PRAgent:
|
|||||||
|
|
||||||
async def handle_request(self, pr_url, request, notify=None) -> bool:
|
async def handle_request(self, pr_url, request, notify=None) -> bool:
|
||||||
# First, apply repo specific settings if exists
|
# First, apply repo specific settings if exists
|
||||||
apply_repo_settings(pr_url)
|
if get_settings().config.use_repo_settings_file:
|
||||||
|
repo_settings_file = None
|
||||||
|
try:
|
||||||
|
git_provider = get_git_provider()(pr_url)
|
||||||
|
repo_settings = git_provider.get_repo_settings()
|
||||||
|
if repo_settings:
|
||||||
|
repo_settings_file = None
|
||||||
|
fd, repo_settings_file = tempfile.mkstemp(suffix='.toml')
|
||||||
|
os.write(fd, repo_settings)
|
||||||
|
get_settings().load_file(repo_settings_file)
|
||||||
|
finally:
|
||||||
|
if repo_settings_file:
|
||||||
|
try:
|
||||||
|
os.remove(repo_settings_file)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to remove temporary settings file {repo_settings_file}", e)
|
||||||
|
|
||||||
# Then, apply user specific settings if exists
|
# Then, apply user specific settings if exists
|
||||||
request = request.replace("'", "\\'")
|
request = request.replace("'", "\\'")
|
||||||
@ -53,8 +65,8 @@ class PRAgent:
|
|||||||
args = update_settings_from_args(args)
|
args = update_settings_from_args(args)
|
||||||
|
|
||||||
action = action.lstrip("/").lower()
|
action = action.lstrip("/").lower()
|
||||||
if action == "reflect_and_review":
|
if action == "reflect_and_review" and not get_settings().pr_reviewer.ask_and_reflect:
|
||||||
get_settings().pr_reviewer.ask_and_reflect = True
|
action = "review"
|
||||||
if action == "answer":
|
if action == "answer":
|
||||||
if notify:
|
if notify:
|
||||||
notify()
|
notify()
|
||||||
@ -68,4 +80,3 @@ class PRAgent:
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
MAX_TOKENS = {
|
MAX_TOKENS = {
|
||||||
'text-embedding-ada-002': 8000,
|
|
||||||
'gpt-3.5-turbo': 4000,
|
'gpt-3.5-turbo': 4000,
|
||||||
'gpt-3.5-turbo-0613': 4000,
|
'gpt-3.5-turbo-0613': 4000,
|
||||||
'gpt-3.5-turbo-0301': 4000,
|
'gpt-3.5-turbo-0301': 4000,
|
||||||
@ -12,5 +11,4 @@ MAX_TOKENS = {
|
|||||||
'claude-2': 100000,
|
'claude-2': 100000,
|
||||||
'command-nightly': 4096,
|
'command-nightly': 4096,
|
||||||
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
|
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
|
||||||
'meta-llama/Llama-2-7b-chat-hf': 4096
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import os
|
import logging
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
import openai
|
import openai
|
||||||
from litellm import acompletion
|
from litellm import acompletion
|
||||||
from openai.error import APIError, RateLimitError, Timeout, TryAgain
|
from openai.error import APIError, RateLimitError, Timeout, TryAgain
|
||||||
from retry import retry
|
from retry import retry
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
OPENAI_RETRIES = 5
|
OPENAI_RETRIES = 5
|
||||||
|
|
||||||
@ -26,11 +26,7 @@ class AiHandler:
|
|||||||
try:
|
try:
|
||||||
openai.api_key = get_settings().openai.key
|
openai.api_key = get_settings().openai.key
|
||||||
litellm.openai_key = get_settings().openai.key
|
litellm.openai_key = get_settings().openai.key
|
||||||
if get_settings().get("litellm.use_client"):
|
litellm.debugger = get_settings().config.litellm_debugger
|
||||||
litellm_token = get_settings().get("litellm.LITELLM_TOKEN")
|
|
||||||
assert litellm_token, "LITELLM_TOKEN is required"
|
|
||||||
os.environ["LITELLM_TOKEN"] = litellm_token
|
|
||||||
litellm.use_client = True
|
|
||||||
self.azure = False
|
self.azure = False
|
||||||
if get_settings().get("OPENAI.ORG", None):
|
if get_settings().get("OPENAI.ORG", None):
|
||||||
litellm.organization = get_settings().openai.org
|
litellm.organization = get_settings().openai.org
|
||||||
@ -52,8 +48,6 @@ class AiHandler:
|
|||||||
litellm.replicate_key = get_settings().replicate.key
|
litellm.replicate_key = get_settings().replicate.key
|
||||||
if get_settings().get("HUGGINGFACE.KEY", None):
|
if get_settings().get("HUGGINGFACE.KEY", None):
|
||||||
litellm.huggingface_key = get_settings().huggingface.key
|
litellm.huggingface_key = get_settings().huggingface.key
|
||||||
if get_settings().get("HUGGINGFACE.API_BASE", None):
|
|
||||||
litellm.api_base = get_settings().huggingface.api_base
|
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
raise ValueError("OpenAI key is required") from e
|
raise ValueError("OpenAI key is required") from e
|
||||||
|
|
||||||
@ -89,34 +83,33 @@ class AiHandler:
|
|||||||
try:
|
try:
|
||||||
deployment_id = self.deployment_id
|
deployment_id = self.deployment_id
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().debug(
|
logging.debug(
|
||||||
f"Generating completion with {model}"
|
f"Generating completion with {model}"
|
||||||
f"{(' from deployment ' + deployment_id) if deployment_id else ''}"
|
f"{(' from deployment ' + deployment_id) if deployment_id else ''}"
|
||||||
)
|
)
|
||||||
if self.azure:
|
|
||||||
model = 'azure/' + model
|
|
||||||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
|
||||||
response = await acompletion(
|
response = await acompletion(
|
||||||
model=model,
|
model=model,
|
||||||
deployment_id=deployment_id,
|
deployment_id=deployment_id,
|
||||||
messages=messages,
|
messages=[
|
||||||
|
{"role": "system", "content": system},
|
||||||
|
{"role": "user", "content": user}
|
||||||
|
],
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
|
azure=self.azure,
|
||||||
force_timeout=get_settings().config.ai_timeout
|
force_timeout=get_settings().config.ai_timeout
|
||||||
)
|
)
|
||||||
except (APIError, Timeout, TryAgain) as e:
|
except (APIError, Timeout, TryAgain) as e:
|
||||||
get_logger().error("Error during OpenAI inference: ", e)
|
logging.error("Error during OpenAI inference: ", e)
|
||||||
raise
|
raise
|
||||||
except (RateLimitError) as e:
|
except (RateLimitError) as e:
|
||||||
get_logger().error("Rate limit error during OpenAI inference: ", e)
|
logging.error("Rate limit error during OpenAI inference: ", e)
|
||||||
raise
|
raise
|
||||||
except (Exception) as e:
|
except (Exception) as e:
|
||||||
get_logger().error("Unknown error during OpenAI inference: ", e)
|
logging.error("Unknown error during OpenAI inference: ", e)
|
||||||
raise TryAgain from e
|
raise TryAgain from e
|
||||||
if response is None or len(response["choices"]) == 0:
|
if response is None or len(response["choices"]) == 0:
|
||||||
raise TryAgain
|
raise TryAgain
|
||||||
resp = response["choices"][0]['message']['content']
|
resp = response["choices"][0]['message']['content']
|
||||||
finish_reason = response["choices"][0]["finish_reason"]
|
finish_reason = response["choices"][0]["finish_reason"]
|
||||||
usage = response.get("usage")
|
print(resp, finish_reason)
|
||||||
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
|
|
||||||
model=model, usage=usage)
|
|
||||||
return resp, finish_reason
|
return resp, finish_reason
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import fnmatch
|
|
||||||
import re
|
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
|
||||||
|
|
||||||
def filter_ignored(files):
|
|
||||||
"""
|
|
||||||
Filter out files that match the ignore patterns.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
# load regex patterns, and translate glob patterns to regex
|
|
||||||
patterns = get_settings().ignore.regex
|
|
||||||
patterns += [fnmatch.translate(glob) for glob in get_settings().ignore.glob]
|
|
||||||
|
|
||||||
# compile all valid patterns
|
|
||||||
compiled_patterns = []
|
|
||||||
for r in patterns:
|
|
||||||
try:
|
|
||||||
compiled_patterns.append(re.compile(r))
|
|
||||||
except re.error:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# keep filenames that _don't_ match the ignore regex
|
|
||||||
for r in compiled_patterns:
|
|
||||||
files = [f for f in files if not r.match(f.filename)]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not filter file list: {e}")
|
|
||||||
|
|
||||||
return files
|
|
@ -1,9 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
def extend_patch(original_file_str, patch_str, num_lines) -> str:
|
def extend_patch(original_file_str, patch_str, num_lines) -> str:
|
||||||
@ -41,16 +40,12 @@ def extend_patch(original_file_str, patch_str, num_lines) -> str:
|
|||||||
extended_patch_lines.extend(
|
extended_patch_lines.extend(
|
||||||
original_lines[start1 + size1 - 1:start1 + size1 - 1 + num_lines])
|
original_lines[start1 + size1 - 1:start1 + size1 - 1 + num_lines])
|
||||||
|
|
||||||
res = list(match.groups())
|
|
||||||
for i in range(len(res)):
|
|
||||||
if res[i] is None:
|
|
||||||
res[i] = 0
|
|
||||||
try:
|
try:
|
||||||
start1, size1, start2, size2 = map(int, res[:4])
|
start1, size1, start2, size2 = map(int, match.groups()[:4])
|
||||||
except: # '@@ -0,0 +1 @@' case
|
except: # '@@ -0,0 +1 @@' case
|
||||||
start1, size1, size2 = map(int, res[:3])
|
start1, size1, size2 = map(int, match.groups()[:3])
|
||||||
start2 = 0
|
start2 = 0
|
||||||
section_header = res[4]
|
section_header = match.groups()[4]
|
||||||
extended_start1 = max(1, start1 - num_lines)
|
extended_start1 = max(1, start1 - num_lines)
|
||||||
extended_size1 = size1 + (start1 - extended_start1) + num_lines
|
extended_size1 = size1 + (start1 - extended_start1) + num_lines
|
||||||
extended_start2 = max(1, start2 - num_lines)
|
extended_start2 = max(1, start2 - num_lines)
|
||||||
@ -64,7 +59,7 @@ def extend_patch(original_file_str, patch_str, num_lines) -> str:
|
|||||||
extended_patch_lines.append(line)
|
extended_patch_lines.append(line)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().error(f"Failed to extend patch: {e}")
|
logging.error(f"Failed to extend patch: {e}")
|
||||||
return patch_str
|
return patch_str
|
||||||
|
|
||||||
# finish previous hunk
|
# finish previous hunk
|
||||||
@ -135,14 +130,14 @@ def handle_patch_deletions(patch: str, original_file_content_str: str,
|
|||||||
if not new_file_content_str:
|
if not new_file_content_str:
|
||||||
# logic for handling deleted files - don't show patch, just show that the file was deleted
|
# logic for handling deleted files - don't show patch, just show that the file was deleted
|
||||||
if get_settings().config.verbosity_level > 0:
|
if get_settings().config.verbosity_level > 0:
|
||||||
get_logger().info(f"Processing file: {file_name}, minimizing deletion file")
|
logging.info(f"Processing file: {file_name}, minimizing deletion file")
|
||||||
patch = None # file was deleted
|
patch = None # file was deleted
|
||||||
else:
|
else:
|
||||||
patch_lines = patch.splitlines()
|
patch_lines = patch.splitlines()
|
||||||
patch_new = omit_deletion_hunks(patch_lines)
|
patch_new = omit_deletion_hunks(patch_lines)
|
||||||
if patch != patch_new:
|
if patch != patch_new:
|
||||||
if get_settings().config.verbosity_level > 0:
|
if get_settings().config.verbosity_level > 0:
|
||||||
get_logger().info(f"Processing file: {file_name}, hunks were deleted")
|
logging.info(f"Processing file: {file_name}, hunks were deleted")
|
||||||
patch = patch_new
|
patch = patch_new
|
||||||
return patch
|
return patch
|
||||||
|
|
||||||
@ -212,15 +207,10 @@ __old hunk__
|
|||||||
old_content_lines = []
|
old_content_lines = []
|
||||||
if match:
|
if match:
|
||||||
prev_header_line = header_line
|
prev_header_line = header_line
|
||||||
|
|
||||||
res = list(match.groups())
|
|
||||||
for i in range(len(res)):
|
|
||||||
if res[i] is None:
|
|
||||||
res[i] = 0
|
|
||||||
try:
|
try:
|
||||||
start1, size1, start2, size2 = map(int, res[:4])
|
start1, size1, start2, size2 = map(int, match.groups()[:4])
|
||||||
except: # '@@ -0,0 +1 @@' case
|
except: # '@@ -0,0 +1 @@' case
|
||||||
start1, size1, size2 = map(int, res[:3])
|
start1, size1, size2 = map(int, match.groups()[:3])
|
||||||
start2 = 0
|
start2 = 0
|
||||||
|
|
||||||
elif line.startswith('+'):
|
elif line.startswith('+'):
|
||||||
|
@ -42,11 +42,6 @@ def sort_files_by_main_languages(languages: Dict, files: list):
|
|||||||
files_sorted = []
|
files_sorted = []
|
||||||
rest_files = {}
|
rest_files = {}
|
||||||
|
|
||||||
# if no languages detected, put all files in the "Other" category
|
|
||||||
if not languages:
|
|
||||||
files_sorted = [({"language": "Other", "files": list(files_filtered)})]
|
|
||||||
return files_sorted
|
|
||||||
|
|
||||||
main_extensions_flat = []
|
main_extensions_flat = []
|
||||||
for ext in main_extensions:
|
for ext in main_extensions:
|
||||||
main_extensions_flat.extend(ext)
|
main_extensions_flat.extend(ext)
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import difflib
|
import logging
|
||||||
import re
|
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Callable, List, Tuple
|
from typing import Callable, List, Tuple
|
||||||
|
|
||||||
from github import RateLimitExceededException
|
from github import RateLimitExceededException
|
||||||
|
|
||||||
from pr_agent.algo import MAX_TOKENS
|
from pr_agent.algo import MAX_TOKENS
|
||||||
from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions
|
from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions
|
||||||
from pr_agent.algo.language_handler import sort_files_by_main_languages
|
from pr_agent.algo.language_handler import sort_files_by_main_languages
|
||||||
from pr_agent.algo.file_filter import filter_ignored
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.token_handler import TokenHandler, get_token_encoder
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.git_provider import FilePatchInfo, GitProvider
|
from pr_agent.git_providers.git_provider import GitProvider
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
DELETED_FILES_ = "Deleted files:\n"
|
DELETED_FILES_ = "Deleted files:\n"
|
||||||
|
|
||||||
@ -22,6 +19,7 @@ MORE_MODIFIED_FILES_ = "More modified files:\n"
|
|||||||
|
|
||||||
OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1000
|
OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD = 1000
|
||||||
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 600
|
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD = 600
|
||||||
|
PATCH_EXTRA_LINES = 3
|
||||||
|
|
||||||
def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: str,
|
def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: str,
|
||||||
add_line_numbers_to_hunks: bool = False, disable_extra_lines: bool = False) -> str:
|
add_line_numbers_to_hunks: bool = False, disable_extra_lines: bool = False) -> str:
|
||||||
@ -44,24 +42,21 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if disable_extra_lines:
|
if disable_extra_lines:
|
||||||
|
global PATCH_EXTRA_LINES
|
||||||
PATCH_EXTRA_LINES = 0
|
PATCH_EXTRA_LINES = 0
|
||||||
else:
|
|
||||||
PATCH_EXTRA_LINES = get_settings().config.patch_extra_lines
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
diff_files = git_provider.get_diff_files()
|
diff_files = git_provider.get_diff_files()
|
||||||
except RateLimitExceededException as e:
|
except RateLimitExceededException as e:
|
||||||
get_logger().error(f"Rate limit exceeded for git provider API. original message {e}")
|
logging.error(f"Rate limit exceeded for git provider API. original message {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
diff_files = filter_ignored(diff_files)
|
|
||||||
|
|
||||||
# get pr languages
|
# get pr languages
|
||||||
pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files)
|
pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files)
|
||||||
|
|
||||||
# generate a standard diff string, with patch extension
|
# generate a standard diff string, with patch extension
|
||||||
patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff(
|
patches_extended, total_tokens, patches_extended_tokens = pr_generate_extended_diff(pr_languages, token_handler,
|
||||||
pr_languages, token_handler, add_line_numbers_to_hunks, patch_extra_lines=PATCH_EXTRA_LINES)
|
add_line_numbers_to_hunks)
|
||||||
|
|
||||||
# if we are under the limit, return the full diff
|
# if we are under the limit, return the full diff
|
||||||
if total_tokens + OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD < MAX_TOKENS[model]:
|
if total_tokens + OUTPUT_BUFFER_TOKENS_SOFT_THRESHOLD < MAX_TOKENS[model]:
|
||||||
@ -83,8 +78,7 @@ def get_pr_diff(git_provider: GitProvider, token_handler: TokenHandler, model: s
|
|||||||
|
|
||||||
def pr_generate_extended_diff(pr_languages: list,
|
def pr_generate_extended_diff(pr_languages: list,
|
||||||
token_handler: TokenHandler,
|
token_handler: TokenHandler,
|
||||||
add_line_numbers_to_hunks: bool,
|
add_line_numbers_to_hunks: bool) -> Tuple[list, int, list]:
|
||||||
patch_extra_lines: int = 0) -> Tuple[list, int, list]:
|
|
||||||
"""
|
"""
|
||||||
Generate a standard diff string with patch extension, while counting the number of tokens used and applying diff
|
Generate a standard diff string with patch extension, while counting the number of tokens used and applying diff
|
||||||
minimization techniques if needed.
|
minimization techniques if needed.
|
||||||
@ -106,7 +100,7 @@ def pr_generate_extended_diff(pr_languages: list,
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# extend each patch with extra lines of context
|
# extend each patch with extra lines of context
|
||||||
extended_patch = extend_patch(original_file_content_str, patch, num_lines=patch_extra_lines)
|
extended_patch = extend_patch(original_file_content_str, patch, num_lines=PATCH_EXTRA_LINES)
|
||||||
full_extended_patch = f"\n\n## {file.filename}\n\n{extended_patch}\n"
|
full_extended_patch = f"\n\n## {file.filename}\n\n{extended_patch}\n"
|
||||||
|
|
||||||
if add_line_numbers_to_hunks:
|
if add_line_numbers_to_hunks:
|
||||||
@ -180,7 +174,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
|
|||||||
|
|
||||||
# Hard Stop, no more tokens
|
# Hard Stop, no more tokens
|
||||||
if total_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD:
|
if total_tokens > MAX_TOKENS[model] - OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD:
|
||||||
get_logger().warning(f"File was fully skipped, no more tokens: {file.filename}.")
|
logging.warning(f"File was fully skipped, no more tokens: {file.filename}.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If the patch is too large, just show the file name
|
# If the patch is too large, just show the file name
|
||||||
@ -189,7 +183,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
|
|||||||
# TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens
|
# TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens
|
||||||
# until we meet the requirements
|
# until we meet the requirements
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().warning(f"Patch too large, minimizing it, {file.filename}")
|
logging.warning(f"Patch too large, minimizing it, {file.filename}")
|
||||||
if not modified_files_list:
|
if not modified_files_list:
|
||||||
total_tokens += token_handler.count_tokens(MORE_MODIFIED_FILES_)
|
total_tokens += token_handler.count_tokens(MORE_MODIFIED_FILES_)
|
||||||
modified_files_list.append(file.filename)
|
modified_files_list.append(file.filename)
|
||||||
@ -204,7 +198,7 @@ def pr_generate_compressed_diff(top_langs: list, token_handler: TokenHandler, mo
|
|||||||
patches.append(patch_final)
|
patches.append(patch_final)
|
||||||
total_tokens += token_handler.count_tokens(patch_final)
|
total_tokens += token_handler.count_tokens(patch_final)
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Tokens: {total_tokens}, last filename: {file.filename}")
|
logging.info(f"Tokens: {total_tokens}, last filename: {file.filename}")
|
||||||
|
|
||||||
return patches, modified_files_list, deleted_files_list
|
return patches, modified_files_list, deleted_files_list
|
||||||
|
|
||||||
@ -218,7 +212,7 @@ async def retry_with_fallback_models(f: Callable):
|
|||||||
get_settings().set("openai.deployment_id", deployment_id)
|
get_settings().set("openai.deployment_id", deployment_id)
|
||||||
return await f(model)
|
return await f(model)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().warning(
|
logging.warning(
|
||||||
f"Failed to generate prediction with {model}"
|
f"Failed to generate prediction with {model}"
|
||||||
f"{(' from deployment ' + deployment_id) if deployment_id else ''}: "
|
f"{(' from deployment ' + deployment_id) if deployment_id else ''}: "
|
||||||
f"{traceback.format_exc()}"
|
f"{traceback.format_exc()}"
|
||||||
@ -251,99 +245,6 @@ def _get_all_deployments(all_models: List[str]) -> List[str]:
|
|||||||
return all_deployments
|
return all_deployments
|
||||||
|
|
||||||
|
|
||||||
def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
|
|
||||||
relevant_file: str,
|
|
||||||
relevant_line_in_file: str) -> Tuple[int, int]:
|
|
||||||
"""
|
|
||||||
Find the line number and absolute position of a relevant line in a file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
diff_files (List[FilePatchInfo]): A list of FilePatchInfo objects representing the patches of files.
|
|
||||||
relevant_file (str): The name of the file where the relevant line is located.
|
|
||||||
relevant_line_in_file (str): The content of the relevant line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[int, int]: A tuple containing the line number and absolute position of the relevant line in the file.
|
|
||||||
"""
|
|
||||||
position = -1
|
|
||||||
absolute_position = -1
|
|
||||||
re_hunk_header = re.compile(
|
|
||||||
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
|
||||||
|
|
||||||
for file in diff_files:
|
|
||||||
if file.filename.strip() == relevant_file:
|
|
||||||
patch = file.patch
|
|
||||||
patch_lines = patch.splitlines()
|
|
||||||
|
|
||||||
# try to find the line in the patch using difflib, with some margin of error
|
|
||||||
matches_difflib: list[str | Any] = difflib.get_close_matches(relevant_line_in_file,
|
|
||||||
patch_lines, n=3, cutoff=0.93)
|
|
||||||
if len(matches_difflib) == 1 and matches_difflib[0].startswith('+'):
|
|
||||||
relevant_line_in_file = matches_difflib[0]
|
|
||||||
|
|
||||||
delta = 0
|
|
||||||
start1, size1, start2, size2 = 0, 0, 0, 0
|
|
||||||
for i, line in enumerate(patch_lines):
|
|
||||||
if line.startswith('@@'):
|
|
||||||
delta = 0
|
|
||||||
match = re_hunk_header.match(line)
|
|
||||||
start1, size1, start2, size2 = map(int, match.groups()[:4])
|
|
||||||
elif not line.startswith('-'):
|
|
||||||
delta += 1
|
|
||||||
|
|
||||||
if relevant_line_in_file in line and line[0] != '-':
|
|
||||||
position = i
|
|
||||||
absolute_position = start2 + delta - 1
|
|
||||||
break
|
|
||||||
|
|
||||||
if position == -1 and relevant_line_in_file[0] == '+':
|
|
||||||
no_plus_line = relevant_line_in_file[1:].lstrip()
|
|
||||||
for i, line in enumerate(patch_lines):
|
|
||||||
if line.startswith('@@'):
|
|
||||||
delta = 0
|
|
||||||
match = re_hunk_header.match(line)
|
|
||||||
start1, size1, start2, size2 = map(int, match.groups()[:4])
|
|
||||||
elif not line.startswith('-'):
|
|
||||||
delta += 1
|
|
||||||
|
|
||||||
if no_plus_line in line and line[0] != '-':
|
|
||||||
# The model might add a '+' to the beginning of the relevant_line_in_file even if originally
|
|
||||||
# it's a context line
|
|
||||||
position = i
|
|
||||||
absolute_position = start2 + delta - 1
|
|
||||||
break
|
|
||||||
return position, absolute_position
|
|
||||||
|
|
||||||
|
|
||||||
def clip_tokens(text: str, max_tokens: int) -> str:
|
|
||||||
"""
|
|
||||||
Clip the number of tokens in a string to a maximum number of tokens.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): The string to clip.
|
|
||||||
max_tokens (int): The maximum number of tokens allowed in the string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The clipped string.
|
|
||||||
"""
|
|
||||||
if not text:
|
|
||||||
return text
|
|
||||||
|
|
||||||
try:
|
|
||||||
encoder = get_token_encoder()
|
|
||||||
num_input_tokens = len(encoder.encode(text))
|
|
||||||
if num_input_tokens <= max_tokens:
|
|
||||||
return text
|
|
||||||
num_chars = len(text)
|
|
||||||
chars_per_token = num_chars / num_input_tokens
|
|
||||||
num_output_chars = int(chars_per_token * max_tokens)
|
|
||||||
clipped_text = text[:num_output_chars]
|
|
||||||
return clipped_text
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().warning(f"Failed to clip tokens: {e}")
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
def get_pr_multi_diffs(git_provider: GitProvider,
|
def get_pr_multi_diffs(git_provider: GitProvider,
|
||||||
token_handler: TokenHandler,
|
token_handler: TokenHandler,
|
||||||
model: str,
|
model: str,
|
||||||
@ -351,27 +252,25 @@ def get_pr_multi_diffs(git_provider: GitProvider,
|
|||||||
"""
|
"""
|
||||||
Retrieves the diff files from a Git provider, sorts them by main language, and generates patches for each file.
|
Retrieves the diff files from a Git provider, sorts them by main language, and generates patches for each file.
|
||||||
The patches are split into multiple groups based on the maximum number of tokens allowed for the given model.
|
The patches are split into multiple groups based on the maximum number of tokens allowed for the given model.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
git_provider (GitProvider): An object that provides access to Git provider APIs.
|
git_provider (GitProvider): An object that provides access to Git provider APIs.
|
||||||
token_handler (TokenHandler): An object that handles tokens in the context of a pull request.
|
token_handler (TokenHandler): An object that handles tokens in the context of a pull request.
|
||||||
model (str): The name of the model.
|
model (str): The name of the model.
|
||||||
max_calls (int, optional): The maximum number of calls to retrieve diff files. Defaults to 5.
|
max_calls (int, optional): The maximum number of calls to retrieve diff files. Defaults to 5.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[str]: A list of final diff strings, split into multiple groups based on the maximum number of tokens allowed for the given model.
|
List[str]: A list of final diff strings, split into multiple groups based on the maximum number of tokens allowed for the given model.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
RateLimitExceededException: If the rate limit for the Git provider API is exceeded.
|
RateLimitExceededException: If the rate limit for the Git provider API is exceeded.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
diff_files = git_provider.get_diff_files()
|
diff_files = git_provider.get_diff_files()
|
||||||
except RateLimitExceededException as e:
|
except RateLimitExceededException as e:
|
||||||
get_logger().error(f"Rate limit exceeded for git provider API. original message {e}")
|
logging.error(f"Rate limit exceeded for git provider API. original message {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
diff_files = filter_ignored(diff_files)
|
|
||||||
|
|
||||||
# Sort files by main language
|
# Sort files by main language
|
||||||
pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files)
|
pr_languages = sort_files_by_main_languages(git_provider.get_languages(), diff_files)
|
||||||
|
|
||||||
@ -387,7 +286,7 @@ def get_pr_multi_diffs(git_provider: GitProvider,
|
|||||||
for file in sorted_files:
|
for file in sorted_files:
|
||||||
if call_number > max_calls:
|
if call_number > max_calls:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Reached max calls ({max_calls})")
|
logging.info(f"Reached max calls ({max_calls})")
|
||||||
break
|
break
|
||||||
|
|
||||||
original_file_content_str = file.base_file
|
original_file_content_str = file.base_file
|
||||||
@ -410,13 +309,13 @@ def get_pr_multi_diffs(git_provider: GitProvider,
|
|||||||
total_tokens = token_handler.prompt_tokens
|
total_tokens = token_handler.prompt_tokens
|
||||||
call_number += 1
|
call_number += 1
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Call number: {call_number}")
|
logging.info(f"Call number: {call_number}")
|
||||||
|
|
||||||
if patch:
|
if patch:
|
||||||
patches.append(patch)
|
patches.append(patch)
|
||||||
total_tokens += new_patch_tokens
|
total_tokens += new_patch_tokens
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Tokens: {total_tokens}, last filename: {file.filename}")
|
logging.info(f"Tokens: {total_tokens}, last filename: {file.filename}")
|
||||||
|
|
||||||
# Add the last chunk
|
# Add the last chunk
|
||||||
if patches:
|
if patches:
|
||||||
|
@ -21,7 +21,7 @@ class TokenHandler:
|
|||||||
method.
|
method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pr=None, vars: dict = {}, system="", user=""):
|
def __init__(self, vars: dict, system, user):
|
||||||
"""
|
"""
|
||||||
Initializes the TokenHandler object.
|
Initializes the TokenHandler object.
|
||||||
|
|
||||||
@ -32,10 +32,9 @@ class TokenHandler:
|
|||||||
- user: The user string.
|
- user: The user string.
|
||||||
"""
|
"""
|
||||||
self.encoder = get_token_encoder()
|
self.encoder = get_token_encoder()
|
||||||
if pr is not None:
|
self.prompt_tokens = self._get_system_user_tokens(self.encoder, vars, system, user)
|
||||||
self.prompt_tokens = self._get_system_user_tokens(pr, self.encoder, vars, system, user)
|
|
||||||
|
|
||||||
def _get_system_user_tokens(self, pr, encoder, vars: dict, system, user):
|
def _get_system_user_tokens(self, encoder, vars: dict, system, user):
|
||||||
"""
|
"""
|
||||||
Calculates the number of tokens in the system and user strings.
|
Calculates the number of tokens in the system and user strings.
|
||||||
|
|
||||||
|
@ -2,16 +2,26 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import difflib
|
import difflib
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, List
|
from enum import Enum
|
||||||
|
from typing import Any, List, Tuple, Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from starlette_context import context
|
from starlette_context import context
|
||||||
from pr_agent.config_loader import get_settings, global_settings
|
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
from pr_agent.algo.token_handler import get_token_encoder
|
||||||
|
from pr_agent.config_loader import get_settings, global_settings
|
||||||
|
|
||||||
|
|
||||||
|
class EDIT_TYPE(Enum):
|
||||||
|
ADDED = 1
|
||||||
|
DELETED = 2
|
||||||
|
MODIFIED = 3
|
||||||
|
RENAMED = 4
|
||||||
|
|
||||||
def get_setting(key: str) -> Any:
|
def get_setting(key: str) -> Any:
|
||||||
try:
|
try:
|
||||||
@ -20,7 +30,7 @@ def get_setting(key: str) -> Any:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return global_settings.get(key, None)
|
return global_settings.get(key, None)
|
||||||
|
|
||||||
def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
|
def convert_to_markdown(output_data: dict) -> str:
|
||||||
"""
|
"""
|
||||||
Convert a dictionary of data into markdown format.
|
Convert a dictionary of data into markdown format.
|
||||||
Args:
|
Args:
|
||||||
@ -42,7 +52,6 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
|
|||||||
"General suggestions": "💡",
|
"General suggestions": "💡",
|
||||||
"Insights from user's answers": "📝",
|
"Insights from user's answers": "📝",
|
||||||
"Code feedback": "🤖",
|
"Code feedback": "🤖",
|
||||||
"Estimated effort to review [1-5]": "⏱️",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value in output_data.items():
|
for key, value in output_data.items():
|
||||||
@ -50,33 +59,27 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
|
|||||||
continue
|
continue
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
markdown_text += f"## {key}\n\n"
|
markdown_text += f"## {key}\n\n"
|
||||||
markdown_text += convert_to_markdown(value, gfm_supported)
|
markdown_text += convert_to_markdown(value)
|
||||||
elif isinstance(value, list):
|
elif isinstance(value, list):
|
||||||
emoji = emojis.get(key, "")
|
emoji = emojis.get(key, "")
|
||||||
if key.lower() == 'code feedback':
|
if key.lower() == 'code feedback':
|
||||||
if gfm_supported:
|
markdown_text += f"\n\n- **<details><summary> { emoji } Code feedback:**</summary>\n\n"
|
||||||
markdown_text += f"\n\n- **<details><summary> { emoji } Code feedback:**</summary>\n\n"
|
|
||||||
else:
|
|
||||||
markdown_text += f"\n\n- **{emoji} Code feedback:**\n\n"
|
|
||||||
else:
|
else:
|
||||||
markdown_text += f"- {emoji} **{key}:**\n\n"
|
markdown_text += f"- {emoji} **{key}:**\n\n"
|
||||||
for item in value:
|
for item in value:
|
||||||
if isinstance(item, dict) and key.lower() == 'code feedback':
|
if isinstance(item, dict) and key.lower() == 'code feedback':
|
||||||
markdown_text += parse_code_suggestion(item, gfm_supported)
|
markdown_text += parse_code_suggestion(item)
|
||||||
elif item:
|
elif item:
|
||||||
markdown_text += f" - {item}\n"
|
markdown_text += f" - {item}\n"
|
||||||
if key.lower() == 'code feedback':
|
if key.lower() == 'code feedback':
|
||||||
if gfm_supported:
|
markdown_text += "</details>\n\n"
|
||||||
markdown_text += "</details>\n\n"
|
|
||||||
else:
|
|
||||||
markdown_text += "\n\n"
|
|
||||||
elif value != 'n/a':
|
elif value != 'n/a':
|
||||||
emoji = emojis.get(key, "")
|
emoji = emojis.get(key, "")
|
||||||
markdown_text += f"- {emoji} **{key}:** {value}\n"
|
markdown_text += f"- {emoji} **{key}:** {value}\n"
|
||||||
return markdown_text
|
return markdown_text
|
||||||
|
|
||||||
|
|
||||||
def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> str:
|
def parse_code_suggestion(code_suggestions: dict) -> str:
|
||||||
"""
|
"""
|
||||||
Convert a dictionary of data into markdown format.
|
Convert a dictionary of data into markdown format.
|
||||||
|
|
||||||
@ -99,9 +102,6 @@ def parse_code_suggestion(code_suggestions: dict, gfm_supported: bool=True) -> s
|
|||||||
markdown_text += f"\n - **{sub_key}:** {sub_value}\n"
|
markdown_text += f"\n - **{sub_key}:** {sub_value}\n"
|
||||||
else:
|
else:
|
||||||
markdown_text += f" **{sub_key}:** {sub_value}\n"
|
markdown_text += f" **{sub_key}:** {sub_value}\n"
|
||||||
if not gfm_supported:
|
|
||||||
if "relevant line" not in sub_key.lower(): # nicer presentation
|
|
||||||
markdown_text = markdown_text.rstrip('\n') + "\\\n"
|
|
||||||
|
|
||||||
markdown_text += "\n"
|
markdown_text += "\n"
|
||||||
return markdown_text
|
return markdown_text
|
||||||
@ -159,7 +159,7 @@ def try_fix_json(review, max_iter=10, code_suggestions=False):
|
|||||||
iter_count += 1
|
iter_count += 1
|
||||||
|
|
||||||
if not valid_json:
|
if not valid_json:
|
||||||
get_logger().error("Unable to decode JSON response from AI")
|
logging.error("Unable to decode JSON response from AI")
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@ -178,7 +178,7 @@ def fix_json_escape_char(json_message=None):
|
|||||||
Raises:
|
Raises:
|
||||||
None
|
None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = json.loads(json_message)
|
result = json.loads(json_message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -205,7 +205,7 @@ def convert_str_to_datetime(date_str):
|
|||||||
Example:
|
Example:
|
||||||
>>> convert_str_to_datetime('Mon, 01 Jan 2022 12:00:00 UTC')
|
>>> convert_str_to_datetime('Mon, 01 Jan 2022 12:00:00 UTC')
|
||||||
datetime.datetime(2022, 1, 1, 12, 0, 0)
|
datetime.datetime(2022, 1, 1, 12, 0, 0)
|
||||||
"""
|
"""
|
||||||
datetime_format = '%a, %d %b %Y %H:%M:%S %Z'
|
datetime_format = '%a, %d %b %Y %H:%M:%S %Z'
|
||||||
return datetime.strptime(date_str, datetime_format)
|
return datetime.strptime(date_str, datetime_format)
|
||||||
|
|
||||||
@ -230,7 +230,7 @@ def load_large_diff(filename, new_file_content_str: str, original_file_content_s
|
|||||||
diff = difflib.unified_diff(original_file_content_str.splitlines(keepends=True),
|
diff = difflib.unified_diff(original_file_content_str.splitlines(keepends=True),
|
||||||
new_file_content_str.splitlines(keepends=True))
|
new_file_content_str.splitlines(keepends=True))
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().warning(f"File was modified, but no patch was found. Manually creating patch: {filename}.")
|
logging.warning(f"File was modified, but no patch was found. Manually creating patch: {filename}.")
|
||||||
patch = ''.join(diff)
|
patch = ''.join(diff)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@ -262,12 +262,12 @@ def update_settings_from_args(args: List[str]) -> List[str]:
|
|||||||
vals = arg.split('=', 1)
|
vals = arg.split('=', 1)
|
||||||
if len(vals) != 2:
|
if len(vals) != 2:
|
||||||
if len(vals) > 2: # --extended is a valid argument
|
if len(vals) > 2: # --extended is a valid argument
|
||||||
get_logger().error(f'Invalid argument format: {arg}')
|
logging.error(f'Invalid argument format: {arg}')
|
||||||
other_args.append(arg)
|
other_args.append(arg)
|
||||||
continue
|
continue
|
||||||
key, value = _fix_key_value(*vals)
|
key, value = _fix_key_value(*vals)
|
||||||
get_settings().set(key, value)
|
get_settings().set(key, value)
|
||||||
get_logger().info(f'Updated setting {key} to: "{value}"')
|
logging.info(f'Updated setting {key} to: "{value}"')
|
||||||
else:
|
else:
|
||||||
other_args.append(arg)
|
other_args.append(arg)
|
||||||
return other_args
|
return other_args
|
||||||
@ -279,7 +279,7 @@ def _fix_key_value(key: str, value: str):
|
|||||||
try:
|
try:
|
||||||
value = yaml.safe_load(value)
|
value = yaml.safe_load(value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to parse YAML for config override {key}={value}", exc_info=e)
|
logging.error(f"Failed to parse YAML for config override {key}={value}", exc_info=e)
|
||||||
return key, value
|
return key, value
|
||||||
|
|
||||||
|
|
||||||
@ -288,7 +288,7 @@ def load_yaml(review_text: str) -> dict:
|
|||||||
try:
|
try:
|
||||||
data = yaml.safe_load(review_text)
|
data = yaml.safe_load(review_text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to parse AI prediction: {e}")
|
logging.error(f"Failed to parse AI prediction: {e}")
|
||||||
data = try_fix_yaml(review_text)
|
data = try_fix_yaml(review_text)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -299,24 +299,113 @@ def try_fix_yaml(review_text: str) -> dict:
|
|||||||
review_text_lines_tmp = '\n'.join(review_text_lines[:-i])
|
review_text_lines_tmp = '\n'.join(review_text_lines[:-i])
|
||||||
try:
|
try:
|
||||||
data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader)
|
data = yaml.load(review_text_lines_tmp, Loader=yaml.SafeLoader)
|
||||||
get_logger().info(f"Successfully parsed AI prediction after removing {i} lines")
|
logging.info(f"Successfully parsed AI prediction after removing {i} lines")
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def clip_tokens(text: str, max_tokens: int) -> str:
|
||||||
|
"""
|
||||||
|
Clip the number of tokens in a string to a maximum number of tokens.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The string to clip.
|
||||||
|
max_tokens (int): The maximum number of tokens allowed in the string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The clipped string.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
|
||||||
|
try:
|
||||||
|
encoder = get_token_encoder()
|
||||||
|
num_input_tokens = len(encoder.encode(text))
|
||||||
|
if num_input_tokens <= max_tokens:
|
||||||
|
return text
|
||||||
|
num_chars = len(text)
|
||||||
|
chars_per_token = num_chars / num_input_tokens
|
||||||
|
num_output_chars = int(chars_per_token * max_tokens)
|
||||||
|
clipped_text = text[:num_output_chars]
|
||||||
|
return clipped_text
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Failed to clip tokens: {e}")
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FilePatchInfo:
|
||||||
|
base_file: str
|
||||||
|
head_file: str
|
||||||
|
patch: str
|
||||||
|
filename: str
|
||||||
|
tokens: int = -1
|
||||||
|
edit_type: EDIT_TYPE = EDIT_TYPE.MODIFIED
|
||||||
|
old_filename: str = None
|
||||||
|
language: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
def find_line_number_of_relevant_line_in_file(diff_files: List[FilePatchInfo],
|
||||||
|
relevant_file: str,
|
||||||
|
relevant_line_in_file: str) -> Tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Find the line number and absolute position of a relevant line in a file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diff_files (List[FilePatchInfo]): A list of FilePatchInfo objects representing the patches of files.
|
||||||
|
relevant_file (str): The name of the file where the relevant line is located.
|
||||||
|
relevant_line_in_file (str): The content of the relevant line.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[int, int]: A tuple containing the line number and absolute position of the relevant line in the file.
|
||||||
|
"""
|
||||||
|
position = -1
|
||||||
|
absolute_position = -1
|
||||||
|
re_hunk_header = re.compile(
|
||||||
|
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
||||||
|
|
||||||
|
for file in diff_files:
|
||||||
|
if file.filename.strip() == relevant_file:
|
||||||
|
patch = file.patch
|
||||||
|
patch_lines = patch.splitlines()
|
||||||
|
|
||||||
|
# try to find the line in the patch using difflib, with some margin of error
|
||||||
|
matches_difflib: list[str | Any] = difflib.get_close_matches(relevant_line_in_file,
|
||||||
|
patch_lines, n=3, cutoff=0.93)
|
||||||
|
if len(matches_difflib) == 1 and matches_difflib[0].startswith('+'):
|
||||||
|
relevant_line_in_file = matches_difflib[0]
|
||||||
|
|
||||||
|
delta = 0
|
||||||
|
start1, size1, start2, size2 = 0, 0, 0, 0
|
||||||
|
for i, line in enumerate(patch_lines):
|
||||||
|
if line.startswith('@@'):
|
||||||
|
delta = 0
|
||||||
|
match = re_hunk_header.match(line)
|
||||||
|
start1, size1, start2, size2 = map(int, match.groups()[:4])
|
||||||
|
elif not line.startswith('-'):
|
||||||
|
delta += 1
|
||||||
|
|
||||||
|
if relevant_line_in_file in line and line[0] != '-':
|
||||||
|
position = i
|
||||||
|
absolute_position = start2 + delta - 1
|
||||||
|
break
|
||||||
|
|
||||||
|
if position == -1 and relevant_line_in_file[0] == '+':
|
||||||
|
no_plus_line = relevant_line_in_file[1:].lstrip()
|
||||||
|
for i, line in enumerate(patch_lines):
|
||||||
|
if line.startswith('@@'):
|
||||||
|
delta = 0
|
||||||
|
match = re_hunk_header.match(line)
|
||||||
|
start1, size1, start2, size2 = map(int, match.groups()[:4])
|
||||||
|
elif not line.startswith('-'):
|
||||||
|
delta += 1
|
||||||
|
|
||||||
|
if no_plus_line in line and line[0] != '-':
|
||||||
|
# The model might add a '+' to the beginning of the relevant_line_in_file even if originally
|
||||||
|
# it's a context line
|
||||||
|
position = i
|
||||||
|
absolute_position = start2 + delta - 1
|
||||||
|
break
|
||||||
|
return position, absolute_position
|
||||||
|
|
||||||
def set_custom_labels(variables):
|
|
||||||
labels = get_settings().custom_labels
|
|
||||||
if not labels:
|
|
||||||
# set default labels
|
|
||||||
labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Refactoring', 'Enhancement', 'Documentation', 'Other']
|
|
||||||
labels_list = "\n - ".join(labels) if labels else ""
|
|
||||||
labels_list = f" - {labels_list}" if labels_list else ""
|
|
||||||
variables["custom_labels"] = labels_list
|
|
||||||
return
|
|
||||||
final_labels = ""
|
|
||||||
for k, v in labels.items():
|
|
||||||
final_labels += f" - {k} ({v['description']})\n"
|
|
||||||
variables["custom_labels"] = final_labels
|
|
||||||
variables["custom_labels_examples"] = f" - {list(labels.keys())[0]}"
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent, commands
|
from pr_agent.agent.pr_agent import PRAgent, commands
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import setup_logger
|
|
||||||
|
|
||||||
setup_logger()
|
|
||||||
|
|
||||||
def run(inargs=None):
|
def run(inargs=None):
|
||||||
parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage=
|
parser = argparse.ArgumentParser(description='AI based pull request analyzer', usage=
|
||||||
@ -18,7 +17,6 @@ For example:
|
|||||||
- cli.py --pr_url=... improve
|
- cli.py --pr_url=... improve
|
||||||
- cli.py --pr_url=... ask "write me a poem about this PR"
|
- cli.py --pr_url=... ask "write me a poem about this PR"
|
||||||
- cli.py --pr_url=... reflect
|
- cli.py --pr_url=... reflect
|
||||||
- cli.py --issue_url=... similar_issue
|
|
||||||
|
|
||||||
Supported commands:
|
Supported commands:
|
||||||
-review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement.
|
-review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement.
|
||||||
@ -39,21 +37,14 @@ Configuration:
|
|||||||
To edit any configuration parameter from 'configuration.toml', just add -config_path=<value>.
|
To edit any configuration parameter from 'configuration.toml', just add -config_path=<value>.
|
||||||
For example: 'python cli.py --pr_url=... review --pr_reviewer.extra_instructions="focus on the file: ..."'
|
For example: 'python cli.py --pr_url=... review --pr_reviewer.extra_instructions="focus on the file: ..."'
|
||||||
""")
|
""")
|
||||||
parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', default=None)
|
parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True)
|
||||||
parser.add_argument('--issue_url', type=str, help='The URL of the Issue to review', default=None)
|
|
||||||
parser.add_argument('command', type=str, help='The', choices=commands, default='review')
|
parser.add_argument('command', type=str, help='The', choices=commands, default='review')
|
||||||
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
|
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
|
||||||
args = parser.parse_args(inargs)
|
args = parser.parse_args(inargs)
|
||||||
if not args.pr_url and not args.issue_url:
|
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
||||||
parser.print_help()
|
|
||||||
return
|
|
||||||
|
|
||||||
command = args.command.lower()
|
command = args.command.lower()
|
||||||
get_settings().set("CONFIG.CLI_MODE", True)
|
get_settings().set("CONFIG.CLI_MODE", True)
|
||||||
if args.issue_url:
|
result = asyncio.run(PRAgent().handle_request(args.pr_url, command + " " + " ".join(args.rest)))
|
||||||
result = asyncio.run(PRAgent().handle_request(args.issue_url, command + " " + " ".join(args.rest)))
|
|
||||||
else:
|
|
||||||
result = asyncio.run(PRAgent().handle_request(args.pr_url, command + " " + " ".join(args.rest)))
|
|
||||||
if not result:
|
if not result:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
@ -14,7 +14,6 @@ global_settings = Dynaconf(
|
|||||||
settings_files=[join(current_dir, f) for f in [
|
settings_files=[join(current_dir, f) for f in [
|
||||||
"settings/.secrets.toml",
|
"settings/.secrets.toml",
|
||||||
"settings/configuration.toml",
|
"settings/configuration.toml",
|
||||||
"settings/ignore.toml",
|
|
||||||
"settings/language_extensions.toml",
|
"settings/language_extensions.toml",
|
||||||
"settings/pr_reviewer_prompts.toml",
|
"settings/pr_reviewer_prompts.toml",
|
||||||
"settings/pr_questions_prompts.toml",
|
"settings/pr_questions_prompts.toml",
|
||||||
@ -23,10 +22,7 @@ global_settings = Dynaconf(
|
|||||||
"settings/pr_sort_code_suggestions_prompts.toml",
|
"settings/pr_sort_code_suggestions_prompts.toml",
|
||||||
"settings/pr_information_from_user_prompts.toml",
|
"settings/pr_information_from_user_prompts.toml",
|
||||||
"settings/pr_update_changelog_prompts.toml",
|
"settings/pr_update_changelog_prompts.toml",
|
||||||
"settings/pr_custom_labels.toml",
|
"settings_prod/.secrets.toml"
|
||||||
"settings/pr_add_docs.toml",
|
|
||||||
"settings_prod/.secrets.toml",
|
|
||||||
"settings/custom_labels.toml"
|
|
||||||
]]
|
]]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,8 +5,6 @@ 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
|
||||||
from pr_agent.git_providers.local_git_provider import LocalGitProvider
|
from pr_agent.git_providers.local_git_provider import LocalGitProvider
|
||||||
from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider
|
from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider
|
||||||
from pr_agent.git_providers.gerrit_provider import GerritProvider
|
|
||||||
|
|
||||||
|
|
||||||
_GIT_PROVIDERS = {
|
_GIT_PROVIDERS = {
|
||||||
'github': GithubProvider,
|
'github': GithubProvider,
|
||||||
@ -14,8 +12,7 @@ _GIT_PROVIDERS = {
|
|||||||
'bitbucket': BitbucketProvider,
|
'bitbucket': BitbucketProvider,
|
||||||
'azure': AzureDevopsProvider,
|
'azure': AzureDevopsProvider,
|
||||||
'codecommit': CodeCommitProvider,
|
'codecommit': CodeCommitProvider,
|
||||||
'local' : LocalGitProvider,
|
'local' : LocalGitProvider
|
||||||
'gerrit': GerritProvider,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_git_provider():
|
def get_git_provider():
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ..log import get_logger
|
|
||||||
|
|
||||||
AZURE_DEVOPS_AVAILABLE = True
|
AZURE_DEVOPS_AVAILABLE = True
|
||||||
try:
|
try:
|
||||||
from msrest.authentication import BasicAuthentication
|
from msrest.authentication import BasicAuthentication
|
||||||
@ -14,11 +13,9 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
AZURE_DEVOPS_AVAILABLE = False
|
AZURE_DEVOPS_AVAILABLE = False
|
||||||
|
|
||||||
from ..algo.pr_processing import clip_tokens
|
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from ..algo.utils import load_large_diff
|
from ..algo.utils import load_large_diff, FilePatchInfo, EDIT_TYPE, clip_tokens
|
||||||
from ..algo.language_handler import is_valid_file
|
from ..algo.language_handler import is_valid_file
|
||||||
from .git_provider import EDIT_TYPE, FilePatchInfo
|
|
||||||
|
|
||||||
|
|
||||||
class AzureDevopsProvider:
|
class AzureDevopsProvider:
|
||||||
@ -39,8 +36,7 @@ class AzureDevopsProvider:
|
|||||||
self.set_pr(pr_url)
|
self.set_pr(pr_url)
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
def is_supported(self, capability: str) -> bool:
|
||||||
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels',
|
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels', 'remove_initial_comment']:
|
||||||
'remove_initial_comment', 'gfm_markdown']:
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -56,7 +52,7 @@ class AzureDevopsProvider:
|
|||||||
path=".pr_agent.toml")
|
path=".pr_agent.toml")
|
||||||
return contents
|
return contents
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception("get repo settings error")
|
logging.exception("get repo settings error")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_files(self):
|
def get_files(self):
|
||||||
@ -89,8 +85,6 @@ class AzureDevopsProvider:
|
|||||||
changes_obj = self.azure_devops_client.get_changes(project=self.workspace_slug,
|
changes_obj = self.azure_devops_client.get_changes(project=self.workspace_slug,
|
||||||
repository_id=self.repo_slug, commit_id=c.commit_id)
|
repository_id=self.repo_slug, commit_id=c.commit_id)
|
||||||
for i in changes_obj.changes:
|
for i in changes_obj.changes:
|
||||||
if(i['item']['gitObjectType'] == 'tree'):
|
|
||||||
continue
|
|
||||||
diffs.append(i['item']['path'])
|
diffs.append(i['item']['path'])
|
||||||
diff_types[i['item']['path']] = i['changeType']
|
diff_types[i['item']['path']] = i['changeType']
|
||||||
|
|
||||||
@ -101,18 +95,14 @@ class AzureDevopsProvider:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
version = GitVersionDescriptor(version=head_sha.commit_id, version_type='commit')
|
version = GitVersionDescriptor(version=head_sha.commit_id, version_type='commit')
|
||||||
try:
|
new_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug,
|
||||||
new_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug,
|
path=file,
|
||||||
path=file,
|
project=self.workspace_slug,
|
||||||
project=self.workspace_slug,
|
version_descriptor=version,
|
||||||
version_descriptor=version,
|
download=False,
|
||||||
download=False,
|
include_content=True)
|
||||||
include_content=True)
|
|
||||||
|
|
||||||
new_file_content_str = new_file_content_str.content
|
new_file_content_str = new_file_content_str.content
|
||||||
except Exception as error:
|
|
||||||
get_logger().error("Failed to retrieve new file content of %s at version %s. Error: %s", file, version, str(error))
|
|
||||||
new_file_content_str = ""
|
|
||||||
|
|
||||||
edit_type = EDIT_TYPE.MODIFIED
|
edit_type = EDIT_TYPE.MODIFIED
|
||||||
if diff_types[file] == 'add':
|
if diff_types[file] == 'add':
|
||||||
@ -123,17 +113,13 @@ class AzureDevopsProvider:
|
|||||||
edit_type = EDIT_TYPE.RENAMED
|
edit_type = EDIT_TYPE.RENAMED
|
||||||
|
|
||||||
version = GitVersionDescriptor(version=base_sha.commit_id, version_type='commit')
|
version = GitVersionDescriptor(version=base_sha.commit_id, version_type='commit')
|
||||||
try:
|
original_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug,
|
||||||
original_file_content_str = self.azure_devops_client.get_item(repository_id=self.repo_slug,
|
|
||||||
path=file,
|
path=file,
|
||||||
project=self.workspace_slug,
|
project=self.workspace_slug,
|
||||||
version_descriptor=version,
|
version_descriptor=version,
|
||||||
download=False,
|
download=False,
|
||||||
include_content=True)
|
include_content=True)
|
||||||
original_file_content_str = original_file_content_str.content
|
original_file_content_str = original_file_content_str.content
|
||||||
except Exception as error:
|
|
||||||
get_logger().error("Failed to retrieve original file content of %s at version %s. Error: %s", file, version, str(error))
|
|
||||||
original_file_content_str = ""
|
|
||||||
|
|
||||||
patch = load_large_diff(file, new_file_content_str, original_file_content_str)
|
patch = load_large_diff(file, new_file_content_str, original_file_content_str)
|
||||||
|
|
||||||
@ -167,7 +153,7 @@ class AzureDevopsProvider:
|
|||||||
pull_request_id=self.pr_num,
|
pull_request_id=self.pr_num,
|
||||||
git_pull_request_to_update=updated_pr)
|
git_pull_request_to_update=updated_pr)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Could not update pull request {self.pr_num} description: {e}")
|
logging.exception(f"Could not update pull request {self.pr_num} description: {e}")
|
||||||
|
|
||||||
def remove_initial_comment(self):
|
def remove_initial_comment(self):
|
||||||
return "" # not implemented yet
|
return "" # not implemented yet
|
||||||
@ -236,6 +222,9 @@ class AzureDevopsProvider:
|
|||||||
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)
|
||||||
|
|
||||||
|
if 'azure.com' not in parsed_url.netloc:
|
||||||
|
raise ValueError("The provided URL is not a valid Azure DevOps URL")
|
||||||
|
|
||||||
path_parts = parsed_url.path.strip('/').split('/')
|
path_parts = parsed_url.path.strip('/').split('/')
|
||||||
|
|
||||||
if len(path_parts) < 6 or path_parts[4] != 'pullrequest':
|
if len(path_parts) < 6 or path_parts[4] != 'pullrequest':
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -6,10 +7,9 @@ import requests
|
|||||||
from atlassian.bitbucket import Cloud
|
from atlassian.bitbucket import Cloud
|
||||||
from starlette_context import context
|
from starlette_context import context
|
||||||
|
|
||||||
from ..algo.pr_processing import find_line_number_of_relevant_line_in_file
|
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from ..log import get_logger
|
from .git_provider import GitProvider
|
||||||
from .git_provider import FilePatchInfo, GitProvider
|
from ..algo.utils import FilePatchInfo
|
||||||
|
|
||||||
|
|
||||||
class BitbucketProvider(GitProvider):
|
class BitbucketProvider(GitProvider):
|
||||||
@ -36,8 +36,9 @@ class BitbucketProvider(GitProvider):
|
|||||||
self.incremental = incremental
|
self.incremental = incremental
|
||||||
if pr_url:
|
if pr_url:
|
||||||
self.set_pr(pr_url)
|
self.set_pr(pr_url)
|
||||||
self.bitbucket_comment_api_url = self.pr._BitbucketBase__data["links"]["comments"]["href"]
|
self.bitbucket_comment_api_url = self.pr._BitbucketBase__data["links"][
|
||||||
self.bitbucket_pull_request_api_url = self.pr._BitbucketBase__data["links"]['self']['href']
|
"comments"
|
||||||
|
]["href"]
|
||||||
|
|
||||||
def get_repo_settings(self):
|
def get_repo_settings(self):
|
||||||
try:
|
try:
|
||||||
@ -61,14 +62,14 @@ class BitbucketProvider(GitProvider):
|
|||||||
|
|
||||||
if not relevant_lines_start or relevant_lines_start == -1:
|
if not relevant_lines_start or relevant_lines_start == -1:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().exception(
|
logging.exception(
|
||||||
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
|
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if relevant_lines_end < relevant_lines_start:
|
if relevant_lines_end < relevant_lines_start:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().exception(
|
logging.exception(
|
||||||
f"Failed to publish code suggestion, "
|
f"Failed to publish code suggestion, "
|
||||||
f"relevant_lines_end is {relevant_lines_end} and "
|
f"relevant_lines_end is {relevant_lines_end} and "
|
||||||
f"relevant_lines_start is {relevant_lines_start}"
|
f"relevant_lines_start is {relevant_lines_start}"
|
||||||
@ -97,11 +98,16 @@ class BitbucketProvider(GitProvider):
|
|||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().error(f"Failed to publish code suggestion, error: {e}")
|
logging.error(f"Failed to publish code suggestion, error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
def is_supported(self, capability: str) -> bool:
|
||||||
if capability in ['get_issue_comments', 'publish_inline_comments', 'get_labels', 'gfm_markdown']:
|
if capability in [
|
||||||
|
"get_issue_comments",
|
||||||
|
"create_inline_comment",
|
||||||
|
"publish_inline_comments",
|
||||||
|
"get_labels",
|
||||||
|
]:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -144,32 +150,19 @@ class BitbucketProvider(GitProvider):
|
|||||||
for comment in self.temp_comments:
|
for comment in self.temp_comments:
|
||||||
self.pr.delete(f"comments/{comment}")
|
self.pr.delete(f"comments/{comment}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to remove temp comments, error: {e}")
|
logging.exception(f"Failed to remove temp comments, error: {e}")
|
||||||
|
|
||||||
|
def publish_inline_comment(
|
||||||
# funtion to create_inline_comment
|
self, comment: str, from_line: int, to_line: int, file: str
|
||||||
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
):
|
||||||
position, absolute_position = find_line_number_of_relevant_line_in_file(self.get_diff_files(), relevant_file.strip('`'), relevant_line_in_file)
|
payload = json.dumps(
|
||||||
if position == -1:
|
{
|
||||||
if get_settings().config.verbosity_level >= 2:
|
"content": {
|
||||||
get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
"raw": comment,
|
||||||
subject_type = "FILE"
|
},
|
||||||
else:
|
"inline": {"to": from_line, "path": file},
|
||||||
subject_type = "LINE"
|
}
|
||||||
path = relevant_file.strip()
|
)
|
||||||
return dict(body=body, path=path, position=absolute_position) if subject_type == "LINE" else {}
|
|
||||||
|
|
||||||
|
|
||||||
def publish_inline_comment(self, comment: str, from_line: int, file: str):
|
|
||||||
payload = json.dumps( {
|
|
||||||
"content": {
|
|
||||||
"raw": comment,
|
|
||||||
},
|
|
||||||
"inline": {
|
|
||||||
"to": from_line,
|
|
||||||
"path": file
|
|
||||||
},
|
|
||||||
})
|
|
||||||
response = requests.request(
|
response = requests.request(
|
||||||
"POST", self.bitbucket_comment_api_url, data=payload, headers=self.headers
|
"POST", self.bitbucket_comment_api_url, data=payload, headers=self.headers
|
||||||
)
|
)
|
||||||
@ -177,7 +170,9 @@ class BitbucketProvider(GitProvider):
|
|||||||
|
|
||||||
def publish_inline_comments(self, comments: list[dict]):
|
def publish_inline_comments(self, comments: list[dict]):
|
||||||
for comment in comments:
|
for comment in comments:
|
||||||
self.publish_inline_comment(comment['body'], comment['start_line'], comment['path'])
|
self.publish_inline_comment(
|
||||||
|
comment["body"], comment["start_line"], comment["line"], comment["path"]
|
||||||
|
)
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.pr.title
|
return self.pr.title
|
||||||
@ -244,22 +239,16 @@ class BitbucketProvider(GitProvider):
|
|||||||
|
|
||||||
def get_commit_messages(self):
|
def get_commit_messages(self):
|
||||||
return "" # not implemented yet
|
return "" # not implemented yet
|
||||||
|
|
||||||
# bitbucket does not support labels
|
|
||||||
def publish_description(self, pr_title: str, description: str):
|
|
||||||
payload = json.dumps({
|
|
||||||
"description": description,
|
|
||||||
"title": pr_title
|
|
||||||
|
|
||||||
})
|
def publish_description(self, pr_title: str, pr_body: str):
|
||||||
|
|
||||||
response = requests.request("PUT", self.bitbucket_pull_request_api_url, headers=self.headers, data=payload)
|
|
||||||
return response
|
|
||||||
|
|
||||||
# bitbucket does not support labels
|
|
||||||
def publish_labels(self, pr_types: list):
|
|
||||||
pass
|
pass
|
||||||
|
def create_inline_comment(
|
||||||
# bitbucket does not support labels
|
self, body: str, relevant_file: str, relevant_line_in_file: str
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def publish_labels(self, labels):
|
||||||
|
pass
|
||||||
|
|
||||||
def get_labels(self):
|
def get_labels(self):
|
||||||
pass
|
pass
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import boto3
|
try: # Allow this module to be imported without requiring boto3
|
||||||
import botocore
|
import boto3
|
||||||
|
import botocore
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
boto3 = None
|
||||||
|
botocore = None
|
||||||
|
|
||||||
class CodeCommitDifferencesResponse:
|
class CodeCommitDifferencesResponse:
|
||||||
"""
|
"""
|
||||||
@ -54,22 +57,17 @@ class CodeCommitClient:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.boto_client = None
|
self.boto_client = None
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
|
||||||
if capability in ["gfm_markdown"]:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _connect_boto_client(self):
|
def _connect_boto_client(self):
|
||||||
try:
|
try:
|
||||||
self.boto_client = boto3.client("codecommit")
|
self.boto_client = boto3.client("codecommit")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Failed to connect to AWS CodeCommit: {e}") from e
|
raise ValueError(f"Failed to connect to AWS CodeCommit: {e}")
|
||||||
|
|
||||||
def get_differences(self, repo_name: int, destination_commit: str, source_commit: str):
|
def get_differences(self, repo_name: int, destination_commit: str, source_commit: str):
|
||||||
"""
|
"""
|
||||||
Get the differences between two commits in CodeCommit.
|
Get the differences between two commits in CodeCommit.
|
||||||
|
|
||||||
Args:
|
Parameters:
|
||||||
- repo_name: Name of the repository
|
- repo_name: Name of the repository
|
||||||
- destination_commit: Commit hash you want to merge into (the "before" hash) (usually on the main or master branch)
|
- destination_commit: Commit hash you want to merge into (the "before" hash) (usually on the main or master branch)
|
||||||
- source_commit: Commit hash of the code you are adding (the "after" branch)
|
- source_commit: Commit hash of the code you are adding (the "after" branch)
|
||||||
@ -78,8 +76,8 @@ class CodeCommitClient:
|
|||||||
- List of CodeCommitDifferencesResponse objects
|
- List of CodeCommitDifferencesResponse objects
|
||||||
|
|
||||||
Boto3 Documentation:
|
Boto3 Documentation:
|
||||||
- aws codecommit get-differences
|
aws codecommit get-differences
|
||||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_differences.html
|
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_differences.html
|
||||||
"""
|
"""
|
||||||
if self.boto_client is None:
|
if self.boto_client is None:
|
||||||
self._connect_boto_client()
|
self._connect_boto_client()
|
||||||
@ -95,11 +93,7 @@ class CodeCommitClient:
|
|||||||
):
|
):
|
||||||
differences.extend(page.get("differences", []))
|
differences.extend(page.get("differences", []))
|
||||||
except botocore.exceptions.ClientError as e:
|
except botocore.exceptions.ClientError as e:
|
||||||
if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException':
|
raise ValueError(f"Failed to retrieve differences from CodeCommit PR #{self.pr_num}") from e
|
||||||
raise ValueError(f"CodeCommit cannot retrieve differences: Repository does not exist: {repo_name}") from e
|
|
||||||
raise ValueError(f"CodeCommit cannot retrieve differences for {source_commit}..{destination_commit}") from e
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"CodeCommit cannot retrieve differences for {source_commit}..{destination_commit}") from e
|
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
for json in differences:
|
for json in differences:
|
||||||
@ -110,7 +104,7 @@ class CodeCommitClient:
|
|||||||
"""
|
"""
|
||||||
Retrieve a file from CodeCommit.
|
Retrieve a file from CodeCommit.
|
||||||
|
|
||||||
Args:
|
Parameters:
|
||||||
- repo_name: Name of the repository
|
- repo_name: Name of the repository
|
||||||
- file_path: Path to the file you are retrieving
|
- file_path: Path to the file you are retrieving
|
||||||
- sha_hash: Commit hash of the file you are retrieving
|
- sha_hash: Commit hash of the file you are retrieving
|
||||||
@ -119,8 +113,8 @@ class CodeCommitClient:
|
|||||||
- File contents
|
- File contents
|
||||||
|
|
||||||
Boto3 Documentation:
|
Boto3 Documentation:
|
||||||
- aws codecommit get_file
|
aws codecommit get_file
|
||||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_file.html
|
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_file.html
|
||||||
"""
|
"""
|
||||||
if not file_path:
|
if not file_path:
|
||||||
return ""
|
return ""
|
||||||
@ -131,8 +125,6 @@ class CodeCommitClient:
|
|||||||
try:
|
try:
|
||||||
response = self.boto_client.get_file(repositoryName=repo_name, commitSpecifier=sha_hash, filePath=file_path)
|
response = self.boto_client.get_file(repositoryName=repo_name, commitSpecifier=sha_hash, filePath=file_path)
|
||||||
except botocore.exceptions.ClientError as e:
|
except botocore.exceptions.ClientError as e:
|
||||||
if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException':
|
|
||||||
raise ValueError(f"CodeCommit cannot retrieve PR: Repository does not exist: {repo_name}") from e
|
|
||||||
# if the file does not exist, but is flagged as optional, then return an empty string
|
# if the file does not exist, but is flagged as optional, then return an empty string
|
||||||
if optional and e.response["Error"]["Code"] == 'FileDoesNotExistException':
|
if optional and e.response["Error"]["Code"] == 'FileDoesNotExistException':
|
||||||
return ""
|
return ""
|
||||||
@ -144,20 +136,19 @@ class CodeCommitClient:
|
|||||||
|
|
||||||
return response.get("fileContent", "")
|
return response.get("fileContent", "")
|
||||||
|
|
||||||
def get_pr(self, repo_name: str, pr_number: int):
|
def get_pr(self, pr_number: int):
|
||||||
"""
|
"""
|
||||||
Get a information about a CodeCommit PR.
|
Get a information about a CodeCommit PR.
|
||||||
|
|
||||||
Args:
|
Parameters:
|
||||||
- repo_name: Name of the repository
|
|
||||||
- pr_number: The PR number you are requesting
|
- pr_number: The PR number you are requesting
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- CodeCommitPullRequestResponse object
|
- CodeCommitPullRequestResponse object
|
||||||
|
|
||||||
Boto3 Documentation:
|
Boto3 Documentation:
|
||||||
- aws codecommit get_pull_request
|
aws codecommit get_pull_request
|
||||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_pull_request.html
|
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/get_pull_request.html
|
||||||
"""
|
"""
|
||||||
if self.boto_client is None:
|
if self.boto_client is None:
|
||||||
self._connect_boto_client()
|
self._connect_boto_client()
|
||||||
@ -167,8 +158,6 @@ class CodeCommitClient:
|
|||||||
except botocore.exceptions.ClientError as e:
|
except botocore.exceptions.ClientError as e:
|
||||||
if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException':
|
if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException':
|
||||||
raise ValueError(f"CodeCommit cannot retrieve PR: PR number does not exist: {pr_number}") from e
|
raise ValueError(f"CodeCommit cannot retrieve PR: PR number does not exist: {pr_number}") from e
|
||||||
if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException':
|
|
||||||
raise ValueError(f"CodeCommit cannot retrieve PR: Repository does not exist: {repo_name}") from e
|
|
||||||
raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}: boto client error") from e
|
raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}: boto client error") from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}") from e
|
raise ValueError(f"CodeCommit cannot retrieve PR: {pr_number}") from e
|
||||||
@ -178,95 +167,35 @@ class CodeCommitClient:
|
|||||||
|
|
||||||
return CodeCommitPullRequestResponse(response.get("pullRequest", {}))
|
return CodeCommitPullRequestResponse(response.get("pullRequest", {}))
|
||||||
|
|
||||||
def publish_description(self, pr_number: int, pr_title: str, pr_body: str):
|
def publish_comment(self, repo_name: str, pr_number: int, destination_commit: str, source_commit: str, comment: str):
|
||||||
"""
|
|
||||||
Set the title and description on a pull request
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- pr_number: the AWS CodeCommit pull request number
|
|
||||||
- pr_title: title of the pull request
|
|
||||||
- pr_body: body of the pull request
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- None
|
|
||||||
|
|
||||||
Boto3 Documentation:
|
|
||||||
- aws codecommit update_pull_request_title
|
|
||||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/update_pull_request_title.html
|
|
||||||
- aws codecommit update_pull_request_description
|
|
||||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/update_pull_request_description.html
|
|
||||||
"""
|
|
||||||
if self.boto_client is None:
|
|
||||||
self._connect_boto_client()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.boto_client.update_pull_request_title(pullRequestId=str(pr_number), title=pr_title)
|
|
||||||
self.boto_client.update_pull_request_description(pullRequestId=str(pr_number), description=pr_body)
|
|
||||||
except botocore.exceptions.ClientError as e:
|
|
||||||
if e.response["Error"]["Code"] == 'PullRequestDoesNotExistException':
|
|
||||||
raise ValueError(f"PR number does not exist: {pr_number}") from e
|
|
||||||
if e.response["Error"]["Code"] == 'InvalidTitleException':
|
|
||||||
raise ValueError(f"Invalid title for PR number: {pr_number}") from e
|
|
||||||
if e.response["Error"]["Code"] == 'InvalidDescriptionException':
|
|
||||||
raise ValueError(f"Invalid description for PR number: {pr_number}") from e
|
|
||||||
if e.response["Error"]["Code"] == 'PullRequestAlreadyClosedException':
|
|
||||||
raise ValueError(f"PR is already closed: PR number: {pr_number}") from e
|
|
||||||
raise ValueError(f"Boto3 client error calling publish_description") from e
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"Error calling publish_description") from e
|
|
||||||
|
|
||||||
def publish_comment(self, repo_name: str, pr_number: int, destination_commit: str, source_commit: str, comment: str, annotation_file: str = None, annotation_line: int = None):
|
|
||||||
"""
|
"""
|
||||||
Publish a comment to a pull request
|
Publish a comment to a pull request
|
||||||
|
|
||||||
Args:
|
Parameters:
|
||||||
- repo_name: name of the repository
|
- repo_name: name of the repository
|
||||||
- pr_number: number of the pull request
|
- pr_number: number of the pull request
|
||||||
- destination_commit: The commit hash you want to merge into (the "before" hash) (usually on the main or master branch)
|
- destination_commit: The commit hash you want to merge into (the "before" hash) (usually on the main or master branch)
|
||||||
- source_commit: The commit hash of the code you are adding (the "after" branch)
|
- source_commit: The commit hash of the code you are adding (the "after" branch)
|
||||||
- comment: The comment you want to publish
|
- pr_comment: comment
|
||||||
- annotation_file: The file you want to annotate (optional)
|
|
||||||
- annotation_line: The line number you want to annotate (optional)
|
|
||||||
|
|
||||||
Comment annotations for CodeCommit are different than GitHub.
|
|
||||||
CodeCommit only designates the starting line number for the comment.
|
|
||||||
It does not support the ending line number to highlight a range of lines.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- None
|
- None
|
||||||
|
|
||||||
Boto3 Documentation:
|
Boto3 Documentation:
|
||||||
- aws codecommit post_comment_for_pull_request
|
aws codecommit post_comment_for_pull_request
|
||||||
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_pull_request.html
|
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_pull_request.html
|
||||||
"""
|
"""
|
||||||
if self.boto_client is None:
|
if self.boto_client is None:
|
||||||
self._connect_boto_client()
|
self._connect_boto_client()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# If the comment has code annotations,
|
self.boto_client.post_comment_for_pull_request(
|
||||||
# then set the file path and line number in the location dictionary
|
pullRequestId=str(pr_number),
|
||||||
if annotation_file and annotation_line:
|
repositoryName=repo_name,
|
||||||
self.boto_client.post_comment_for_pull_request(
|
beforeCommitId=destination_commit,
|
||||||
pullRequestId=str(pr_number),
|
afterCommitId=source_commit,
|
||||||
repositoryName=repo_name,
|
content=comment,
|
||||||
beforeCommitId=destination_commit,
|
)
|
||||||
afterCommitId=source_commit,
|
|
||||||
content=comment,
|
|
||||||
location={
|
|
||||||
"filePath": annotation_file,
|
|
||||||
"filePosition": annotation_line,
|
|
||||||
"relativeFileVersion": "AFTER",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# The comment does not have code annotations
|
|
||||||
self.boto_client.post_comment_for_pull_request(
|
|
||||||
pullRequestId=str(pr_number),
|
|
||||||
repositoryName=repo_name,
|
|
||||||
beforeCommitId=destination_commit,
|
|
||||||
afterCommitId=source_commit,
|
|
||||||
content=comment,
|
|
||||||
)
|
|
||||||
except botocore.exceptions.ClientError as e:
|
except botocore.exceptions.ClientError as e:
|
||||||
if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException':
|
if e.response["Error"]["Code"] == 'RepositoryDoesNotExistException':
|
||||||
raise ValueError(f"Repository does not exist: {repo_name}") from e
|
raise ValueError(f"Repository does not exist: {repo_name}") from e
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@ -7,9 +7,8 @@ from urllib.parse import urlparse
|
|||||||
from pr_agent.git_providers.codecommit_client import CodeCommitClient
|
from pr_agent.git_providers.codecommit_client import CodeCommitClient
|
||||||
|
|
||||||
from ..algo.language_handler import is_valid_file, language_extension_map
|
from ..algo.language_handler import is_valid_file, language_extension_map
|
||||||
from ..algo.utils import load_large_diff
|
from ..algo.utils import EDIT_TYPE, FilePatchInfo, load_large_diff
|
||||||
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
|
from .git_provider import GitProvider
|
||||||
from ..log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class PullRequestCCMimic:
|
class PullRequestCCMimic:
|
||||||
@ -73,7 +72,6 @@ class CodeCommitProvider(GitProvider):
|
|||||||
"create_inline_comment",
|
"create_inline_comment",
|
||||||
"publish_inline_comments",
|
"publish_inline_comments",
|
||||||
"get_labels",
|
"get_labels",
|
||||||
"gfm_markdown"
|
|
||||||
]:
|
]:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@ -154,63 +152,26 @@ class CodeCommitProvider(GitProvider):
|
|||||||
return self.diff_files
|
return self.diff_files
|
||||||
|
|
||||||
def publish_description(self, pr_title: str, pr_body: str):
|
def publish_description(self, pr_title: str, pr_body: str):
|
||||||
try:
|
return "" # not implemented yet
|
||||||
self.codecommit_client.publish_description(
|
|
||||||
pr_number=self.pr_num,
|
|
||||||
pr_title=pr_title,
|
|
||||||
pr_body=CodeCommitProvider._add_additional_newlines(pr_body),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"CodeCommit Cannot publish description for PR: {self.pr_num}") from e
|
|
||||||
|
|
||||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||||
if is_temporary:
|
if is_temporary:
|
||||||
get_logger().info(pr_comment)
|
logging.info(pr_comment)
|
||||||
return
|
return
|
||||||
|
|
||||||
pr_comment = CodeCommitProvider._remove_markdown_html(pr_comment)
|
|
||||||
pr_comment = CodeCommitProvider._add_additional_newlines(pr_comment)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.codecommit_client.publish_comment(
|
self.codecommit_client.publish_comment(
|
||||||
repo_name=self.repo_name,
|
repo_name=self.repo_name,
|
||||||
pr_number=self.pr_num,
|
pr_number=str(self.pr_num),
|
||||||
destination_commit=self.pr.destination_commit,
|
destination_commit=self.pr.destination_commit,
|
||||||
source_commit=self.pr.source_commit,
|
source_commit=self.pr.source_commit,
|
||||||
comment=pr_comment,
|
comment=pr_comment,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"CodeCommit Cannot publish comment for PR: {self.pr_num}") from e
|
raise ValueError(f"CodeCommit Cannot post comment for PR: {self.pr_num}") from e
|
||||||
|
|
||||||
def publish_code_suggestions(self, code_suggestions: list) -> bool:
|
def publish_code_suggestions(self, code_suggestions: list) -> bool:
|
||||||
counter = 1
|
return [""] # not implemented yet
|
||||||
for suggestion in code_suggestions:
|
|
||||||
# Verify that each suggestion has the required keys
|
|
||||||
if not all(key in suggestion for key in ["body", "relevant_file", "relevant_lines_start"]):
|
|
||||||
get_logger().warning(f"Skipping code suggestion #{counter}: Each suggestion must have 'body', 'relevant_file', 'relevant_lines_start' keys")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Publish the code suggestion to CodeCommit
|
|
||||||
try:
|
|
||||||
get_logger().debug(f"Code Suggestion #{counter} in file: {suggestion['relevant_file']}: {suggestion['relevant_lines_start']}")
|
|
||||||
self.codecommit_client.publish_comment(
|
|
||||||
repo_name=self.repo_name,
|
|
||||||
pr_number=self.pr_num,
|
|
||||||
destination_commit=self.pr.destination_commit,
|
|
||||||
source_commit=self.pr.source_commit,
|
|
||||||
comment=suggestion["body"],
|
|
||||||
annotation_file=suggestion["relevant_file"],
|
|
||||||
annotation_line=suggestion["relevant_lines_start"],
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"CodeCommit Cannot publish code suggestions for PR: {self.pr_num}") from e
|
|
||||||
|
|
||||||
counter += 1
|
|
||||||
|
|
||||||
# The calling function passes in a list of code suggestions, and this function publishes each suggestion one at a time.
|
|
||||||
# If we were to return False here, the calling function will attempt to publish the same list of code suggestions again, one at a time.
|
|
||||||
# Since this function publishes the suggestions one at a time anyway, we always return True here to avoid the retry.
|
|
||||||
return True
|
|
||||||
|
|
||||||
def publish_labels(self, labels):
|
def publish_labels(self, labels):
|
||||||
return [""] # not implemented yet
|
return [""] # not implemented yet
|
||||||
@ -222,7 +183,6 @@ class CodeCommitProvider(GitProvider):
|
|||||||
return "" # not implemented yet
|
return "" # not implemented yet
|
||||||
|
|
||||||
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html
|
|
||||||
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
|
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
|
||||||
|
|
||||||
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
@ -232,26 +192,14 @@ class CodeCommitProvider(GitProvider):
|
|||||||
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
|
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.pr.title
|
return self.pr.get("title", "")
|
||||||
|
|
||||||
def get_pr_id(self):
|
|
||||||
"""
|
|
||||||
Returns the PR ID in the format: "repo_name/pr_number".
|
|
||||||
Note: This is an internal identifier for PR-Agent,
|
|
||||||
and is not the same as the CodeCommit PR identifier.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
pr_id = f"{self.repo_name}/{self.pr_num}"
|
|
||||||
return pr_id
|
|
||||||
except:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_languages(self):
|
def get_languages(self):
|
||||||
"""
|
"""
|
||||||
Returns a dictionary of languages, containing the percentage of each language used in the PR.
|
Returns a dictionary of languages, containing the percentage of each language used in the PR.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR.
|
dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR.
|
||||||
"""
|
"""
|
||||||
commit_files = self.get_files()
|
commit_files = self.get_files()
|
||||||
filenames = [ item.filename for item in commit_files ]
|
filenames = [ item.filename for item in commit_files ]
|
||||||
@ -295,29 +243,18 @@ class CodeCommitProvider(GitProvider):
|
|||||||
return self.codecommit_client.get_file(self.repo_name, settings_filename, self.pr.source_commit, optional=True)
|
return self.codecommit_client.get_file(self.repo_name, settings_filename, self.pr.source_commit, optional=True)
|
||||||
|
|
||||||
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
|
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
|
||||||
get_logger().info("CodeCommit provider does not support eyes reaction yet")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
|
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
|
||||||
get_logger().info("CodeCommit provider does not support removing reactions yet")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
|
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
|
||||||
"""
|
|
||||||
Parse the CodeCommit PR URL and return the repository name and PR number.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- pr_url: the full AWS CodeCommit pull request URL
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- Tuple[str, int]: A tuple containing the repository name and PR number.
|
|
||||||
"""
|
|
||||||
# Example PR URL:
|
# Example PR URL:
|
||||||
# https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/__MY_REPO__/pull-requests/123456"
|
# https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/__MY_REPO__/pull-requests/123456"
|
||||||
parsed_url = urlparse(pr_url)
|
parsed_url = urlparse(pr_url)
|
||||||
|
|
||||||
if not CodeCommitProvider._is_valid_codecommit_hostname(parsed_url.netloc):
|
if "us-east-1.console.aws.amazon.com" not in parsed_url.netloc:
|
||||||
raise ValueError(f"The provided URL is not a valid CodeCommit URL: {pr_url}")
|
raise ValueError(f"The provided URL is not a valid CodeCommit URL: {pr_url}")
|
||||||
|
|
||||||
path_parts = parsed_url.path.strip("/").split("/")
|
path_parts = parsed_url.path.strip("/").split("/")
|
||||||
@ -340,33 +277,17 @@ class CodeCommitProvider(GitProvider):
|
|||||||
|
|
||||||
return repo_name, pr_number
|
return repo_name, pr_number
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_valid_codecommit_hostname(hostname: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if the provided hostname is a valid AWS CodeCommit hostname.
|
|
||||||
|
|
||||||
This is not an exhaustive check of AWS region names,
|
|
||||||
but instead uses a regex to check for matching AWS region patterns.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- hostname: the hostname to check
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- bool: True if the hostname is valid, False otherwise.
|
|
||||||
"""
|
|
||||||
return re.match(r"^[a-z]{2}-(gov-)?[a-z]+-\d\.console\.aws\.amazon\.com$", hostname) is not None
|
|
||||||
|
|
||||||
def _get_pr(self):
|
def _get_pr(self):
|
||||||
response = self.codecommit_client.get_pr(self.repo_name, self.pr_num)
|
response = self.codecommit_client.get_pr(self.pr_num)
|
||||||
|
|
||||||
if len(response.targets) == 0:
|
if len(response.targets) == 0:
|
||||||
raise ValueError(f"No files found in CodeCommit PR: {self.pr_num}")
|
raise ValueError(f"No files found in CodeCommit PR: {self.pr_num}")
|
||||||
|
|
||||||
# TODO: implement support for multiple targets in one CodeCommit PR
|
# TODO: implement support for multiple commits in one CodeCommit PR
|
||||||
# for now, we are only using the first target in the PR
|
# for now, we are only using the first commit in the PR
|
||||||
if len(response.targets) > 1:
|
if len(response.targets) > 1:
|
||||||
get_logger().warning(
|
logging.warning(
|
||||||
"Multiple targets in one PR is not supported for CodeCommit yet. Continuing, using the first target only..."
|
"Multiple commits in one PR is not supported for CodeCommit yet. Continuing, using the first commit only..."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return our object that mimics PullRequest class from the PyGithub library
|
# Return our object that mimics PullRequest class from the PyGithub library
|
||||||
@ -384,52 +305,13 @@ class CodeCommitProvider(GitProvider):
|
|||||||
return "" # not implemented yet
|
return "" # not implemented yet
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _add_additional_newlines(body: str) -> str:
|
def _get_edit_type(codecommit_change_type):
|
||||||
"""
|
|
||||||
Replace single newlines in a PR body with double newlines.
|
|
||||||
|
|
||||||
CodeCommit Markdown does not seem to render as well as GitHub Markdown,
|
|
||||||
so we add additional newlines to the PR body to make it more readable in CodeCommit.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- body: the PR body
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- str: the PR body with the double newlines added
|
|
||||||
"""
|
|
||||||
return re.sub(r'(?<!\n)\n(?!\n)', '\n\n', body)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _remove_markdown_html(comment: str) -> str:
|
|
||||||
"""
|
|
||||||
Remove the HTML tags from a PR comment.
|
|
||||||
|
|
||||||
CodeCommit Markdown does not seem to render as well as GitHub Markdown,
|
|
||||||
so we remove the HTML tags from the PR comment to make it more readable in CodeCommit.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- comment: the PR comment
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- str: the PR comment with the HTML tags removed
|
|
||||||
"""
|
|
||||||
comment = comment.replace("<details>", "")
|
|
||||||
comment = comment.replace("</details>", "")
|
|
||||||
comment = comment.replace("<summary>", "")
|
|
||||||
comment = comment.replace("</summary>", "")
|
|
||||||
return comment
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_edit_type(codecommit_change_type: str):
|
|
||||||
"""
|
"""
|
||||||
Convert the CodeCommit change type string to the EDIT_TYPE enum.
|
Convert the CodeCommit change type string to the EDIT_TYPE enum.
|
||||||
The CodeCommit change type string is returned from the get_differences SDK method.
|
The CodeCommit change type string is returned from the get_differences SDK method.
|
||||||
|
|
||||||
Args:
|
|
||||||
- codecommit_change_type: the CodeCommit change type string
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- An EDIT_TYPE enum representing the modified, added, deleted, or renamed file in the PR diff.
|
An EDIT_TYPE enum representing the modified, added, deleted, or renamed file in the PR diff.
|
||||||
"""
|
"""
|
||||||
t = codecommit_change_type.upper()
|
t = codecommit_change_type.upper()
|
||||||
edit_type = None
|
edit_type = None
|
||||||
@ -450,12 +332,6 @@ class CodeCommitProvider(GitProvider):
|
|||||||
The returned extensions will include the dot "." prefix,
|
The returned extensions will include the dot "." prefix,
|
||||||
to accommodate for the dots in the existing language_extension_map settings.
|
to accommodate for the dots in the existing language_extension_map settings.
|
||||||
Filenames with no extension will return an empty string for the extension.
|
Filenames with no extension will return an empty string for the extension.
|
||||||
|
|
||||||
Args:
|
|
||||||
- filenames: a list of filenames
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- list: A list of file extensions, including the dot "." prefix.
|
|
||||||
"""
|
"""
|
||||||
extensions = []
|
extensions = []
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
@ -472,12 +348,6 @@ class CodeCommitProvider(GitProvider):
|
|||||||
Return a dictionary containing the programming language name (as the key),
|
Return a dictionary containing the programming language name (as the key),
|
||||||
and the percentage that language is used (as the value),
|
and the percentage that language is used (as the value),
|
||||||
given a list of file extensions.
|
given a list of file extensions.
|
||||||
|
|
||||||
Args:
|
|
||||||
- extensions: a list of file extensions
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- dict: A dictionary where each key is a language name and the corresponding value is the percentage of that language in the PR.
|
|
||||||
"""
|
"""
|
||||||
total_files = len(extensions)
|
total_files = len(extensions)
|
||||||
if total_files == 0:
|
if total_files == 0:
|
||||||
|
@ -1,400 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import pathlib
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import uuid
|
|
||||||
from collections import Counter, namedtuple
|
|
||||||
from pathlib import Path
|
|
||||||
from tempfile import NamedTemporaryFile, mkdtemp
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import urllib3.util
|
|
||||||
from git import Repo
|
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
|
||||||
from pr_agent.git_providers.git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
|
|
||||||
from pr_agent.git_providers.local_git_provider import PullRequestMimic
|
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
def _call(*command, **kwargs) -> (int, str, str):
|
|
||||||
res = subprocess.run(
|
|
||||||
command,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
check=True,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
return res.stdout.decode()
|
|
||||||
|
|
||||||
|
|
||||||
def clone(url, directory):
|
|
||||||
get_logger().info("Cloning %s to %s", url, directory)
|
|
||||||
stdout = _call('git', 'clone', "--depth", "1", url, directory)
|
|
||||||
get_logger().info(stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def fetch(url, refspec, cwd):
|
|
||||||
get_logger().info("Fetching %s %s", url, refspec)
|
|
||||||
stdout = _call(
|
|
||||||
'git', 'fetch', '--depth', '2', url, refspec,
|
|
||||||
cwd=cwd
|
|
||||||
)
|
|
||||||
get_logger().info(stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def checkout(cwd):
|
|
||||||
get_logger().info("Checking out")
|
|
||||||
stdout = _call('git', 'checkout', "FETCH_HEAD", cwd=cwd)
|
|
||||||
get_logger().info(stdout)
|
|
||||||
|
|
||||||
|
|
||||||
def show(*args, cwd=None):
|
|
||||||
get_logger().info("Show")
|
|
||||||
return _call('git', 'show', *args, cwd=cwd)
|
|
||||||
|
|
||||||
|
|
||||||
def diff(*args, cwd=None):
|
|
||||||
get_logger().info("Diff")
|
|
||||||
patch = _call('git', 'diff', *args, cwd=cwd)
|
|
||||||
if not patch:
|
|
||||||
get_logger().warning("No changes found")
|
|
||||||
return
|
|
||||||
return patch
|
|
||||||
|
|
||||||
|
|
||||||
def reset_local_changes(cwd):
|
|
||||||
get_logger().info("Reset local changes")
|
|
||||||
_call('git', 'checkout', "--force", cwd=cwd)
|
|
||||||
|
|
||||||
|
|
||||||
def add_comment(url: urllib3.util.Url, refspec, message):
|
|
||||||
*_, patchset, changenum = refspec.rsplit("/")
|
|
||||||
message = "'" + message.replace("'", "'\"'\"'") + "'"
|
|
||||||
return _call(
|
|
||||||
"ssh",
|
|
||||||
"-p", str(url.port),
|
|
||||||
f"{url.auth}@{url.host}",
|
|
||||||
"gerrit", "review",
|
|
||||||
"--message", message,
|
|
||||||
# "--code-review", score,
|
|
||||||
f"{patchset},{changenum}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def list_comments(url: urllib3.util.Url, refspec):
|
|
||||||
*_, patchset, _ = refspec.rsplit("/")
|
|
||||||
stdout = _call(
|
|
||||||
"ssh",
|
|
||||||
"-p", str(url.port),
|
|
||||||
f"{url.auth}@{url.host}",
|
|
||||||
"gerrit", "query",
|
|
||||||
"--comments",
|
|
||||||
"--current-patch-set", patchset,
|
|
||||||
"--format", "JSON",
|
|
||||||
)
|
|
||||||
change_set, *_ = stdout.splitlines()
|
|
||||||
return json.loads(change_set)["currentPatchSet"]["comments"]
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_repo(url: urllib3.util.Url, project, refspec):
|
|
||||||
repo_url = (f"{url.scheme}://{url.auth}@{url.host}:{url.port}/{project}")
|
|
||||||
|
|
||||||
directory = pathlib.Path(mkdtemp())
|
|
||||||
clone(repo_url, directory),
|
|
||||||
fetch(repo_url, refspec, cwd=directory)
|
|
||||||
checkout(cwd=directory)
|
|
||||||
return directory
|
|
||||||
|
|
||||||
|
|
||||||
def adopt_to_gerrit_message(message):
|
|
||||||
lines = message.splitlines()
|
|
||||||
buf = []
|
|
||||||
for line in lines:
|
|
||||||
# remove markdown formatting
|
|
||||||
line = (line.replace("*", "")
|
|
||||||
.replace("``", "`")
|
|
||||||
.replace("<details>", "")
|
|
||||||
.replace("</details>", "")
|
|
||||||
.replace("<summary>", "")
|
|
||||||
.replace("</summary>", ""))
|
|
||||||
|
|
||||||
line = line.strip()
|
|
||||||
if line.startswith('#'):
|
|
||||||
buf.append("\n" +
|
|
||||||
line.replace('#', '').removesuffix(":").strip() +
|
|
||||||
":")
|
|
||||||
continue
|
|
||||||
elif line.startswith('-'):
|
|
||||||
buf.append(line.removeprefix('-').strip())
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
buf.append(line)
|
|
||||||
return "\n".join(buf).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def add_suggestion(src_filename, context: str, start, end: int):
|
|
||||||
with (
|
|
||||||
NamedTemporaryFile("w", delete=False) as tmp,
|
|
||||||
open(src_filename, "r") as src
|
|
||||||
):
|
|
||||||
lines = src.readlines()
|
|
||||||
tmp.writelines(lines[:start - 1])
|
|
||||||
if context:
|
|
||||||
tmp.write(context)
|
|
||||||
tmp.writelines(lines[end:])
|
|
||||||
|
|
||||||
shutil.copy(tmp.name, src_filename)
|
|
||||||
os.remove(tmp.name)
|
|
||||||
|
|
||||||
|
|
||||||
def upload_patch(patch, path):
|
|
||||||
patch_server_endpoint = get_settings().get(
|
|
||||||
'gerrit.patch_server_endpoint')
|
|
||||||
patch_server_token = get_settings().get(
|
|
||||||
'gerrit.patch_server_token')
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
patch_server_endpoint,
|
|
||||||
json={
|
|
||||||
"content": patch,
|
|
||||||
"path": path,
|
|
||||||
},
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": f"Bearer {patch_server_token}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
patch_server_endpoint = patch_server_endpoint.rstrip("/")
|
|
||||||
return patch_server_endpoint + "/" + path
|
|
||||||
|
|
||||||
|
|
||||||
class GerritProvider(GitProvider):
|
|
||||||
|
|
||||||
def __init__(self, key: str, incremental=False):
|
|
||||||
self.project, self.refspec = key.split(':')
|
|
||||||
assert self.project, "Project name is required"
|
|
||||||
assert self.refspec, "Refspec is required"
|
|
||||||
base_url = get_settings().get('gerrit.url')
|
|
||||||
assert base_url, "Gerrit URL is required"
|
|
||||||
user = get_settings().get('gerrit.user')
|
|
||||||
assert user, "Gerrit user is required"
|
|
||||||
|
|
||||||
parsed = urllib3.util.parse_url(base_url)
|
|
||||||
self.parsed_url = urllib3.util.parse_url(
|
|
||||||
f"{parsed.scheme}://{user}@{parsed.host}:{parsed.port}"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.repo_path = prepare_repo(
|
|
||||||
self.parsed_url, self.project, self.refspec
|
|
||||||
)
|
|
||||||
self.repo = Repo(self.repo_path)
|
|
||||||
assert self.repo
|
|
||||||
|
|
||||||
self.pr = PullRequestMimic(self.get_pr_title(), self.get_diff_files())
|
|
||||||
|
|
||||||
def get_pr_title(self):
|
|
||||||
"""
|
|
||||||
Substitutes the branch-name as the PR-mimic title.
|
|
||||||
"""
|
|
||||||
return self.repo.branches[0].name
|
|
||||||
|
|
||||||
def get_issue_comments(self):
|
|
||||||
comments = list_comments(self.parsed_url, self.refspec)
|
|
||||||
Comments = namedtuple('Comments', ['reversed'])
|
|
||||||
Comment = namedtuple('Comment', ['body'])
|
|
||||||
return Comments([Comment(c['message']) for c in reversed(comments)])
|
|
||||||
|
|
||||||
def get_labels(self):
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Getting labels is not implemented for the gerrit provider')
|
|
||||||
|
|
||||||
def add_eyes_reaction(self, issue_comment_id: int):
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Adding reactions is not implemented for the gerrit provider')
|
|
||||||
|
|
||||||
def remove_reaction(self, issue_comment_id: int, reaction_id: int):
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Removing reactions is not implemented for the gerrit provider')
|
|
||||||
|
|
||||||
def get_commit_messages(self):
|
|
||||||
return [self.repo.head.commit.message]
|
|
||||||
|
|
||||||
def get_repo_settings(self):
|
|
||||||
try:
|
|
||||||
with open(self.repo_path / ".pr_agent.toml", 'rb') as f:
|
|
||||||
contents = f.read()
|
|
||||||
return contents
|
|
||||||
except OSError:
|
|
||||||
return b""
|
|
||||||
|
|
||||||
def get_diff_files(self) -> list[FilePatchInfo]:
|
|
||||||
diffs = self.repo.head.commit.diff(
|
|
||||||
self.repo.head.commit.parents[0], # previous commit
|
|
||||||
create_patch=True,
|
|
||||||
R=True
|
|
||||||
)
|
|
||||||
|
|
||||||
diff_files = []
|
|
||||||
for diff_item in diffs:
|
|
||||||
if diff_item.a_blob is not None:
|
|
||||||
original_file_content_str = (
|
|
||||||
diff_item.a_blob.data_stream.read().decode('utf-8')
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
original_file_content_str = "" # empty file
|
|
||||||
if diff_item.b_blob is not None:
|
|
||||||
new_file_content_str = diff_item.b_blob.data_stream.read(). \
|
|
||||||
decode('utf-8')
|
|
||||||
else:
|
|
||||||
new_file_content_str = "" # empty file
|
|
||||||
edit_type = EDIT_TYPE.MODIFIED
|
|
||||||
if diff_item.new_file:
|
|
||||||
edit_type = EDIT_TYPE.ADDED
|
|
||||||
elif diff_item.deleted_file:
|
|
||||||
edit_type = EDIT_TYPE.DELETED
|
|
||||||
elif diff_item.renamed_file:
|
|
||||||
edit_type = EDIT_TYPE.RENAMED
|
|
||||||
diff_files.append(
|
|
||||||
FilePatchInfo(
|
|
||||||
original_file_content_str,
|
|
||||||
new_file_content_str,
|
|
||||||
diff_item.diff.decode('utf-8'),
|
|
||||||
diff_item.b_path,
|
|
||||||
edit_type=edit_type,
|
|
||||||
old_filename=None
|
|
||||||
if diff_item.a_path == diff_item.b_path
|
|
||||||
else diff_item.a_path
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.diff_files = diff_files
|
|
||||||
return diff_files
|
|
||||||
|
|
||||||
def get_files(self):
|
|
||||||
diff_index = self.repo.head.commit.diff(
|
|
||||||
self.repo.head.commit.parents[0], # previous commit
|
|
||||||
R=True
|
|
||||||
)
|
|
||||||
# Get the list of changed files
|
|
||||||
diff_files = [item.a_path for item in diff_index]
|
|
||||||
return diff_files
|
|
||||||
|
|
||||||
def get_languages(self):
|
|
||||||
"""
|
|
||||||
Calculate percentage of languages in repository. Used for hunk
|
|
||||||
prioritisation.
|
|
||||||
"""
|
|
||||||
# Get all files in repository
|
|
||||||
filepaths = [Path(item.path) for item in
|
|
||||||
self.repo.tree().traverse() if item.type == 'blob']
|
|
||||||
# Identify language by file extension and count
|
|
||||||
lang_count = Counter(
|
|
||||||
ext.lstrip('.') for filepath in filepaths for ext in
|
|
||||||
[filepath.suffix.lower()])
|
|
||||||
# Convert counts to percentages
|
|
||||||
total_files = len(filepaths)
|
|
||||||
lang_percentage = {lang: count / total_files * 100 for lang, count
|
|
||||||
in lang_count.items()}
|
|
||||||
return lang_percentage
|
|
||||||
|
|
||||||
def get_pr_description_full(self):
|
|
||||||
return self.repo.head.commit.message
|
|
||||||
|
|
||||||
def get_user_id(self):
|
|
||||||
return self.repo.head.commit.author.email
|
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
|
||||||
if capability in [
|
|
||||||
# 'get_issue_comments',
|
|
||||||
'create_inline_comment',
|
|
||||||
'publish_inline_comments',
|
|
||||||
'get_labels',
|
|
||||||
'gfm_markdown'
|
|
||||||
]:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def split_suggestion(self, msg) -> tuple[str, str]:
|
|
||||||
is_code_context = False
|
|
||||||
description = []
|
|
||||||
context = []
|
|
||||||
for line in msg.splitlines():
|
|
||||||
if line.startswith('```suggestion'):
|
|
||||||
is_code_context = True
|
|
||||||
continue
|
|
||||||
if line.startswith('```'):
|
|
||||||
is_code_context = False
|
|
||||||
continue
|
|
||||||
if is_code_context:
|
|
||||||
context.append(line)
|
|
||||||
else:
|
|
||||||
description.append(
|
|
||||||
line.replace('*', '')
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
'\n'.join(description),
|
|
||||||
'\n'.join(context) + '\n' if context else ''
|
|
||||||
)
|
|
||||||
|
|
||||||
def publish_code_suggestions(self, code_suggestions: list):
|
|
||||||
msg = []
|
|
||||||
for suggestion in code_suggestions:
|
|
||||||
description, code = self.split_suggestion(suggestion['body'])
|
|
||||||
add_suggestion(
|
|
||||||
pathlib.Path(self.repo_path) / suggestion["relevant_file"],
|
|
||||||
code,
|
|
||||||
suggestion["relevant_lines_start"],
|
|
||||||
suggestion["relevant_lines_end"],
|
|
||||||
)
|
|
||||||
patch = diff(cwd=self.repo_path)
|
|
||||||
patch_id = uuid.uuid4().hex[0:4]
|
|
||||||
path = "/".join(["codium-ai", self.refspec, patch_id])
|
|
||||||
full_path = upload_patch(patch, path)
|
|
||||||
reset_local_changes(self.repo_path)
|
|
||||||
msg.append(f'* {description}\n{full_path}')
|
|
||||||
|
|
||||||
if msg:
|
|
||||||
add_comment(self.parsed_url, self.refspec, "\n".join(msg))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
|
||||||
if not is_temporary:
|
|
||||||
msg = adopt_to_gerrit_message(pr_comment)
|
|
||||||
add_comment(self.parsed_url, self.refspec, msg)
|
|
||||||
|
|
||||||
def publish_description(self, pr_title: str, pr_body: str):
|
|
||||||
msg = adopt_to_gerrit_message(pr_body)
|
|
||||||
add_comment(self.parsed_url, self.refspec, pr_title + '\n' + msg)
|
|
||||||
|
|
||||||
def publish_inline_comments(self, comments: list[dict]):
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Publishing inline comments is not implemented for the gerrit '
|
|
||||||
'provider')
|
|
||||||
|
|
||||||
def publish_inline_comment(self, body: str, relevant_file: str,
|
|
||||||
relevant_line_in_file: str):
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Publishing inline comments is not implemented for the gerrit '
|
|
||||||
'provider')
|
|
||||||
|
|
||||||
def create_inline_comment(self, body: str, relevant_file: str,
|
|
||||||
relevant_line_in_file: str):
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Creating inline comments is not implemented for the gerrit '
|
|
||||||
'provider')
|
|
||||||
|
|
||||||
def publish_labels(self, labels):
|
|
||||||
# Not applicable to the local git provider,
|
|
||||||
# but required by the interface
|
|
||||||
pass
|
|
||||||
|
|
||||||
def remove_initial_comment(self):
|
|
||||||
# remove repo, cloned in previous steps
|
|
||||||
# shutil.rmtree(self.repo_path)
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_pr_branch(self):
|
|
||||||
return self.repo.head
|
|
@ -1,29 +1,10 @@
|
|||||||
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
|
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
|
||||||
from enum import Enum
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.algo.utils import FilePatchInfo
|
||||||
|
|
||||||
|
|
||||||
class EDIT_TYPE(Enum):
|
|
||||||
ADDED = 1
|
|
||||||
DELETED = 2
|
|
||||||
MODIFIED = 3
|
|
||||||
RENAMED = 4
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class FilePatchInfo:
|
|
||||||
base_file: str
|
|
||||||
head_file: str
|
|
||||||
patch: str
|
|
||||||
filename: str
|
|
||||||
tokens: int = -1
|
|
||||||
edit_type: EDIT_TYPE = EDIT_TYPE.MODIFIED
|
|
||||||
old_filename: str = None
|
|
||||||
|
|
||||||
|
|
||||||
class GitProvider(ABC):
|
class GitProvider(ABC):
|
||||||
@ -87,11 +68,11 @@ class GitProvider(ABC):
|
|||||||
def get_pr_description_full(self) -> str:
|
def get_pr_description_full(self) -> str:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_pr_description(self, *, full: bool = True) -> str:
|
def get_pr_description(self) -> str:
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.algo.pr_processing import clip_tokens
|
from pr_agent.algo.utils import clip_tokens
|
||||||
max_tokens = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
|
max_tokens = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
|
||||||
description = self.get_pr_description_full() if full else self.get_user_description()
|
description = self.get_pr_description_full()
|
||||||
if max_tokens:
|
if max_tokens:
|
||||||
return clip_tokens(description, max_tokens)
|
return clip_tokens(description, max_tokens)
|
||||||
return description
|
return description
|
||||||
@ -128,18 +109,11 @@ class GitProvider(ABC):
|
|||||||
def get_commit_messages(self):
|
def get_commit_messages(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_pr_id(self):
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def get_main_pr_language(languages, files) -> str:
|
def get_main_pr_language(languages, files) -> str:
|
||||||
"""
|
"""
|
||||||
Get the main language of the commit. Return an empty string if cannot determine.
|
Get the main language of the commit. Return an empty string if cannot determine.
|
||||||
"""
|
"""
|
||||||
main_language_str = ""
|
main_language_str = ""
|
||||||
if not languages:
|
|
||||||
get_logger().info("No languages detected")
|
|
||||||
return main_language_str
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
top_language = max(languages, key=languages.get).lower()
|
top_language = max(languages, key=languages.get).lower()
|
||||||
|
|
||||||
@ -169,11 +143,12 @@ def get_main_pr_language(languages, files) -> str:
|
|||||||
most_common_extension == 'scala' and top_language == 'scala' or \
|
most_common_extension == 'scala' and top_language == 'scala' or \
|
||||||
most_common_extension == 'kt' and top_language == 'kotlin' or \
|
most_common_extension == 'kt' and top_language == 'kotlin' or \
|
||||||
most_common_extension == 'pl' and top_language == 'perl' or \
|
most_common_extension == 'pl' and top_language == 'perl' or \
|
||||||
|
most_common_extension == 'swift' and top_language == 'swift' or \
|
||||||
most_common_extension == top_language:
|
most_common_extension == top_language:
|
||||||
main_language_str = top_language
|
main_language_str = top_language
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(e)
|
logging.exception(e)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return main_language_str
|
return main_language_str
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple, Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from github import AppAuthentication, Auth, Github, GithubException
|
from github import AppAuthentication, Auth, Github, GithubException, Reaction
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from starlette_context import context
|
from starlette_context import context
|
||||||
|
|
||||||
|
from .git_provider import GitProvider, IncrementalPR
|
||||||
from ..algo.language_handler import is_valid_file
|
from ..algo.language_handler import is_valid_file
|
||||||
from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file
|
from ..algo.utils import load_large_diff, clip_tokens, find_line_number_of_relevant_line_in_file, FilePatchInfo
|
||||||
from ..algo.utils import load_large_diff
|
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from ..log import get_logger
|
|
||||||
from ..servers.utils import RateLimitExceeded
|
from ..servers.utils import RateLimitExceeded
|
||||||
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR
|
|
||||||
|
|
||||||
|
|
||||||
class GithubProvider(GitProvider):
|
class GithubProvider(GitProvider):
|
||||||
@ -31,7 +31,7 @@ class GithubProvider(GitProvider):
|
|||||||
self.diff_files = None
|
self.diff_files = None
|
||||||
self.git_files = None
|
self.git_files = None
|
||||||
self.incremental = incremental
|
self.incremental = incremental
|
||||||
if pr_url and 'pull' in pr_url:
|
if pr_url:
|
||||||
self.set_pr(pr_url)
|
self.set_pr(pr_url)
|
||||||
self.last_commit_id = list(self.pr.get_commits())[-1]
|
self.last_commit_id = list(self.pr.get_commits())[-1]
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ class GithubProvider(GitProvider):
|
|||||||
self.file_set = dict()
|
self.file_set = dict()
|
||||||
for commit in self.incremental.commits_range:
|
for commit in self.incremental.commits_range:
|
||||||
if commit.commit.message.startswith(f"Merge branch '{self._get_repo().default_branch}'"):
|
if commit.commit.message.startswith(f"Merge branch '{self._get_repo().default_branch}'"):
|
||||||
get_logger().info(f"Skipping merge commit {commit.commit.message}")
|
logging.info(f"Skipping merge commit {commit.commit.message}")
|
||||||
continue
|
continue
|
||||||
self.file_set.update({file.filename: file for file in commit.files})
|
self.file_set.update({file.filename: file for file in commit.files})
|
||||||
|
|
||||||
@ -77,7 +77,7 @@ class GithubProvider(GitProvider):
|
|||||||
self.previous_review = None
|
self.previous_review = None
|
||||||
self.comments = list(self.pr.get_issue_comments())
|
self.comments = list(self.pr.get_issue_comments())
|
||||||
for index in range(len(self.comments) - 1, -1, -1):
|
for index in range(len(self.comments) - 1, -1, -1):
|
||||||
if self.comments[index].body.startswith("## PR Analysis") or self.comments[index].body.startswith("## Incremental PR Review"):
|
if self.comments[index].body.startswith("## PR Analysis"):
|
||||||
self.previous_review = self.comments[index]
|
self.previous_review = self.comments[index]
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ class GithubProvider(GitProvider):
|
|||||||
return diff_files
|
return diff_files
|
||||||
|
|
||||||
except GithubException.RateLimitExceededException as e:
|
except GithubException.RateLimitExceededException as e:
|
||||||
get_logger().error(f"Rate limit exceeded for GitHub API. Original message: {e}")
|
logging.error(f"Rate limit exceeded for GitHub API. Original message: {e}")
|
||||||
raise RateLimitExceeded("Rate limit exceeded for GitHub API.") from e
|
raise RateLimitExceeded("Rate limit exceeded for GitHub API.") from e
|
||||||
|
|
||||||
def publish_description(self, pr_title: str, pr_body: str):
|
def publish_description(self, pr_title: str, pr_body: str):
|
||||||
@ -137,7 +137,7 @@ class GithubProvider(GitProvider):
|
|||||||
|
|
||||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||||
if is_temporary and not get_settings().config.publish_output_progress:
|
if is_temporary and not get_settings().config.publish_output_progress:
|
||||||
get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}")
|
logging.debug(f"Skipping publish_comment for temporary comment: {pr_comment}")
|
||||||
return
|
return
|
||||||
response = self.pr.create_issue_comment(pr_comment)
|
response = self.pr.create_issue_comment(pr_comment)
|
||||||
if hasattr(response, "user") and hasattr(response.user, "login"):
|
if hasattr(response, "user") and hasattr(response.user, "login"):
|
||||||
@ -155,7 +155,7 @@ class GithubProvider(GitProvider):
|
|||||||
position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file)
|
position, absolute_position = find_line_number_of_relevant_line_in_file(self.diff_files, relevant_file.strip('`'), relevant_line_in_file)
|
||||||
if position == -1:
|
if position == -1:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
||||||
subject_type = "FILE"
|
subject_type = "FILE"
|
||||||
else:
|
else:
|
||||||
subject_type = "LINE"
|
subject_type = "LINE"
|
||||||
@ -178,13 +178,13 @@ class GithubProvider(GitProvider):
|
|||||||
|
|
||||||
if not relevant_lines_start or relevant_lines_start == -1:
|
if not relevant_lines_start or relevant_lines_start == -1:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().exception(
|
logging.exception(
|
||||||
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
|
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if relevant_lines_end < relevant_lines_start:
|
if relevant_lines_end < relevant_lines_start:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().exception(f"Failed to publish code suggestion, "
|
logging.exception(f"Failed to publish code suggestion, "
|
||||||
f"relevant_lines_end is {relevant_lines_end} and "
|
f"relevant_lines_end is {relevant_lines_end} and "
|
||||||
f"relevant_lines_start is {relevant_lines_start}")
|
f"relevant_lines_start is {relevant_lines_start}")
|
||||||
continue
|
continue
|
||||||
@ -211,7 +211,7 @@ class GithubProvider(GitProvider):
|
|||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().error(f"Failed to publish code suggestion, error: {e}")
|
logging.error(f"Failed to publish code suggestion, error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove_initial_comment(self):
|
def remove_initial_comment(self):
|
||||||
@ -220,7 +220,7 @@ class GithubProvider(GitProvider):
|
|||||||
if comment.is_temporary:
|
if comment.is_temporary:
|
||||||
comment.delete()
|
comment.delete()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to remove initial comment, error: {e}")
|
logging.exception(f"Failed to remove initial comment, error: {e}")
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.pr.title
|
return self.pr.title
|
||||||
@ -238,10 +238,9 @@ class GithubProvider(GitProvider):
|
|||||||
def get_user_id(self):
|
def get_user_id(self):
|
||||||
if not self.github_user_id:
|
if not self.github_user_id:
|
||||||
try:
|
try:
|
||||||
self.github_user_id = self.github_client.get_user().raw_data['login']
|
self.github_user_id = self.github_client.get_user().login
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.github_user_id = ""
|
logging.exception(f"Failed to get user id, error: {e}")
|
||||||
# logging.exception(f"Failed to get user id, error: {e}")
|
|
||||||
return self.github_user_id
|
return self.github_user_id
|
||||||
|
|
||||||
def get_notifications(self, since: datetime):
|
def get_notifications(self, since: datetime):
|
||||||
@ -258,10 +257,7 @@ class GithubProvider(GitProvider):
|
|||||||
|
|
||||||
def get_repo_settings(self):
|
def get_repo_settings(self):
|
||||||
try:
|
try:
|
||||||
# contents = self.repo_obj.get_contents(".pr_agent.toml", ref=self.pr.head.sha).decoded_content
|
contents = self.repo_obj.get_contents(".pr_agent.toml", ref=self.pr.head.sha).decoded_content
|
||||||
|
|
||||||
# more logical to take 'pr_agent.toml' from the default branch
|
|
||||||
contents = self.repo_obj.get_contents(".pr_agent.toml").decoded_content
|
|
||||||
return contents
|
return contents
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
@ -271,7 +267,7 @@ class GithubProvider(GitProvider):
|
|||||||
reaction = self.pr.get_issue_comment(issue_comment_id).create_reaction("eyes")
|
reaction = self.pr.get_issue_comment(issue_comment_id).create_reaction("eyes")
|
||||||
return reaction.id
|
return reaction.id
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to add eyes reaction, error: {e}")
|
logging.exception(f"Failed to add eyes reaction, error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
|
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
|
||||||
@ -279,7 +275,7 @@ class GithubProvider(GitProvider):
|
|||||||
self.pr.get_issue_comment(issue_comment_id).delete_reaction(reaction_id)
|
self.pr.get_issue_comment(issue_comment_id).delete_reaction(reaction_id)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to remove eyes reaction, error: {e}")
|
logging.exception(f"Failed to remove eyes reaction, error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -312,35 +308,6 @@ class GithubProvider(GitProvider):
|
|||||||
|
|
||||||
return repo_name, pr_number
|
return repo_name, pr_number
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parse_issue_url(issue_url: str) -> Tuple[str, int]:
|
|
||||||
parsed_url = urlparse(issue_url)
|
|
||||||
|
|
||||||
if 'github.com' not in parsed_url.netloc:
|
|
||||||
raise ValueError("The provided URL is not a valid GitHub URL")
|
|
||||||
|
|
||||||
path_parts = parsed_url.path.strip('/').split('/')
|
|
||||||
if 'api.github.com' in parsed_url.netloc:
|
|
||||||
if len(path_parts) < 5 or path_parts[3] != 'issues':
|
|
||||||
raise ValueError("The provided URL does not appear to be a GitHub ISSUE URL")
|
|
||||||
repo_name = '/'.join(path_parts[1:3])
|
|
||||||
try:
|
|
||||||
issue_number = int(path_parts[4])
|
|
||||||
except ValueError as e:
|
|
||||||
raise ValueError("Unable to convert issue number to integer") from e
|
|
||||||
return repo_name, issue_number
|
|
||||||
|
|
||||||
if len(path_parts) < 4 or path_parts[2] != 'issues':
|
|
||||||
raise ValueError("The provided URL does not appear to be a GitHub PR issue")
|
|
||||||
|
|
||||||
repo_name = '/'.join(path_parts[:2])
|
|
||||||
try:
|
|
||||||
issue_number = int(path_parts[3])
|
|
||||||
except ValueError as e:
|
|
||||||
raise ValueError("Unable to convert issue number to integer") from e
|
|
||||||
|
|
||||||
return repo_name, issue_number
|
|
||||||
|
|
||||||
def _get_github_client(self):
|
def _get_github_client(self):
|
||||||
deployment_type = get_settings().get("GITHUB.DEPLOYMENT_TYPE", "user")
|
deployment_type = get_settings().get("GITHUB.DEPLOYMENT_TYPE", "user")
|
||||||
|
|
||||||
@ -398,13 +365,13 @@ class GithubProvider(GitProvider):
|
|||||||
"PUT", f"{self.pr.issue_url}/labels", input=post_parameters
|
"PUT", f"{self.pr.issue_url}/labels", input=post_parameters
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to publish labels, error: {e}")
|
logging.exception(f"Failed to publish labels, error: {e}")
|
||||||
|
|
||||||
def get_labels(self):
|
def get_labels(self):
|
||||||
try:
|
try:
|
||||||
return [label.name for label in self.pr.labels]
|
return [label.name for label in self.pr.labels]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to get labels, error: {e}")
|
logging.exception(f"Failed to get labels, error: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_commit_messages(self):
|
def get_commit_messages(self):
|
||||||
@ -446,13 +413,6 @@ class GithubProvider(GitProvider):
|
|||||||
return link
|
return link
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Failed adding line link, error: {e}")
|
logging.info(f"Failed adding line link, error: {e}")
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_pr_id(self):
|
|
||||||
try:
|
|
||||||
pr_id = f"{self.repo}/{self.pr_num}"
|
|
||||||
return pr_id
|
|
||||||
except:
|
|
||||||
return ""
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import hashlib
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@ -7,12 +7,11 @@ import gitlab
|
|||||||
from gitlab import GitlabGetError
|
from gitlab import GitlabGetError
|
||||||
|
|
||||||
from ..algo.language_handler import is_valid_file
|
from ..algo.language_handler import is_valid_file
|
||||||
from ..algo.pr_processing import clip_tokens, find_line_number_of_relevant_line_in_file
|
from ..algo.utils import load_large_diff, clip_tokens, EDIT_TYPE, FilePatchInfo
|
||||||
from ..algo.utils import load_large_diff
|
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from .git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
|
from .git_provider import GitProvider
|
||||||
from ..log import get_logger
|
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
class DiffNotFoundError(Exception):
|
class DiffNotFoundError(Exception):
|
||||||
"""Raised when the diff for a merge request cannot be found."""
|
"""Raised when the diff for a merge request cannot be found."""
|
||||||
@ -43,7 +42,7 @@ class GitLabProvider(GitProvider):
|
|||||||
self.incremental = incremental
|
self.incremental = incremental
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
def is_supported(self, capability: str) -> bool:
|
||||||
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'gfm_markdown']:
|
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ class GitLabProvider(GitProvider):
|
|||||||
try:
|
try:
|
||||||
self.last_diff = self.mr.diffs.list(get_all=True)[-1]
|
self.last_diff = self.mr.diffs.list(get_all=True)[-1]
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
get_logger().error(f"Could not get diff for merge request {self.id_mr}")
|
logger.error(f"Could not get diff for merge request {self.id_mr}")
|
||||||
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") from e
|
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}") from e
|
||||||
|
|
||||||
|
|
||||||
@ -98,7 +97,7 @@ class GitLabProvider(GitProvider):
|
|||||||
if isinstance(new_file_content_str, bytes):
|
if isinstance(new_file_content_str, bytes):
|
||||||
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
|
new_file_content_str = bytes.decode(new_file_content_str, 'utf-8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
get_logger().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}")
|
||||||
|
|
||||||
edit_type = EDIT_TYPE.MODIFIED
|
edit_type = EDIT_TYPE.MODIFIED
|
||||||
@ -134,7 +133,7 @@ class GitLabProvider(GitProvider):
|
|||||||
self.mr.description = pr_body
|
self.mr.description = pr_body
|
||||||
self.mr.save()
|
self.mr.save()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Could not update merge request {self.id_mr} description: {e}")
|
logging.exception(f"Could not update merge request {self.id_mr} description: {e}")
|
||||||
|
|
||||||
def publish_comment(self, mr_comment: str, is_temporary: bool = False):
|
def publish_comment(self, mr_comment: str, is_temporary: bool = False):
|
||||||
comment = self.mr.notes.create({'body': mr_comment})
|
comment = self.mr.notes.create({'body': mr_comment})
|
||||||
@ -156,12 +155,12 @@ class GitLabProvider(GitProvider):
|
|||||||
def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int,
|
def send_inline_comment(self,body: str,edit_type: str,found: bool,relevant_file: str,relevant_line_in_file: int,
|
||||||
source_line_no: int, target_file: str,target_line_no: int) -> None:
|
source_line_no: int, target_file: str,target_line_no: int) -> None:
|
||||||
if not found:
|
if not found:
|
||||||
get_logger().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:
|
||||||
# in order to have exact sha's we have to find correct diff for this change
|
# in order to have exact sha's we have to find correct diff for this change
|
||||||
diff = self.get_relevant_diff(relevant_file, relevant_line_in_file)
|
diff = self.get_relevant_diff(relevant_file, relevant_line_in_file)
|
||||||
if diff is None:
|
if diff is None:
|
||||||
get_logger().error(f"Could not get diff for merge request {self.id_mr}")
|
logger.error(f"Could not get diff for merge request {self.id_mr}")
|
||||||
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}")
|
raise DiffNotFoundError(f"Could not get diff for merge request {self.id_mr}")
|
||||||
pos_obj = {'position_type': 'text',
|
pos_obj = {'position_type': 'text',
|
||||||
'new_path': target_file.filename,
|
'new_path': target_file.filename,
|
||||||
@ -174,23 +173,24 @@ class GitLabProvider(GitProvider):
|
|||||||
else:
|
else:
|
||||||
pos_obj['new_line'] = target_line_no - 1
|
pos_obj['new_line'] = target_line_no - 1
|
||||||
pos_obj['old_line'] = source_line_no - 1
|
pos_obj['old_line'] = source_line_no - 1
|
||||||
get_logger().debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}")
|
logging.debug(f"Creating comment in {self.id_mr} with body {body} and position {pos_obj}")
|
||||||
self.mr.discussions.create({'body': body, 'position': pos_obj})
|
self.mr.discussions.create({'body': body,
|
||||||
|
'position': pos_obj})
|
||||||
|
|
||||||
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]:
|
def get_relevant_diff(self, relevant_file: str, relevant_line_in_file: int) -> Optional[dict]:
|
||||||
changes = self.mr.changes() # Retrieve the changes for the merge request once
|
changes = self.mr.changes() # Retrieve the changes for the merge request once
|
||||||
if not changes:
|
if not changes:
|
||||||
get_logger().error('No changes found for the merge request.')
|
logging.error('No changes found for the merge request.')
|
||||||
return None
|
return None
|
||||||
all_diffs = self.mr.diffs.list(get_all=True)
|
all_diffs = self.mr.diffs.list(get_all=True)
|
||||||
if not all_diffs:
|
if not all_diffs:
|
||||||
get_logger().error('No diffs found for the merge request.')
|
logging.error('No diffs found for the merge request.')
|
||||||
return None
|
return None
|
||||||
for diff in all_diffs:
|
for diff in all_diffs:
|
||||||
for change in changes['changes']:
|
for change in changes['changes']:
|
||||||
if change['new_path'] == relevant_file and relevant_line_in_file in change['diff']:
|
if change['new_path'] == relevant_file and relevant_line_in_file in change['diff']:
|
||||||
return diff
|
return diff
|
||||||
get_logger().debug(
|
logging.debug(
|
||||||
f'No relevant diff found for {relevant_file} {relevant_line_in_file}. Falling back to last diff.')
|
f'No relevant diff found for {relevant_file} {relevant_line_in_file}. Falling back to last diff.')
|
||||||
return self.last_diff # fallback to last_diff if no relevant diff is found
|
return self.last_diff # fallback to last_diff if no relevant diff is found
|
||||||
|
|
||||||
@ -225,10 +225,7 @@ class GitLabProvider(GitProvider):
|
|||||||
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
|
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
|
||||||
target_file, target_line_no)
|
target_file, target_line_no)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}")
|
logging.exception(f"Could not publish code suggestion:\nsuggestion: {suggestion}\nerror: {e}")
|
||||||
|
|
||||||
# note that we publish suggestions one-by-one. so, if one fails, the rest will still be published
|
|
||||||
return True
|
|
||||||
|
|
||||||
def search_line(self, relevant_file, relevant_line_in_file):
|
def search_line(self, relevant_file, relevant_line_in_file):
|
||||||
target_file = None
|
target_file = None
|
||||||
@ -289,7 +286,7 @@ class GitLabProvider(GitProvider):
|
|||||||
for comment in self.temp_comments:
|
for comment in self.temp_comments:
|
||||||
comment.delete()
|
comment.delete()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to remove temp comments, error: {e}")
|
logging.exception(f"Failed to remove temp comments, error: {e}")
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.mr.title
|
return self.mr.title
|
||||||
@ -357,7 +354,7 @@ class GitLabProvider(GitProvider):
|
|||||||
self.mr.labels = list(set(pr_types))
|
self.mr.labels = list(set(pr_types))
|
||||||
self.mr.save()
|
self.mr.save()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to publish labels, error: {e}")
|
logging.exception(f"Failed to publish labels, error: {e}")
|
||||||
|
|
||||||
def publish_inline_comments(self, comments: list[dict]):
|
def publish_inline_comments(self, comments: list[dict]):
|
||||||
pass
|
pass
|
||||||
@ -380,35 +377,4 @@ class GitLabProvider(GitProvider):
|
|||||||
commit_messages_str = ""
|
commit_messages_str = ""
|
||||||
if max_tokens:
|
if max_tokens:
|
||||||
commit_messages_str = clip_tokens(commit_messages_str, max_tokens)
|
commit_messages_str = clip_tokens(commit_messages_str, max_tokens)
|
||||||
return commit_messages_str
|
return commit_messages_str
|
||||||
|
|
||||||
def get_pr_id(self):
|
|
||||||
try:
|
|
||||||
pr_id = self.mr.web_url
|
|
||||||
return pr_id
|
|
||||||
except:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def generate_link_to_relevant_line_number(self, suggestion) -> str:
|
|
||||||
try:
|
|
||||||
relevant_file = suggestion['relevant file'].strip('`').strip("'")
|
|
||||||
relevant_line_str = suggestion['relevant line']
|
|
||||||
if not relevant_line_str:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
position, absolute_position = find_line_number_of_relevant_line_in_file \
|
|
||||||
(self.diff_files, relevant_file, relevant_line_str)
|
|
||||||
|
|
||||||
if absolute_position != -1:
|
|
||||||
# link to right file only
|
|
||||||
link = f"https://gitlab.com/codiumai/pr-agent/-/blob/{self.mr.source_branch}/{relevant_file}?ref_type=heads#L{absolute_position}"
|
|
||||||
|
|
||||||
# # link to diff
|
|
||||||
# sha_file = hashlib.sha1(relevant_file.encode('utf-8')).hexdigest()
|
|
||||||
# link = f"{self.pr.web_url}/diffs#{sha_file}_{absolute_position}_{absolute_position}"
|
|
||||||
return link
|
|
||||||
except Exception as e:
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
|
||||||
get_logger().info(f"Failed adding line link, error: {e}")
|
|
||||||
|
|
||||||
return ""
|
|
79
pr_agent/git_providers/in_memory_provider.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import itertools
|
||||||
|
from collections import Counter
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pr_agent.algo.utils import FilePatchInfo
|
||||||
|
from pr_agent.git_providers.git_provider import GitProvider
|
||||||
|
|
||||||
|
|
||||||
|
class InMemoryProvider(GitProvider):
|
||||||
|
def __init__(self, head_branch: str, target_branch: str, files: List[FilePatchInfo]):
|
||||||
|
self.head_branch = head_branch
|
||||||
|
self.target_branch = target_branch
|
||||||
|
self.files = files
|
||||||
|
|
||||||
|
def is_supported(self, capability: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_files(self) -> list[FilePatchInfo]:
|
||||||
|
return self.files
|
||||||
|
|
||||||
|
def get_diff_files(self) -> list[FilePatchInfo]:
|
||||||
|
return self.get_files()
|
||||||
|
|
||||||
|
def publish_description(self, pr_title: str, pr_body: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def publish_inline_comments(self, comments: list[dict]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def publish_code_suggestions(self, code_suggestions: list) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def publish_labels(self, labels):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_labels(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_initial_comment(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_languages(self):
|
||||||
|
language_count = Counter(file.language for file in self.files)
|
||||||
|
return dict(language_count)
|
||||||
|
|
||||||
|
def get_pr_branch(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_user_id(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_pr_description_full(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_issue_comments(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_repo_settings(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_reaction(self, issue_comment_id: int, reaction_id: int) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_commit_messages(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
@ -5,8 +6,8 @@ from typing import List
|
|||||||
from git import Repo
|
from git import Repo
|
||||||
|
|
||||||
from pr_agent.config_loader import _find_repository_root, get_settings
|
from pr_agent.config_loader import _find_repository_root, get_settings
|
||||||
from pr_agent.git_providers.git_provider import EDIT_TYPE, FilePatchInfo, GitProvider
|
from pr_agent.git_providers.git_provider import GitProvider
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.algo.utils import EDIT_TYPE, FilePatchInfo
|
||||||
|
|
||||||
|
|
||||||
class PullRequestMimic:
|
class PullRequestMimic:
|
||||||
@ -49,15 +50,14 @@ class LocalGitProvider(GitProvider):
|
|||||||
"""
|
"""
|
||||||
Prepare the repository for PR-mimic generation.
|
Prepare the repository for PR-mimic generation.
|
||||||
"""
|
"""
|
||||||
get_logger().debug('Preparing repository for PR-mimic generation...')
|
logging.debug('Preparing repository for PR-mimic generation...')
|
||||||
if self.repo.is_dirty():
|
if self.repo.is_dirty():
|
||||||
raise ValueError('The repository is not in a clean state. Please commit or stash pending changes.')
|
raise ValueError('The repository is not in a clean state. Please commit or stash pending changes.')
|
||||||
if self.target_branch_name not in self.repo.heads:
|
if self.target_branch_name not in self.repo.heads:
|
||||||
raise KeyError(f'Branch: {self.target_branch_name} does not exist')
|
raise KeyError(f'Branch: {self.target_branch_name} does not exist')
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
def is_supported(self, capability: str) -> bool:
|
||||||
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels',
|
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels']:
|
||||||
'gfm_markdown']:
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
import copy
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from dynaconf import Dynaconf
|
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
|
||||||
from pr_agent.git_providers import get_git_provider
|
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
def apply_repo_settings(pr_url):
|
|
||||||
if get_settings().config.use_repo_settings_file:
|
|
||||||
repo_settings_file = None
|
|
||||||
try:
|
|
||||||
git_provider = get_git_provider()(pr_url)
|
|
||||||
repo_settings = git_provider.get_repo_settings()
|
|
||||||
if repo_settings:
|
|
||||||
repo_settings_file = None
|
|
||||||
fd, repo_settings_file = tempfile.mkstemp(suffix='.toml')
|
|
||||||
os.write(fd, repo_settings)
|
|
||||||
new_settings = Dynaconf(settings_files=[repo_settings_file])
|
|
||||||
for section, contents in new_settings.as_dict().items():
|
|
||||||
section_dict = copy.deepcopy(get_settings().as_dict().get(section, {}))
|
|
||||||
for key, value in contents.items():
|
|
||||||
section_dict[key] = value
|
|
||||||
get_settings().unset(section)
|
|
||||||
get_settings().set(section, section_dict, merge=False)
|
|
||||||
get_logger().info(f"Applying repo settings for section {section}, contents: {contents}")
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if repo_settings_file:
|
|
||||||
try:
|
|
||||||
os.remove(repo_settings_file)
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to remove temporary settings file {repo_settings_file}", e)
|
|
@ -1,40 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
|
|
||||||
class LoggingFormat(str, Enum):
|
|
||||||
CONSOLE = "CONSOLE"
|
|
||||||
JSON = "JSON"
|
|
||||||
|
|
||||||
|
|
||||||
def json_format(record: dict) -> str:
|
|
||||||
return record["message"]
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(level: str = "INFO", fmt: LoggingFormat = LoggingFormat.CONSOLE):
|
|
||||||
level: int = logging.getLevelName(level.upper())
|
|
||||||
if type(level) is not int:
|
|
||||||
level = logging.INFO
|
|
||||||
|
|
||||||
if fmt == LoggingFormat.JSON:
|
|
||||||
logger.remove(None)
|
|
||||||
logger.add(
|
|
||||||
sys.stdout,
|
|
||||||
level=level,
|
|
||||||
format="{message}",
|
|
||||||
colorize=False,
|
|
||||||
serialize=True,
|
|
||||||
)
|
|
||||||
elif fmt == LoggingFormat.CONSOLE:
|
|
||||||
logger.remove(None)
|
|
||||||
logger.add(sys.stdout, level=level, colorize=True)
|
|
||||||
|
|
||||||
return logger
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(*args, **kwargs):
|
|
||||||
return logger
|
|
@ -1,8 +1,9 @@
|
|||||||
import ujson
|
import ujson
|
||||||
|
|
||||||
from google.cloud import storage
|
from google.cloud import storage
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.git_providers.gitlab_provider import logger
|
||||||
from pr_agent.secret_providers.secret_provider import SecretProvider
|
from pr_agent.secret_providers.secret_provider import SecretProvider
|
||||||
|
|
||||||
|
|
||||||
@ -14,7 +15,7 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
|
|||||||
self.bucket_name = get_settings().google_cloud_storage.bucket_name
|
self.bucket_name = get_settings().google_cloud_storage.bucket_name
|
||||||
self.bucket = self.client.bucket(self.bucket_name)
|
self.bucket = self.client.bucket(self.bucket_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to initialize Google Cloud Storage Secret Provider: {e}")
|
logger.error(f"Failed to initialize Google Cloud Storage Secret Provider: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def get_secret(self, secret_name: str) -> str:
|
def get_secret(self, secret_name: str) -> str:
|
||||||
@ -22,7 +23,7 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
|
|||||||
blob = self.bucket.blob(secret_name)
|
blob = self.bucket.blob(secret_name)
|
||||||
return blob.download_as_string()
|
return blob.download_as_string()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to get secret {secret_name} from Google Cloud Storage: {e}")
|
logger.error(f"Failed to get secret {secret_name} from Google Cloud Storage: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def store_secret(self, secret_name: str, secret_value: str):
|
def store_secret(self, secret_name: str, secret_value: str):
|
||||||
@ -30,5 +31,5 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
|
|||||||
blob = self.bucket.blob(secret_name)
|
blob = self.bucket.blob(secret_name)
|
||||||
blob.upload_from_string(secret_value)
|
blob.upload_from_string(secret_value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to store secret {secret_name} in Google Cloud Storage: {e}")
|
logger.error(f"Failed to store secret {secret_name} in Google Cloud Storage: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import copy
|
import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
@ -16,10 +18,9 @@ from starlette_context.middleware import RawContextMiddleware
|
|||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.config_loader import get_settings, global_settings
|
from pr_agent.config_loader import get_settings, global_settings
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
|
||||||
from pr_agent.secret_providers import get_secret_provider
|
from pr_agent.secret_providers import get_secret_provider
|
||||||
|
|
||||||
setup_logger(fmt=LoggingFormat.JSON)
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
secret_provider = get_secret_provider()
|
secret_provider = get_secret_provider()
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ async def get_bearer_token(shared_secret: str, client_key: str):
|
|||||||
bearer_token = response.json()["access_token"]
|
bearer_token = response.json()["access_token"]
|
||||||
return bearer_token
|
return bearer_token
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to get bearer token: {e}")
|
logging.error(f"Failed to get bearer token: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
@ -59,23 +60,21 @@ async def handle_manifest(request: Request, response: Response):
|
|||||||
manifest = manifest.replace("app_key", get_settings().bitbucket.app_key)
|
manifest = manifest.replace("app_key", get_settings().bitbucket.app_key)
|
||||||
manifest = manifest.replace("base_url", get_settings().bitbucket.base_url)
|
manifest = manifest.replace("base_url", get_settings().bitbucket.base_url)
|
||||||
except:
|
except:
|
||||||
get_logger().error("Failed to replace api_key in Bitbucket manifest, trying to continue")
|
logging.error("Failed to replace api_key in Bitbucket manifest, trying to continue")
|
||||||
manifest_obj = json.loads(manifest)
|
manifest_obj = json.loads(manifest)
|
||||||
return JSONResponse(manifest_obj)
|
return JSONResponse(manifest_obj)
|
||||||
|
|
||||||
@router.post("/webhook")
|
@router.post("/webhook")
|
||||||
async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Request):
|
async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Request):
|
||||||
log_context = {"server_type": "bitbucket_app"}
|
print(request.headers)
|
||||||
get_logger().debug(request.headers)
|
|
||||||
jwt_header = request.headers.get("authorization", None)
|
jwt_header = request.headers.get("authorization", None)
|
||||||
if jwt_header:
|
if jwt_header:
|
||||||
input_jwt = jwt_header.split(" ")[1]
|
input_jwt = jwt_header.split(" ")[1]
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
get_logger().debug(data)
|
print(data)
|
||||||
async def inner():
|
async def inner():
|
||||||
try:
|
try:
|
||||||
owner = data["data"]["repository"]["owner"]["username"]
|
owner = data["data"]["repository"]["owner"]["username"]
|
||||||
log_context["sender"] = owner
|
|
||||||
secrets = json.loads(secret_provider.get_secret(owner))
|
secrets = json.loads(secret_provider.get_secret(owner))
|
||||||
shared_secret = secrets["shared_secret"]
|
shared_secret = secrets["shared_secret"]
|
||||||
client_key = secrets["client_key"]
|
client_key = secrets["client_key"]
|
||||||
@ -87,19 +86,13 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
|
|||||||
agent = PRAgent()
|
agent = PRAgent()
|
||||||
if event == "pullrequest:created":
|
if event == "pullrequest:created":
|
||||||
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
|
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
|
||||||
log_context["api_url"] = pr_url
|
await agent.handle_request(pr_url, "review")
|
||||||
log_context["event"] = "pull_request"
|
|
||||||
with get_logger().contextualize(**log_context):
|
|
||||||
await agent.handle_request(pr_url, "review")
|
|
||||||
elif event == "pullrequest:comment_created":
|
elif event == "pullrequest:comment_created":
|
||||||
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
|
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
|
||||||
log_context["api_url"] = pr_url
|
|
||||||
log_context["event"] = "comment"
|
|
||||||
comment_body = data["data"]["comment"]["content"]["raw"]
|
comment_body = data["data"]["comment"]["content"]["raw"]
|
||||||
with get_logger().contextualize(**log_context):
|
await agent.handle_request(pr_url, comment_body)
|
||||||
await agent.handle_request(pr_url, comment_body)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to handle webhook: {e}")
|
logging.error(f"Failed to handle webhook: {e}")
|
||||||
background_tasks.add_task(inner)
|
background_tasks.add_task(inner)
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
@ -110,10 +103,9 @@ async def handle_github_webhooks(request: Request, response: Response):
|
|||||||
@router.post("/installed")
|
@router.post("/installed")
|
||||||
async def handle_installed_webhooks(request: Request, response: Response):
|
async def handle_installed_webhooks(request: Request, response: Response):
|
||||||
try:
|
try:
|
||||||
get_logger().info("handle_installed_webhooks")
|
print(request.headers)
|
||||||
get_logger().info(request.headers)
|
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
get_logger().info(data)
|
print(data)
|
||||||
shared_secret = data["sharedSecret"]
|
shared_secret = data["sharedSecret"]
|
||||||
client_key = data["clientKey"]
|
client_key = data["clientKey"]
|
||||||
username = data["principal"]["username"]
|
username = data["principal"]["username"]
|
||||||
@ -123,15 +115,13 @@ async def handle_installed_webhooks(request: Request, response: Response):
|
|||||||
}
|
}
|
||||||
secret_provider.store_secret(username, json.dumps(secrets))
|
secret_provider.store_secret(username, json.dumps(secrets))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to register user: {e}")
|
logging.error(f"Failed to register user: {e}")
|
||||||
return JSONResponse({"error": "Unable to register user"}, status_code=500)
|
return JSONResponse({"error": "Unable to register user"}, status_code=500)
|
||||||
|
|
||||||
@router.post("/uninstalled")
|
@router.post("/uninstalled")
|
||||||
async def handle_uninstalled_webhooks(request: Request, response: Response):
|
async def handle_uninstalled_webhooks(request: Request, response: Response):
|
||||||
get_logger().info("handle_uninstalled_webhooks")
|
|
||||||
|
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
get_logger().info(data)
|
print(data)
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
import copy
|
|
||||||
from enum import Enum
|
|
||||||
from json import JSONDecodeError
|
|
||||||
|
|
||||||
import uvicorn
|
|
||||||
from fastapi import APIRouter, FastAPI, HTTPException
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from starlette.middleware import Middleware
|
|
||||||
from starlette_context import context
|
|
||||||
from starlette_context.middleware import RawContextMiddleware
|
|
||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
|
||||||
from pr_agent.config_loader import get_settings, global_settings
|
|
||||||
from pr_agent.log import get_logger, setup_logger
|
|
||||||
|
|
||||||
setup_logger()
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
class Action(str, Enum):
|
|
||||||
review = "review"
|
|
||||||
describe = "describe"
|
|
||||||
ask = "ask"
|
|
||||||
improve = "improve"
|
|
||||||
reflect = "reflect"
|
|
||||||
answer = "answer"
|
|
||||||
|
|
||||||
|
|
||||||
class Item(BaseModel):
|
|
||||||
refspec: str
|
|
||||||
project: str
|
|
||||||
msg: str
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/v1/gerrit/{action}")
|
|
||||||
async def handle_gerrit_request(action: Action, item: Item):
|
|
||||||
get_logger().debug("Received a Gerrit request")
|
|
||||||
context["settings"] = copy.deepcopy(global_settings)
|
|
||||||
|
|
||||||
if action == Action.ask:
|
|
||||||
if not item.msg:
|
|
||||||
return HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="msg is required for ask command"
|
|
||||||
)
|
|
||||||
await PRAgent().handle_request(
|
|
||||||
f"{item.project}:{item.refspec}",
|
|
||||||
f"/{item.msg.strip()}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_body(request):
|
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
except JSONDecodeError as e:
|
|
||||||
get_logger().error("Error parsing request body", e)
|
|
||||||
return {}
|
|
||||||
return body
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
|
||||||
async def root():
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
# to prevent adding help messages with the output
|
|
||||||
get_settings().set("CONFIG.CLI_MODE", True)
|
|
||||||
middleware = [Middleware(RawContextMiddleware)]
|
|
||||||
app = FastAPI(middleware=middleware)
|
|
||||||
app.include_router(router)
|
|
||||||
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=3000)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
start()
|
|
@ -5,10 +5,6 @@ import os
|
|||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.utils import apply_repo_settings
|
|
||||||
from pr_agent.log import get_logger
|
|
||||||
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
|
||||||
from pr_agent.tools.pr_description import PRDescription
|
|
||||||
from pr_agent.tools.pr_reviewer import PRReviewer
|
from pr_agent.tools.pr_reviewer import PRReviewer
|
||||||
|
|
||||||
|
|
||||||
@ -16,15 +12,12 @@ async def run_action():
|
|||||||
# Get environment variables
|
# Get environment variables
|
||||||
GITHUB_EVENT_NAME = os.environ.get('GITHUB_EVENT_NAME')
|
GITHUB_EVENT_NAME = os.environ.get('GITHUB_EVENT_NAME')
|
||||||
GITHUB_EVENT_PATH = os.environ.get('GITHUB_EVENT_PATH')
|
GITHUB_EVENT_PATH = os.environ.get('GITHUB_EVENT_PATH')
|
||||||
OPENAI_KEY = os.environ.get('OPENAI_KEY') or os.environ.get('OPENAI.KEY')
|
OPENAI_KEY = os.environ.get('OPENAI_KEY')
|
||||||
OPENAI_ORG = os.environ.get('OPENAI_ORG') or os.environ.get('OPENAI.ORG')
|
OPENAI_ORG = os.environ.get('OPENAI_ORG')
|
||||||
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
|
GITHUB_TOKEN = os.environ.get('GITHUB_TOKEN')
|
||||||
CUSTOM_LABELS = os.environ.get('CUSTOM_LABELS')
|
|
||||||
CUSTOM_LABELS_DESCRIPTIONS = os.environ.get('CUSTOM_LABELS_DESCRIPTIONS')
|
|
||||||
# CUSTOM_LABELS is a comma separated list of labels (string), convert to list and strip spaces
|
|
||||||
|
|
||||||
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
|
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
|
||||||
|
|
||||||
|
|
||||||
# Check if required environment variables are set
|
# Check if required environment variables are set
|
||||||
if not GITHUB_EVENT_NAME:
|
if not GITHUB_EVENT_NAME:
|
||||||
print("GITHUB_EVENT_NAME not set")
|
print("GITHUB_EVENT_NAME not set")
|
||||||
@ -38,7 +31,6 @@ async def run_action():
|
|||||||
if not GITHUB_TOKEN:
|
if not GITHUB_TOKEN:
|
||||||
print("GITHUB_TOKEN not set")
|
print("GITHUB_TOKEN not set")
|
||||||
return
|
return
|
||||||
# CUSTOM_LABELS_DICT = handle_custom_labels(CUSTOM_LABELS, CUSTOM_LABELS_DESCRIPTIONS)
|
|
||||||
|
|
||||||
# Set the environment variables in the settings
|
# Set the environment variables in the settings
|
||||||
get_settings().set("OPENAI.KEY", OPENAI_KEY)
|
get_settings().set("OPENAI.KEY", OPENAI_KEY)
|
||||||
@ -46,7 +38,6 @@ async def run_action():
|
|||||||
get_settings().set("OPENAI.ORG", OPENAI_ORG)
|
get_settings().set("OPENAI.ORG", OPENAI_ORG)
|
||||||
get_settings().set("GITHUB.USER_TOKEN", GITHUB_TOKEN)
|
get_settings().set("GITHUB.USER_TOKEN", GITHUB_TOKEN)
|
||||||
get_settings().set("GITHUB.DEPLOYMENT_TYPE", "user")
|
get_settings().set("GITHUB.DEPLOYMENT_TYPE", "user")
|
||||||
# get_settings().set("CUSTOM_LABELS", CUSTOM_LABELS_DICT)
|
|
||||||
|
|
||||||
# Load the event payload
|
# Load the event payload
|
||||||
try:
|
try:
|
||||||
@ -56,30 +47,13 @@ async def run_action():
|
|||||||
print(f"Failed to parse JSON: {e}")
|
print(f"Failed to parse JSON: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
|
||||||
get_logger().info("Applying repo settings")
|
|
||||||
pr_url = event_payload.get("pull_request", {}).get("html_url")
|
|
||||||
if pr_url:
|
|
||||||
apply_repo_settings(pr_url)
|
|
||||||
get_logger().info(f"enable_custom_labels: {get_settings().config.enable_custom_labels}")
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().info(f"github action: failed to apply repo settings: {e}")
|
|
||||||
|
|
||||||
# Handle pull request event
|
# Handle pull request event
|
||||||
if GITHUB_EVENT_NAME == "pull_request":
|
if GITHUB_EVENT_NAME == "pull_request":
|
||||||
action = event_payload.get("action")
|
action = event_payload.get("action")
|
||||||
if action in ["opened", "reopened"]:
|
if action in ["opened", "reopened"]:
|
||||||
pr_url = event_payload.get("pull_request", {}).get("url")
|
pr_url = event_payload.get("pull_request", {}).get("url")
|
||||||
if pr_url:
|
if pr_url:
|
||||||
auto_review = os.environ.get('github_action.auto_review', None)
|
await PRReviewer(pr_url).run()
|
||||||
if auto_review is None or (isinstance(auto_review, str) and auto_review.lower() == 'true'):
|
|
||||||
await PRReviewer(pr_url).run()
|
|
||||||
auto_describe = os.environ.get('github_action.auto_describe', None)
|
|
||||||
if isinstance(auto_describe, str) and auto_describe.lower() == 'true':
|
|
||||||
await PRDescription(pr_url).run()
|
|
||||||
auto_improve = os.environ.get('github_action.auto_improve', None)
|
|
||||||
if isinstance(auto_improve, str) and auto_improve.lower() == 'true':
|
|
||||||
await PRCodeSuggestions(pr_url).run()
|
|
||||||
|
|
||||||
# Handle issue comment event
|
# Handle issue comment event
|
||||||
elif GITHUB_EVENT_NAME == "issue_comment":
|
elif GITHUB_EVENT_NAME == "issue_comment":
|
||||||
@ -87,47 +61,12 @@ async def run_action():
|
|||||||
if action in ["created", "edited"]:
|
if action in ["created", "edited"]:
|
||||||
comment_body = event_payload.get("comment", {}).get("body")
|
comment_body = event_payload.get("comment", {}).get("body")
|
||||||
if comment_body:
|
if comment_body:
|
||||||
is_pr = False
|
pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url")
|
||||||
# check if issue is pull request
|
if pr_url:
|
||||||
if event_payload.get("issue", {}).get("pull_request"):
|
|
||||||
url = event_payload.get("issue", {}).get("pull_request", {}).get("url")
|
|
||||||
is_pr = True
|
|
||||||
else:
|
|
||||||
url = event_payload.get("issue", {}).get("url")
|
|
||||||
if url:
|
|
||||||
body = comment_body.strip().lower()
|
body = comment_body.strip().lower()
|
||||||
comment_id = event_payload.get("comment", {}).get("id")
|
comment_id = event_payload.get("comment", {}).get("id")
|
||||||
provider = get_git_provider()(pr_url=url)
|
provider = get_git_provider()(pr_url=pr_url)
|
||||||
if is_pr:
|
await PRAgent().handle_request(pr_url, body, notify=lambda: provider.add_eyes_reaction(comment_id))
|
||||||
await PRAgent().handle_request(url, body, notify=lambda: provider.add_eyes_reaction(comment_id))
|
|
||||||
else:
|
|
||||||
await PRAgent().handle_request(url, body)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_custom_labels(CUSTOM_LABELS, CUSTOM_LABELS_DESCRIPTIONS):
|
|
||||||
if CUSTOM_LABELS:
|
|
||||||
CUSTOM_LABELS = [x.strip() for x in CUSTOM_LABELS.split(',')]
|
|
||||||
else:
|
|
||||||
# Set default labels
|
|
||||||
CUSTOM_LABELS = ['Bug fix', 'Tests', 'Bug fix with tests', 'Refactoring', 'Enhancement', 'Documentation',
|
|
||||||
'Other']
|
|
||||||
print(f"Using default labels: {CUSTOM_LABELS}")
|
|
||||||
if CUSTOM_LABELS_DESCRIPTIONS:
|
|
||||||
CUSTOM_LABELS_DESCRIPTIONS = [x.strip() for x in CUSTOM_LABELS_DESCRIPTIONS.split(',')]
|
|
||||||
else:
|
|
||||||
# Set default labels
|
|
||||||
CUSTOM_LABELS_DESCRIPTIONS = ['Fixes a bug in the code', 'Adds or modifies tests',
|
|
||||||
'Fixes a bug in the code and adds or modifies tests',
|
|
||||||
'Refactors the code without changing its functionality',
|
|
||||||
'Adds new features or functionality',
|
|
||||||
'Adds or modifies documentation',
|
|
||||||
'Other changes that do not fit in any of the above categories']
|
|
||||||
print(f"Using default labels: {CUSTOM_LABELS_DESCRIPTIONS}")
|
|
||||||
# create a dictionary of labels and descriptions
|
|
||||||
CUSTOM_LABELS_DICT = dict()
|
|
||||||
for i in range(len(CUSTOM_LABELS)):
|
|
||||||
CUSTOM_LABELS_DICT[CUSTOM_LABELS[i]] = {'description': CUSTOM_LABELS_DESCRIPTIONS[i]}
|
|
||||||
return CUSTOM_LABELS_DICT
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
@ -10,15 +12,11 @@ from starlette_context import context
|
|||||||
from starlette_context.middleware import RawContextMiddleware
|
from starlette_context.middleware import RawContextMiddleware
|
||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.algo.utils import update_settings_from_args
|
|
||||||
from pr_agent.config_loader import get_settings, global_settings
|
from pr_agent.config_loader import get_settings, global_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.utils import apply_repo_settings
|
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
|
||||||
from pr_agent.servers.utils import verify_signature
|
from pr_agent.servers.utils import verify_signature
|
||||||
|
|
||||||
setup_logger(fmt=LoggingFormat.JSON)
|
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@ -29,11 +27,11 @@ async def handle_github_webhooks(request: Request, response: Response):
|
|||||||
Verifies the request signature, parses the request body, and passes it to the handle_request function for further
|
Verifies the request signature, parses the request body, and passes it to the handle_request function for further
|
||||||
processing.
|
processing.
|
||||||
"""
|
"""
|
||||||
get_logger().debug("Received a GitHub webhook")
|
logging.debug("Received a GitHub webhook")
|
||||||
|
|
||||||
body = await get_body(request)
|
body = await get_body(request)
|
||||||
|
|
||||||
get_logger().debug(f'Request body:\n{body}')
|
logging.debug(f'Request body:\n{body}')
|
||||||
installation_id = body.get("installation", {}).get("id")
|
installation_id = body.get("installation", {}).get("id")
|
||||||
context["installation_id"] = installation_id
|
context["installation_id"] = installation_id
|
||||||
context["settings"] = copy.deepcopy(global_settings)
|
context["settings"] = copy.deepcopy(global_settings)
|
||||||
@ -45,13 +43,13 @@ async def handle_github_webhooks(request: Request, response: Response):
|
|||||||
@router.post("/api/v1/marketplace_webhooks")
|
@router.post("/api/v1/marketplace_webhooks")
|
||||||
async def handle_marketplace_webhooks(request: Request, response: Response):
|
async def handle_marketplace_webhooks(request: Request, response: Response):
|
||||||
body = await get_body(request)
|
body = await get_body(request)
|
||||||
get_logger().info(f'Request body:\n{body}')
|
logging.info(f'Request body:\n{body}')
|
||||||
|
|
||||||
async def get_body(request):
|
async def get_body(request):
|
||||||
try:
|
try:
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error("Error parsing request body", e)
|
logging.error("Error parsing request body", e)
|
||||||
raise HTTPException(status_code=400, detail="Error parsing request body") from e
|
raise HTTPException(status_code=400, detail="Error parsing request body") from e
|
||||||
webhook_secret = getattr(get_settings().github, 'webhook_secret', None)
|
webhook_secret = getattr(get_settings().github, 'webhook_secret', None)
|
||||||
if webhook_secret:
|
if webhook_secret:
|
||||||
@ -77,8 +75,8 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
return {}
|
return {}
|
||||||
agent = PRAgent()
|
agent = PRAgent()
|
||||||
bot_user = get_settings().github_app.bot_user
|
bot_user = get_settings().github_app.bot_user
|
||||||
sender = body.get("sender", {}).get("login")
|
logging.info(f"action: '{action}'")
|
||||||
log_context = {"action": action, "event": event, "sender": sender, "server_type": "github_app"}
|
logging.info(f"event: '{event}'")
|
||||||
|
|
||||||
if get_settings().github_app.duplicate_requests_cache and _is_duplicate_request(body):
|
if get_settings().github_app.duplicate_requests_cache and _is_duplicate_request(body):
|
||||||
return {}
|
return {}
|
||||||
@ -88,23 +86,21 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
if "comment" not in body:
|
if "comment" not in body:
|
||||||
return {}
|
return {}
|
||||||
comment_body = body.get("comment", {}).get("body")
|
comment_body = body.get("comment", {}).get("body")
|
||||||
|
sender = body.get("sender", {}).get("login")
|
||||||
if sender and bot_user in sender:
|
if sender and bot_user in sender:
|
||||||
get_logger().info(f"Ignoring comment from {bot_user} user")
|
logging.info(f"Ignoring comment from {bot_user} user")
|
||||||
return {}
|
return {}
|
||||||
get_logger().info(f"Processing comment from {sender} user")
|
logging.info(f"Processing comment from {sender} user")
|
||||||
if "issue" in body and "pull_request" in body["issue"] and "url" in body["issue"]["pull_request"]:
|
if "issue" in body and "pull_request" in body["issue"] and "url" in body["issue"]["pull_request"]:
|
||||||
api_url = body["issue"]["pull_request"]["url"]
|
api_url = body["issue"]["pull_request"]["url"]
|
||||||
elif "comment" in body and "pull_request_url" in body["comment"]:
|
elif "comment" in body and "pull_request_url" in body["comment"]:
|
||||||
api_url = body["comment"]["pull_request_url"]
|
api_url = body["comment"]["pull_request_url"]
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
log_context["api_url"] = api_url
|
logging.info(f"Handling comment because of event={event} and action={action}")
|
||||||
get_logger().info(body)
|
|
||||||
get_logger().info(f"Handling comment because of event={event} and action={action}")
|
|
||||||
comment_id = body.get("comment", {}).get("id")
|
comment_id = body.get("comment", {}).get("id")
|
||||||
provider = get_git_provider()(pr_url=api_url)
|
provider = get_git_provider()(pr_url=api_url)
|
||||||
with get_logger().contextualize(**log_context):
|
await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id))
|
||||||
await agent.handle_request(api_url, comment_body, notify=lambda: provider.add_eyes_reaction(comment_id))
|
|
||||||
|
|
||||||
# handle pull_request event:
|
# handle pull_request event:
|
||||||
# automatically review opened/reopened/ready_for_review PRs as long as they're not in draft,
|
# automatically review opened/reopened/ready_for_review PRs as long as they're not in draft,
|
||||||
@ -116,7 +112,6 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
api_url = pull_request.get("url")
|
api_url = pull_request.get("url")
|
||||||
if not api_url:
|
if not api_url:
|
||||||
return {}
|
return {}
|
||||||
log_context["api_url"] = api_url
|
|
||||||
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
|
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
|
||||||
return {}
|
return {}
|
||||||
if action in get_settings().github_app.handle_pr_actions:
|
if action in get_settings().github_app.handle_pr_actions:
|
||||||
@ -126,20 +121,12 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
if pull_request.get("created_at") == pull_request.get("updated_at"):
|
if pull_request.get("created_at") == pull_request.get("updated_at"):
|
||||||
# avoid double reviews when opening a PR for the first time
|
# avoid double reviews when opening a PR for the first time
|
||||||
return {}
|
return {}
|
||||||
get_logger().info(f"Performing review because of event={event} and action={action}")
|
logging.info(f"Performing review because of event={event} and action={action}")
|
||||||
apply_repo_settings(api_url)
|
|
||||||
for command in get_settings().github_app.pr_commands:
|
for command in get_settings().github_app.pr_commands:
|
||||||
split_command = command.split(" ")
|
logging.info(f"Performing command: {command}")
|
||||||
command = split_command[0]
|
await agent.handle_request(api_url, command)
|
||||||
args = split_command[1:]
|
|
||||||
other_args = update_settings_from_args(args)
|
|
||||||
new_command = ' '.join([command] + other_args)
|
|
||||||
get_logger().info(body)
|
|
||||||
get_logger().info(f"Performing command: {new_command}")
|
|
||||||
with get_logger().contextualize(**log_context):
|
|
||||||
await agent.handle_request(api_url, new_command)
|
|
||||||
|
|
||||||
get_logger().info("event or action does not require handling")
|
logging.info("event or action does not require handling")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@ -149,7 +136,7 @@ def _is_duplicate_request(body: Dict[str, Any]) -> bool:
|
|||||||
This function checks if the request is duplicate and if so - ignores it.
|
This function checks if the request is duplicate and if so - ignores it.
|
||||||
"""
|
"""
|
||||||
request_hash = hash(str(body))
|
request_hash = hash(str(body))
|
||||||
get_logger().info(f"request_hash: {request_hash}")
|
logging.info(f"request_hash: {request_hash}")
|
||||||
request_time = time.monotonic()
|
request_time = time.monotonic()
|
||||||
ttl = get_settings().github_app.duplicate_requests_cache_ttl # in seconds
|
ttl = get_settings().github_app.duplicate_requests_cache_ttl # in seconds
|
||||||
to_delete = [key for key, key_time in _duplicate_requests_cache.items() if request_time - key_time > ttl]
|
to_delete = [key for key, key_time in _duplicate_requests_cache.items() if request_time - key_time > ttl]
|
||||||
@ -158,7 +145,7 @@ def _is_duplicate_request(body: Dict[str, Any]) -> bool:
|
|||||||
is_duplicate = request_hash in _duplicate_requests_cache
|
is_duplicate = request_hash in _duplicate_requests_cache
|
||||||
_duplicate_requests_cache[request_hash] = request_time
|
_duplicate_requests_cache[request_hash] = request_time
|
||||||
if is_duplicate:
|
if is_duplicate:
|
||||||
get_logger().info(f"Ignoring duplicate request {request_hash}")
|
logging.info(f"Ignoring duplicate request {request_hash}")
|
||||||
return is_duplicate
|
return is_duplicate
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
@ -6,10 +8,9 @@ import aiohttp
|
|||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
|
||||||
from pr_agent.servers.help import bot_help_text
|
from pr_agent.servers.help import bot_help_text
|
||||||
|
|
||||||
setup_logger(fmt=LoggingFormat.JSON)
|
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
||||||
NOTIFICATION_URL = "https://api.github.com/notifications"
|
NOTIFICATION_URL = "https://api.github.com/notifications"
|
||||||
|
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ async def polling_loop():
|
|||||||
comment_body = comment['body'] if 'body' in comment else ''
|
comment_body = comment['body'] if 'body' in comment else ''
|
||||||
commenter_github_user = comment['user']['login'] \
|
commenter_github_user = comment['user']['login'] \
|
||||||
if 'user' in comment else ''
|
if 'user' in comment else ''
|
||||||
get_logger().info(f"Commenter: {commenter_github_user}\nComment: {comment_body}")
|
logging.info(f"Commenter: {commenter_github_user}\nComment: {comment_body}")
|
||||||
user_tag = "@" + user_id
|
user_tag = "@" + user_id
|
||||||
if user_tag not in comment_body:
|
if user_tag not in comment_body:
|
||||||
continue
|
continue
|
||||||
@ -111,7 +112,7 @@ async def polling_loop():
|
|||||||
print(f"Failed to fetch notifications. Status code: {response.status}")
|
print(f"Failed to fetch notifications. Status code: {response.status}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().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__':
|
||||||
|
@ -1,84 +1,43 @@
|
|||||||
import copy
|
import logging
|
||||||
import json
|
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import APIRouter, FastAPI, Request, status
|
from fastapi import APIRouter, FastAPI, Request, status
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
from starlette.middleware import Middleware
|
|
||||||
from starlette_context import context
|
|
||||||
from starlette_context.middleware import RawContextMiddleware
|
|
||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.config_loader import get_settings, global_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
|
||||||
from pr_agent.secret_providers import get_secret_provider
|
|
||||||
|
|
||||||
setup_logger(fmt=LoggingFormat.JSON)
|
app = FastAPI()
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
|
|
||||||
|
|
||||||
|
|
||||||
def handle_request(background_tasks: BackgroundTasks, url: str, body: str, log_context: dict):
|
|
||||||
log_context["action"] = body
|
|
||||||
log_context["event"] = "pull_request" if body == "/review" else "comment"
|
|
||||||
log_context["api_url"] = url
|
|
||||||
with get_logger().contextualize(**log_context):
|
|
||||||
background_tasks.add_task(PRAgent().handle_request, url, body)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/webhook")
|
@router.post("/webhook")
|
||||||
async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
||||||
log_context = {"server_type": "gitlab_app"}
|
|
||||||
if request.headers.get("X-Gitlab-Token") and secret_provider:
|
|
||||||
request_token = request.headers.get("X-Gitlab-Token")
|
|
||||||
secret = secret_provider.get_secret(request_token)
|
|
||||||
try:
|
|
||||||
secret_dict = json.loads(secret)
|
|
||||||
gitlab_token = secret_dict["gitlab_token"]
|
|
||||||
log_context["sender"] = secret_dict["id"]
|
|
||||||
context["settings"] = copy.deepcopy(global_settings)
|
|
||||||
context["settings"].gitlab.personal_access_token = gitlab_token
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to validate secret {request_token}: {e}")
|
|
||||||
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
|
|
||||||
elif get_settings().get("GITLAB.SHARED_SECRET"):
|
|
||||||
secret = get_settings().get("GITLAB.SHARED_SECRET")
|
|
||||||
if not request.headers.get("X-Gitlab-Token") == secret:
|
|
||||||
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
|
|
||||||
else:
|
|
||||||
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
|
|
||||||
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
|
|
||||||
if not gitlab_token:
|
|
||||||
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content=jsonable_encoder({"message": "unauthorized"}))
|
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
get_logger().info(json.dumps(data))
|
|
||||||
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
||||||
get_logger().info(f"A merge request has been opened: {data['object_attributes'].get('title')}")
|
logging.info(f"A merge request has been opened: {data['object_attributes'].get('title')}")
|
||||||
url = data['object_attributes'].get('url')
|
url = data['object_attributes'].get('url')
|
||||||
handle_request(background_tasks, url, "/review")
|
background_tasks.add_task(PRAgent().handle_request, url, "/review")
|
||||||
elif data.get('object_kind') == 'note' and data['event_type'] == 'note':
|
elif data.get('object_kind') == 'note' and data['event_type'] == 'note':
|
||||||
if 'merge_request' in data:
|
if 'merge_request' in data:
|
||||||
mr = data['merge_request']
|
mr = data['merge_request']
|
||||||
url = mr.get('url')
|
url = mr.get('url')
|
||||||
body = data.get('object_attributes', {}).get('note')
|
body = data.get('object_attributes', {}).get('note')
|
||||||
handle_request(background_tasks, url, body)
|
background_tasks.add_task(PRAgent().handle_request, url, body)
|
||||||
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
|
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
|
||||||
async def root():
|
|
||||||
return {"status": "ok"}
|
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
gitlab_url = get_settings().get("GITLAB.URL", None)
|
gitlab_url = get_settings().get("GITLAB.URL", None)
|
||||||
if not gitlab_url:
|
if not gitlab_url:
|
||||||
raise ValueError("GITLAB.URL is not set")
|
raise ValueError("GITLAB.URL is not set")
|
||||||
|
gitlab_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None)
|
||||||
|
if not gitlab_token:
|
||||||
|
raise ValueError("GITLAB.PERSONAL_ACCESS_TOKEN is not set")
|
||||||
get_settings().config.git_provider = "gitlab"
|
get_settings().config.git_provider = "gitlab"
|
||||||
middleware = [Middleware(RawContextMiddleware)]
|
|
||||||
app = FastAPI(middleware=middleware)
|
app = FastAPI()
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=3000)
|
uvicorn.run(app, host="0.0.0.0", port=3000)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from mangum import Mangum
|
from mangum import Mangum
|
||||||
|
|
||||||
from pr_agent.log import setup_logger
|
|
||||||
from pr_agent.servers.github_app import router
|
from pr_agent.servers.github_app import router
|
||||||
|
|
||||||
setup_logger()
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
@ -16,10 +16,6 @@ key = "" # Acquire through https://platform.openai.com
|
|||||||
#deployment_id = "" # The deployment name you chose when you deployed the engine
|
#deployment_id = "" # The deployment name you chose when you deployed the engine
|
||||||
#fallback_deployments = [] # For each fallback model specified in configuration.toml in the [config] section, specify the appropriate deployment_id
|
#fallback_deployments = [] # For each fallback model specified in configuration.toml in the [config] section, specify the appropriate deployment_id
|
||||||
|
|
||||||
[pinecone]
|
|
||||||
api_key = "..."
|
|
||||||
environment = "gcp-starter"
|
|
||||||
|
|
||||||
[anthropic]
|
[anthropic]
|
||||||
key = "" # Optional, uncomment if you want to use Anthropic. Acquire through https://www.anthropic.com/
|
key = "" # Optional, uncomment if you want to use Anthropic. Acquire through https://www.anthropic.com/
|
||||||
|
|
||||||
@ -28,14 +24,6 @@ key = "" # Optional, uncomment if you want to use Cohere. Acquire through https:
|
|||||||
|
|
||||||
[replicate]
|
[replicate]
|
||||||
key = "" # Optional, uncomment if you want to use Replicate. Acquire through https://replicate.com/
|
key = "" # Optional, uncomment if you want to use Replicate. Acquire through https://replicate.com/
|
||||||
|
|
||||||
[huggingface]
|
|
||||||
key = "" # Optional, uncomment if you want to use Huggingface Inference API. Acquire through https://huggingface.co/docs/api-inference/quicktour
|
|
||||||
api_base = "" # the base url for your huggingface inference endpoint
|
|
||||||
|
|
||||||
[ollama]
|
|
||||||
api_base = "" # the base url for your huggingface inference endpoint
|
|
||||||
|
|
||||||
[github]
|
[github]
|
||||||
# ---- Set the following only for deployment type == "user"
|
# ---- Set the following only for deployment type == "user"
|
||||||
user_token = "" # A GitHub personal access token with 'repo' scope.
|
user_token = "" # A GitHub personal access token with 'repo' scope.
|
||||||
@ -55,12 +43,5 @@ webhook_secret = "<WEBHOOK SECRET>" # Optional, may be commented out.
|
|||||||
personal_access_token = ""
|
personal_access_token = ""
|
||||||
|
|
||||||
[bitbucket]
|
[bitbucket]
|
||||||
# For Bitbucket personal/repository bearer token
|
# Bitbucket personal bearer token
|
||||||
bearer_token = ""
|
bearer_token = ""
|
||||||
|
|
||||||
# For Bitbucket app
|
|
||||||
app_key = ""
|
|
||||||
base_url = ""
|
|
||||||
|
|
||||||
[litellm]
|
|
||||||
LITELLM_TOKEN = "" # see https://docs.litellm.ai/docs/debugging/hosted_debugging for details and instructions on how to get a token
|
|
||||||
|
@ -10,16 +10,14 @@ use_repo_settings_file=true
|
|||||||
ai_timeout=180
|
ai_timeout=180
|
||||||
max_description_tokens = 500
|
max_description_tokens = 500
|
||||||
max_commits_tokens = 500
|
max_commits_tokens = 500
|
||||||
patch_extra_lines = 3
|
litellm_debugger=false
|
||||||
secret_provider="google_cloud_storage"
|
secret_provider="google_cloud_storage"
|
||||||
cli_mode=false
|
|
||||||
|
|
||||||
[pr_reviewer] # /review #
|
[pr_reviewer] # /review #
|
||||||
require_focused_review=false
|
require_focused_review=false
|
||||||
require_score_review=false
|
require_score_review=false
|
||||||
require_tests_review=true
|
require_tests_review=true
|
||||||
require_security_review=true
|
require_security_review=true
|
||||||
require_estimate_effort_to_review=true
|
|
||||||
num_code_suggestions=4
|
num_code_suggestions=4
|
||||||
inline_code_comments = false
|
inline_code_comments = false
|
||||||
ask_and_reflect=false
|
ask_and_reflect=false
|
||||||
@ -27,19 +25,11 @@ automatic_review=true
|
|||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
|
|
||||||
[pr_description] # /describe #
|
[pr_description] # /describe #
|
||||||
publish_labels=true
|
|
||||||
publish_description_as_comment=false
|
publish_description_as_comment=false
|
||||||
add_original_user_description=false
|
add_original_user_description=false
|
||||||
keep_original_user_title=false
|
keep_original_user_title=false
|
||||||
use_bullet_points=true
|
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
|
|
||||||
# markers
|
|
||||||
use_description_markers=false
|
|
||||||
include_generated_by_header=true
|
|
||||||
|
|
||||||
#custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Refactoring', 'Enhancement', 'Documentation', 'Other']
|
|
||||||
|
|
||||||
[pr_questions] # /ask #
|
[pr_questions] # /ask #
|
||||||
|
|
||||||
[pr_code_suggestions] # /improve #
|
[pr_code_suggestions] # /improve #
|
||||||
@ -52,10 +42,6 @@ rank_extended_suggestions = true
|
|||||||
max_number_of_calls = 5
|
max_number_of_calls = 5
|
||||||
final_clip_factor = 0.9
|
final_clip_factor = 0.9
|
||||||
|
|
||||||
[pr_add_docs] # /add_docs #
|
|
||||||
extra_instructions = ""
|
|
||||||
docs_style = "Sphinx Style" # "Google Style with Args, Returns, Attributes...etc", "Numpy Style", "Sphinx Style", "PEP257", "reStructuredText"
|
|
||||||
|
|
||||||
[pr_update_changelog] # /update_changelog #
|
[pr_update_changelog] # /update_changelog #
|
||||||
push_changelog_changes=false
|
push_changelog_changes=false
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
@ -67,11 +53,6 @@ extra_instructions = ""
|
|||||||
deployment_type = "user"
|
deployment_type = "user"
|
||||||
ratelimit_retries = 5
|
ratelimit_retries = 5
|
||||||
|
|
||||||
[github_action]
|
|
||||||
# auto_review = true # set as env var in .github/workflows/pr-agent.yaml
|
|
||||||
# auto_describe = true # set as env var in .github/workflows/pr-agent.yaml
|
|
||||||
# auto_improve = true # set as env var in .github/workflows/pr-agent.yaml
|
|
||||||
|
|
||||||
[github_app]
|
[github_app]
|
||||||
# these toggles allows running the github app from custom deployments
|
# these toggles allows running the github app from custom deployments
|
||||||
bot_user = "github-actions[bot]"
|
bot_user = "github-actions[bot]"
|
||||||
@ -103,27 +84,4 @@ polling_interval_seconds = 30
|
|||||||
[local]
|
[local]
|
||||||
# LocalGitProvider settings - uncomment to use paths other than default
|
# LocalGitProvider settings - uncomment to use paths other than default
|
||||||
# description_path= "path/to/description.md"
|
# description_path= "path/to/description.md"
|
||||||
# review_path= "path/to/review.md"
|
# review_path= "path/to/review.md"
|
||||||
|
|
||||||
[gerrit]
|
|
||||||
# endpoint to the gerrit service
|
|
||||||
# url = "ssh://gerrit.example.com:29418"
|
|
||||||
# user for gerrit authentication
|
|
||||||
# user = "ai-reviewer"
|
|
||||||
# patch server where patches will be saved
|
|
||||||
# patch_server_endpoint = "http://127.0.0.1:5000/patch"
|
|
||||||
# token to authenticate in the patch server
|
|
||||||
# patch_server_token = ""
|
|
||||||
|
|
||||||
[litellm]
|
|
||||||
#use_client = false
|
|
||||||
|
|
||||||
[pr_similar_issue]
|
|
||||||
skip_comments = false
|
|
||||||
force_update_dataset = false
|
|
||||||
max_issues_to_scan = 500
|
|
||||||
|
|
||||||
[pinecone]
|
|
||||||
# fill and place in .secrets.toml
|
|
||||||
#api_key = ...
|
|
||||||
# environment = "gcp-starter"
|
|
@ -1,18 +0,0 @@
|
|||||||
[config]
|
|
||||||
enable_custom_labels=false
|
|
||||||
|
|
||||||
## template for custom labels
|
|
||||||
#[custom_labels."Bug fix"]
|
|
||||||
#description = "Fixes a bug in the code"
|
|
||||||
#[custom_labels."Tests"]
|
|
||||||
#description = "Adds or modifies tests"
|
|
||||||
#[custom_labels."Bug fix with tests"]
|
|
||||||
#description = "Fixes a bug in the code and adds or modifies tests"
|
|
||||||
#[custom_labels."Refactoring"]
|
|
||||||
#description = "Code refactoring without changing functionality"
|
|
||||||
#[custom_labels."Enhancement"]
|
|
||||||
#description = "Adds new features or functionality"
|
|
||||||
#[custom_labels."Documentation"]
|
|
||||||
#description = "Adds or modifies documentation"
|
|
||||||
#[custom_labels."Other"]
|
|
||||||
#description = "Other changes that do not fit in any of the above categories"
|
|
@ -1,11 +0,0 @@
|
|||||||
[ignore]
|
|
||||||
|
|
||||||
glob = [
|
|
||||||
# Ignore files and directories matching these glob patterns.
|
|
||||||
# See https://docs.python.org/3/library/glob.html
|
|
||||||
'vendor/**',
|
|
||||||
]
|
|
||||||
regex = [
|
|
||||||
# Ignore files and directories matching these regex patterns.
|
|
||||||
# See https://learnbyexample.github.io/python-regex-cheatsheet/
|
|
||||||
]
|
|
@ -53,8 +53,7 @@ default = [
|
|||||||
'xz',
|
'xz',
|
||||||
'zip',
|
'zip',
|
||||||
'zst',
|
'zst',
|
||||||
'snap',
|
'snap'
|
||||||
'lockb'
|
|
||||||
]
|
]
|
||||||
extra = [
|
extra = [
|
||||||
'md',
|
'md',
|
||||||
@ -433,6 +432,3 @@ reStructuredText = [".rst", ".rest", ".rest.txt", ".rst.txt", ]
|
|||||||
wisp = [".wisp", ]
|
wisp = [".wisp", ]
|
||||||
xBase = [".prg", ".prw", ]
|
xBase = [".prg", ".prw", ]
|
||||||
|
|
||||||
[docs_blacklist_extensions]
|
|
||||||
# Disable docs for these extensions of text files and scripts that are not programming languages of function, classes and methods
|
|
||||||
docs_blacklist = ['sql', 'txt', 'yaml', 'json', 'xml', 'md', 'rst', 'rest', 'rest.txt', 'rst.txt', 'mdpolicy', 'mdown', 'markdown', 'mdwn', 'mkd', 'mkdn', 'mkdown', 'sh']
|
|
@ -1,117 +0,0 @@
|
|||||||
[pr_add_docs_prompt]
|
|
||||||
system="""You are a language model called PR-Code-Documentation Agent, that specializes in generating documentation for code.
|
|
||||||
Your task is to generate meaningfull {{ docs_for_language }} to a PR (the '+' lines).
|
|
||||||
|
|
||||||
Example for a PR Diff input:
|
|
||||||
'
|
|
||||||
## src/file1.py
|
|
||||||
|
|
||||||
@@ -12,3 +12,5 @@ def func1():
|
|
||||||
__new hunk__
|
|
||||||
12 code line that already existed in the file...
|
|
||||||
13 code line that already existed in the file....
|
|
||||||
14 +new code line1 added in the PR
|
|
||||||
15 +new code line2 added in the PR
|
|
||||||
16 code line that already existed in the file...
|
|
||||||
__old hunk__
|
|
||||||
code line that already existed in the file...
|
|
||||||
-code line that was removed in the PR
|
|
||||||
code line that already existed in the file...
|
|
||||||
|
|
||||||
|
|
||||||
@@ ... @@ def func2():
|
|
||||||
__new hunk__
|
|
||||||
...
|
|
||||||
__old hunk__
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
## src/file2.py
|
|
||||||
...
|
|
||||||
'
|
|
||||||
|
|
||||||
Specific instructions:
|
|
||||||
- Try to identify edited/added code components (classes/functions/methods...) that are undocumented. and generate {{ docs_for_language }} for each one.
|
|
||||||
- If there are documented (any type of {{ language }} documentation) code components in the PR, Don't generate {{ docs_for_language }} for them.
|
|
||||||
- Ignore code components that don't appear fully in the '__new hunk__' section. For example. you must see the component header and body,
|
|
||||||
- Make sure the {{ docs_for_language }} starts and ends with standart {{ language }} {{ docs_for_language }} signs.
|
|
||||||
- The {{ docs_for_language }} should be in standard format.
|
|
||||||
- Provide the exact line number (inclusive) where the {{ docs_for_language }} should be added.
|
|
||||||
|
|
||||||
|
|
||||||
{%- if extra_instructions %}
|
|
||||||
|
|
||||||
Extra instructions from the user:
|
|
||||||
'
|
|
||||||
{{ extra_instructions }}
|
|
||||||
'
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
You must use the following YAML schema to format your answer:
|
|
||||||
```yaml
|
|
||||||
Code Documentation:
|
|
||||||
type: array
|
|
||||||
uniqueItems: true
|
|
||||||
items:
|
|
||||||
relevant file:
|
|
||||||
type: string
|
|
||||||
description: the relevant file full path
|
|
||||||
relevant line:
|
|
||||||
type: integer
|
|
||||||
description: |-
|
|
||||||
The relevant line number from a '__new hunk__' section where the {{ docs_for_language }} should be added.
|
|
||||||
doc placement:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
- before
|
|
||||||
- after
|
|
||||||
description: |-
|
|
||||||
The {{ docs_for_language }} placement relative to the relevant line (code component).
|
|
||||||
documentation:
|
|
||||||
type: string
|
|
||||||
description: |-
|
|
||||||
The {{ docs_for_language }} content. It should be complete, correctly formatted and indented, and without line numbers.
|
|
||||||
```
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```yaml
|
|
||||||
Code Documentation:
|
|
||||||
- relevant file: |-
|
|
||||||
src/file1.py
|
|
||||||
relevant lines: 12
|
|
||||||
doc placement: after
|
|
||||||
documentation: |-
|
|
||||||
\"\"\"
|
|
||||||
This is a python docstring for func1.
|
|
||||||
\"\"\"
|
|
||||||
- ...
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Each YAML output MUST be after a newline, indented, with block scalar indicator ('|-').
|
|
||||||
Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
|
|
||||||
"""
|
|
||||||
|
|
||||||
user="""PR Info:
|
|
||||||
|
|
||||||
Title: '{{ title }}'
|
|
||||||
|
|
||||||
Branch: '{{ branch }}'
|
|
||||||
|
|
||||||
Description: '{{description}}'
|
|
||||||
|
|
||||||
{%- if language %}
|
|
||||||
|
|
||||||
Main language: {{language}}
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
|
|
||||||
The PR Diff:
|
|
||||||
```
|
|
||||||
{{- diff|trim }}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response (should be a valid YAML, and nothing else):
|
|
||||||
```yaml
|
|
||||||
"""
|
|
@ -1,6 +1,6 @@
|
|||||||
[pr_code_suggestions_prompt]
|
[pr_code_suggestions_prompt]
|
||||||
system="""You are a language model called PR-Code-Reviewer, that specializes in suggesting code improvements for Pull Request (PR).
|
system="""You are a language model called PR-Code-Reviewer, that specializes in suggesting code improvements for Pull Request (PR).
|
||||||
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR (the '+' lines in the diff).
|
Your task is to provide meaningful and actionable code suggestions, to improve the new code presented in a PR.
|
||||||
|
|
||||||
Example for a PR Diff input:
|
Example for a PR Diff input:
|
||||||
'
|
'
|
||||||
@ -31,13 +31,14 @@ __old hunk__
|
|||||||
'
|
'
|
||||||
|
|
||||||
Specific instructions:
|
Specific instructions:
|
||||||
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
|
- Provide up to {{ num_code_suggestions }} code suggestions.
|
||||||
- Prioritize suggestions that address major problems, issues and bugs in the code.
|
- Prioritize suggestions that address major problems, issues and bugs in the code.
|
||||||
As a second priority, suggestions should focus on best practices, code readability, maintainability, enhancments, performance, and other aspects.
|
As a second priority, suggestions should focus on best practices, code readability, maintainability, enhancments, performance, and other aspects.
|
||||||
- Don't suggest to add docstring, type hints, or comments.
|
Don't suggest to add docstring or type hints.
|
||||||
|
Try to provide diverse and insightful suggestions.
|
||||||
- Suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+').
|
- Suggestions should refer only to code from the '__new hunk__' sections, and focus on new lines of code (lines starting with '+').
|
||||||
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the '__new hunk__' code.
|
Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the '__new hunk__' code.
|
||||||
- For each suggestion, make sure to take into consideration also the context, meaning the lines before and after the relevant code.
|
For each suggestion, make sure to take into consideration also the context, meaning the lines before and after the relevant code.
|
||||||
- Provide the exact line numbers range (inclusive) for each issue.
|
- Provide the exact line numbers range (inclusive) for each issue.
|
||||||
- Assume there is additional relevant code, that is not included in the diff.
|
- Assume there is additional relevant code, that is not included in the diff.
|
||||||
|
|
||||||
@ -45,9 +46,7 @@ Specific instructions:
|
|||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
Extra instructions from the user:
|
Extra instructions from the user:
|
||||||
'
|
|
||||||
{{ extra_instructions }}
|
{{ extra_instructions }}
|
||||||
'
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
You must use the following YAML schema to format your answer:
|
You must use the following YAML schema to format your answer:
|
||||||
@ -69,17 +68,12 @@ Code suggestions:
|
|||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
a code snippet showing the relevant code lines from a '__new hunk__' section.
|
a code snippet showing the relevant code lines from a '__new hunk__' section.
|
||||||
It must be contiguous, correctly formatted and indented, and without line numbers.
|
It must be continuous, correctly formatted and indented, and without line numbers.
|
||||||
relevant lines start:
|
relevant lines:
|
||||||
type: integer
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
The relevant line number from a '__new hunk__' section where the suggestion starts (inclusive).
|
the relevant lines from a '__new hunk__' section, in the format of 'start_line-end_line'.
|
||||||
Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.
|
For example: '10-15'. They should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.
|
||||||
relevant lines end:
|
|
||||||
type: integer
|
|
||||||
description: |-
|
|
||||||
The relevant line number from a '__new hunk__' section where the suggestion ends (inclusive).
|
|
||||||
Should be derived from the hunk line numbers, and correspond to the 'existing code' snippet above.
|
|
||||||
improved code:
|
improved code:
|
||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
@ -96,8 +90,7 @@ Code suggestions:
|
|||||||
Add a docstring to func1()
|
Add a docstring to func1()
|
||||||
existing code: |-
|
existing code: |-
|
||||||
def func1():
|
def func1():
|
||||||
relevant lines start: 12
|
relevant lines: '12-12'
|
||||||
relevant lines end: 12
|
|
||||||
improved code: |-
|
improved code: |-
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
[pr_custom_labels_prompt]
|
|
||||||
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
|
|
||||||
Your task is to label the type of the PR content.
|
|
||||||
- Make sure not to focus the new PR code (the '+' lines).
|
|
||||||
- If needed, each YAML output should be in block scalar format ('|-')
|
|
||||||
{%- if extra_instructions %}
|
|
||||||
|
|
||||||
Extra instructions from the user:
|
|
||||||
'
|
|
||||||
{{ extra_instructions }}
|
|
||||||
'
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
You must use the following YAML schema to format your answer:
|
|
||||||
```yaml
|
|
||||||
PR Type:
|
|
||||||
type: array
|
|
||||||
{%- if enable_custom_labels %}
|
|
||||||
description: One or more labels that describe the PR type. Don't output the description in the parentheses.
|
|
||||||
{%- endif %}
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
enum:
|
|
||||||
{%- if enable_custom_labels %}
|
|
||||||
{{ custom_labels }}
|
|
||||||
{%- else %}
|
|
||||||
- Bug fix
|
|
||||||
- Tests
|
|
||||||
- Refactoring
|
|
||||||
- Enhancement
|
|
||||||
- Documentation
|
|
||||||
- Other
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```yaml
|
|
||||||
PR Type:
|
|
||||||
{%- if enable_custom_labels %}
|
|
||||||
{{ custom_labels_examples }}
|
|
||||||
{%- else %}
|
|
||||||
- Bug fix
|
|
||||||
- Tests
|
|
||||||
{%- endif %}
|
|
||||||
```
|
|
||||||
|
|
||||||
Make sure to output a valid YAML. Don't repeat the prompt in the answer, and avoid outputting the 'type' and 'description' fields.
|
|
||||||
"""
|
|
||||||
|
|
||||||
user="""PR Info:
|
|
||||||
Previous title: '{{title}}'
|
|
||||||
Previous description: '{{description}}'
|
|
||||||
Branch: '{{branch}}'
|
|
||||||
{%- if language %}
|
|
||||||
|
|
||||||
Main language: {{language}}
|
|
||||||
{%- endif %}
|
|
||||||
{%- if commit_messages_str %}
|
|
||||||
|
|
||||||
Commit messages:
|
|
||||||
{{commit_messages_str}}
|
|
||||||
{%- 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 (should be a valid YAML, and nothing else):
|
|
||||||
```yaml
|
|
||||||
"""
|
|
@ -7,9 +7,7 @@ Your task is to provide full description of the PR content.
|
|||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
Extra instructions from the user:
|
Extra instructions from the user:
|
||||||
'
|
|
||||||
{{ extra_instructions }}
|
{{ extra_instructions }}
|
||||||
'
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
You must use the following YAML schema to format your answer:
|
You must use the following YAML schema to format your answer:
|
||||||
@ -19,26 +17,19 @@ PR Title:
|
|||||||
description: an informative title for the PR, describing its main theme
|
description: an informative title for the PR, describing its main theme
|
||||||
PR Type:
|
PR Type:
|
||||||
type: array
|
type: array
|
||||||
{%- if enable_custom_labels %}
|
|
||||||
description: One or more labels that describe the PR type. Don't output the description in the parentheses.
|
|
||||||
{%- endif %}
|
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
{%- if enable_custom_labels %}
|
|
||||||
{{ custom_labels }}
|
|
||||||
{%- else %}
|
|
||||||
- Bug fix
|
- Bug fix
|
||||||
- Tests
|
- Tests
|
||||||
|
- Bug fix with tests
|
||||||
- Refactoring
|
- Refactoring
|
||||||
- Enhancement
|
- Enhancement
|
||||||
- Documentation
|
- Documentation
|
||||||
- Other
|
- Other
|
||||||
{%- endif %}
|
|
||||||
PR Description:
|
PR Description:
|
||||||
type: string
|
type: string
|
||||||
description: an informative and concise description of the PR.
|
description: an informative and concise description of the PR
|
||||||
{%- if use_bullet_points %} Use bullet points. {% endif %}
|
|
||||||
PR Main Files Walkthrough:
|
PR Main Files Walkthrough:
|
||||||
type: array
|
type: array
|
||||||
maxItems: 10
|
maxItems: 10
|
||||||
@ -58,11 +49,7 @@ Example output:
|
|||||||
PR Title: |-
|
PR Title: |-
|
||||||
...
|
...
|
||||||
PR Type:
|
PR Type:
|
||||||
{%- if enable_custom_labels %}
|
|
||||||
{{ custom_labels_examples }}
|
|
||||||
{%- else %}
|
|
||||||
- Bug fix
|
- Bug fix
|
||||||
{%- endif %}
|
|
||||||
PR Description: |-
|
PR Description: |-
|
||||||
...
|
...
|
||||||
PR Main Files Walkthrough:
|
PR Main Files Walkthrough:
|
||||||
|
@ -22,22 +22,20 @@ code line that already existed in the file....
|
|||||||
...
|
...
|
||||||
'
|
'
|
||||||
|
|
||||||
The review should focus on new code added in the PR (lines starting with '+'), and not on code that already existed in the file (lines starting with '-', or without prefix).
|
Thre review should focus on new code added in the PR (lines starting with '+'), and not on code that already existed in the file (lines starting with '-', or without prefix).
|
||||||
|
|
||||||
{%- if num_code_suggestions > 0 %}
|
{%- if num_code_suggestions > 0 %}
|
||||||
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
|
- Provide up to {{ num_code_suggestions }} code suggestions.
|
||||||
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
|
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
|
||||||
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
|
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
|
||||||
- Don't suggest to add docstring, type hints, or comments.
|
- Don't suggest to add docstring or type hints.
|
||||||
- Suggestions should focus on improving the new code added in the PR (lines starting with '+')
|
- Suggestions should focus on improving the new code added in the PR (lines starting with '+')
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
Extra instructions from the user:
|
Extra instructions from the user:
|
||||||
'
|
|
||||||
{{ extra_instructions }}
|
{{ extra_instructions }}
|
||||||
'
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
You must use the following YAML schema to format your answer:
|
You must use the following YAML schema to format your answer:
|
||||||
@ -51,22 +49,13 @@ PR Analysis:
|
|||||||
description: summary of the PR in 2-3 sentences.
|
description: summary of the PR in 2-3 sentences.
|
||||||
Type of PR:
|
Type of PR:
|
||||||
type: string
|
type: string
|
||||||
{%- if enable_custom_labels %}
|
|
||||||
description: One or more labels that describe the PR type. Don't output the description in the parentheses.
|
|
||||||
{%- endif %}
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
enum:
|
enum:
|
||||||
{%- if enable_custom_labels %}
|
|
||||||
{{ custom_labels }}
|
|
||||||
{%- else %}
|
|
||||||
- Bug fix
|
- Bug fix
|
||||||
- Tests
|
- Tests
|
||||||
- Refactoring
|
- Refactoring
|
||||||
- Enhancement
|
- Enhancement
|
||||||
- Documentation
|
- Documentation
|
||||||
- Other
|
- Other
|
||||||
{%- endif %}
|
|
||||||
{%- if require_score %}
|
{%- if require_score %}
|
||||||
Score:
|
Score:
|
||||||
type: int
|
type: int
|
||||||
@ -96,22 +85,14 @@ PR Analysis:
|
|||||||
code diff changes are too scattered, then the PR is not focused. Explain
|
code diff changes are too scattered, then the PR is not focused. Explain
|
||||||
your answer shortly.
|
your answer shortly.
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if require_estimate_effort_to_review %}
|
|
||||||
Estimated effort to review [1-5]:
|
|
||||||
type: string
|
|
||||||
description: >-
|
|
||||||
Estimate, on a scale of 1-5 (inclusive), the time and effort required to review this PR by an experienced and knowledgeable developer. 1 means short and easy review , 5 means long and hard review.
|
|
||||||
Take into account the size, complexity, quality, and the needed changes of the PR code diff.
|
|
||||||
Explain your answer shortly (1-2 sentences).
|
|
||||||
{%- endif %}
|
|
||||||
PR Feedback:
|
PR Feedback:
|
||||||
General suggestions:
|
General suggestions:
|
||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
General suggestions and feedback for the contributors and maintainers of this PR.
|
General suggestions and feedback for the contributors and maintainers of
|
||||||
May include important suggestions for the overall structure,
|
this PR. May include important suggestions for the overall structure,
|
||||||
primary purpose, best practices, critical bugs, and other aspects of the PR.
|
primary purpose, best practices, critical bugs, and other aspects of the
|
||||||
Don't address PR title and description, or lack of tests. Explain your suggestions.
|
PR. Don't address PR title and description, or lack of tests. Explain your suggestions.
|
||||||
{%- if num_code_suggestions > 0 %}
|
{%- if num_code_suggestions > 0 %}
|
||||||
Code feedback:
|
Code feedback:
|
||||||
type: array
|
type: array
|
||||||
@ -124,10 +105,11 @@ PR Feedback:
|
|||||||
suggestion:
|
suggestion:
|
||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
a concrete suggestion for meaningfully improving the new PR code.
|
a concrete suggestion for meaningfully improving the new PR code. Also
|
||||||
Also describe how, specifically, the suggestion can be applied to new PR code.
|
describe how, specifically, the suggestion can be applied to new PR
|
||||||
Add tags with importance measure that matches each suggestion ('important' or 'medium').
|
code. Add tags with importance measure that matches each suggestion
|
||||||
Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.
|
('important' or 'medium'). Do not make suggestions for updating or
|
||||||
|
adding docstrings, renaming PR title and description, or linter like.
|
||||||
relevant line:
|
relevant line:
|
||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
@ -139,7 +121,8 @@ PR Feedback:
|
|||||||
Security concerns:
|
Security concerns:
|
||||||
type: string
|
type: string
|
||||||
description: >-
|
description: >-
|
||||||
yes\\no question: does this PR code introduce possible vulnerabilities such as exposure of sensitive information (e.g., API keys, secrets, passwords), or security concerns like SQL injection, XSS, CSRF, and others ? If answered 'yes', explain your answer briefly.
|
yes\\no question: does this PR code introduce possible security concerns or
|
||||||
|
issues, like SQL injection, XSS, CSRF, and others ? If answered 'yes',explain your answer shortly
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -160,9 +143,6 @@ PR Analysis:
|
|||||||
{%- if require_focused %}
|
{%- if require_focused %}
|
||||||
Focused PR: no, because ...
|
Focused PR: no, because ...
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if require_estimate_effort_to_review %}
|
|
||||||
Estimated effort to review [1-5]: 3, because ...
|
|
||||||
{%- endif %}
|
|
||||||
PR Feedback:
|
PR Feedback:
|
||||||
General PR suggestions: |-
|
General PR suggestions: |-
|
||||||
...
|
...
|
||||||
@ -205,9 +185,7 @@ Here are questions to better understand the PR. Use the answers to provide bette
|
|||||||
{{question_str|trim}}
|
{{question_str|trim}}
|
||||||
|
|
||||||
User answers:
|
User answers:
|
||||||
'
|
|
||||||
{{answer_str|trim}}
|
{{answer_str|trim}}
|
||||||
'
|
|
||||||
######
|
######
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
@ -8,9 +8,7 @@ Your task is to update the CHANGELOG.md file of the project, to shortly summariz
|
|||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
Extra instructions from the user:
|
Extra instructions from the user:
|
||||||
'
|
|
||||||
{{ extra_instructions }}
|
{{ extra_instructions }}
|
||||||
'
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1,179 +0,0 @@
|
|||||||
import copy
|
|
||||||
import textwrap
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
|
||||||
|
|
||||||
from pr_agent.algo.ai_handler import AiHandler
|
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
|
||||||
from pr_agent.algo.utils import load_yaml
|
|
||||||
from pr_agent.config_loader import get_settings
|
|
||||||
from pr_agent.git_providers import get_git_provider
|
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class PRAddDocs:
|
|
||||||
def __init__(self, pr_url: str, cli_mode=False, args: list = None):
|
|
||||||
|
|
||||||
self.git_provider = get_git_provider()(pr_url)
|
|
||||||
self.main_language = get_main_pr_language(
|
|
||||||
self.git_provider.get_languages(), self.git_provider.get_files()
|
|
||||||
)
|
|
||||||
|
|
||||||
self.ai_handler = AiHandler()
|
|
||||||
self.patches_diff = None
|
|
||||||
self.prediction = None
|
|
||||||
self.cli_mode = cli_mode
|
|
||||||
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_language,
|
|
||||||
"diff": "", # empty diff for initial calculation
|
|
||||||
"extra_instructions": get_settings().pr_add_docs.extra_instructions,
|
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
|
||||||
'docs_for_language': get_docs_for_language(self.main_language,
|
|
||||||
get_settings().pr_add_docs.docs_style),
|
|
||||||
}
|
|
||||||
self.token_handler = TokenHandler(self.git_provider.pr,
|
|
||||||
self.vars,
|
|
||||||
get_settings().pr_add_docs_prompt.system,
|
|
||||||
get_settings().pr_add_docs_prompt.user)
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
try:
|
|
||||||
get_logger().info('Generating code Docs for PR...')
|
|
||||||
if get_settings().config.publish_output:
|
|
||||||
self.git_provider.publish_comment("Generating Documentation...", is_temporary=True)
|
|
||||||
|
|
||||||
get_logger().info('Preparing PR documentation...')
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
|
||||||
data = self._prepare_pr_code_docs()
|
|
||||||
if (not data) or (not 'Code Documentation' in data):
|
|
||||||
get_logger().info('No code documentation found for PR.')
|
|
||||||
return
|
|
||||||
|
|
||||||
if get_settings().config.publish_output:
|
|
||||||
get_logger().info('Pushing PR documentation...')
|
|
||||||
self.git_provider.remove_initial_comment()
|
|
||||||
get_logger().info('Pushing inline code documentation...')
|
|
||||||
self.push_inline_docs(data)
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to generate code documentation for PR, error: {e}")
|
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str):
|
|
||||||
get_logger().info('Getting PR diff...')
|
|
||||||
|
|
||||||
# Disable adding docs to scripts and other non-relevant text files
|
|
||||||
from pr_agent.algo.language_handler import bad_extensions
|
|
||||||
bad_extensions += get_settings().docs_blacklist_extensions.docs_blacklist
|
|
||||||
|
|
||||||
self.patches_diff = get_pr_diff(self.git_provider,
|
|
||||||
self.token_handler,
|
|
||||||
model,
|
|
||||||
add_line_numbers_to_hunks=True,
|
|
||||||
disable_extra_lines=False)
|
|
||||||
|
|
||||||
get_logger().info('Getting AI prediction...')
|
|
||||||
self.prediction = await self._get_prediction(model)
|
|
||||||
|
|
||||||
async def _get_prediction(self, model: str):
|
|
||||||
variables = copy.deepcopy(self.vars)
|
|
||||||
variables["diff"] = self.patches_diff # update diff
|
|
||||||
environment = Environment(undefined=StrictUndefined)
|
|
||||||
system_prompt = environment.from_string(get_settings().pr_add_docs_prompt.system).render(variables)
|
|
||||||
user_prompt = environment.from_string(get_settings().pr_add_docs_prompt.user).render(variables)
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
|
||||||
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_code_docs(self) -> Dict:
|
|
||||||
docs = self.prediction.strip()
|
|
||||||
data = load_yaml(docs)
|
|
||||||
if isinstance(data, list):
|
|
||||||
data = {'Code Documentation': data}
|
|
||||||
return data
|
|
||||||
|
|
||||||
def push_inline_docs(self, data):
|
|
||||||
docs = []
|
|
||||||
|
|
||||||
if not data['Code Documentation']:
|
|
||||||
return self.git_provider.publish_comment('No code documentation found to improve this PR.')
|
|
||||||
|
|
||||||
for d in data['Code Documentation']:
|
|
||||||
try:
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
|
||||||
get_logger().info(f"add_docs: {d}")
|
|
||||||
relevant_file = d['relevant file'].strip()
|
|
||||||
relevant_line = int(d['relevant line']) # absolute position
|
|
||||||
documentation = d['documentation']
|
|
||||||
doc_placement = d['doc placement'].strip()
|
|
||||||
if documentation:
|
|
||||||
new_code_snippet = self.dedent_code(relevant_file, relevant_line, documentation, doc_placement,
|
|
||||||
add_original_line=True)
|
|
||||||
|
|
||||||
body = f"**Suggestion:** Proposed documentation\n```suggestion\n" + new_code_snippet + "\n```"
|
|
||||||
docs.append({'body': body, 'relevant_file': relevant_file,
|
|
||||||
'relevant_lines_start': relevant_line,
|
|
||||||
'relevant_lines_end': relevant_line})
|
|
||||||
except Exception:
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
|
||||||
get_logger().info(f"Could not parse code docs: {d}")
|
|
||||||
|
|
||||||
is_successful = self.git_provider.publish_code_suggestions(docs)
|
|
||||||
if not is_successful:
|
|
||||||
get_logger().info("Failed to publish code docs, trying to publish each docs separately")
|
|
||||||
for doc_suggestion in docs:
|
|
||||||
self.git_provider.publish_code_suggestions([doc_suggestion])
|
|
||||||
|
|
||||||
def dedent_code(self, relevant_file, relevant_lines_start, new_code_snippet, doc_placement='after',
|
|
||||||
add_original_line=False):
|
|
||||||
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()
|
|
||||||
original_initial_line = None
|
|
||||||
for file in self.diff_files:
|
|
||||||
if file.filename.strip() == relevant_file:
|
|
||||||
original_initial_line = file.head_file.splitlines()[relevant_lines_start - 1]
|
|
||||||
break
|
|
||||||
if original_initial_line:
|
|
||||||
if doc_placement == 'after':
|
|
||||||
line = file.head_file.splitlines()[relevant_lines_start]
|
|
||||||
else:
|
|
||||||
line = original_initial_line
|
|
||||||
suggested_initial_line = new_code_snippet.splitlines()[0]
|
|
||||||
original_initial_spaces = len(line) - len(line.lstrip())
|
|
||||||
suggested_initial_spaces = len(suggested_initial_line) - len(suggested_initial_line.lstrip())
|
|
||||||
delta_spaces = original_initial_spaces - suggested_initial_spaces
|
|
||||||
if delta_spaces > 0:
|
|
||||||
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n')
|
|
||||||
if add_original_line:
|
|
||||||
if doc_placement == 'after':
|
|
||||||
new_code_snippet = original_initial_line + "\n" + new_code_snippet
|
|
||||||
else:
|
|
||||||
new_code_snippet = new_code_snippet.rstrip() + "\n" + original_initial_line
|
|
||||||
except Exception as e:
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
|
||||||
get_logger().info(f"Could not dedent code snippet for file {relevant_file}, error: {e}")
|
|
||||||
|
|
||||||
return new_code_snippet
|
|
||||||
|
|
||||||
|
|
||||||
def get_docs_for_language(language, style):
|
|
||||||
language = language.lower()
|
|
||||||
if language == 'java':
|
|
||||||
return "Javadocs"
|
|
||||||
elif language in ['python', 'lisp', 'clojure']:
|
|
||||||
return f"Docstring ({style})"
|
|
||||||
elif language in ['javascript', 'typescript']:
|
|
||||||
return "JSdocs"
|
|
||||||
elif language == 'c++':
|
|
||||||
return "Doxygen"
|
|
||||||
else:
|
|
||||||
return "Docs"
|
|
@ -1,17 +1,16 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import logging
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Dict, List
|
from typing import List, Dict
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
|
|
||||||
from pr_agent.algo.ai_handler import AiHandler
|
from pr_agent.algo.ai_handler import AiHandler
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models, get_pr_multi_diffs
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import load_yaml
|
from pr_agent.algo.utils import load_yaml
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import 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
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class PRCodeSuggestions:
|
class PRCodeSuggestions:
|
||||||
@ -23,10 +22,7 @@ class PRCodeSuggestions:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# extended mode
|
# extended mode
|
||||||
try:
|
self.is_extended = any(["extended" in arg for arg in args])
|
||||||
self.is_extended = any(["extended" in arg for arg in args])
|
|
||||||
except:
|
|
||||||
self.is_extended = False
|
|
||||||
if self.is_extended:
|
if self.is_extended:
|
||||||
num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions_per_chunk
|
num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions_per_chunk
|
||||||
else:
|
else:
|
||||||
@ -46,49 +42,41 @@ class PRCodeSuggestions:
|
|||||||
"extra_instructions": get_settings().pr_code_suggestions.extra_instructions,
|
"extra_instructions": get_settings().pr_code_suggestions.extra_instructions,
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
}
|
}
|
||||||
self.token_handler = TokenHandler(self.git_provider.pr,
|
self.token_handler = TokenHandler(self.vars, get_settings().pr_code_suggestions_prompt.system,
|
||||||
self.vars,
|
|
||||||
get_settings().pr_code_suggestions_prompt.system,
|
|
||||||
get_settings().pr_code_suggestions_prompt.user)
|
get_settings().pr_code_suggestions_prompt.user)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
try:
|
logging.info('Generating code suggestions for PR...')
|
||||||
get_logger().info('Generating code suggestions for PR...')
|
if get_settings().config.publish_output:
|
||||||
if get_settings().config.publish_output:
|
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
|
||||||
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
|
|
||||||
|
|
||||||
get_logger().info('Preparing PR review...')
|
logging.info('Preparing PR review...')
|
||||||
if not self.is_extended:
|
if not self.is_extended:
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
data = self._prepare_pr_code_suggestions()
|
data = self._prepare_pr_code_suggestions()
|
||||||
else:
|
else:
|
||||||
data = await retry_with_fallback_models(self._prepare_prediction_extended)
|
data = await retry_with_fallback_models(self._prepare_prediction_extended)
|
||||||
if (not data) or (not 'Code suggestions' in data):
|
|
||||||
get_logger().info('No code suggestions found for PR.')
|
|
||||||
return
|
|
||||||
|
|
||||||
if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \
|
if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \
|
||||||
(self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions):
|
(self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions):
|
||||||
get_logger().info('Ranking Suggestions...')
|
logging.info('Ranking Suggestions...')
|
||||||
data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions'])
|
data['Code suggestions'] = await self.rank_suggestions(data['Code suggestions'])
|
||||||
|
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
get_logger().info('Pushing PR review...')
|
logging.info('Pushing PR review...')
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
get_logger().info('Pushing inline code suggestions...')
|
logging.info('Pushing inline code suggestions...')
|
||||||
self.push_inline_code_suggestions(data)
|
self.push_inline_code_suggestions(data)
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
|
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str):
|
async def _prepare_prediction(self, model: str):
|
||||||
get_logger().info('Getting PR diff...')
|
logging.info('Getting PR diff...')
|
||||||
self.patches_diff = get_pr_diff(self.git_provider,
|
self.patches_diff = get_pr_diff(self.git_provider,
|
||||||
self.token_handler,
|
self.token_handler,
|
||||||
model,
|
model,
|
||||||
add_line_numbers_to_hunks=True,
|
add_line_numbers_to_hunks=True,
|
||||||
disable_extra_lines=True)
|
disable_extra_lines=True)
|
||||||
|
|
||||||
get_logger().info('Getting AI prediction...')
|
logging.info('Getting AI prediction...')
|
||||||
self.prediction = await self._get_prediction(model)
|
self.prediction = await self._get_prediction(model)
|
||||||
|
|
||||||
async def _get_prediction(self, model: str):
|
async def _get_prediction(self, model: str):
|
||||||
@ -98,8 +86,8 @@ class PRCodeSuggestions:
|
|||||||
system_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.system).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables)
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
logging.info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
logging.info(f"\nUser prompt:\n{user_prompt}")
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
||||||
system=system_prompt, user=user_prompt)
|
system=system_prompt, user=user_prompt)
|
||||||
|
|
||||||
@ -121,10 +109,13 @@ class PRCodeSuggestions:
|
|||||||
for d in data['Code suggestions']:
|
for d in data['Code suggestions']:
|
||||||
try:
|
try:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"suggestion: {d}")
|
logging.info(f"suggestion: {d}")
|
||||||
relevant_file = d['relevant file'].strip()
|
relevant_file = d['relevant file'].strip()
|
||||||
relevant_lines_start = int(d['relevant lines start']) # absolute position
|
relevant_lines_str = d['relevant lines'].strip()
|
||||||
relevant_lines_end = int(d['relevant lines end'])
|
if ',' in relevant_lines_str: # handling 'relevant lines': '181, 190' or '178-184, 188-194'
|
||||||
|
relevant_lines_str = relevant_lines_str.split(',')[0]
|
||||||
|
relevant_lines_start = int(relevant_lines_str.split('-')[0]) # absolute position
|
||||||
|
relevant_lines_end = int(relevant_lines_str.split('-')[-1])
|
||||||
content = d['suggestion content']
|
content = d['suggestion content']
|
||||||
new_code_snippet = d['improved code']
|
new_code_snippet = d['improved code']
|
||||||
|
|
||||||
@ -137,11 +128,11 @@ class PRCodeSuggestions:
|
|||||||
'relevant_lines_end': relevant_lines_end})
|
'relevant_lines_end': relevant_lines_end})
|
||||||
except Exception:
|
except Exception:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Could not parse suggestion: {d}")
|
logging.info(f"Could not parse suggestion: {d}")
|
||||||
|
|
||||||
is_successful = self.git_provider.publish_code_suggestions(code_suggestions)
|
is_successful = self.git_provider.publish_code_suggestions(code_suggestions)
|
||||||
if not is_successful:
|
if not is_successful:
|
||||||
get_logger().info("Failed to publish code suggestions, trying to publish each suggestion separately")
|
logging.info("Failed to publish code suggestions, trying to publish each suggestion separately")
|
||||||
for code_suggestion in code_suggestions:
|
for code_suggestion in code_suggestions:
|
||||||
self.git_provider.publish_code_suggestions([code_suggestion])
|
self.git_provider.publish_code_suggestions([code_suggestion])
|
||||||
|
|
||||||
@ -163,19 +154,19 @@ class PRCodeSuggestions:
|
|||||||
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n')
|
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().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}")
|
||||||
|
|
||||||
return new_code_snippet
|
return new_code_snippet
|
||||||
|
|
||||||
async def _prepare_prediction_extended(self, model: str) -> dict:
|
async def _prepare_prediction_extended(self, model: str) -> dict:
|
||||||
get_logger().info('Getting PR diff...')
|
logging.info('Getting PR diff...')
|
||||||
patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model,
|
patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model,
|
||||||
max_calls=get_settings().pr_code_suggestions.max_number_of_calls)
|
max_calls=get_settings().pr_code_suggestions.max_number_of_calls)
|
||||||
|
|
||||||
get_logger().info('Getting multi AI predictions...')
|
logging.info('Getting multi AI predictions...')
|
||||||
prediction_list = []
|
prediction_list = []
|
||||||
for i, patches_diff in enumerate(patches_diff_list):
|
for i, patches_diff in enumerate(patches_diff_list):
|
||||||
get_logger().info(f"Processing chunk {i + 1} of {len(patches_diff_list)}")
|
logging.info(f"Processing chunk {i + 1} of {len(patches_diff_list)}")
|
||||||
self.patches_diff = patches_diff
|
self.patches_diff = patches_diff
|
||||||
prediction = await self._get_prediction(model)
|
prediction = await self._get_prediction(model)
|
||||||
prediction_list.append(prediction)
|
prediction_list.append(prediction)
|
||||||
@ -223,8 +214,8 @@ class PRCodeSuggestions:
|
|||||||
variables)
|
variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.user).render(variables)
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
logging.info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
logging.info(f"\nUser prompt:\n{user_prompt}")
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, system=system_prompt,
|
response, finish_reason = await self.ai_handler.chat_completion(model=model, system=system_prompt,
|
||||||
user=user_prompt)
|
user=user_prompt)
|
||||||
|
|
||||||
@ -239,7 +230,7 @@ class PRCodeSuggestions:
|
|||||||
data_sorted = data_sorted[:new_len]
|
data_sorted = data_sorted[:new_len]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if get_settings().config.verbosity_level >= 1:
|
if get_settings().config.verbosity_level >= 1:
|
||||||
get_logger().info(f"Could not sort suggestions, error: {e}")
|
logging.info(f"Could not sort suggestions, error: {e}")
|
||||||
data_sorted = suggestion_list
|
data_sorted = suggestion_list
|
||||||
|
|
||||||
return data_sorted
|
return data_sorted
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class PRConfig:
|
class PRConfig:
|
||||||
@ -18,11 +19,11 @@ class PRConfig:
|
|||||||
self.git_provider = get_git_provider()(pr_url)
|
self.git_provider = get_git_provider()(pr_url)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
get_logger().info('Getting configuration settings...')
|
logging.info('Getting configuration settings...')
|
||||||
get_logger().info('Preparing configs...')
|
logging.info('Preparing configs...')
|
||||||
pr_comment = self._prepare_pr_configs()
|
pr_comment = self._prepare_pr_configs()
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
get_logger().info('Pushing configs...')
|
logging.info('Pushing configs...')
|
||||||
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()
|
||||||
return ""
|
return ""
|
||||||
@ -43,5 +44,5 @@ class PRConfig:
|
|||||||
comment_str += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}"
|
comment_str += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}"
|
||||||
comment_str += " "
|
comment_str += " "
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"comment_str:\n{comment_str}")
|
logging.info(f"comment_str:\n{comment_str}")
|
||||||
return comment_str
|
return comment_str
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
import re
|
import json
|
||||||
|
import logging
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
@ -7,11 +8,10 @@ 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, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import load_yaml, set_custom_labels
|
from pr_agent.algo.utils import load_yaml
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class PRDescription:
|
class PRDescription:
|
||||||
@ -28,7 +28,6 @@ class PRDescription:
|
|||||||
self.main_pr_language = get_main_pr_language(
|
self.main_pr_language = get_main_pr_language(
|
||||||
self.git_provider.get_languages(), self.git_provider.get_files()
|
self.git_provider.get_languages(), self.git_provider.get_files()
|
||||||
)
|
)
|
||||||
self.pr_id = self.git_provider.get_pr_id()
|
|
||||||
|
|
||||||
# Initialize the AI handler
|
# Initialize the AI handler
|
||||||
self.ai_handler = AiHandler()
|
self.ai_handler = AiHandler()
|
||||||
@ -37,26 +36,18 @@ 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_pr_description(full=False),
|
"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
|
||||||
"use_bullet_points": get_settings().pr_description.use_bullet_points,
|
|
||||||
"extra_instructions": get_settings().pr_description.extra_instructions,
|
"extra_instructions": get_settings().pr_description.extra_instructions,
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages()
|
||||||
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
|
||||||
"custom_labels": "",
|
|
||||||
"custom_labels_examples": "",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.user_description = self.git_provider.get_user_description()
|
self.user_description = self.git_provider.get_user_description()
|
||||||
|
|
||||||
# Initialize the token handler
|
# Initialize the token handler
|
||||||
self.token_handler = TokenHandler(
|
self.token_handler = TokenHandler(self.vars, get_settings().pr_description_prompt.system,
|
||||||
self.git_provider.pr,
|
get_settings().pr_description_prompt.user)
|
||||||
self.vars,
|
|
||||||
get_settings().pr_description_prompt.system,
|
|
||||||
get_settings().pr_description_prompt.user,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize patches_diff and prediction attributes
|
# Initialize patches_diff and prediction attributes
|
||||||
self.patches_diff = None
|
self.patches_diff = None
|
||||||
@ -66,44 +57,27 @@ class PRDescription:
|
|||||||
"""
|
"""
|
||||||
Generates a PR description using an AI model and publishes it to the PR.
|
Generates a PR description using an AI model and publishes it to the PR.
|
||||||
"""
|
"""
|
||||||
|
logging.info('Generating a PR description...')
|
||||||
try:
|
if get_settings().config.publish_output:
|
||||||
get_logger().info(f"Generating a PR description {self.pr_id}")
|
self.git_provider.publish_comment("Preparing pr description...", is_temporary=True)
|
||||||
if get_settings().config.publish_output:
|
|
||||||
self.git_provider.publish_comment("Preparing PR description...", is_temporary=True)
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
logging.info('Preparing answer...')
|
||||||
|
pr_title, pr_body, pr_types, markdown_text = self._prepare_pr_answer()
|
||||||
get_logger().info(f"Preparing answer {self.pr_id}")
|
|
||||||
if self.prediction:
|
if get_settings().config.publish_output:
|
||||||
self._prepare_data()
|
logging.info('Pushing answer...')
|
||||||
|
if get_settings().pr_description.publish_description_as_comment:
|
||||||
|
self.git_provider.publish_comment(markdown_text)
|
||||||
else:
|
else:
|
||||||
return None
|
self.git_provider.publish_description(pr_title, pr_body)
|
||||||
|
if self.git_provider.is_supported("get_labels"):
|
||||||
pr_labels = []
|
current_labels = self.git_provider.get_labels()
|
||||||
if get_settings().pr_description.publish_labels:
|
if current_labels is None:
|
||||||
pr_labels = self._prepare_labels()
|
current_labels = []
|
||||||
|
self.git_provider.publish_labels(pr_types + current_labels)
|
||||||
if get_settings().pr_description.use_description_markers:
|
self.git_provider.remove_initial_comment()
|
||||||
pr_title, pr_body = self._prepare_pr_answer_with_markers()
|
|
||||||
else:
|
|
||||||
pr_title, pr_body, = self._prepare_pr_answer()
|
|
||||||
full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}"
|
|
||||||
|
|
||||||
if get_settings().config.publish_output:
|
|
||||||
get_logger().info(f"Pushing answer {self.pr_id}")
|
|
||||||
if get_settings().pr_description.publish_description_as_comment:
|
|
||||||
self.git_provider.publish_comment(full_markdown_description)
|
|
||||||
else:
|
|
||||||
self.git_provider.publish_description(pr_title, pr_body)
|
|
||||||
if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"):
|
|
||||||
current_labels = self.git_provider.get_labels()
|
|
||||||
if current_labels is None:
|
|
||||||
current_labels = []
|
|
||||||
self.git_provider.publish_labels(pr_labels + current_labels)
|
|
||||||
self.git_provider.remove_initial_comment()
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Error generating PR description {self.pr_id}: {e}")
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@ -121,12 +95,9 @@ class PRDescription:
|
|||||||
Any exceptions raised by the 'get_pr_diff' and '_get_prediction' functions.
|
Any exceptions raised by the 'get_pr_diff' and '_get_prediction' functions.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description:
|
logging.info('Getting PR diff...')
|
||||||
return None
|
|
||||||
|
|
||||||
get_logger().info(f"Getting PR diff {self.pr_id}")
|
|
||||||
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
||||||
get_logger().info(f"Getting AI prediction {self.pr_id}")
|
logging.info('Getting AI prediction...')
|
||||||
self.prediction = await self._get_prediction(model)
|
self.prediction = await self._get_prediction(model)
|
||||||
|
|
||||||
async def _get_prediction(self, model: str) -> str:
|
async def _get_prediction(self, model: str) -> str:
|
||||||
@ -143,13 +114,12 @@ class PRDescription:
|
|||||||
variables["diff"] = self.patches_diff # update diff
|
variables["diff"] = self.patches_diff # update diff
|
||||||
|
|
||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
set_custom_labels(variables)
|
|
||||||
system_prompt = environment.from_string(get_settings().pr_description_prompt.system).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_description_prompt.system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_description_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_description_prompt.user).render(variables)
|
||||||
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
logging.info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
logging.info(f"\nUser prompt:\n{user_prompt}")
|
||||||
|
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
model=model,
|
model=model,
|
||||||
@ -160,75 +130,34 @@ class PRDescription:
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _prepare_data(self):
|
def _prepare_pr_answer(self) -> Tuple[str, str, List[str], str]:
|
||||||
# Load the AI prediction data into a dictionary
|
|
||||||
self.data = load_yaml(self.prediction.strip())
|
|
||||||
|
|
||||||
if get_settings().pr_description.add_original_user_description and self.user_description:
|
|
||||||
self.data["User Description"] = self.user_description
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_labels(self) -> List[str]:
|
|
||||||
pr_types = []
|
|
||||||
|
|
||||||
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
|
|
||||||
if 'PR Type' in self.data:
|
|
||||||
if type(self.data['PR Type']) == list:
|
|
||||||
pr_types = self.data['PR Type']
|
|
||||||
elif type(self.data['PR Type']) == str:
|
|
||||||
pr_types = self.data['PR Type'].split(',')
|
|
||||||
|
|
||||||
return pr_types
|
|
||||||
|
|
||||||
def _prepare_pr_answer_with_markers(self) -> Tuple[str, str]:
|
|
||||||
get_logger().info(f"Using description marker replacements {self.pr_id}")
|
|
||||||
title = self.vars["title"]
|
|
||||||
body = self.user_description
|
|
||||||
if get_settings().pr_description.include_generated_by_header:
|
|
||||||
ai_header = f"### 🤖 Generated by PR Agent at {self.git_provider.last_commit_id.sha}\n\n"
|
|
||||||
else:
|
|
||||||
ai_header = ""
|
|
||||||
|
|
||||||
ai_type = self.data.get('PR Type')
|
|
||||||
if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body):
|
|
||||||
pr_type = f"{ai_header}{ai_type}"
|
|
||||||
body = body.replace('pr_agent:type', pr_type)
|
|
||||||
|
|
||||||
ai_summary = self.data.get('PR Description')
|
|
||||||
if ai_summary and not re.search(r'<!--\s*pr_agent:summary\s*-->', body):
|
|
||||||
summary = f"{ai_header}{ai_summary}"
|
|
||||||
body = body.replace('pr_agent:summary', summary)
|
|
||||||
|
|
||||||
if not re.search(r'<!--\s*pr_agent:walkthrough\s*-->', body):
|
|
||||||
ai_walkthrough = self.data.get('PR Main Files Walkthrough')
|
|
||||||
if ai_walkthrough:
|
|
||||||
walkthrough = str(ai_header)
|
|
||||||
for file in ai_walkthrough:
|
|
||||||
filename = file['filename'].replace("'", "`")
|
|
||||||
description = file['changes in file'].replace("'", "`")
|
|
||||||
walkthrough += f'- `{filename}`: {description}\n'
|
|
||||||
|
|
||||||
body = body.replace('pr_agent:walkthrough', walkthrough)
|
|
||||||
|
|
||||||
return title, body
|
|
||||||
|
|
||||||
def _prepare_pr_answer(self) -> Tuple[str, str]:
|
|
||||||
"""
|
"""
|
||||||
Prepare the PR description based on the AI prediction data.
|
Prepare the PR description based on the AI prediction data.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- title: a string containing the PR title.
|
- title: a string containing the PR title.
|
||||||
- pr_body: a string containing the PR description body in a markdown format.
|
- pr_body: a string containing the PR body in a markdown format.
|
||||||
|
- pr_types: a list of strings containing the PR types.
|
||||||
|
- markdown_text: a string containing the AI prediction data in a markdown format. used for publishing a comment
|
||||||
"""
|
"""
|
||||||
|
# Load the AI prediction data into a dictionary
|
||||||
|
data = load_yaml(self.prediction.strip())
|
||||||
|
|
||||||
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
|
if get_settings().pr_description.add_original_user_description and self.user_description:
|
||||||
markdown_text = ""
|
data["User Description"] = self.user_description
|
||||||
for key, value in self.data.items():
|
|
||||||
markdown_text += f"## {key}\n\n"
|
# Initialization
|
||||||
markdown_text += f"{value}\n\n"
|
pr_types = []
|
||||||
|
|
||||||
|
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
|
||||||
|
if 'PR Type' in data:
|
||||||
|
if type(data['PR Type']) == list:
|
||||||
|
pr_types = data['PR Type']
|
||||||
|
elif type(data['PR Type']) == str:
|
||||||
|
pr_types = data['PR Type'].split(',')
|
||||||
|
|
||||||
# Remove the 'PR Title' key from the dictionary
|
# Remove the 'PR Title' key from the dictionary
|
||||||
ai_title = self.data.pop('PR Title', self.vars["title"])
|
ai_title = data.pop('PR Title')
|
||||||
if get_settings().pr_description.keep_original_user_title:
|
if get_settings().pr_description.keep_original_user_title:
|
||||||
# Assign the original PR title to the 'title' variable
|
# Assign the original PR title to the 'title' variable
|
||||||
title = self.vars["title"]
|
title = self.vars["title"]
|
||||||
@ -239,27 +168,25 @@ class PRDescription:
|
|||||||
# Iterate over the remaining dictionary items and append the key and value to 'pr_body' in a markdown format,
|
# Iterate over the remaining dictionary items and append the key and value to 'pr_body' in a markdown format,
|
||||||
# except for the items containing the word 'walkthrough'
|
# except for the items containing the word 'walkthrough'
|
||||||
pr_body = ""
|
pr_body = ""
|
||||||
for idx, (key, value) in enumerate(self.data.items()):
|
for idx, (key, value) in enumerate(data.items()):
|
||||||
pr_body += f"## {key}:\n"
|
pr_body += f"## {key}:\n"
|
||||||
if 'walkthrough' in key.lower():
|
if 'walkthrough' in key.lower():
|
||||||
# for filename, description in value.items():
|
# for filename, description in value.items():
|
||||||
if self.git_provider.is_supported("gfm_markdown"):
|
|
||||||
pr_body += "<details> <summary>files:</summary>\n\n"
|
|
||||||
for file in value:
|
for file in value:
|
||||||
filename = file['filename'].replace("'", "`")
|
filename = file['filename'].replace("'", "`")
|
||||||
description = file['changes in file']
|
description = file['changes in file']
|
||||||
pr_body += f'`{filename}`: {description}\n'
|
pr_body += f'`{filename}`: {description}\n'
|
||||||
if self.git_provider.is_supported("gfm_markdown"):
|
|
||||||
pr_body +="</details>\n"
|
|
||||||
else:
|
else:
|
||||||
# if the value is a list, join its items by comma
|
# if the value is a list, join its items by comma
|
||||||
if type(value) == list:
|
if type(value) == list:
|
||||||
value = ', '.join(v for v in value)
|
value = ', '.join(v for v in value)
|
||||||
pr_body += f"{value}\n"
|
pr_body += f"{value}\n"
|
||||||
if idx < len(self.data) - 1:
|
if idx < len(data) - 1:
|
||||||
pr_body += "\n___\n"
|
pr_body += "\n___\n"
|
||||||
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
markdown_text = f"## Title\n\n{title}\n\n___\n{pr_body}"
|
||||||
get_logger().info(f"title:\n{title}\n{pr_body}")
|
|
||||||
|
|
||||||
return title, pr_body
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
logging.info(f"title:\n{title}\n{pr_body}")
|
||||||
|
|
||||||
|
return title, pr_body, pr_types, markdown_text
|
@ -1,163 +0,0 @@
|
|||||||
import copy
|
|
||||||
import re
|
|
||||||
from typing import List, Tuple
|
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
|
||||||
|
|
||||||
from pr_agent.algo.ai_handler import AiHandler
|
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
|
||||||
from pr_agent.algo.utils import load_yaml, set_custom_labels
|
|
||||||
from pr_agent.config_loader import get_settings
|
|
||||||
from pr_agent.git_providers import get_git_provider
|
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class PRGenerateLabels:
|
|
||||||
def __init__(self, pr_url: str, args: list = None):
|
|
||||||
"""
|
|
||||||
Initialize the PRGenerateLabels object with the necessary attributes and objects for generating labels
|
|
||||||
corresponding to the PR using an AI model.
|
|
||||||
Args:
|
|
||||||
pr_url (str): The URL of the pull request.
|
|
||||||
args (list, optional): List of arguments passed to the PRGenerateLabels class. Defaults to None.
|
|
||||||
"""
|
|
||||||
# Initialize the git provider and main PR language
|
|
||||||
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.pr_id = self.git_provider.get_pr_id()
|
|
||||||
|
|
||||||
# Initialize the AI handler
|
|
||||||
self.ai_handler = AiHandler()
|
|
||||||
|
|
||||||
# Initialize the variables dictionary
|
|
||||||
self.vars = {
|
|
||||||
"title": self.git_provider.pr.title,
|
|
||||||
"branch": self.git_provider.get_pr_branch(),
|
|
||||||
"description": self.git_provider.get_pr_description(full=False),
|
|
||||||
"language": self.main_pr_language,
|
|
||||||
"diff": "", # empty diff for initial calculation
|
|
||||||
"use_bullet_points": get_settings().pr_description.use_bullet_points,
|
|
||||||
"extra_instructions": get_settings().pr_description.extra_instructions,
|
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
|
||||||
"custom_labels": "",
|
|
||||||
"custom_labels_examples": "",
|
|
||||||
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Initialize the token handler
|
|
||||||
self.token_handler = TokenHandler(
|
|
||||||
self.git_provider.pr,
|
|
||||||
self.vars,
|
|
||||||
get_settings().pr_custom_labels_prompt.system,
|
|
||||||
get_settings().pr_custom_labels_prompt.user,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize patches_diff and prediction attributes
|
|
||||||
self.patches_diff = None
|
|
||||||
self.prediction = None
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
"""
|
|
||||||
Generates a PR labels using an AI model and publishes it to the PR.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
get_logger().info(f"Generating a PR labels {self.pr_id}")
|
|
||||||
if get_settings().config.publish_output:
|
|
||||||
self.git_provider.publish_comment("Preparing PR labels...", is_temporary=True)
|
|
||||||
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
|
||||||
|
|
||||||
get_logger().info(f"Preparing answer {self.pr_id}")
|
|
||||||
if self.prediction:
|
|
||||||
self._prepare_data()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
pr_labels = self._prepare_labels()
|
|
||||||
|
|
||||||
if get_settings().config.publish_output:
|
|
||||||
get_logger().info(f"Pushing labels {self.pr_id}")
|
|
||||||
if self.git_provider.is_supported("get_labels"):
|
|
||||||
current_labels = self.git_provider.get_labels()
|
|
||||||
if current_labels is None:
|
|
||||||
current_labels = []
|
|
||||||
self.git_provider.publish_labels(pr_labels + current_labels)
|
|
||||||
self.git_provider.remove_initial_comment()
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Error generating PR labels {self.pr_id}: {e}")
|
|
||||||
|
|
||||||
return ""
|
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str) -> None:
|
|
||||||
"""
|
|
||||||
Prepare the AI prediction for the PR labels based on the provided model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model (str): The name of the model to be used for generating the prediction.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Any exceptions raised by the 'get_pr_diff' and '_get_prediction' functions.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
get_logger().info(f"Getting PR diff {self.pr_id}")
|
|
||||||
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
|
||||||
get_logger().info(f"Getting AI prediction {self.pr_id}")
|
|
||||||
self.prediction = await self._get_prediction(model)
|
|
||||||
|
|
||||||
async def _get_prediction(self, model: str) -> str:
|
|
||||||
"""
|
|
||||||
Generate an AI prediction for the PR labels based on the provided model.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model (str): The name of the model to be used for generating the prediction.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The generated AI prediction.
|
|
||||||
"""
|
|
||||||
variables = copy.deepcopy(self.vars)
|
|
||||||
variables["diff"] = self.patches_diff # update diff
|
|
||||||
|
|
||||||
environment = Environment(undefined=StrictUndefined)
|
|
||||||
set_custom_labels(variables)
|
|
||||||
system_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.system).render(variables)
|
|
||||||
user_prompt = environment.from_string(get_settings().pr_custom_labels_prompt.user).render(variables)
|
|
||||||
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
|
||||||
|
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(
|
|
||||||
model=model,
|
|
||||||
temperature=0.2,
|
|
||||||
system=system_prompt,
|
|
||||||
user=user_prompt
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _prepare_data(self):
|
|
||||||
# Load the AI prediction data into a dictionary
|
|
||||||
self.data = load_yaml(self.prediction.strip())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_labels(self) -> List[str]:
|
|
||||||
pr_types = []
|
|
||||||
|
|
||||||
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
|
|
||||||
if 'PR Type' in self.data:
|
|
||||||
if type(self.data['PR Type']) == list:
|
|
||||||
pr_types = self.data['PR Type']
|
|
||||||
elif type(self.data['PR Type']) == str:
|
|
||||||
pr_types = self.data['PR Type'].split(',')
|
|
||||||
|
|
||||||
return pr_types
|
|
@ -1,4 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import logging
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
|
|
||||||
@ -8,7 +9,6 @@ from pr_agent.algo.token_handler import TokenHandler
|
|||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class PRInformationFromUser:
|
class PRInformationFromUser:
|
||||||
@ -26,30 +26,28 @@ class PRInformationFromUser:
|
|||||||
"diff": "", # empty diff for initial calculation
|
"diff": "", # empty diff for initial calculation
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
}
|
}
|
||||||
self.token_handler = TokenHandler(self.git_provider.pr,
|
self.token_handler = TokenHandler(self.vars, get_settings().pr_information_from_user_prompt.system,
|
||||||
self.vars,
|
|
||||||
get_settings().pr_information_from_user_prompt.system,
|
|
||||||
get_settings().pr_information_from_user_prompt.user)
|
get_settings().pr_information_from_user_prompt.user)
|
||||||
self.patches_diff = None
|
self.patches_diff = None
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
get_logger().info('Generating question to the user...')
|
logging.info('Generating question to the user...')
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
self.git_provider.publish_comment("Preparing questions...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing questions...", is_temporary=True)
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
get_logger().info('Preparing questions...')
|
logging.info('Preparing questions...')
|
||||||
pr_comment = self._prepare_pr_answer()
|
pr_comment = self._prepare_pr_answer()
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
get_logger().info('Pushing questions...')
|
logging.info('Pushing questions...')
|
||||||
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()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def _prepare_prediction(self, model):
|
async def _prepare_prediction(self, model):
|
||||||
get_logger().info('Getting PR diff...')
|
logging.info('Getting PR diff...')
|
||||||
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
||||||
get_logger().info('Getting AI prediction...')
|
logging.info('Getting AI prediction...')
|
||||||
self.prediction = await self._get_prediction(model)
|
self.prediction = await self._get_prediction(model)
|
||||||
|
|
||||||
async def _get_prediction(self, model: str):
|
async def _get_prediction(self, model: str):
|
||||||
@ -59,8 +57,8 @@ class PRInformationFromUser:
|
|||||||
system_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.system).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.user).render(variables)
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
logging.info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
logging.info(f"\nUser prompt:\n{user_prompt}")
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
||||||
system=system_prompt, user=user_prompt)
|
system=system_prompt, user=user_prompt)
|
||||||
return response
|
return response
|
||||||
@ -68,7 +66,7 @@ class PRInformationFromUser:
|
|||||||
def _prepare_pr_answer(self) -> str:
|
def _prepare_pr_answer(self) -> str:
|
||||||
model_output = self.prediction.strip()
|
model_output = self.prediction.strip()
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"answer_str:\n{model_output}")
|
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" +\
|
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"
|
"\n>/answer\n>1) ...\n>2) ...\n>...\n"
|
||||||
return answer_str
|
return answer_str
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import logging
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
|
|
||||||
@ -8,7 +9,6 @@ from pr_agent.algo.token_handler import TokenHandler
|
|||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
|
|
||||||
class PRQuestions:
|
class PRQuestions:
|
||||||
@ -29,9 +29,7 @@ class PRQuestions:
|
|||||||
"questions": self.question_str,
|
"questions": self.question_str,
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
}
|
}
|
||||||
self.token_handler = TokenHandler(self.git_provider.pr,
|
self.token_handler = TokenHandler(self.vars, get_settings().pr_questions_prompt.system,
|
||||||
self.vars,
|
|
||||||
get_settings().pr_questions_prompt.system,
|
|
||||||
get_settings().pr_questions_prompt.user)
|
get_settings().pr_questions_prompt.user)
|
||||||
self.patches_diff = None
|
self.patches_diff = None
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
@ -44,22 +42,22 @@ class PRQuestions:
|
|||||||
return question_str
|
return question_str
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
get_logger().info('Answering a PR question...')
|
logging.info('Answering a PR question...')
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
self.git_provider.publish_comment("Preparing answer...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing answer...", is_temporary=True)
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
get_logger().info('Preparing answer...')
|
logging.info('Preparing answer...')
|
||||||
pr_comment = self._prepare_pr_answer()
|
pr_comment = self._prepare_pr_answer()
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
get_logger().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()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str):
|
async def _prepare_prediction(self, model: str):
|
||||||
get_logger().info('Getting PR diff...')
|
logging.info('Getting PR diff...')
|
||||||
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
||||||
get_logger().info('Getting AI prediction...')
|
logging.info('Getting AI prediction...')
|
||||||
self.prediction = await self._get_prediction(model)
|
self.prediction = await self._get_prediction(model)
|
||||||
|
|
||||||
async def _get_prediction(self, model: str):
|
async def _get_prediction(self, model: str):
|
||||||
@ -69,8 +67,8 @@ class PRQuestions:
|
|||||||
system_prompt = environment.from_string(get_settings().pr_questions_prompt.system).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_questions_prompt.system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_questions_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_questions_prompt.user).render(variables)
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
logging.info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
logging.info(f"\nUser prompt:\n{user_prompt}")
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
||||||
system=system_prompt, user=user_prompt)
|
system=system_prompt, user=user_prompt)
|
||||||
return response
|
return response
|
||||||
@ -79,5 +77,5 @@ class PRQuestions:
|
|||||||
answer_str = f"Question: {self.question_str}\n\n"
|
answer_str = f"Question: {self.question_str}\n\n"
|
||||||
answer_str += f"Answer:\n{self.prediction.strip()}\n\n"
|
answer_str += f"Answer:\n{self.prediction.strip()}\n\n"
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"answer_str:\n{answer_str}")
|
logging.info(f"answer_str:\n{answer_str}")
|
||||||
return answer_str
|
return answer_str
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
@ -9,11 +11,10 @@ from yaml import SafeLoader
|
|||||||
from pr_agent.algo.ai_handler import AiHandler
|
from pr_agent.algo.ai_handler import AiHandler
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import convert_to_markdown, load_yaml, try_fix_yaml, set_custom_labels
|
from pr_agent.algo.utils import convert_to_markdown, try_fix_json, try_fix_yaml, load_yaml
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.git_provider import IncrementalPR, get_main_pr_language
|
from pr_agent.git_providers.git_provider import IncrementalPR, get_main_pr_language
|
||||||
from pr_agent.log import get_logger
|
|
||||||
from pr_agent.servers.help import actions_help_text, bot_help_text
|
from pr_agent.servers.help import actions_help_text, bot_help_text
|
||||||
|
|
||||||
|
|
||||||
@ -57,22 +58,15 @@ class PRReviewer:
|
|||||||
"require_tests": get_settings().pr_reviewer.require_tests_review,
|
"require_tests": get_settings().pr_reviewer.require_tests_review,
|
||||||
"require_security": get_settings().pr_reviewer.require_security_review,
|
"require_security": get_settings().pr_reviewer.require_security_review,
|
||||||
"require_focused": get_settings().pr_reviewer.require_focused_review,
|
"require_focused": get_settings().pr_reviewer.require_focused_review,
|
||||||
"require_estimate_effort_to_review": get_settings().pr_reviewer.require_estimate_effort_to_review,
|
|
||||||
'num_code_suggestions': get_settings().pr_reviewer.num_code_suggestions,
|
'num_code_suggestions': get_settings().pr_reviewer.num_code_suggestions,
|
||||||
'question_str': question_str,
|
'question_str': question_str,
|
||||||
'answer_str': answer_str,
|
'answer_str': answer_str,
|
||||||
"extra_instructions": get_settings().pr_reviewer.extra_instructions,
|
"extra_instructions": get_settings().pr_reviewer.extra_instructions,
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
"custom_labels": "",
|
|
||||||
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.token_handler = TokenHandler(
|
self.token_handler = TokenHandler(self.vars, get_settings().pr_review_prompt.system,
|
||||||
self.git_provider.pr,
|
get_settings().pr_review_prompt.user)
|
||||||
self.vars,
|
|
||||||
get_settings().pr_review_prompt.system,
|
|
||||||
get_settings().pr_review_prompt.user
|
|
||||||
)
|
|
||||||
|
|
||||||
def parse_args(self, args: List[str]) -> None:
|
def parse_args(self, args: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -95,32 +89,28 @@ class PRReviewer:
|
|||||||
"""
|
"""
|
||||||
Review the pull request and generate feedback.
|
Review the pull request and generate feedback.
|
||||||
"""
|
"""
|
||||||
|
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
||||||
|
logging.info(f'Automatic review is disabled {self.pr_url}')
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
logging.info(f'Reviewing PR: {self.pr_url} ...')
|
||||||
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
|
||||||
get_logger().info(f'Automatic review is disabled {self.pr_url}')
|
|
||||||
return None
|
|
||||||
|
|
||||||
get_logger().info(f'Reviewing PR: {self.pr_url} ...')
|
if get_settings().config.publish_output:
|
||||||
|
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
|
||||||
if get_settings().config.publish_output:
|
|
||||||
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
logging.info('Preparing PR review...')
|
||||||
|
pr_comment = self._prepare_pr_review()
|
||||||
get_logger().info('Preparing PR review...')
|
|
||||||
pr_comment = self._prepare_pr_review()
|
if get_settings().config.publish_output:
|
||||||
|
logging.info('Pushing PR review...')
|
||||||
if get_settings().config.publish_output:
|
self.git_provider.publish_comment(pr_comment)
|
||||||
get_logger().info('Pushing PR review...')
|
self.git_provider.remove_initial_comment()
|
||||||
self.git_provider.publish_comment(pr_comment)
|
|
||||||
self.git_provider.remove_initial_comment()
|
if get_settings().pr_reviewer.inline_code_comments:
|
||||||
|
logging.info('Pushing inline code comments...')
|
||||||
if get_settings().pr_reviewer.inline_code_comments:
|
self._publish_inline_code_comments()
|
||||||
get_logger().info('Pushing inline code comments...')
|
|
||||||
self._publish_inline_code_comments()
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to review PR: {e}")
|
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str) -> None:
|
async def _prepare_prediction(self, model: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -132,9 +122,9 @@ class PRReviewer:
|
|||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
get_logger().info('Getting PR diff...')
|
logging.info('Getting PR diff...')
|
||||||
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
||||||
get_logger().info('Getting AI prediction...')
|
logging.info('Getting AI prediction...')
|
||||||
self.prediction = await self._get_prediction(model)
|
self.prediction = await self._get_prediction(model)
|
||||||
|
|
||||||
async def _get_prediction(self, model: str) -> str:
|
async def _get_prediction(self, model: str) -> str:
|
||||||
@ -151,13 +141,12 @@ class PRReviewer:
|
|||||||
variables["diff"] = self.patches_diff # update diff
|
variables["diff"] = self.patches_diff # update diff
|
||||||
|
|
||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
set_custom_labels(variables)
|
|
||||||
system_prompt = environment.from_string(get_settings().pr_review_prompt.system).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_review_prompt.system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_review_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_review_prompt.user).render(variables)
|
||||||
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
logging.info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
logging.info(f"\nUser prompt:\n{user_prompt}")
|
||||||
|
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
model=model,
|
model=model,
|
||||||
@ -210,22 +199,6 @@ class PRReviewer:
|
|||||||
link = self.git_provider.generate_link_to_relevant_line_number(suggestion)
|
link = self.git_provider.generate_link_to_relevant_line_number(suggestion)
|
||||||
if link:
|
if link:
|
||||||
suggestion['relevant line'] = f"[{suggestion['relevant line']}]({link})"
|
suggestion['relevant line'] = f"[{suggestion['relevant line']}]({link})"
|
||||||
else:
|
|
||||||
pass
|
|
||||||
# try:
|
|
||||||
# relevant_file = suggestion['relevant file'].strip('`').strip("'")
|
|
||||||
# relevant_line_str = suggestion['relevant line']
|
|
||||||
# if not relevant_line_str:
|
|
||||||
# return ""
|
|
||||||
#
|
|
||||||
# position, absolute_position = find_line_number_of_relevant_line_in_file(
|
|
||||||
# self.git_provider.diff_files, relevant_file, relevant_line_str)
|
|
||||||
# if absolute_position != -1:
|
|
||||||
# suggestion[
|
|
||||||
# 'relevant line'] = f"{suggestion['relevant line']} (line {absolute_position})"
|
|
||||||
# except:
|
|
||||||
# pass
|
|
||||||
|
|
||||||
|
|
||||||
# Add incremental review section
|
# Add incremental review section
|
||||||
if self.incremental.is_incremental:
|
if self.incremental.is_incremental:
|
||||||
@ -236,21 +209,20 @@ class PRReviewer:
|
|||||||
"⏮️ Review for commits since previous PR-Agent review": f"Starting from commit {last_commit_url}"}})
|
"⏮️ Review for commits since previous PR-Agent review": f"Starting from commit {last_commit_url}"}})
|
||||||
data.move_to_end('Incremental PR Review', last=False)
|
data.move_to_end('Incremental PR Review', last=False)
|
||||||
|
|
||||||
markdown_text = convert_to_markdown(data, self.git_provider.is_supported("gfm_markdown"))
|
markdown_text = convert_to_markdown(data)
|
||||||
user = self.git_provider.get_user_id()
|
user = self.git_provider.get_user_id()
|
||||||
|
|
||||||
# Add help text if not in CLI mode
|
# Add help text if not in CLI§ mode
|
||||||
if not get_settings().get("CONFIG.CLI_MODE", False):
|
if not get_settings().get("CONFIG.CLI§_MODE", False):
|
||||||
markdown_text += "\n### How to use\n"
|
markdown_text += "\n### How to use\n"
|
||||||
bot_user = "[bot]" if get_settings().github_app.override_deployment_type else get_settings().github_app.bot_user
|
if user and '[bot]' not in user:
|
||||||
if user and bot_user not in user:
|
|
||||||
markdown_text += bot_help_text(user)
|
markdown_text += bot_help_text(user)
|
||||||
else:
|
else:
|
||||||
markdown_text += actions_help_text
|
markdown_text += actions_help_text
|
||||||
|
|
||||||
# Log markdown response if verbosity level is high
|
# Log markdown response if verbosity level is high
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Markdown response:\n{markdown_text}")
|
logging.info(f"Markdown response:\n{markdown_text}")
|
||||||
|
|
||||||
if markdown_text == None or len(markdown_text) == 0:
|
if markdown_text == None or len(markdown_text) == 0:
|
||||||
markdown_text = ""
|
markdown_text = ""
|
||||||
@ -269,7 +241,7 @@ class PRReviewer:
|
|||||||
try:
|
try:
|
||||||
data = yaml.load(review_text, Loader=SafeLoader)
|
data = yaml.load(review_text, Loader=SafeLoader)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to parse AI prediction: {e}")
|
logging.error(f"Failed to parse AI prediction: {e}")
|
||||||
data = try_fix_yaml(review_text)
|
data = try_fix_yaml(review_text)
|
||||||
|
|
||||||
comments: List[str] = []
|
comments: List[str] = []
|
||||||
@ -278,7 +250,7 @@ class PRReviewer:
|
|||||||
relevant_line_in_file = suggestion.get('relevant line', '').strip()
|
relevant_line_in_file = suggestion.get('relevant line', '').strip()
|
||||||
content = suggestion.get('suggestion', '')
|
content = suggestion.get('suggestion', '')
|
||||||
if not relevant_file or not relevant_line_in_file or not content:
|
if not relevant_file or not relevant_line_in_file or not content:
|
||||||
get_logger().info("Skipping inline comment with missing file/line/content")
|
logging.info("Skipping inline comment with missing file/line/content")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.git_provider.is_supported("create_inline_comment"):
|
if self.git_provider.is_supported("create_inline_comment"):
|
||||||
@ -289,7 +261,7 @@ class PRReviewer:
|
|||||||
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)
|
||||||
|
|
||||||
if comments:
|
if comments:
|
||||||
self.git_provider.publish_inline_comments(comments)
|
self.git_provider.publish_inline_comments(comments)
|
||||||
|
|
||||||
def _get_user_answers(self) -> Tuple[str, str]:
|
def _get_user_answers(self) -> Tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
|
@ -1,294 +0,0 @@
|
|||||||
import time
|
|
||||||
from enum import Enum
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
import openai
|
|
||||||
import pandas as pd
|
|
||||||
import pinecone
|
|
||||||
from pinecone_datasets import Dataset, DatasetMetadata
|
|
||||||
from pydantic import BaseModel, Field
|
|
||||||
|
|
||||||
from pr_agent.algo import MAX_TOKENS
|
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
|
||||||
from pr_agent.config_loader import get_settings
|
|
||||||
from pr_agent.git_providers import get_git_provider
|
|
||||||
from pr_agent.log import get_logger
|
|
||||||
|
|
||||||
MODEL = "text-embedding-ada-002"
|
|
||||||
|
|
||||||
|
|
||||||
class PRSimilarIssue:
|
|
||||||
def __init__(self, issue_url: str, args: list = None):
|
|
||||||
if get_settings().config.git_provider != "github":
|
|
||||||
raise Exception("Only github is supported for similar issue tool")
|
|
||||||
|
|
||||||
self.cli_mode = get_settings().CONFIG.CLI_MODE
|
|
||||||
self.max_issues_to_scan = get_settings().pr_similar_issue.max_issues_to_scan
|
|
||||||
self.issue_url = issue_url
|
|
||||||
self.git_provider = get_git_provider()()
|
|
||||||
repo_name, issue_number = self.git_provider._parse_issue_url(issue_url.split('=')[-1])
|
|
||||||
self.git_provider.repo = repo_name
|
|
||||||
self.git_provider.repo_obj = self.git_provider.github_client.get_repo(repo_name)
|
|
||||||
self.token_handler = TokenHandler()
|
|
||||||
repo_obj = self.git_provider.repo_obj
|
|
||||||
repo_name_for_index = self.repo_name_for_index = repo_obj.full_name.lower().replace('/', '-').replace('_/', '-')
|
|
||||||
index_name = self.index_name = "codium-ai-pr-agent-issues"
|
|
||||||
|
|
||||||
# assuming pinecone api key and environment are set in secrets file
|
|
||||||
try:
|
|
||||||
api_key = get_settings().pinecone.api_key
|
|
||||||
environment = get_settings().pinecone.environment
|
|
||||||
except Exception:
|
|
||||||
if not self.cli_mode:
|
|
||||||
repo_name, original_issue_number = self.git_provider._parse_issue_url(self.issue_url.split('=')[-1])
|
|
||||||
issue_main = self.git_provider.repo_obj.get_issue(original_issue_number)
|
|
||||||
issue_main.create_comment("Please set pinecone api key and environment in secrets file")
|
|
||||||
raise Exception("Please set pinecone api key and environment in secrets file")
|
|
||||||
|
|
||||||
# check if index exists, and if repo is already indexed
|
|
||||||
run_from_scratch = False
|
|
||||||
if run_from_scratch: # for debugging
|
|
||||||
pinecone.init(api_key=api_key, environment=environment)
|
|
||||||
if index_name in pinecone.list_indexes():
|
|
||||||
get_logger().info('Removing index...')
|
|
||||||
pinecone.delete_index(index_name)
|
|
||||||
get_logger().info('Done')
|
|
||||||
|
|
||||||
upsert = True
|
|
||||||
pinecone.init(api_key=api_key, environment=environment)
|
|
||||||
if not index_name in pinecone.list_indexes():
|
|
||||||
run_from_scratch = True
|
|
||||||
upsert = False
|
|
||||||
else:
|
|
||||||
if get_settings().pr_similar_issue.force_update_dataset:
|
|
||||||
upsert = True
|
|
||||||
else:
|
|
||||||
pinecone_index = pinecone.Index(index_name=index_name)
|
|
||||||
res = pinecone_index.fetch([f"example_issue_{repo_name_for_index}"]).to_dict()
|
|
||||||
if res["vectors"]:
|
|
||||||
upsert = False
|
|
||||||
|
|
||||||
if run_from_scratch or upsert: # index the entire repo
|
|
||||||
get_logger().info('Indexing the entire repo...')
|
|
||||||
|
|
||||||
get_logger().info('Getting issues...')
|
|
||||||
issues = list(repo_obj.get_issues(state='all'))
|
|
||||||
get_logger().info('Done')
|
|
||||||
self._update_index_with_issues(issues, repo_name_for_index, upsert=upsert)
|
|
||||||
else: # update index if needed
|
|
||||||
pinecone_index = pinecone.Index(index_name=index_name)
|
|
||||||
issues_to_update = []
|
|
||||||
issues_paginated_list = repo_obj.get_issues(state='all')
|
|
||||||
counter = 1
|
|
||||||
for issue in issues_paginated_list:
|
|
||||||
if issue.pull_request:
|
|
||||||
continue
|
|
||||||
issue_str, comments, number = self._process_issue(issue)
|
|
||||||
issue_key = f"issue_{number}"
|
|
||||||
id = issue_key + "." + "issue"
|
|
||||||
res = pinecone_index.fetch([id]).to_dict()
|
|
||||||
is_new_issue = True
|
|
||||||
for vector in res["vectors"].values():
|
|
||||||
if vector['metadata']['repo'] == repo_name_for_index:
|
|
||||||
is_new_issue = False
|
|
||||||
break
|
|
||||||
if is_new_issue:
|
|
||||||
counter += 1
|
|
||||||
issues_to_update.append(issue)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if issues_to_update:
|
|
||||||
get_logger().info(f'Updating index with {counter} new issues...')
|
|
||||||
self._update_index_with_issues(issues_to_update, repo_name_for_index, upsert=True)
|
|
||||||
else:
|
|
||||||
get_logger().info('No new issues to update')
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
get_logger().info('Getting issue...')
|
|
||||||
repo_name, original_issue_number = self.git_provider._parse_issue_url(self.issue_url.split('=')[-1])
|
|
||||||
issue_main = self.git_provider.repo_obj.get_issue(original_issue_number)
|
|
||||||
issue_str, comments, number = self._process_issue(issue_main)
|
|
||||||
openai.api_key = get_settings().openai.key
|
|
||||||
get_logger().info('Done')
|
|
||||||
|
|
||||||
get_logger().info('Querying...')
|
|
||||||
res = openai.Embedding.create(input=[issue_str], engine=MODEL)
|
|
||||||
embeds = [record['embedding'] for record in res['data']]
|
|
||||||
pinecone_index = pinecone.Index(index_name=self.index_name)
|
|
||||||
res = pinecone_index.query(embeds[0],
|
|
||||||
top_k=5,
|
|
||||||
filter={"repo": self.repo_name_for_index},
|
|
||||||
include_metadata=True).to_dict()
|
|
||||||
relevant_issues_number_list = []
|
|
||||||
relevant_comment_number_list = []
|
|
||||||
score_list = []
|
|
||||||
for r in res['matches']:
|
|
||||||
# skip example issue
|
|
||||||
if 'example_issue_' in r["id"]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
try:
|
|
||||||
issue_number = int(r["id"].split('.')[0].split('_')[-1])
|
|
||||||
except:
|
|
||||||
get_logger().debug(f"Failed to parse issue number from {r['id']}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if original_issue_number == issue_number:
|
|
||||||
continue
|
|
||||||
if issue_number not in relevant_issues_number_list:
|
|
||||||
relevant_issues_number_list.append(issue_number)
|
|
||||||
if 'comment' in r["id"]:
|
|
||||||
relevant_comment_number_list.append(int(r["id"].split('.')[1].split('_')[-1]))
|
|
||||||
else:
|
|
||||||
relevant_comment_number_list.append(-1)
|
|
||||||
score_list.append(str("{:.2f}".format(r['score'])))
|
|
||||||
get_logger().info('Done')
|
|
||||||
|
|
||||||
get_logger().info('Publishing response...')
|
|
||||||
similar_issues_str = "### Similar Issues\n___\n\n"
|
|
||||||
for i, issue_number_similar in enumerate(relevant_issues_number_list):
|
|
||||||
issue = self.git_provider.repo_obj.get_issue(issue_number_similar)
|
|
||||||
title = issue.title
|
|
||||||
url = issue.html_url
|
|
||||||
if relevant_comment_number_list[i] != -1:
|
|
||||||
url = list(issue.get_comments())[relevant_comment_number_list[i]].html_url
|
|
||||||
similar_issues_str += f"{i + 1}. **[{title}]({url})** (score={score_list[i]})\n\n"
|
|
||||||
if get_settings().config.publish_output:
|
|
||||||
response = issue_main.create_comment(similar_issues_str)
|
|
||||||
get_logger().info(similar_issues_str)
|
|
||||||
get_logger().info('Done')
|
|
||||||
|
|
||||||
def _process_issue(self, issue):
|
|
||||||
header = issue.title
|
|
||||||
body = issue.body
|
|
||||||
number = issue.number
|
|
||||||
if get_settings().pr_similar_issue.skip_comments:
|
|
||||||
comments = []
|
|
||||||
else:
|
|
||||||
comments = list(issue.get_comments())
|
|
||||||
issue_str = f"Issue Header: \"{header}\"\n\nIssue Body:\n{body}"
|
|
||||||
return issue_str, comments, number
|
|
||||||
|
|
||||||
def _update_index_with_issues(self, issues_list, repo_name_for_index, upsert=False):
|
|
||||||
get_logger().info('Processing issues...')
|
|
||||||
corpus = Corpus()
|
|
||||||
example_issue_record = Record(
|
|
||||||
id=f"example_issue_{repo_name_for_index}",
|
|
||||||
text="example_issue",
|
|
||||||
metadata=Metadata(repo=repo_name_for_index)
|
|
||||||
)
|
|
||||||
corpus.append(example_issue_record)
|
|
||||||
|
|
||||||
counter = 0
|
|
||||||
for issue in issues_list:
|
|
||||||
if issue.pull_request:
|
|
||||||
continue
|
|
||||||
|
|
||||||
counter += 1
|
|
||||||
if counter % 100 == 0:
|
|
||||||
get_logger().info(f"Scanned {counter} issues")
|
|
||||||
if counter >= self.max_issues_to_scan:
|
|
||||||
get_logger().info(f"Scanned {self.max_issues_to_scan} issues, stopping")
|
|
||||||
break
|
|
||||||
|
|
||||||
issue_str, comments, number = self._process_issue(issue)
|
|
||||||
issue_key = f"issue_{number}"
|
|
||||||
username = issue.user.login
|
|
||||||
created_at = str(issue.created_at)
|
|
||||||
if len(issue_str) < 8000 or \
|
|
||||||
self.token_handler.count_tokens(issue_str) < MAX_TOKENS[MODEL]: # fast reject first
|
|
||||||
issue_record = Record(
|
|
||||||
id=issue_key + "." + "issue",
|
|
||||||
text=issue_str,
|
|
||||||
metadata=Metadata(repo=repo_name_for_index,
|
|
||||||
username=username,
|
|
||||||
created_at=created_at,
|
|
||||||
level=IssueLevel.ISSUE)
|
|
||||||
)
|
|
||||||
corpus.append(issue_record)
|
|
||||||
if comments:
|
|
||||||
for j, comment in enumerate(comments):
|
|
||||||
comment_body = comment.body
|
|
||||||
num_words_comment = len(comment_body.split())
|
|
||||||
if num_words_comment < 10 or not isinstance(comment_body, str):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(comment_body) < 8000 or \
|
|
||||||
self.token_handler.count_tokens(comment_body) < MAX_TOKENS[MODEL]:
|
|
||||||
comment_record = Record(
|
|
||||||
id=issue_key + ".comment_" + str(j + 1),
|
|
||||||
text=comment_body,
|
|
||||||
metadata=Metadata(repo=repo_name_for_index,
|
|
||||||
username=username, # use issue username for all comments
|
|
||||||
created_at=created_at,
|
|
||||||
level=IssueLevel.COMMENT)
|
|
||||||
)
|
|
||||||
corpus.append(comment_record)
|
|
||||||
df = pd.DataFrame(corpus.dict()["documents"])
|
|
||||||
get_logger().info('Done')
|
|
||||||
|
|
||||||
get_logger().info('Embedding...')
|
|
||||||
openai.api_key = get_settings().openai.key
|
|
||||||
list_to_encode = list(df["text"].values)
|
|
||||||
try:
|
|
||||||
res = openai.Embedding.create(input=list_to_encode, engine=MODEL)
|
|
||||||
embeds = [record['embedding'] for record in res['data']]
|
|
||||||
except:
|
|
||||||
embeds = []
|
|
||||||
get_logger().error('Failed to embed entire list, embedding one by one...')
|
|
||||||
for i, text in enumerate(list_to_encode):
|
|
||||||
try:
|
|
||||||
res = openai.Embedding.create(input=[text], engine=MODEL)
|
|
||||||
embeds.append(res['data'][0]['embedding'])
|
|
||||||
except:
|
|
||||||
embeds.append([0] * 1536)
|
|
||||||
df["values"] = embeds
|
|
||||||
meta = DatasetMetadata.empty()
|
|
||||||
meta.dense_model.dimension = len(embeds[0])
|
|
||||||
ds = Dataset.from_pandas(df, meta)
|
|
||||||
get_logger().info('Done')
|
|
||||||
|
|
||||||
api_key = get_settings().pinecone.api_key
|
|
||||||
environment = get_settings().pinecone.environment
|
|
||||||
if not upsert:
|
|
||||||
get_logger().info('Creating index from scratch...')
|
|
||||||
ds.to_pinecone_index(self.index_name, api_key=api_key, environment=environment)
|
|
||||||
time.sleep(15) # wait for pinecone to finalize indexing before querying
|
|
||||||
else:
|
|
||||||
get_logger().info('Upserting index...')
|
|
||||||
namespace = ""
|
|
||||||
batch_size: int = 100
|
|
||||||
concurrency: int = 10
|
|
||||||
pinecone.init(api_key=api_key, environment=environment)
|
|
||||||
ds._upsert_to_index(self.index_name, namespace, batch_size, concurrency)
|
|
||||||
time.sleep(5) # wait for pinecone to finalize upserting before querying
|
|
||||||
get_logger().info('Done')
|
|
||||||
|
|
||||||
|
|
||||||
class IssueLevel(str, Enum):
|
|
||||||
ISSUE = "issue"
|
|
||||||
COMMENT = "comment"
|
|
||||||
|
|
||||||
|
|
||||||
class Metadata(BaseModel):
|
|
||||||
repo: str
|
|
||||||
username: str = Field(default="@codium")
|
|
||||||
created_at: str = Field(default="01-01-1970 00:00:00.00000")
|
|
||||||
level: IssueLevel = Field(default=IssueLevel.ISSUE)
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
use_enum_values = True
|
|
||||||
|
|
||||||
|
|
||||||
class Record(BaseModel):
|
|
||||||
id: str
|
|
||||||
text: str
|
|
||||||
metadata: Metadata
|
|
||||||
|
|
||||||
|
|
||||||
class Corpus(BaseModel):
|
|
||||||
documents: List[Record] = Field(default=[])
|
|
||||||
|
|
||||||
def append(self, r: Record):
|
|
||||||
self.documents.append(r)
|
|
@ -1,4 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
|
import logging
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
@ -9,9 +10,8 @@ from pr_agent.algo.ai_handler import AiHandler
|
|||||||
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import GithubProvider, 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.log import get_logger
|
|
||||||
|
|
||||||
CHANGELOG_LINES = 50
|
CHANGELOG_LINES = 50
|
||||||
|
|
||||||
@ -40,34 +40,32 @@ class PRUpdateChangelog:
|
|||||||
"extra_instructions": get_settings().pr_update_changelog.extra_instructions,
|
"extra_instructions": get_settings().pr_update_changelog.extra_instructions,
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
}
|
}
|
||||||
self.token_handler = TokenHandler(self.git_provider.pr,
|
self.token_handler = TokenHandler(self.vars, get_settings().pr_update_changelog_prompt.system,
|
||||||
self.vars,
|
|
||||||
get_settings().pr_update_changelog_prompt.system,
|
|
||||||
get_settings().pr_update_changelog_prompt.user)
|
get_settings().pr_update_changelog_prompt.user)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
# assert type(self.git_provider) == GithubProvider, "Currently only Github is supported"
|
assert type(self.git_provider) == GithubProvider, "Currently only Github is supported"
|
||||||
|
|
||||||
get_logger().info('Updating the changelog...')
|
logging.info('Updating the changelog...')
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
self.git_provider.publish_comment("Preparing changelog updates...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing changelog updates...", is_temporary=True)
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
get_logger().info('Preparing PR changelog updates...')
|
logging.info('Preparing PR changelog updates...')
|
||||||
new_file_content, answer = self._prepare_changelog_update()
|
new_file_content, answer = self._prepare_changelog_update()
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
get_logger().info('Publishing changelog updates...')
|
logging.info('Publishing changelog updates...')
|
||||||
if self.commit_changelog:
|
if self.commit_changelog:
|
||||||
get_logger().info('Pushing PR changelog updates to repo...')
|
logging.info('Pushing PR changelog updates to repo...')
|
||||||
self._push_changelog_update(new_file_content, answer)
|
self._push_changelog_update(new_file_content, answer)
|
||||||
else:
|
else:
|
||||||
get_logger().info('Publishing PR changelog as comment...')
|
logging.info('Publishing PR changelog as comment...')
|
||||||
self.git_provider.publish_comment(f"**Changelog updates:**\n\n{answer}")
|
self.git_provider.publish_comment(f"**Changelog updates:**\n\n{answer}")
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str):
|
async def _prepare_prediction(self, model: str):
|
||||||
get_logger().info('Getting PR diff...')
|
logging.info('Getting PR diff...')
|
||||||
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
||||||
get_logger().info('Getting AI prediction...')
|
logging.info('Getting AI prediction...')
|
||||||
self.prediction = await self._get_prediction(model)
|
self.prediction = await self._get_prediction(model)
|
||||||
|
|
||||||
async def _get_prediction(self, model: str):
|
async def _get_prediction(self, model: str):
|
||||||
@ -77,8 +75,8 @@ class PRUpdateChangelog:
|
|||||||
system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables)
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
logging.info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
logging.info(f"\nUser prompt:\n{user_prompt}")
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
response, finish_reason = await self.ai_handler.chat_completion(model=model, temperature=0.2,
|
||||||
system=system_prompt, user=user_prompt)
|
system=system_prompt, user=user_prompt)
|
||||||
|
|
||||||
@ -100,7 +98,7 @@ class PRUpdateChangelog:
|
|||||||
"\n>'/update_changelog --pr_update_changelog.push_changelog_changes=true'\n"
|
"\n>'/update_changelog --pr_update_changelog.push_changelog_changes=true'\n"
|
||||||
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"answer:\n{answer}")
|
logging.info(f"answer:\n{answer}")
|
||||||
|
|
||||||
return new_file_content, answer
|
return new_file_content, answer
|
||||||
|
|
||||||
@ -149,7 +147,7 @@ Example:
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.changelog_file_str = ""
|
self.changelog_file_str = ""
|
||||||
if self.commit_changelog:
|
if self.commit_changelog:
|
||||||
get_logger().info("No CHANGELOG.md file found in the repository. Creating one...")
|
logging.info("No CHANGELOG.md file found in the repository. Creating one...")
|
||||||
changelog_file = self.git_provider.repo_obj.create_file(path="CHANGELOG.md",
|
changelog_file = self.git_provider.repo_obj.create_file(path="CHANGELOG.md",
|
||||||
message='add CHANGELOG.md',
|
message='add CHANGELOG.md',
|
||||||
content="",
|
content="",
|
||||||
|
@ -35,12 +35,12 @@ dependencies = {file = ["requirements.txt"]}
|
|||||||
"Homepage" = "https://github.com/Codium-ai/pr-agent"
|
"Homepage" = "https://github.com/Codium-ai/pr-agent"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
include-package-data = false
|
include-package-data = true
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
include = ["pr_agent"]
|
include = ["pr_agent", "pr_agent.*"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
pr-agent = "pr_agent.cli:run"
|
pr-agent = "pr_agent.cli:run"
|
||||||
|
@ -1,24 +1,19 @@
|
|||||||
dynaconf==3.1.12
|
dynaconf~=3.1.12
|
||||||
fastapi==0.99.0
|
fastapi~=0.103.0
|
||||||
PyGithub==1.59.*
|
PyGithub~=1.59.0
|
||||||
retry==0.9.2
|
retry~=0.9.2
|
||||||
openai==0.27.8
|
openai~=0.27.8
|
||||||
Jinja2==3.1.2
|
Jinja2~=3.1.2
|
||||||
tiktoken==0.4.0
|
tiktoken~=0.4.0
|
||||||
uvicorn==0.22.0
|
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
|
atlassian-python-api~=3.39.0
|
||||||
GitPython==3.1.32
|
GitPython~=3.1.32
|
||||||
PyYAML==6.0
|
PyYAML~=6.0
|
||||||
starlette-context==0.3.6
|
starlette-context~=0.3.6
|
||||||
litellm~=0.1.574
|
litellm~=0.1.445
|
||||||
boto3==1.28.25
|
boto3~=1.28.25
|
||||||
google-cloud-storage==2.10.0
|
google-cloud-storage~=2.10.0
|
||||||
ujson==5.8.0
|
ujson~=5.8.0
|
||||||
azure-devops==7.1.0b3
|
|
||||||
msrest==0.7.1
|
|
||||||
pinecone-client
|
|
||||||
pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main
|
|
||||||
loguru==0.7.2
|
|
||||||
|
@ -110,7 +110,7 @@ class TestCodeCommitProvider:
|
|||||||
# Mock the response from the AWS client for get_pull_request method
|
# Mock the response from the AWS client for get_pull_request method
|
||||||
api.boto_client.get_pull_request.return_value = {
|
api.boto_client.get_pull_request.return_value = {
|
||||||
"pullRequest": {
|
"pullRequest": {
|
||||||
"pullRequestId": "321",
|
"pullRequestId": "3",
|
||||||
"title": "My PR",
|
"title": "My PR",
|
||||||
"description": "My PR description",
|
"description": "My PR description",
|
||||||
"pullRequestTargets": [
|
"pullRequestTargets": [
|
||||||
@ -125,7 +125,7 @@ class TestCodeCommitProvider:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pr = api.get_pr("my_test_repo", 321)
|
pr = api.get_pr(321)
|
||||||
|
|
||||||
assert pr.title == "My PR"
|
assert pr.title == "My PR"
|
||||||
assert pr.description == "My PR description"
|
assert pr.description == "My PR description"
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch
|
|
||||||
from pr_agent.git_providers.codecommit_provider import CodeCommitFile
|
from pr_agent.git_providers.codecommit_provider import CodeCommitFile
|
||||||
from pr_agent.git_providers.codecommit_provider import CodeCommitProvider
|
from pr_agent.git_providers.codecommit_provider import CodeCommitProvider
|
||||||
from pr_agent.git_providers.codecommit_provider import PullRequestCCMimic
|
from pr_agent.algo.utils import EDIT_TYPE
|
||||||
from pr_agent.git_providers.git_provider import EDIT_TYPE
|
|
||||||
|
|
||||||
|
|
||||||
class TestCodeCommitFile:
|
class TestCodeCommitFile:
|
||||||
@ -27,64 +25,12 @@ class TestCodeCommitFile:
|
|||||||
|
|
||||||
|
|
||||||
class TestCodeCommitProvider:
|
class TestCodeCommitProvider:
|
||||||
def test_get_title(self):
|
|
||||||
# Test that the get_title() function returns the PR title
|
|
||||||
with patch.object(CodeCommitProvider, "__init__", lambda x, y: None):
|
|
||||||
provider = CodeCommitProvider(None)
|
|
||||||
provider.pr = PullRequestCCMimic("My Test PR Title", [])
|
|
||||||
assert provider.get_title() == "My Test PR Title"
|
|
||||||
|
|
||||||
def test_get_pr_id(self):
|
|
||||||
# Test that the get_pr_id() function returns the correct ID
|
|
||||||
with patch.object(CodeCommitProvider, "__init__", lambda x, y: None):
|
|
||||||
provider = CodeCommitProvider(None)
|
|
||||||
provider.repo_name = "my_test_repo"
|
|
||||||
provider.pr_num = 321
|
|
||||||
assert provider.get_pr_id() == "my_test_repo/321"
|
|
||||||
|
|
||||||
def test_parse_pr_url(self):
|
def test_parse_pr_url(self):
|
||||||
# Test that the _parse_pr_url() function can extract the repo name and PR number from a CodeCommit URL
|
|
||||||
url = "https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/my_test_repo/pull-requests/321"
|
url = "https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/my_test_repo/pull-requests/321"
|
||||||
repo_name, pr_number = CodeCommitProvider._parse_pr_url(url)
|
repo_name, pr_number = CodeCommitProvider._parse_pr_url(url)
|
||||||
assert repo_name == "my_test_repo"
|
assert repo_name == "my_test_repo"
|
||||||
assert pr_number == 321
|
assert pr_number == 321
|
||||||
|
|
||||||
def test_is_valid_codecommit_hostname(self):
|
|
||||||
# Test the various AWS regions
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("af-south-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-east-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-2.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-northeast-3.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-south-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-south-2.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-2.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-3.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ap-southeast-4.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("ca-central-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-central-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-central-2.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-north-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-south-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-south-2.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-2.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("eu-west-3.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("il-central-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("me-central-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("me-south-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("sa-east-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-east-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-east-2.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-gov-east-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-gov-west-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-west-1.console.aws.amazon.com")
|
|
||||||
assert CodeCommitProvider._is_valid_codecommit_hostname("us-west-2.console.aws.amazon.com")
|
|
||||||
# Test non-AWS regions
|
|
||||||
assert not CodeCommitProvider._is_valid_codecommit_hostname("no-such-region.console.aws.amazon.com")
|
|
||||||
assert not CodeCommitProvider._is_valid_codecommit_hostname("console.aws.amazon.com")
|
|
||||||
|
|
||||||
# Test that an error is raised when an invalid CodeCommit URL is provided to the set_pr() method of the CodeCommitProvider class.
|
# Test that an error is raised when an invalid CodeCommit URL is provided to the set_pr() method of the CodeCommitProvider class.
|
||||||
# Generated by CodiumAI
|
# Generated by CodiumAI
|
||||||
def test_invalid_codecommit_url(self):
|
def test_invalid_codecommit_url(self):
|
||||||
@ -160,7 +106,6 @@ class TestCodeCommitProvider:
|
|||||||
assert percentages == {}
|
assert percentages == {}
|
||||||
|
|
||||||
def test_get_edit_type(self):
|
def test_get_edit_type(self):
|
||||||
# Test that the _get_edit_type() function can convert a CodeCommit letter to an EDIT_TYPE enum
|
|
||||||
assert CodeCommitProvider._get_edit_type("A") == EDIT_TYPE.ADDED
|
assert CodeCommitProvider._get_edit_type("A") == EDIT_TYPE.ADDED
|
||||||
assert CodeCommitProvider._get_edit_type("D") == EDIT_TYPE.DELETED
|
assert CodeCommitProvider._get_edit_type("D") == EDIT_TYPE.DELETED
|
||||||
assert CodeCommitProvider._get_edit_type("M") == EDIT_TYPE.MODIFIED
|
assert CodeCommitProvider._get_edit_type("M") == EDIT_TYPE.MODIFIED
|
||||||
@ -172,18 +117,3 @@ class TestCodeCommitProvider:
|
|||||||
assert CodeCommitProvider._get_edit_type("r") == EDIT_TYPE.RENAMED
|
assert CodeCommitProvider._get_edit_type("r") == EDIT_TYPE.RENAMED
|
||||||
|
|
||||||
assert CodeCommitProvider._get_edit_type("X") is None
|
assert CodeCommitProvider._get_edit_type("X") is None
|
||||||
|
|
||||||
def test_add_additional_newlines(self):
|
|
||||||
# a short string to test adding double newlines
|
|
||||||
input = "abc\ndef\n\n___\nghi\njkl\nmno\n\npqr\n"
|
|
||||||
expect = "abc\n\ndef\n\n___\n\nghi\n\njkl\n\nmno\n\npqr\n\n"
|
|
||||||
assert CodeCommitProvider._add_additional_newlines(input) == expect
|
|
||||||
# a test example from a real PR
|
|
||||||
input = "## PR Type:\nEnhancement\n\n___\n## PR Description:\nThis PR introduces a new feature to the script, allowing users to filter servers by name.\n\n___\n## PR Main Files Walkthrough:\n`foo`: The foo script has been updated to include a new command line option `-f` or `--filter`.\n`bar`: The bar script has been updated to list stopped servers.\n"
|
|
||||||
expect = "## PR Type:\n\nEnhancement\n\n___\n\n## PR Description:\n\nThis PR introduces a new feature to the script, allowing users to filter servers by name.\n\n___\n\n## PR Main Files Walkthrough:\n\n`foo`: The foo script has been updated to include a new command line option `-f` or `--filter`.\n\n`bar`: The bar script has been updated to list stopped servers.\n\n"
|
|
||||||
assert CodeCommitProvider._add_additional_newlines(input) == expect
|
|
||||||
|
|
||||||
def test_remove_markdown_html(self):
|
|
||||||
input = "## PR Feedback\n<details><summary>Code feedback:</summary>\nfile foo\n</summary>\n"
|
|
||||||
expect = "## PR Feedback\nCode feedback:\nfile foo\n\n"
|
|
||||||
assert CodeCommitProvider._remove_markdown_html(input) == expect
|
|
||||||
|