mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-04 12:50:38 +08:00
Compare commits
2 Commits
v0.8
...
ok/update_
Author | SHA1 | Date | |
---|---|---|---|
eee6252f6d | |||
dd8c992dad |
3
.github/workflows/pr-agent-review.yaml
vendored
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 }}
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
FROM python:3.10 as base
|
|
||||||
|
|
||||||
ENV OPENAI_API_KEY=${OPENAI_API_KEY} \
|
|
||||||
BITBUCKET_BEARER_TOKEN=${BITBUCKET_BEARER_TOKEN} \
|
|
||||||
BITBUCKET_PR_ID=${BITBUCKET_PR_ID} \
|
|
||||||
BITBUCKET_REPO_SLUG=${BITBUCKET_REPO_SLUG} \
|
|
||||||
BITBUCKET_WORKSPACE=${BITBUCKET_WORKSPACE}
|
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ADD pyproject.toml .
|
|
||||||
ADD requirements.txt .
|
|
||||||
RUN pip install . && rm pyproject.toml requirements.txt
|
|
||||||
ENV PYTHONPATH=/app
|
|
||||||
ADD pr_agent pr_agent
|
|
||||||
ADD bitbucket_pipeline/entrypoint.sh /
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
@ -1 +1 @@
|
|||||||
FROM codiumai/pr-agent:0.8-github_action
|
FROM codiumai/pr-agent:github_action
|
||||||
|
78
INSTALL.md
78
INSTALL.md
@ -16,7 +16,6 @@ There are several ways to use PR-Agent:
|
|||||||
- [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 8: Run a GitLab webhook server](INSTALL.md#method-8---run-a-gitlab-webhook-server)
|
||||||
- [Method 9: Run as a Bitbucket Pipeline](INSTALL.md#method-9-run-as-a-bitbucket-pipeline)
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Method 1: Use Docker image (no installation required)
|
### Method 1: Use Docker image (no installation required)
|
||||||
@ -25,15 +24,9 @@ To request a review for a PR, or ask a question about a PR, you can run directly
|
|||||||
|
|
||||||
1. To request a review for a PR, run the following command:
|
1. To request a review for a PR, run the following command:
|
||||||
|
|
||||||
For GitHub:
|
|
||||||
```
|
```
|
||||||
docker run --rm -it -e OPENAI.KEY=<your key> -e GITHUB.USER_TOKEN=<your token> codiumai/pr-agent --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 --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:
|
2. To ask a question about a PR, run the following command:
|
||||||
|
|
||||||
@ -123,7 +116,7 @@ jobs:
|
|||||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
```
|
```
|
||||||
** if you want to pin your action to a specific release (v0.7 for example) for stability reasons, use:
|
** if you want to pin your action to a specific commit for stability reasons
|
||||||
```yaml
|
```yaml
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
@ -140,7 +133,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: PR Agent action step
|
- name: PR Agent action step
|
||||||
id: pragent
|
id: pragent
|
||||||
uses: Codium-ai/pr-agent@v0.7
|
uses: Codium-ai/pr-agent@<commit_sha>
|
||||||
env:
|
env:
|
||||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@ -156,7 +149,7 @@ The GITHUB_TOKEN secret is automatically created by GitHub.
|
|||||||
3. Merge this change to your main branch.
|
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.
|
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:
|
4. You may configure PR-Agent by adding environment variables under the env section corresponding to any configurable property in the [configuration](./Usage.md) file. Some examples:
|
||||||
```yaml
|
```yaml
|
||||||
env:
|
env:
|
||||||
# ... previous environment values
|
# ... previous environment values
|
||||||
@ -361,7 +354,7 @@ PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
|
|||||||
```
|
```
|
||||||
WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
|
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.
|
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).
|
||||||
4. In the secrets file, fill in the following:
|
4. In the secrets file, fill in the following:
|
||||||
- Your OpenAI key.
|
- 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.
|
- 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.
|
||||||
@ -370,64 +363,11 @@ WEBHOOK_SECRET=$(python -c "import secrets; print(secrets.token_hex(10))")
|
|||||||
In the "Trigger" section, check the ‘comments’ and ‘merge request events’ boxes.
|
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.
|
6. Test your installation by opening a merge request or commenting or a merge request using one of CodiumAI's commands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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).
|
||||||
|
|
||||||
### Method 9: Run as a Bitbucket Pipeline
|
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'.
|
||||||
|
|
||||||
|
<img src="./pics/debugger.png" width="800"/>
|
||||||
You can use our pre-build Bitbucket-Pipeline docker image to run as Bitbucket-Pipeline.
|
|
||||||
|
|
||||||
1. Add the following file in your repository bitbucket_pipelines.yml
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
pipelines:
|
|
||||||
pull-requests:
|
|
||||||
'**':
|
|
||||||
- step:
|
|
||||||
name: PR Agent Pipeline
|
|
||||||
caches:
|
|
||||||
- pip
|
|
||||||
image: python:3.8
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
script:
|
|
||||||
- git clone https://github.com/Codium-ai/pr-agent.git
|
|
||||||
- cd pr-agent
|
|
||||||
- docker build -t bitbucket_runner:latest -f Dockerfile.bitbucket_pipeline .
|
|
||||||
- docker run -e OPENAI_API_KEY=$OPENAI_API_KEY -e BITBUCKET_BEARER_TOKEN=$BITBUCKET_BEARER_TOKEN -e BITBUCKET_PR_ID=$BITBUCKET_PR_ID -e BITBUCKET_REPO_SLUG=$BITBUCKET_REPO_SLUG -e BITBUCKET_WORKSPACE=$BITBUCKET_WORKSPACE bitbucket_runner:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Add the following secret to your repository under Repository settings > Pipelines > Repository variables.
|
|
||||||
OPENAI_API_KEY: <your key>
|
|
||||||
BITBUCKET_BEARER_TOKEN: <your token>
|
|
||||||
|
|
||||||
3. To get BITBUCKET_BEARER_TOKEN follow these steps
|
|
||||||
So here is my step by step tutorial
|
|
||||||
i) Insert your workspace name instead of {workspace_name} and go to the following link in order to create an OAuth consumer.
|
|
||||||
|
|
||||||
https://bitbucket.org/{workspace_name}/workspace/settings/api
|
|
||||||
|
|
||||||
set callback URL to http://localhost:8976 (doesn't need to be a real server there)
|
|
||||||
select permissions: repository -> read
|
|
||||||
|
|
||||||
ii) use consumer's Key as a {client_id} and open the following URL in the browser
|
|
||||||
|
|
||||||
https://bitbucket.org/site/oauth2/authorize?client_id={client_id}&response_type=code
|
|
||||||
|
|
||||||
iii)
|
|
||||||
after you press "Grant access" in the browser it will redirect you to
|
|
||||||
|
|
||||||
http://localhost:8976?code=<CODE>
|
|
||||||
|
|
||||||
iv) use the code from the previous step and consumer's Key as a {client_id}, and consumer's Secret as {client_secret}
|
|
||||||
|
|
||||||
curl -X POST -u "{client_id}:{client_secret}" \
|
|
||||||
https://bitbucket.org/site/oauth2/access_token \
|
|
||||||
-d grant_type=authorization_code \
|
|
||||||
-d code={code} \
|
|
||||||
|
|
||||||
|
|
||||||
After completing this steps, you just to place this access token in the repository varibles.
|
|
||||||
|
|
||||||
|
|
||||||
=======
|
|
45
README.md
45
README.md
@ -15,24 +15,20 @@ Making pull requests less painful with an AI agent
|
|||||||
</div>
|
</div>
|
||||||
<div style="text-align:left;">
|
<div style="text-align:left;">
|
||||||
|
|
||||||
CodiumAI `PR-Agent` is an open-source tool 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`)**: Automatically generating [PR description](https://github.com/Codium-ai/pr-agent/pull/229#issue-1860711415) - title, type, summary, code walkthrough and labels.
|
**Auto Description (/describe)**: 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`)**: [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.
|
**Auto Review (/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 ...`)**: Answering [free-text questions](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021332) about the PR.
|
**Question Answering (/ask ...)**: Answering [free-text questions](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021332) about the PR.
|
||||||
\
|
\
|
||||||
‣ **Code Suggestions (`/improve`)**: [Committable code suggestions](https://github.com/Codium-ai/pr-agent/pull/229#discussion_r1306919276) for improving the PR.
|
**Code Suggestions (/improve)**: [Committable code suggestions](https://github.com/Codium-ai/pr-agent/pull/229#discussion_r1306919276) for improving the PR.
|
||||||
\
|
\
|
||||||
‣ **Update Changelog (`/update_changelog`)**: Automatically updating the CHANGELOG.md file with the [PR changes](https://github.com/Codium-ai/pr-agent/pull/168#discussion_r1282077645).
|
**Update Changelog (/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`)**: Automatically retrieves and presents [similar issues](https://github.com/Alibaba-MIIL/ASL/issues/107).
|
|
||||||
|
|
||||||
|
|
||||||
See the [usage guide](./Usage.md) for instructions how to run the different tools from [CLI](./Usage.md#working-from-a-local-repo-cli), or by [online usage](./Usage.md#online-usage), as well as additional details on optional commands and configurations.
|
See the [usage guide](./Usage.md) for instructions how to run the different tools from [CLI](./Usage.md#working-from-a-local-repo-cli), or by [online usage](./Usage.md#online-usage).
|
||||||
|
|
||||||
[Release notes](./RELEASE_NOTES.md)
|
|
||||||
|
|
||||||
<h3>Example results:</h3>
|
<h3>Example results:</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -91,8 +87,9 @@ See the [usage guide](./Usage.md) for instructions how to run the different tool
|
|||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Try it now](#try-it-now)
|
- [Try it now](#try-it-now)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
|
- [Usage guide](./Usage.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)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -103,12 +100,11 @@ See the [usage guide](./Usage.md) for instructions how to run the different tool
|
|||||||
|-------|---------------------------------------------|:------:|:------:|:---------:|:----------:|:----------:|:----------:|
|
|-------|---------------------------------------------|:------:|:------:|:---------:|:----------:|:----------:|:----------:|
|
||||||
| 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: | :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: | :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: | | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| | Improve Code | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
|
| | Improve Code | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | :white_check_mark: |
|
||||||
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: |
|
| | ⮑ Extended | :white_check_mark: | :white_check_mark: | | :white_check_mark: | | :white_check_mark: |
|
||||||
| | Reflect and Review | :white_check_mark: | :white_check_mark: | :white_check_mark: | | :white_check_mark: | :white_check_mark: |
|
| | Reflect and Review | :white_check_mark: | | | | :white_check_mark: | :white_check_mark: |
|
||||||
| | Update CHANGELOG.md | :white_check_mark: | :white_check_mark: | :white_check_mark: | | | |
|
| | Update CHANGELOG.md | :white_check_mark: | | | | | |
|
||||||
| | Find similar issue | :white_check_mark: | | | | | |
|
|
||||||
| | | | | | | |
|
| | | | | | | |
|
||||||
| USAGE | CLI | :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: | :white_check_mark: |
|
||||||
| | App / webhook | :white_check_mark: | :white_check_mark: | | | |
|
| | App / webhook | :white_check_mark: | :white_check_mark: | | | |
|
||||||
@ -159,7 +155,6 @@ There are several ways to use PR-Agent:
|
|||||||
- [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 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
|
||||||
|
|
||||||
@ -187,7 +182,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
|
||||||
@ -205,13 +200,3 @@ Here are some advantages of PR-Agent:
|
|||||||
- [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)
|
||||||
|
|
||||||
## 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,41 +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.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
122
Usage.md
122
Usage.md
@ -50,12 +50,12 @@ When running from your local repo (CLI), your local configuration file will be u
|
|||||||
|
|
||||||
Examples for invoking the different tools via the CLI:
|
Examples for invoking the different tools via the CLI:
|
||||||
|
|
||||||
- **Review**: `python cli.py --pr_url=<pr_url> review`
|
- **Review**: `python cli.py --pr_url=<pr_url> /review`
|
||||||
- **Describe**: `python cli.py --pr_url=<pr_url> describe`
|
- **Describe**: `python cli.py --pr_url=<pr_url> /describe`
|
||||||
- **Improve**: `python cli.py --pr_url=<pr_url> improve`
|
- **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"`
|
- **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`
|
- **Reflect**: `python cli.py --pr_url=<pr_url> /reflect`
|
||||||
- **Update Changelog**: `python cli.py --pr_url=<pr_url> update_changelog`
|
- **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).
|
`<pr_url>` is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).
|
||||||
|
|
||||||
@ -142,104 +142,22 @@ 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).
|
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
|
### 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:
|
TBD
|
||||||
```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.
|
|
||||||
|
|
||||||
|
|
||||||
### Appendix - additional configurations walkthrough
|
### Appendix - additional configurations walkthrough
|
||||||
|
|
||||||
#### Changing a model
|
#### Changing a model
|
||||||
See [here](pr_agent/algo/__init__.py) for the list of available models.
|
See [here](pr_agent/algo/__init__.py) for the list of available models.
|
||||||
|
|
||||||
#### Azure
|
To use Llama2 model, for example, set:
|
||||||
To use Azure, set:
|
|
||||||
```
|
|
||||||
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"
|
|
||||||
deployment_id = "" # The deployment name you chose when you deployed the engine
|
|
||||||
```
|
|
||||||
in your .secrets.toml
|
|
||||||
|
|
||||||
and
|
|
||||||
```
|
```
|
||||||
[config]
|
[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"
|
model = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
|
||||||
[replicate] # in .secrets.toml
|
[replicate]
|
||||||
key = ...
|
key = ...
|
||||||
```
|
```
|
||||||
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
|
(you can obtain a Llama2 key from [here](https://replicate.com/replicate/llama-2-70b-chat/api))
|
||||||
|
|
||||||
|
|
||||||
Also review the [AiHandler](pr_agent/algo/ai_handler.py) file for instruction how to set keys for other models.
|
Also review the [AiHandler](pr_agent/algo/ai_handler.py) file for instruction how to set keys for other models.
|
||||||
|
|
||||||
#### Extra instructions
|
#### Extra instructions
|
||||||
@ -262,25 +180,3 @@ And use the following settings (you have to replace the values) in .secrets.toml
|
|||||||
org = "https://dev.azure.com/YOUR_ORGANIZATION/"
|
org = "https://dev.azure.com/YOUR_ORGANIZATION/"
|
||||||
pat = "YOUR_PAT_TOKEN"
|
pat = "YOUR_PAT_TOKEN"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Similar issue tool
|
|
||||||
|
|
||||||
[Example usage](https://github.com/Alibaba-MIIL/ASL/issues/107)
|
|
||||||
|
|
||||||
<img src=./pics/similar_issue_tool.png width="768">
|
|
||||||
|
|
||||||
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/).
|
|
||||||
|
|
||||||
- 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,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
python /app/pr_agent/servers/bitbucket_pipeline_runner.py
|
|
BIN
pics/debugger.png
Normal file
BIN
pics/debugger.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 534 KiB |
Binary file not shown.
Before Width: | Height: | Size: 210 KiB |
@ -9,7 +9,6 @@ from pr_agent.git_providers import get_git_provider
|
|||||||
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
||||||
from pr_agent.tools.pr_description import PRDescription
|
from pr_agent.tools.pr_description import PRDescription
|
||||||
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
|
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
|
||||||
from pr_agent.tools.pr_similar_issue import PRSimilarIssue
|
|
||||||
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_update_changelog import PRUpdateChangelog
|
from pr_agent.tools.pr_update_changelog import PRUpdateChangelog
|
||||||
@ -31,7 +30,6 @@ command2class = {
|
|||||||
"update_changelog": PRUpdateChangelog,
|
"update_changelog": PRUpdateChangelog,
|
||||||
"config": PRConfig,
|
"config": PRConfig,
|
||||||
"settings": PRConfig,
|
"settings": PRConfig,
|
||||||
"similar_issue": PRSimilarIssue,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commands = list(command2class.keys())
|
commands = list(command2class.keys())
|
||||||
|
@ -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,13 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
OPENAI_RETRIES = 5
|
OPENAI_RETRIES = 5
|
||||||
|
|
||||||
|
|
||||||
@ -25,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().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
|
||||||
@ -51,8 +48,8 @@ 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):
|
if get_settings().get("LITELLM.DEBUGGER") and get_settings().get("LITELLM.EMAIL"):
|
||||||
litellm.api_base = get_settings().huggingface.api_base
|
litellm.email = get_settings().get("LITELLM.EMAIL", None)
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
raise ValueError("OpenAI key is required") from e
|
raise ValueError("OpenAI key is required") from e
|
||||||
|
|
||||||
|
@ -40,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)
|
||||||
@ -211,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)
|
||||||
|
@ -21,7 +21,7 @@ class TokenHandler:
|
|||||||
method.
|
method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pr=None, vars: dict = {}, system="", user=""):
|
def __init__(self, pr, vars: dict, system, user):
|
||||||
"""
|
"""
|
||||||
Initializes the TokenHandler object.
|
Initializes the TokenHandler object.
|
||||||
|
|
||||||
@ -32,7 +32,6 @@ 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(pr, 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, pr, encoder, vars: dict, system, user):
|
||||||
|
@ -20,7 +20,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 +42,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,14 +49,11 @@ 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:
|
||||||
@ -66,10 +62,7 @@ def convert_to_markdown(output_data: dict, gfm_supported: bool=True) -> str:
|
|||||||
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"
|
||||||
|
@ -17,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.
|
||||||
@ -38,21 +37,13 @@ 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:
|
|
||||||
parser.print_help()
|
|
||||||
return
|
|
||||||
|
|
||||||
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
||||||
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.issue_url, command + " " + " ".join(args.rest)))
|
|
||||||
else:
|
|
||||||
result = asyncio.run(PRAgent().handle_request(args.pr_url, command + " " + " ".join(args.rest)))
|
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()
|
||||||
|
@ -38,8 +38,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
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ 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 clip_tokens, find_line_number_of_relevant_line_in_file
|
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from .git_provider import FilePatchInfo, GitProvider
|
from .git_provider import FilePatchInfo, GitProvider
|
||||||
|
|
||||||
@ -36,8 +35,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:
|
||||||
@ -101,7 +101,12 @@ class BitbucketProvider(GitProvider):
|
|||||||
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
|
||||||
|
|
||||||
@ -146,30 +151,17 @@ class BitbucketProvider(GitProvider):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.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:
|
|
||||||
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
|
||||||
subject_type = "FILE"
|
|
||||||
else:
|
|
||||||
subject_type = "LINE"
|
|
||||||
path = relevant_file.strip()
|
|
||||||
return dict(body=body, path=path, position=absolute_position) if subject_type == "LINE" else {}
|
|
||||||
|
|
||||||
|
|
||||||
def publish_inline_comment(self, comment: str, from_line: int, file: str):
|
|
||||||
payload = json.dumps( {
|
|
||||||
"content": {
|
"content": {
|
||||||
"raw": comment,
|
"raw": comment,
|
||||||
},
|
},
|
||||||
"inline": {
|
"inline": {"to": from_line, "path": file},
|
||||||
"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 +169,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
|
||||||
@ -245,21 +239,15 @@ 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, pr_body: str):
|
||||||
def publish_description(self, pr_title: str, description: str):
|
pass
|
||||||
payload = json.dumps({
|
def create_inline_comment(
|
||||||
"description": description,
|
self, body: str, relevant_file: str, relevant_line_in_file: str
|
||||||
"title": pr_title
|
):
|
||||||
|
pass
|
||||||
})
|
|
||||||
|
def publish_labels(self, labels):
|
||||||
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
|
||||||
|
|
||||||
# bitbucket does not support labels
|
|
||||||
def get_labels(self):
|
def get_labels(self):
|
||||||
pass
|
pass
|
||||||
|
@ -54,16 +54,11 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
@ -74,7 +74,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
|
||||||
@ -233,19 +232,7 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
@ -115,14 +115,7 @@ def adopt_to_gerrit_message(message):
|
|||||||
lines = message.splitlines()
|
lines = message.splitlines()
|
||||||
buf = []
|
buf = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
# remove markdown formatting
|
line = line.replace("*", "").replace("``", "`")
|
||||||
line = (line.replace("*", "")
|
|
||||||
.replace("``", "`")
|
|
||||||
.replace("<details>", "")
|
|
||||||
.replace("</details>", "")
|
|
||||||
.replace("<summary>", "")
|
|
||||||
.replace("</summary>", ""))
|
|
||||||
|
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line.startswith('#'):
|
if line.startswith('#'):
|
||||||
buf.append("\n" +
|
buf.append("\n" +
|
||||||
@ -226,12 +219,10 @@ class GerritProvider(GitProvider):
|
|||||||
return [self.repo.head.commit.message]
|
return [self.repo.head.commit.message]
|
||||||
|
|
||||||
def get_repo_settings(self):
|
def get_repo_settings(self):
|
||||||
try:
|
"""
|
||||||
with open(self.repo_path / ".pr_agent.toml", 'rb') as f:
|
TODO: Implement support of .pr_agent.toml
|
||||||
contents = f.read()
|
"""
|
||||||
return contents
|
return ""
|
||||||
except OSError:
|
|
||||||
return b""
|
|
||||||
|
|
||||||
def get_diff_files(self) -> list[FilePatchInfo]:
|
def get_diff_files(self) -> list[FilePatchInfo]:
|
||||||
diffs = self.repo.head.commit.diff(
|
diffs = self.repo.head.commit.diff(
|
||||||
@ -313,8 +304,7 @@ class GerritProvider(GitProvider):
|
|||||||
# 'get_issue_comments',
|
# 'get_issue_comments',
|
||||||
'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
|
||||||
|
@ -127,18 +127,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:
|
|
||||||
logging.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()
|
||||||
|
|
||||||
|
@ -32,7 +32,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]
|
||||||
|
|
||||||
@ -239,10 +239,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):
|
||||||
@ -310,35 +309,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")
|
||||||
|
|
||||||
@ -447,10 +417,3 @@ class GithubProvider(GitProvider):
|
|||||||
logging.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 ""
|
|
||||||
|
@ -43,7 +43,7 @@ class GitLabProvider(GitProvider):
|
|||||||
self.incremental = incremental
|
self.incremental = incremental
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
def is_supported(self, capability: str) -> bool:
|
||||||
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'gfm_markdown']:
|
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -379,10 +379,3 @@ class GitLabProvider(GitProvider):
|
|||||||
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 ""
|
|
||||||
|
@ -56,8 +56,7 @@ class LocalGitProvider(GitProvider):
|
|||||||
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,34 +0,0 @@
|
|||||||
import os
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
|
||||||
from pr_agent.config_loader import get_settings
|
|
||||||
from pr_agent.tools.pr_reviewer import PRReviewer
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
async def run_action():
|
|
||||||
try:
|
|
||||||
pull_request_id = os.environ.get("BITBUCKET_PR_ID", '')
|
|
||||||
slug = os.environ.get("BITBUCKET_REPO_SLUG", '')
|
|
||||||
workspace = os.environ.get("BITBUCKET_WORKSPACE", '')
|
|
||||||
bearer_token = os.environ.get('BITBUCKET_BEARER_TOKEN', None)
|
|
||||||
OPENAI_KEY = os.environ.get('OPENAI_API_KEY') or os.environ.get('OPENAI.KEY')
|
|
||||||
OPENAI_ORG = os.environ.get('OPENAI_ORG') or os.environ.get('OPENAI.ORG')
|
|
||||||
# Check if required environment variables are set
|
|
||||||
if not bearer_token:
|
|
||||||
print("BITBUCKET_BEARER_TOKEN not set")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not OPENAI_KEY:
|
|
||||||
print("OPENAI_KEY not set")
|
|
||||||
return
|
|
||||||
# Set the environment variables in the settings
|
|
||||||
get_settings().set("BITBUCKET.BEARER_TOKEN", bearer_token)
|
|
||||||
get_settings().set("OPENAI.KEY", OPENAI_KEY)
|
|
||||||
if OPENAI_ORG:
|
|
||||||
get_settings().set("OPENAI.ORG", OPENAI_ORG)
|
|
||||||
if pull_request_id and slug and workspace:
|
|
||||||
pr_url = f"https://bitbucket.org/{workspace}/{slug}/pull-requests/{pull_request_id}"
|
|
||||||
await PRReviewer(pr_url).run()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"An error occurred: {e}")
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(run_action())
|
|
@ -5,8 +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.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
|
||||||
|
|
||||||
|
|
||||||
@ -14,8 +12,8 @@ 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')
|
||||||
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
|
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
|
||||||
|
|
||||||
@ -55,15 +53,7 @@ async def run_action():
|
|||||||
if action in ["opened", "reopened"]:
|
if action in ["opened", "reopened"]:
|
||||||
pr_url = event_payload.get("pull_request", {}).get("url")
|
pr_url = event_payload.get("pull_request", {}).get("url")
|
||||||
if pr_url:
|
if pr_url:
|
||||||
auto_review = os.environ.get('github_action.auto_review', None)
|
|
||||||
if auto_review is None or (isinstance(auto_review, str) and auto_review.lower() == 'true'):
|
|
||||||
await PRReviewer(pr_url).run()
|
await PRReviewer(pr_url).run()
|
||||||
auto_describe = os.environ.get('github_action.auto_describe', None)
|
|
||||||
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":
|
||||||
@ -71,21 +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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -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
|
|
||||||
|
@ -11,14 +11,12 @@ ai_timeout=180
|
|||||||
max_description_tokens = 500
|
max_description_tokens = 500
|
||||||
max_commits_tokens = 500
|
max_commits_tokens = 500
|
||||||
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
|
||||||
@ -26,14 +24,10 @@ 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
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
# markers
|
|
||||||
use_description_markers=false
|
|
||||||
include_generated_by_header=true
|
|
||||||
|
|
||||||
[pr_questions] # /ask #
|
[pr_questions] # /ask #
|
||||||
|
|
||||||
@ -58,11 +52,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]"
|
||||||
@ -107,14 +96,5 @@ polling_interval_seconds = 30
|
|||||||
# patch_server_token = ""
|
# patch_server_token = ""
|
||||||
|
|
||||||
[litellm]
|
[litellm]
|
||||||
#use_client = false
|
debugger=false
|
||||||
|
#email="youremail@example.com"
|
||||||
[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"
|
|
||||||
|
@ -34,7 +34,7 @@ Specific instructions:
|
|||||||
- Provide up to {{ num_code_suggestions }} code 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.
|
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.
|
||||||
|
@ -22,13 +22,13 @@ 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.
|
- 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 %}
|
||||||
|
|
||||||
@ -85,14 +85,6 @@ 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
|
||||||
|
@ -22,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:
|
||||||
@ -51,7 +48,6 @@ class PRCodeSuggestions:
|
|||||||
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...')
|
logging.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)
|
||||||
@ -62,9 +58,6 @@ class PRCodeSuggestions:
|
|||||||
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):
|
|
||||||
logging.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):
|
||||||
@ -76,8 +69,6 @@ class PRCodeSuggestions:
|
|||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
logging.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:
|
|
||||||
logging.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):
|
||||||
logging.info('Getting PR diff...')
|
logging.info('Getting PR diff...')
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
import logging
|
import logging
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
@ -29,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()
|
||||||
@ -63,44 +61,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:
|
|
||||||
logging.info(f"Generating a PR description {self.pr_id}")
|
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
self.git_provider.publish_comment("Preparing PR description...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing pr description...", is_temporary=True)
|
||||||
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction)
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
|
|
||||||
logging.info(f"Preparing answer {self.pr_id}")
|
logging.info('Preparing answer...')
|
||||||
if self.prediction:
|
pr_title, pr_body, pr_types, markdown_text = self._prepare_pr_answer()
|
||||||
self._prepare_data()
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
pr_labels = []
|
|
||||||
if get_settings().pr_description.publish_labels:
|
|
||||||
pr_labels = self._prepare_labels()
|
|
||||||
|
|
||||||
if get_settings().pr_description.use_description_markers:
|
|
||||||
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:
|
if get_settings().config.publish_output:
|
||||||
logging.info(f"Pushing answer {self.pr_id}")
|
logging.info('Pushing answer...')
|
||||||
if get_settings().pr_description.publish_description_as_comment:
|
if get_settings().pr_description.publish_description_as_comment:
|
||||||
self.git_provider.publish_comment(full_markdown_description)
|
self.git_provider.publish_comment(markdown_text)
|
||||||
else:
|
else:
|
||||||
self.git_provider.publish_description(pr_title, pr_body)
|
self.git_provider.publish_description(pr_title, pr_body)
|
||||||
if get_settings().pr_description.publish_labels and self.git_provider.is_supported("get_labels"):
|
if self.git_provider.is_supported("get_labels"):
|
||||||
current_labels = self.git_provider.get_labels()
|
current_labels = self.git_provider.get_labels()
|
||||||
if current_labels is None:
|
if current_labels is None:
|
||||||
current_labels = []
|
current_labels = []
|
||||||
self.git_provider.publish_labels(pr_labels + current_labels)
|
self.git_provider.publish_labels(pr_types + current_labels)
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Error generating PR description {self.pr_id}: {e}")
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@ -118,12 +99,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
|
|
||||||
|
|
||||||
logging.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)
|
||||||
logging.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:
|
||||||
@ -156,71 +134,34 @@ class PRDescription:
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def _prepare_pr_answer(self) -> Tuple[str, str, List[str], str]:
|
||||||
def _prepare_data(self):
|
|
||||||
# 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]:
|
|
||||||
logging.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_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"]
|
||||||
@ -231,27 +172,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"
|
||||||
|
|
||||||
|
markdown_text = f"## Title\n\n{title}\n\n___\n{pr_body}"
|
||||||
|
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
logging.info(f"title:\n{title}\n{pr_body}")
|
logging.info(f"title:\n{title}\n{pr_body}")
|
||||||
|
|
||||||
return title, pr_body
|
return title, pr_body, pr_types, markdown_text
|
@ -59,7 +59,6 @@ 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,
|
||||||
@ -95,8 +94,6 @@ class PRReviewer:
|
|||||||
"""
|
"""
|
||||||
Review the pull request and generate feedback.
|
Review the pull request and generate feedback.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
|
||||||
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
||||||
logging.info(f'Automatic review is disabled {self.pr_url}')
|
logging.info(f'Automatic review is disabled {self.pr_url}')
|
||||||
return None
|
return None
|
||||||
@ -119,8 +116,6 @@ class PRReviewer:
|
|||||||
if get_settings().pr_reviewer.inline_code_comments:
|
if get_settings().pr_reviewer.inline_code_comments:
|
||||||
logging.info('Pushing inline code comments...')
|
logging.info('Pushing inline code comments...')
|
||||||
self._publish_inline_code_comments()
|
self._publish_inline_code_comments()
|
||||||
except Exception as e:
|
|
||||||
logging.error(f"Failed to review PR: {e}")
|
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str) -> None:
|
async def _prepare_prediction(self, model: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -219,7 +214,7 @@ 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
|
||||||
|
@ -1,276 +0,0 @@
|
|||||||
import copy
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from enum import Enum
|
|
||||||
from typing import List, Tuple
|
|
||||||
import pinecone
|
|
||||||
import openai
|
|
||||||
import pandas as pd
|
|
||||||
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 pinecone_datasets import Dataset, DatasetMetadata
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
logging.info('Indexing the entire repo...')
|
|
||||||
|
|
||||||
logging.info('Getting issues...')
|
|
||||||
issues = list(repo_obj.get_issues(state='all'))
|
|
||||||
logging.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:
|
|
||||||
logging.info(f'Updating index with {counter} new issues...')
|
|
||||||
self._update_index_with_issues(issues_to_update, repo_name_for_index, upsert=True)
|
|
||||||
else:
|
|
||||||
logging.info('No new issues to update')
|
|
||||||
|
|
||||||
async def run(self):
|
|
||||||
logging.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
|
|
||||||
logging.info('Done')
|
|
||||||
|
|
||||||
logging.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']:
|
|
||||||
issue_number = int(r["id"].split('.')[0].split('_')[-1])
|
|
||||||
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'])))
|
|
||||||
logging.info('Done')
|
|
||||||
|
|
||||||
logging.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)
|
|
||||||
logging.info(similar_issues_str)
|
|
||||||
logging.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):
|
|
||||||
logging.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:
|
|
||||||
logging.info(f"Scanned {counter} issues")
|
|
||||||
if counter >= self.max_issues_to_scan:
|
|
||||||
logging.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"])
|
|
||||||
logging.info('Done')
|
|
||||||
|
|
||||||
logging.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 = []
|
|
||||||
logging.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)
|
|
||||||
logging.info('Done')
|
|
||||||
|
|
||||||
api_key = get_settings().pinecone.api_key
|
|
||||||
environment = get_settings().pinecone.environment
|
|
||||||
if not upsert:
|
|
||||||
logging.info('Creating index from scratch...')
|
|
||||||
ds.to_pinecone_index(self.index_name, api_key=api_key, environment=environment)
|
|
||||||
else:
|
|
||||||
logging.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)
|
|
||||||
logging.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)
|
|
@ -46,7 +46,7 @@ class PRUpdateChangelog:
|
|||||||
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"
|
||||||
|
|
||||||
logging.info('Updating the changelog...')
|
logging.info('Updating the changelog...')
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
|
@ -7,17 +7,15 @@ 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.504
|
||||||
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
|
azure-devops==7.1.0b3
|
||||||
msrest==0.7.1
|
msrest==0.7.1
|
||||||
pinecone-client
|
|
||||||
pinecone-datasets @ git+https://github.com/mrT23/pinecone-datasets.git@main
|
|
@ -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": [
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
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.git_providers.git_provider import EDIT_TYPE
|
from pr_agent.git_providers.git_provider import EDIT_TYPE
|
||||||
|
|
||||||
|
|
||||||
@ -27,21 +25,6 @@ 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
|
# 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"
|
||||||
|
@ -61,7 +61,7 @@ class TestSortFilesByMainLanguages:
|
|||||||
type('', (object,), {'filename': 'file1.py'})(),
|
type('', (object,), {'filename': 'file1.py'})(),
|
||||||
type('', (object,), {'filename': 'file2.java'})()
|
type('', (object,), {'filename': 'file2.java'})()
|
||||||
]
|
]
|
||||||
expected_output = [{'language': 'Other', 'files': files}]
|
expected_output = [{'language': 'Other', 'files': []}]
|
||||||
assert sort_files_by_main_languages(languages, files) == expected_output
|
assert sort_files_by_main_languages(languages, files) == expected_output
|
||||||
|
|
||||||
# Tests that function handles empty files list
|
# Tests that function handles empty files list
|
||||||
|
Reference in New Issue
Block a user