mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-08 23:00:43 +08:00
Compare commits
1 Commits
test_best_
...
mrT23-patc
Author | SHA1 | Date | |
---|---|---|---|
b9e3e5603b |
2
.github/workflows/build-and-test.yaml
vendored
2
.github/workflows/build-and-test.yaml
vendored
@ -37,3 +37,5 @@ jobs:
|
|||||||
name: Test dev docker
|
name: Test dev docker
|
||||||
run: |
|
run: |
|
||||||
docker run --rm codiumai/pr-agent:test pytest -v tests/unittest
|
docker run --rm codiumai/pr-agent:test pytest -v tests/unittest
|
||||||
|
|
||||||
|
|
||||||
|
3
.github/workflows/pr-agent-review.yaml
vendored
3
.github/workflows/pr-agent-review.yaml
vendored
@ -30,3 +30,6 @@ jobs:
|
|||||||
GITHUB_ACTION_CONFIG.AUTO_DESCRIBE: true
|
GITHUB_ACTION_CONFIG.AUTO_DESCRIBE: true
|
||||||
GITHUB_ACTION_CONFIG.AUTO_REVIEW: true
|
GITHUB_ACTION_CONFIG.AUTO_REVIEW: true
|
||||||
GITHUB_ACTION_CONFIG.AUTO_IMPROVE: true
|
GITHUB_ACTION_CONFIG.AUTO_IMPROVE: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
17
.github/workflows/pre-commit.yml
vendored
17
.github/workflows/pre-commit.yml
vendored
@ -1,17 +0,0 @@
|
|||||||
# disabled. We might run it manually if needed.
|
|
||||||
name: pre-commit
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# pull_request:
|
|
||||||
# push:
|
|
||||||
# branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre-commit:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-python@v5
|
|
||||||
# SEE https://github.com/pre-commit/action
|
|
||||||
- uses: pre-commit/action@v3.0.1
|
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,7 +1,6 @@
|
|||||||
.idea/
|
.idea/
|
||||||
.lsp/
|
.lsp/
|
||||||
.vscode/
|
.vscode/
|
||||||
.env
|
|
||||||
venv/
|
venv/
|
||||||
pr_agent/settings/.secrets.toml
|
pr_agent/settings/.secrets.toml
|
||||||
__pycache__
|
__pycache__
|
||||||
@ -10,4 +9,3 @@ dist/
|
|||||||
build/
|
build/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
docs/.cache/
|
docs/.cache/
|
||||||
.qodo
|
|
||||||
|
@ -1,46 +0,0 @@
|
|||||||
# See https://pre-commit.com for more information
|
|
||||||
# See https://pre-commit.com/hooks.html for more hooks
|
|
||||||
|
|
||||||
default_language_version:
|
|
||||||
python: python3
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v5.0.0
|
|
||||||
hooks:
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-toml
|
|
||||||
- id: check-yaml
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
- id: trailing-whitespace
|
|
||||||
# - repo: https://github.com/rhysd/actionlint
|
|
||||||
# rev: v1.7.3
|
|
||||||
# hooks:
|
|
||||||
# - id: actionlint
|
|
||||||
- repo: https://github.com/pycqa/isort
|
|
||||||
# rev must match what's in dev-requirements.txt
|
|
||||||
rev: 5.13.2
|
|
||||||
hooks:
|
|
||||||
- id: isort
|
|
||||||
# - repo: https://github.com/PyCQA/bandit
|
|
||||||
# rev: 1.7.10
|
|
||||||
# hooks:
|
|
||||||
# - id: bandit
|
|
||||||
# args: [
|
|
||||||
# "-c", "pyproject.toml",
|
|
||||||
# ]
|
|
||||||
# - repo: https://github.com/astral-sh/ruff-pre-commit
|
|
||||||
# rev: v0.7.1
|
|
||||||
# hooks:
|
|
||||||
# - id: ruff
|
|
||||||
# args:
|
|
||||||
# - --fix
|
|
||||||
# - id: ruff-format
|
|
||||||
# - repo: https://github.com/PyCQA/autoflake
|
|
||||||
# rev: v2.3.1
|
|
||||||
# hooks:
|
|
||||||
# - id: autoflake
|
|
||||||
# args:
|
|
||||||
# - --in-place
|
|
||||||
# - --remove-all-unused-imports
|
|
||||||
# - --remove-unused-variables
|
|
155
README.md
155
README.md
@ -13,10 +13,12 @@
|
|||||||
Qode Merge PR-Agent aims to help efficiently review and handle pull requests, by providing AI feedback and suggestions
|
Qode Merge PR-Agent aims to help efficiently review and handle pull requests, by providing AI feedback and suggestions
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
[](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
|
||||||
[](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl)
|
[](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl)
|
||||||
[](https://github.com/apps/qodo-merge-pro/)
|
[](https://pr-agent-docs.codium.ai/finetuning_benchmark/)
|
||||||
[](https://github.com/apps/qodo-merge-pro-for-open-source/)
|
|
||||||
[](https://discord.com/channels/1057273017547378788/1126104260430528613)
|
[](https://discord.com/channels/1057273017547378788/1126104260430528613)
|
||||||
|
[](https://twitter.com/codiumai)
|
||||||
|
[](https://www.codium.ai/images/pr_agent/cheat_sheet.pdf)
|
||||||
<a href="https://github.com/Codium-ai/pr-agent/commits/main">
|
<a href="https://github.com/Codium-ai/pr-agent/commits/main">
|
||||||
<img alt="GitHub" src="https://img.shields.io/github/last-commit/Codium-ai/pr-agent/main?style=for-the-badge" height="20">
|
<img alt="GitHub" src="https://img.shields.io/github/last-commit/Codium-ai/pr-agent/main?style=for-the-badge" height="20">
|
||||||
</a>
|
</a>
|
||||||
@ -41,53 +43,43 @@ Qode Merge PR-Agent aims to help efficiently review and handle pull requests, by
|
|||||||
|
|
||||||
## News and Updates
|
## News and Updates
|
||||||
|
|
||||||
### Jan 2, 2025
|
### October 27, 2024
|
||||||
|
|
||||||
New tool [/Implement](https://qodo-merge-docs.qodo.ai/tools/implement/) (💎), which converts human code review discussions and feedback into ready-to-commit code changes.
|
Qodo Merge PR Agent will now automatically document accepted code suggestions in a dedicated wiki page (`.pr_agent_accepted_suggestions`), enabling users to track historical changes, assess the tool's effectiveness, and learn from previously implemented recommendations in the repository.
|
||||||
|
|
||||||
<kbd><img src="https://codium.ai/images/pr_agent/implement1.png" width="512"></kbd>
|
This dedicated wiki page will also serve as a foundation for future AI model improvements, allowing it to learn from historically implemented suggestions and generate more targeted, contextually relevant recommendations.
|
||||||
|
Read more about this novel feature [here](https://qodo-merge-docs.qodo.ai/tools/improve/#suggestion-tracking).
|
||||||
|
|
||||||
|
<kbd><img href="https://qodo.ai/images/pr_agent/pr_agent_accepted_suggestions1.png" src="https://qodo.ai/images/pr_agent/pr_agent_accepted_suggestions1.png" width="768"></kbd>
|
||||||
|
|
||||||
|
|
||||||
### Jan 1, 2025
|
|
||||||
|
|
||||||
Update logic and [documentation](https://qodo-merge-docs.qodo.ai/usage-guide/changing_a_model/#ollama) for running local models via Ollama.
|
### October 21, 2024
|
||||||
|
**Disable publishing labels by default:**
|
||||||
|
|
||||||
### December 30, 2024
|
The default setting for `pr_description.publish_labels` has been updated to `false`. This means that labels generated by the `/describe` tool will no longer be published, unless this configuration is explicitly set to `true`.
|
||||||
|
|
||||||
Following [feedback](https://research.kudelskisecurity.com/2024/08/29/careful-where-you-code-multiple-vulnerabilities-in-ai-powered-pr-agent/) from the community, we have addressed two vulnerabilities identified in the open-source PR-Agent project. The fixes are now included in the newly released version (v0.26), available as of today.
|
We constantly strive to balance informative AI analysis with reducing unnecessary noise. User feedback indicated that in many cases, the original PR title alone provides sufficient information, making the generated labels (`enhancement`, `documentation`, `bug fix`, ...) redundant.
|
||||||
|
The [`review_effort`](https://qodo-merge-docs.qodo.ai/tools/review/#configuration-options) label, generated by the `review` tool, will still be published by default, as it provides valuable information enabling reviewers to prioritize small PRs first.
|
||||||
|
|
||||||
### December 25, 2024
|
However, every user has different preferences. To still publish the `describe` labels, set `pr_description.publish_labels=true` in the [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/).
|
||||||
|
For more tailored and relevant labeling, we recommend using the [`custom_labels 💎`](https://qodo-merge-docs.qodo.ai/tools/custom_labels/) tool, that allows generating labels specific to your project's needs.
|
||||||
|
|
||||||
The `review` tool previously included a legacy feature for providing code suggestions (controlled by '--pr_reviewer.num_code_suggestion'). This functionality has been deprecated. Use instead the [`improve`](https://qodo-merge-docs.qodo.ai/tools/improve/) tool, which offers higher quality and more actionable code suggestions.
|
<kbd></kbd>
|
||||||
|
|
||||||
### December 2, 2024
|
→
|
||||||
|
|
||||||
Open-source repositories can now freely use Qodo Merge Pro, and enjoy easy one-click installation using a marketplace [app](https://github.com/apps/qodo-merge-pro-for-open-source).
|
<kbd></kbd>
|
||||||
|
|
||||||
<kbd><img src="https://github.com/user-attachments/assets/b0838724-87b9-43b0-ab62-73739a3a855c" width="512"></kbd>
|
|
||||||
|
|
||||||
See [here](https://qodo-merge-docs.qodo.ai/installation/pr_agent_pro/) for more details about installing Qodo Merge Pro for private repositories.
|
|
||||||
|
|
||||||
|
|
||||||
### November 18, 2024
|
|
||||||
|
|
||||||
A new mode was enabled by default for code suggestions - `--pr_code_suggestions.focus_only_on_problems=true`:
|
### October 14, 2024
|
||||||
|
Improved support for GitHub enterprise server with [GitHub Actions](https://qodo-merge-docs.qodo.ai/installation/github/#action-for-github-enterprise-server)
|
||||||
|
|
||||||
- This option reduces the number of code suggestions received
|
### October 10, 2024
|
||||||
- The suggestions will focus more on identifying and fixing code problems, rather than style considerations like best practices, maintainability, or readability.
|
New ability for the `review` tool - **ticket compliance feedback**. If the PR contains a ticket number, PR-Agent will check if the PR code actually [complies](https://github.com/Codium-ai/pr-agent/pull/1279#issuecomment-2404042130) with the ticket requirements.
|
||||||
- The suggestions will be categorized into just two groups: "Possible Issues" and "General".
|
|
||||||
|
|
||||||
Still, if you prefer the previous mode, you can set `--pr_code_suggestions.focus_only_on_problems=false` in the [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/).
|
<kbd><img src="https://github.com/user-attachments/assets/4a2a728b-5f47-40fa-80cc-16efd296938c" width="768"></kbd>
|
||||||
|
|
||||||
**Example results:**
|
|
||||||
|
|
||||||
Original mode
|
|
||||||
|
|
||||||
<kbd><img src="https://qodo.ai/images/pr_agent/code_suggestions_original_mode.png" width="512"></kbd>
|
|
||||||
|
|
||||||
Focused mode
|
|
||||||
|
|
||||||
<kbd><img src="https://qodo.ai/images/pr_agent/code_suggestions_focused_mode.png" width="512"></kbd>
|
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
@ -95,42 +87,39 @@ Focused mode
|
|||||||
|
|
||||||
Supported commands per platform:
|
Supported commands per platform:
|
||||||
|
|
||||||
| | | GitHub | GitLab | Bitbucket | Azure DevOps |
|
| | | GitHub | Gitlab | Bitbucket | Azure DevOps |
|
||||||
|-------|---------------------------------------------------------------------------------------------------------|:--------------------:|:--------------------:|:--------------------:|:------------:|
|
|-------|---------------------------------------------------------------------------------------------------------|:--------------------:|:--------------------:|:--------------------:|:------------:|
|
||||||
| TOOLS | [Review](https://qodo-merge-docs.qodo.ai/tools/review/) | ✅ | ✅ | ✅ | ✅ |
|
| TOOLS | Review | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Describe](https://qodo-merge-docs.qodo.ai/tools/describe/) | ✅ | ✅ | ✅ | ✅ |
|
| | ⮑ Incremental | ✅ | | | |
|
||||||
| | [Improve](https://qodo-merge-docs.qodo.ai/tools/improve/) | ✅ | ✅ | ✅ | ✅ |
|
| | Describe | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Ask](https://qodo-merge-docs.qodo.ai/tools/ask/) | ✅ | ✅ | ✅ | ✅ |
|
| | ⮑ [Inline File Summary](https://pr-agent-docs.codium.ai/tools/describe#inline-file-summary) 💎 | ✅ | | | |
|
||||||
|
| | Improve | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| | ⮑ Extended | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| | Ask | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | ⮑ [Ask on code lines](https://pr-agent-docs.codium.ai/tools/ask#ask-lines) | ✅ | ✅ | | |
|
| | ⮑ [Ask on code lines](https://pr-agent-docs.codium.ai/tools/ask#ask-lines) | ✅ | ✅ | | |
|
||||||
| | [Update CHANGELOG](https://qodo-merge-docs.qodo.ai/tools/update_changelog/) | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| | [Ticket Context](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/) 💎 | ✅ | ✅ | ✅ | |
|
|
||||||
| | [Utilizing Best Practices](https://qodo-merge-docs.qodo.ai/tools/improve/#best-practices) 💎 | ✅ | ✅ | ✅ | |
|
|
||||||
| | [PR Chat](https://qodo-merge-docs.qodo.ai/chrome-extension/features/#pr-chat) 💎 | ✅ | | | |
|
|
||||||
| | [Suggestion Tracking](https://qodo-merge-docs.qodo.ai/tools/improve/#suggestion-tracking) 💎 | ✅ | ✅ | | |
|
|
||||||
| | [CI Feedback](https://pr-agent-docs.codium.ai/tools/ci_feedback/) 💎 | ✅ | | | |
|
|
||||||
| | [PR Documentation](https://pr-agent-docs.codium.ai/tools/documentation/) 💎 | ✅ | ✅ | | |
|
|
||||||
| | [Custom Labels](https://pr-agent-docs.codium.ai/tools/custom_labels/) 💎 | ✅ | ✅ | | |
|
|
||||||
| | [Analyze](https://pr-agent-docs.codium.ai/tools/analyze/) 💎 | ✅ | ✅ | | |
|
|
||||||
| | [Similar Code](https://pr-agent-docs.codium.ai/tools/similar_code/) 💎 | ✅ | | | |
|
|
||||||
| | [Custom Prompt](https://pr-agent-docs.codium.ai/tools/custom_prompt/) 💎 | ✅ | ✅ | ✅ | |
|
| | [Custom Prompt](https://pr-agent-docs.codium.ai/tools/custom_prompt/) 💎 | ✅ | ✅ | ✅ | |
|
||||||
| | [Test](https://pr-agent-docs.codium.ai/tools/test/) 💎 | ✅ | ✅ | | |
|
| | [Test](https://pr-agent-docs.codium.ai/tools/test/) 💎 | ✅ | ✅ | | |
|
||||||
| | [Implement](https://pr-agent-docs.codium.ai/tools/implement/) 💎 | ✅ | ✅ | ✅ | |
|
| | Reflect and Review | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| | Update CHANGELOG.md | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| | Find Similar Issue | ✅ | | | |
|
||||||
|
| | [Add PR Documentation](https://pr-agent-docs.codium.ai/tools/documentation/) 💎 | ✅ | ✅ | | |
|
||||||
|
| | [Custom Labels](https://pr-agent-docs.codium.ai/tools/custom_labels/) 💎 | ✅ | ✅ | | |
|
||||||
|
| | [Analyze](https://pr-agent-docs.codium.ai/tools/analyze/) 💎 | ✅ | ✅ | | |
|
||||||
|
| | [CI Feedback](https://pr-agent-docs.codium.ai/tools/ci_feedback/) 💎 | ✅ | | | |
|
||||||
|
| | [Similar Code](https://pr-agent-docs.codium.ai/tools/similar_code/) 💎 | ✅ | | | |
|
||||||
| | | | | | |
|
| | | | | | |
|
||||||
| USAGE | [CLI](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#local-repo-cli) | ✅ | ✅ | ✅ | ✅ |
|
| USAGE | CLI | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [App / webhook](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-app) | ✅ | ✅ | ✅ | ✅ |
|
| | App / webhook | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Tagging bot](https://github.com/Codium-ai/pr-agent#try-it-now) | ✅ | | | |
|
| | Tagging bot | ✅ | | | |
|
||||||
| | [Actions](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-action) | ✅ |✅| ✅ |✅|
|
| | Actions | ✅ |✅| ✅ |✅|
|
||||||
| | | | | | |
|
| | | | | | |
|
||||||
| CORE | [PR compression](https://qodo-merge-docs.qodo.ai/core-abilities/compression_strategy/) | ✅ | ✅ | ✅ | ✅ |
|
| CORE | PR compression | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| | Repo language prioritization | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | Adaptive and token-aware file patch fitting | ✅ | ✅ | ✅ | ✅ |
|
| | Adaptive and token-aware file patch fitting | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Multiple models support](https://qodo-merge-docs.qodo.ai/usage-guide/changing_a_model/) | ✅ | ✅ | ✅ | ✅ |
|
| | Multiple models support | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Local and global metadata](https://qodo-merge-docs.qodo.ai/core-abilities/metadata/) | ✅ | ✅ | ✅ | ✅ |
|
| | [Static code analysis](https://pr-agent-docs.codium.ai/core-abilities/#static-code-analysis) 💎 | ✅ | ✅ | ✅ | |
|
||||||
| | [Dynamic context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic_context/) | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| | [Self reflection](https://qodo-merge-docs.qodo.ai/core-abilities/self_reflection/) | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| | [Static code analysis](https://qodo-merge-docs.qodo.ai/core-abilities/static_code_analysis/) 💎 | ✅ | ✅ | ✅ | |
|
|
||||||
| | [Global and wiki configurations](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/) 💎 | ✅ | ✅ | ✅ | |
|
| | [Global and wiki configurations](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/) 💎 | ✅ | ✅ | ✅ | |
|
||||||
| | [PR interactive actions](https://www.codium.ai/images/pr_agent/pr-actions.mp4) 💎 | ✅ | ✅ | | |
|
| | [PR interactive actions](https://www.codium.ai/images/pr_agent/pr-actions.mp4) 💎 | ✅ | ✅ | | |
|
||||||
| | [Impact Evaluation](https://qodo-merge-docs.qodo.ai/core-abilities/impact_evaluation/) 💎 | ✅ | ✅ | | |
|
|
||||||
- 💎 means this feature is available only in [PR-Agent Pro](https://www.codium.ai/pricing/)
|
- 💎 means this feature is available only in [PR-Agent Pro](https://www.codium.ai/pricing/)
|
||||||
|
|
||||||
[//]: # (- Support for additional git providers is described in [here](./docs/Full_environments.md))
|
[//]: # (- Support for additional git providers is described in [here](./docs/Full_environments.md))
|
||||||
@ -154,8 +143,6 @@ ___
|
|||||||
\
|
\
|
||||||
‣ **Analyze 💎 ([`/analyze`](https://pr-agent-docs.codium.ai/tools/analyze/))**: Identify code components that changed in the PR, and enables to interactively generate tests, docs, and code suggestions for each component.
|
‣ **Analyze 💎 ([`/analyze`](https://pr-agent-docs.codium.ai/tools/analyze/))**: Identify code components that changed in the PR, and enables to interactively generate tests, docs, and code suggestions for each component.
|
||||||
\
|
\
|
||||||
‣ **Test 💎 ([`/test`](https://pr-agent-docs.codium.ai/tools/test/))**: Generate tests for a selected component, based on the PR code changes.
|
|
||||||
\
|
|
||||||
‣ **Custom Prompt 💎 ([`/custom_prompt`](https://pr-agent-docs.codium.ai/tools/custom_prompt/))**: Automatically generates custom suggestions for improving the PR code, based on specific guidelines defined by the user.
|
‣ **Custom Prompt 💎 ([`/custom_prompt`](https://pr-agent-docs.codium.ai/tools/custom_prompt/))**: Automatically generates custom suggestions for improving the PR code, based on specific guidelines defined by the user.
|
||||||
\
|
\
|
||||||
‣ **Generate Tests 💎 ([`/test component_name`](https://pr-agent-docs.codium.ai/tools/test/))**: Generates unit tests for a selected component, based on the PR code changes.
|
‣ **Generate Tests 💎 ([`/test component_name`](https://pr-agent-docs.codium.ai/tools/test/))**: Generates unit tests for a selected component, based on the PR code changes.
|
||||||
@ -163,8 +150,6 @@ ___
|
|||||||
‣ **CI Feedback 💎 ([`/checks ci_job`](https://pr-agent-docs.codium.ai/tools/ci_feedback/))**: Automatically generates feedback and analysis for a failed CI job.
|
‣ **CI Feedback 💎 ([`/checks ci_job`](https://pr-agent-docs.codium.ai/tools/ci_feedback/))**: Automatically generates feedback and analysis for a failed CI job.
|
||||||
\
|
\
|
||||||
‣ **Similar Code 💎 ([`/find_similar_component`](https://pr-agent-docs.codium.ai/tools/similar_code/))**: Retrieves the most similar code components from inside the organization's codebase, or from open-source code.
|
‣ **Similar Code 💎 ([`/find_similar_component`](https://pr-agent-docs.codium.ai/tools/similar_code/))**: Retrieves the most similar code components from inside the organization's codebase, or from open-source code.
|
||||||
\
|
|
||||||
‣ **Implement 💎 ([`/implement`](https://qodo-merge-docs.qodo.ai/tools/implement/))**: Generates implementation code from review suggestions.
|
|
||||||
___
|
___
|
||||||
|
|
||||||
## Example results
|
## Example results
|
||||||
@ -195,8 +180,50 @@ ___
|
|||||||
</kbd>
|
</kbd>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h4><a href="https://github.com/Codium-ai/pr-agent/pull/530">/generate_labels</a></h4>
|
||||||
|
<div align="center">
|
||||||
|
<p float="center">
|
||||||
|
<kbd><img src="https://www.codium.ai/images/pr_agent/geneare_custom_labels_main_short.png" width="300"></kbd>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/78#issuecomment-1639739496">/reflect_and_review:</a></h4>)
|
||||||
|
|
||||||
|
[//]: # (<div align="center">)
|
||||||
|
|
||||||
|
[//]: # (<p float="center">)
|
||||||
|
|
||||||
|
[//]: # (<img src="https://www.codium.ai/images/reflect_and_review.gif" width="800">)
|
||||||
|
|
||||||
|
[//]: # (</p>)
|
||||||
|
|
||||||
|
[//]: # (</div>)
|
||||||
|
|
||||||
|
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695020538">/ask:</a></h4>)
|
||||||
|
|
||||||
|
[//]: # (<div align="center">)
|
||||||
|
|
||||||
|
[//]: # (<p float="center">)
|
||||||
|
|
||||||
|
[//]: # (<img src="https://www.codium.ai/images/ask-2.gif" width="800">)
|
||||||
|
|
||||||
|
[//]: # (</p>)
|
||||||
|
|
||||||
|
[//]: # (</div>)
|
||||||
|
|
||||||
|
[//]: # (<h4><a href="https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695024952">/improve:</a></h4>)
|
||||||
|
|
||||||
|
[//]: # (<div align="center">)
|
||||||
|
|
||||||
|
[//]: # (<p float="center">)
|
||||||
|
|
||||||
|
[//]: # (<img src="https://www.codium.ai/images/improve-2.gif" width="800">)
|
||||||
|
|
||||||
|
[//]: # (</p>)
|
||||||
|
|
||||||
|
[//]: # (</div>)
|
||||||
<div align="left">
|
<div align="left">
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,3 +2,4 @@ We take your code's security and privacy seriously:
|
|||||||
|
|
||||||
- The Chrome extension will not send your code to any external servers.
|
- The Chrome extension will not send your code to any external servers.
|
||||||
- For private repositories, we will first validate the user's identity and permissions. After authentication, we generate responses using the existing Qodo Merge Pro integration.
|
- For private repositories, we will first validate the user's identity and permissions. After authentication, we generate responses using the existing Qodo Merge Pro integration.
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
[Qodo Merge Chrome extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl){:target="_blank"} is a collection of tools that integrates seamlessly with your GitHub environment, aiming to enhance your Git usage experience, and providing AI-powered capabilities to your PRs.
|
[Qodo Merge Chrome extension](https://chromewebstore.google.com/detail/pr-agent-chrome-extension/ephlnjeghhogofkifjloamocljapahnl) is a collection of tools that integrates seamlessly with your GitHub environment, aiming to enhance your Git usage experience, and providing AI-powered capabilities to your PRs.
|
||||||
|
|
||||||
With a single-click installation you will gain access to a context-aware chat on your pull requests code, a toolbar extension with multiple AI feedbacks, Qodo Merge filters, and additional abilities.
|
With a single-click installation you will gain access to a context-aware chat on your pull requests code, a toolbar extension with multiple AI feedbacks, Qodo Merge filters, and additional abilities.
|
||||||
|
|
||||||
The extension is powered by top code models like Claude 3.5 Sonnet and GPT4. All the extension's features are free to use on public repositories.
|
The extension is powered by top code models like Claude 3.5 Sonnet and GPT4. All the extension's features are free to use on public repositories.
|
||||||
|
|
||||||
For private repositories, you will need to install [Qodo Merge Pro](https://github.com/apps/qodo-merge-pro){:target="_blank"} in addition to the extension (Quick GitHub app setup with a 14-day free trial. No credit card needed).
|
For private repositories, you will need to install [Qodo Merge Pro](https://github.com/apps/codiumai-pr-agent-pro) in addition to the extension (Quick GitHub app setup with a 14-day free trial. No credit card needed).
|
||||||
For a demonstration of how to install Qodo Merge Pro and use it with the Chrome extension, please refer to the tutorial video at the provided [link](https://codium.ai/images/pr_agent/private_repos.mp4){:target="_blank"}.
|
For a demonstration of how to install Qodo Merge Pro and use it with the Chrome extension, please refer to the tutorial video at the provided [link](https://codium.ai/images/pr_agent/private_repos.mp4).
|
||||||
|
|
||||||
<img src="https://codium.ai/images/pr_agent/PR-AgentChat.gif" width="768">
|
<img src="https://codium.ai/images/pr_agent/PR-AgentChat.gif" width="768">
|
||||||
|
|
||||||
|
@ -1,170 +0,0 @@
|
|||||||
# Fetching Ticket Context for PRs
|
|
||||||
`Supported Git Platforms: GitHub, GitLab, Bitbucket`
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Qodo Merge PR Agent streamlines code review workflows by seamlessly connecting with multiple ticket management systems.
|
|
||||||
This integration enriches the review process by automatically surfacing relevant ticket information and context alongside code changes.
|
|
||||||
|
|
||||||
## Ticket systems supported
|
|
||||||
- GitHub
|
|
||||||
- Jira (💎)
|
|
||||||
|
|
||||||
Ticket data fetched:
|
|
||||||
|
|
||||||
1. Ticket Title
|
|
||||||
2. Ticket Description
|
|
||||||
3. Custom Fields (Acceptance criteria)
|
|
||||||
4. Subtasks (linked tasks)
|
|
||||||
5. Labels
|
|
||||||
6. Attached Images/Screenshots
|
|
||||||
|
|
||||||
## Affected Tools
|
|
||||||
|
|
||||||
Ticket Recognition Requirements:
|
|
||||||
|
|
||||||
- The PR description should contain a link to the ticket or if the branch name starts with the ticket id / number.
|
|
||||||
- For Jira tickets, you should follow the instructions in [Jira Integration](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/#jira-integration) in order to authenticate with Jira.
|
|
||||||
|
|
||||||
### Describe tool
|
|
||||||
Qodo Merge PR Agent will recognize the ticket and use the ticket content (title, description, labels) to provide additional context for the code changes.
|
|
||||||
By understanding the reasoning and intent behind modifications, the LLM can offer more insightful and relevant code analysis.
|
|
||||||
|
|
||||||
### Review tool
|
|
||||||
Similarly to the `describe` tool, the `review` tool will use the ticket content to provide additional context for the code changes.
|
|
||||||
|
|
||||||
In addition, this feature will evaluate how well a Pull Request (PR) adheres to its original purpose/intent as defined by the associated ticket or issue mentioned in the PR description.
|
|
||||||
Each ticket will be assigned a label (Compliance/Alignment level), Indicates the degree to which the PR fulfills its original purpose, Options: Fully compliant, Partially compliant or Not compliant.
|
|
||||||
|
|
||||||
|
|
||||||
{width=768}
|
|
||||||
|
|
||||||
By default, the tool will automatically validate if the PR complies with the referenced ticket.
|
|
||||||
If you want to disable this feedback, add the following line to your configuration file:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[pr_reviewer]
|
|
||||||
require_ticket_analysis_review=false
|
|
||||||
```
|
|
||||||
|
|
||||||
## Providers
|
|
||||||
|
|
||||||
### Github Issues Integration
|
|
||||||
|
|
||||||
Qodo Merge PR Agent will automatically recognize Github issues mentioned in the PR description and fetch the issue content.
|
|
||||||
Examples of valid GitHub issue references:
|
|
||||||
|
|
||||||
- `https://github.com/<ORG_NAME>/<REPO_NAME>/issues/<ISSUE_NUMBER>`
|
|
||||||
- `#<ISSUE_NUMBER>`
|
|
||||||
- `<ORG_NAME>/<REPO_NAME>#<ISSUE_NUMBER>`
|
|
||||||
|
|
||||||
Since Qodo Merge PR Agent is integrated with GitHub, it doesn't require any additional configuration to fetch GitHub issues.
|
|
||||||
|
|
||||||
### Jira Integration 💎
|
|
||||||
|
|
||||||
We support both Jira Cloud and Jira Server/Data Center.
|
|
||||||
To integrate with Jira, you can link your PR to a ticket using either of these methods:
|
|
||||||
|
|
||||||
**Method 1: Description Reference:**
|
|
||||||
|
|
||||||
Include a ticket reference in your PR description using either the complete URL format https://<JIRA_ORG>.atlassian.net/browse/ISSUE-123 or the shortened ticket ID ISSUE-123.
|
|
||||||
|
|
||||||
**Method 2: Branch Name Detection:**
|
|
||||||
|
|
||||||
Name your branch with the ticket ID as a prefix (e.g., `ISSUE-123-feature-description` or `ISSUE-123/feature-description`).
|
|
||||||
|
|
||||||
!!! note "Jira Base URL"
|
|
||||||
For shortened ticket IDs or branch detection (method 2), you must configure the Jira base URL in your configuration file under the [jira] section:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[jira]
|
|
||||||
jira_base_url = "https://<JIRA_ORG>.atlassian.net"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Jira Cloud 💎
|
|
||||||
There are two ways to authenticate with Jira Cloud:
|
|
||||||
|
|
||||||
**1) Jira App Authentication**
|
|
||||||
|
|
||||||
The recommended way to authenticate with Jira Cloud is to install the Qodo Merge app in your Jira Cloud instance. This will allow Qodo Merge to access Jira data on your behalf.
|
|
||||||
|
|
||||||
Installation steps:
|
|
||||||
|
|
||||||
1. Click [here](https://auth.atlassian.com/authorize?audience=api.atlassian.com&client_id=8krKmA4gMD8mM8z24aRCgPCSepZNP1xf&scope=read%3Ajira-work%20offline_access&redirect_uri=https%3A%2F%2Fregister.jira.pr-agent.codium.ai&state=qodomerge&response_type=code&prompt=consent) to install the Qodo Merge app in your Jira Cloud instance, click the `accept` button.<br>
|
|
||||||
{width=384}
|
|
||||||
|
|
||||||
2. After installing the app, you will be redirected to the Qodo Merge registration page. and you will see a success message.<br>
|
|
||||||
{width=384}
|
|
||||||
|
|
||||||
3. Now you can use the Jira integration in Qodo Merge PR Agent.
|
|
||||||
|
|
||||||
**2) Email/Token Authentication**
|
|
||||||
|
|
||||||
You can create an API token from your Atlassian account:
|
|
||||||
|
|
||||||
1. Log in to https://id.atlassian.com/manage-profile/security/api-tokens.
|
|
||||||
|
|
||||||
2. Click Create API token.
|
|
||||||
|
|
||||||
3. From the dialog that appears, enter a name for your new token and click Create.
|
|
||||||
|
|
||||||
4. Click Copy to clipboard.
|
|
||||||
|
|
||||||
{width=384}
|
|
||||||
|
|
||||||
5. In your [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/) add the following lines:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[jira]
|
|
||||||
jira_api_token = "YOUR_API_TOKEN"
|
|
||||||
jira_api_email = "YOUR_EMAIL"
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
#### Jira Data Center/Server 💎
|
|
||||||
|
|
||||||
##### Local App Authentication (For Qodo Merge On-Premise Customers)
|
|
||||||
|
|
||||||
##### 1. Step 1: Set up an application link in Jira Data Center/Server
|
|
||||||
* Go to Jira Administration > Applications > Application Links > Click on `Create link`
|
|
||||||
|
|
||||||
{width=384}
|
|
||||||
* Choose `External application` and set the direction to `Incoming` and then click `Continue`
|
|
||||||
|
|
||||||
{width=256}
|
|
||||||
* In the following screen, enter the following details:
|
|
||||||
* Name: `Qodo Merge`
|
|
||||||
* Redirect URL: Enter your Qodo Merge URL followed `https://{QODO_MERGE_ENDPOINT}/register_ticket_provider`
|
|
||||||
* Permission: Select `Read`
|
|
||||||
* Click `Save`
|
|
||||||
|
|
||||||
{width=384}
|
|
||||||
* Copy the `Client ID` and `Client secret` and set them in your `.secrets` file:
|
|
||||||
|
|
||||||
{width=256}
|
|
||||||
```toml
|
|
||||||
[jira]
|
|
||||||
jira_app_secret = "..."
|
|
||||||
jira_client_id = "..."
|
|
||||||
```
|
|
||||||
|
|
||||||
##### 2. Step 2: Authenticate with Jira Data Center/Server
|
|
||||||
* Open this URL in your browser: `https://{QODO_MERGE_ENDPOINT}/jira_auth`
|
|
||||||
* Click on link
|
|
||||||
|
|
||||||
{width=384}
|
|
||||||
|
|
||||||
* You will be redirected to Jira Data Center/Server, click `Allow`
|
|
||||||
* You will be redirected back to Qodo Merge PR Agent and you will see a success message.
|
|
||||||
|
|
||||||
|
|
||||||
##### Personal Access Token (PAT) Authentication
|
|
||||||
We also support Personal Access Token (PAT) Authentication method.
|
|
||||||
|
|
||||||
1. Create a [Personal Access Token (PAT)](https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html) in your Jira account
|
|
||||||
2. In your Configuration file/Environment variables/Secrets file, add the following lines:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[jira]
|
|
||||||
jira_base_url = "YOUR_JIRA_BASE_URL" # e.g. https://jira.example.com
|
|
||||||
jira_api_token = "YOUR_API_TOKEN"
|
|
||||||
```
|
|
@ -1,7 +1,6 @@
|
|||||||
# Core Abilities
|
# Core Abilities
|
||||||
Qodo Merge utilizes a variety of core abilities to provide a comprehensive and efficient code review experience. These abilities include:
|
Qodo Merge utilizes a variety of core abilities to provide a comprehensive and efficient code review experience. These abilities include:
|
||||||
|
|
||||||
- [Fetching ticket context](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/)
|
|
||||||
- [Local and global metadata](https://qodo-merge-docs.qodo.ai/core-abilities/metadata/)
|
- [Local and global metadata](https://qodo-merge-docs.qodo.ai/core-abilities/metadata/)
|
||||||
- [Dynamic context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic_context/)
|
- [Dynamic context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic_context/)
|
||||||
- [Self-reflection](https://qodo-merge-docs.qodo.ai/core-abilities/self_reflection/)
|
- [Self-reflection](https://qodo-merge-docs.qodo.ai/core-abilities/self_reflection/)
|
||||||
|
@ -32,14 +32,14 @@ For example, when generating code suggestions for different files, Qodo Merge ca
|
|||||||
|
|
||||||
@@ ... @@ def func1():
|
@@ ... @@ def func1():
|
||||||
__new hunk__
|
__new hunk__
|
||||||
11 unchanged code line0
|
11 unchanged code line0 in the PR
|
||||||
12 unchanged code line1
|
12 unchanged code line1 in the PR
|
||||||
13 +new code line2 added
|
13 +new code line2 added in the PR
|
||||||
14 unchanged code line3
|
14 unchanged code line3 in the PR
|
||||||
__old hunk__
|
__old hunk__
|
||||||
unchanged code line0
|
unchanged code line0
|
||||||
unchanged code line1
|
unchanged code line1
|
||||||
-old code line2 removed
|
-old code line2 removed in the PR
|
||||||
unchanged code line3
|
unchanged code line3
|
||||||
|
|
||||||
@@ ... @@ def func2():
|
@@ ... @@ def func2():
|
||||||
|
@ -46,5 +46,6 @@ This results in a more refined and valuable set of suggestions for the user, sav
|
|||||||
## Appendix - Relevant Configuration Options
|
## Appendix - Relevant Configuration Options
|
||||||
```
|
```
|
||||||
[pr_code_suggestions]
|
[pr_code_suggestions]
|
||||||
|
self_reflect_on_suggestions = true # Enable self-reflection on code suggestions
|
||||||
suggestions_score_threshold = 0 # Filter out suggestions with a score below this threshold (0-10)
|
suggestions_score_threshold = 0 # Filter out suggestions with a score below this threshold (0-10)
|
||||||
```
|
```
|
@ -61,7 +61,7 @@ Or be triggered interactively by using the `analyze` tool.
|
|||||||
|
|
||||||
### Find Similar Code
|
### Find Similar Code
|
||||||
|
|
||||||
The [`similar code`](https://qodo-merge-docs.qodo.ai/tools/similar_code/) tool retrieves the most similar code components from inside the organization's codebase or from open-source code, including details about the license associated with each repository.
|
The [`similar code`](https://qodo-merge-docs.qodo.ai/tools/similar_code/) tool retrieves the most similar code components from inside the organization's codebase, or from open-source code.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# FAQ
|
# FAQ
|
||||||
|
|
||||||
??? note "Q: Can Qodo Merge serve as a substitute for a human reviewer?"
|
??? note "Question: Can Qodo Merge serve as a substitute for a human reviewer?"
|
||||||
#### Answer:<span style="display:none;">1</span>
|
#### Answer:<span style="display:none;">1</span>
|
||||||
|
|
||||||
Qodo Merge is designed to assist, not replace, human reviewers.
|
Qodo Merge is designed to assist, not replace, human reviewers.
|
||||||
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
1. Preserves user's original PR header
|
1. Preserves user's original PR header
|
||||||
2. Places user's description above the AI-generated PR description
|
2. Places user's description above the AI-generated PR description
|
||||||
3. Won't approve PRs; approval remains reviewer's responsibility
|
3. Cannot approve PRs; approval remains reviewer's responsibility
|
||||||
4. The code suggestions are optional, and aim to:
|
4. The code suggestions are optional, and aim to:
|
||||||
- Encourage self-review and self-reflection
|
- Encourage self-review and self-reflection
|
||||||
- Highlight potential bugs or oversights
|
- Highlight potential bugs or oversights
|
||||||
@ -22,15 +22,15 @@
|
|||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
??? note "Q: I received an incorrect or irrelevant suggestion. Why?"
|
??? note "Question: I received an incorrect or irrelevant suggestion. Why?"
|
||||||
|
|
||||||
#### Answer:<span style="display:none;">2</span>
|
#### Answer:<span style="display:none;">2</span>
|
||||||
|
|
||||||
- Modern AI models, like Claude 3.5 Sonnet and GPT-4, are improving rapidly but remain imperfect. Users should critically evaluate all suggestions rather than accepting them automatically.
|
- Modern AI models, like Claude 3.5 Sonnet and GPT-4, are improving rapidly but remain imperfect. Users should critically evaluate all suggestions rather than accepting them automatically.
|
||||||
- AI errors are rare, but possible. A main value from reviewing the code suggestions lies in their high probability of catching **mistakes or bugs made by the PR author**. We believe it's worth spending 30-60 seconds reviewing suggestions, even if some aren't relevant, as this practice can enhance code quality and prevent bugs in production.
|
- AI errors are rare, but possible. A main value from reviewing the code suggestions lies in their high probability of catching **mistakes or bugs made by the PR author**. We believe it's worth spending 30-60 seconds reviewing suggestions, even if some aren't relevant, as this practice can enhances code quality and prevent bugs in production.
|
||||||
|
|
||||||
|
|
||||||
- The hierarchical structure of the suggestions is designed to help the user _quickly_ understand them, and to decide which ones are relevant and which are not:
|
- The hierarchical structure of the suggestions is designed to help the user to _quickly_ understand them, and to decide which ones are relevant and which are not:
|
||||||
|
|
||||||
- Only if the `Category` header is relevant, the user should move to the summarized suggestion description.
|
- Only if the `Category` header is relevant, the user should move to the summarized suggestion description.
|
||||||
- Only if the summarized suggestion description is relevant, the user should click on the collapsible, to read the full suggestion description with a code preview example.
|
- Only if the summarized suggestion description is relevant, the user should click on the collapsible, to read the full suggestion description with a code preview example.
|
||||||
@ -40,14 +40,14 @@ ___
|
|||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
??? note "Q: How can I get more tailored suggestions?"
|
??? note "Question: How can I get more tailored suggestions?"
|
||||||
#### Answer:<span style="display:none;">3</span>
|
#### Answer:<span style="display:none;">3</span>
|
||||||
|
|
||||||
See [here](https://qodo-merge-docs.qodo.ai/tools/improve/#extra-instructions-and-best-practices) for more information on how to use the `extra_instructions` and `best_practices` configuration options, to guide the model to more tailored suggestions.
|
See [here](https://qodo-merge-docs.qodo.ai/tools/improve/#extra-instructions-and-best-practices) for more information on how to use the `extra_instructions` and `best_practices` configuration options, to guide the model to more tailored suggestions.
|
||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
??? note "Q: Will you store my code? Are you using my code to train models?"
|
??? note "Question: Will you store my code ? Are you using my code to train models?"
|
||||||
#### Answer:<span style="display:none;">4</span>
|
#### Answer:<span style="display:none;">4</span>
|
||||||
|
|
||||||
No. Qodo Merge strict privacy policy ensures that your code is not stored or used for training purposes.
|
No. Qodo Merge strict privacy policy ensures that your code is not stored or used for training purposes.
|
||||||
@ -56,35 +56,12 @@ ___
|
|||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
??? note "Q: Can I use my own LLM keys with Qodo Merge?"
|
??? note "Question: Can I use my own LLM keys with Qodo Merge?"
|
||||||
#### Answer:<span style="display:none;">5</span>
|
#### Answer:<span style="display:none;">5</span>
|
||||||
|
|
||||||
When you self-host the [open-source](https://github.com/Codium-ai/pr-agent) version, you use your own keys.
|
When you self-host, you use your own keys.
|
||||||
|
|
||||||
Qodo Merge Pro with SaaS deployment is a hosted version of Qodo Merge, where Qodo manages the infrastructure and the keys.
|
Qodo Merge Pro with SaaS deployment is a hosted version of Qodo Merge, where Qodo manages the infrastructure and the keys.
|
||||||
For enterprise customers, on-prem deployment is also available. [Contact us](https://www.codium.ai/contact/#pricing) for more information.
|
For enterprise customers, on-prem deployment is also available. [Contact us](https://www.codium.ai/contact/#pricing) for more information.
|
||||||
___
|
|
||||||
|
|
||||||
??? note "Q: Can Qodo Merge review draft/offline PRs?"
|
|
||||||
#### Answer:<span style="display:none;">5</span>
|
|
||||||
|
|
||||||
Yes. While Qodo Merge won't automatically review draft PRs, you can still get feedback by manually requesting it through [online commenting](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#online-usage).
|
|
||||||
|
|
||||||
For active PRs, you can customize the automatic feedback settings [here](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#qodo-merge-automatic-feedback) to match your team's workflow.
|
|
||||||
___
|
|
||||||
|
|
||||||
??? note "Q: Can the 'Review effort' feedback be calibrated or customized?"
|
|
||||||
#### Answer:<span style="display:none;">5</span>
|
|
||||||
|
|
||||||
Yes, you can customize review effort estimates using the `extra_instructions` configuration option (see [documentation](https://qodo-merge-docs.qodo.ai/tools/review/#configuration-options)).
|
|
||||||
|
|
||||||
Example mapping:
|
|
||||||
|
|
||||||
- Effort 1: < 30 minutes review time
|
|
||||||
- Effort 2: 30-60 minutes review time
|
|
||||||
- Effort 3: 60-90 minutes review time
|
|
||||||
- ...
|
|
||||||
|
|
||||||
Note: The effort levels (1-5) are primarily meant for _comparative_ purposes, helping teams prioritize reviewing smaller PRs first. The actual review duration may vary, as the focus is on providing consistent relative effort estimates.
|
|
||||||
|
|
||||||
___
|
___
|
@ -25,44 +25,36 @@ To search the documentation site using natural language:
|
|||||||
|
|
||||||
Qodo Merge offers extensive pull request functionalities across various git providers.
|
Qodo Merge offers extensive pull request functionalities across various git providers.
|
||||||
|
|
||||||
| | | GitHub | GitLab | Bitbucket | Azure DevOps |
|
| | | GitHub | Gitlab | Bitbucket | Azure DevOps |
|
||||||
|-------|---------------------------------------------------------------------------------------------------------|:--------------------:|:--------------------:|:--------------------:|:------------:|
|
|-------|-----------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|
|
||||||
| TOOLS | [Review](https://qodo-merge-docs.qodo.ai/tools/review/) | ✅ | ✅ | ✅ | ✅ |
|
| TOOLS | Review | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Describe](https://qodo-merge-docs.qodo.ai/tools/describe/) | ✅ | ✅ | ✅ | ✅ |
|
| | ⮑ Incremental | ✅ | | | |
|
||||||
| | [Improve](https://qodo-merge-docs.qodo.ai/tools/improve/) | ✅ | ✅ | ✅ | ✅ |
|
| | Ask | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Ask](https://qodo-merge-docs.qodo.ai/tools/ask/) | ✅ | ✅ | ✅ | ✅ |
|
| | Describe | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | ⮑ [Ask on code lines](https://pr-agent-docs.codium.ai/tools/ask#ask-lines) | ✅ | ✅ | | |
|
| | ⮑ [Inline file summary](https://qodo-merge-docs.qodo.ai/tools/describe/#inline-file-summary){:target="_blank"} 💎 | ✅ | ✅ | | |
|
||||||
| | [Update CHANGELOG](https://qodo-merge-docs.qodo.ai/tools/update_changelog/) | ✅ | ✅ | ✅ | ✅ |
|
| | Improve | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Ticket Context](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/) 💎 | ✅ | ✅ | ✅ | |
|
| | ⮑ Extended | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Utilizing Best Practices](https://qodo-merge-docs.qodo.ai/tools/improve/#best-practices) 💎 | ✅ | ✅ | ✅ | |
|
| | [Custom Prompt](./tools/custom_prompt.md){:target="_blank"} 💎 | ✅ | ✅ | ✅ | |
|
||||||
| | [PR Chat](https://qodo-merge-docs.qodo.ai/chrome-extension/features/#pr-chat) 💎 | ✅ | | | |
|
| | Reflect and Review | ✅ | ✅ | ✅ | |
|
||||||
| | [Suggestion Tracking](https://qodo-merge-docs.qodo.ai/tools/improve/#suggestion-tracking) 💎 | ✅ | ✅ | | |
|
| | Update CHANGELOG.md | ✅ | ✅ | ✅ | ️ |
|
||||||
| | [CI Feedback](https://pr-agent-docs.codium.ai/tools/ci_feedback/) 💎 | ✅ | | | |
|
| | Find Similar Issue | ✅ | | | ️ |
|
||||||
| | [PR Documentation](https://pr-agent-docs.codium.ai/tools/documentation/) 💎 | ✅ | ✅ | | |
|
| | [Add PR Documentation](./tools/documentation.md){:target="_blank"} 💎 | ✅ | ✅ | | |
|
||||||
| | [Custom Labels](https://pr-agent-docs.codium.ai/tools/custom_labels/) 💎 | ✅ | ✅ | | |
|
| | [Generate Custom Labels](./tools/describe.md#handle-custom-labels-from-the-repos-labels-page-💎){:target="_blank"} 💎 | ✅ | ✅ | | |
|
||||||
| | [Analyze](https://pr-agent-docs.codium.ai/tools/analyze/) 💎 | ✅ | ✅ | | |
|
| | [Analyze PR Components](./tools/analyze.md){:target="_blank"} 💎 | ✅ | ✅ | | |
|
||||||
| | [Similar Code](https://pr-agent-docs.codium.ai/tools/similar_code/) 💎 | ✅ | | | |
|
| | | | | | ️ |
|
||||||
| | [Custom Prompt](https://pr-agent-docs.codium.ai/tools/custom_prompt/) 💎 | ✅ | ✅ | ✅ | |
|
| USAGE | CLI | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Test](https://pr-agent-docs.codium.ai/tools/test/) 💎 | ✅ | ✅ | | |
|
| | App / webhook | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Implement](https://pr-agent-docs.codium.ai/tools/implement/) 💎 | ✅ | ✅ | ✅ | |
|
| | Actions | ✅ | | | ️ |
|
||||||
| | | | | | |
|
| | | | | |
|
||||||
| USAGE | [CLI](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#local-repo-cli) | ✅ | ✅ | ✅ | ✅ |
|
| CORE | PR compression | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [App / webhook](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-app) | ✅ | ✅ | ✅ | ✅ |
|
| | Repo language prioritization | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Tagging bot](https://github.com/Codium-ai/pr-agent#try-it-now) | ✅ | | | |
|
|
||||||
| | [Actions](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-action) | ✅ |✅| ✅ |✅|
|
|
||||||
| | | | | | |
|
|
||||||
| CORE | [PR compression](https://qodo-merge-docs.qodo.ai/core-abilities/compression_strategy/) | ✅ | ✅ | ✅ | ✅ |
|
|
||||||
| | Adaptive and token-aware file patch fitting | ✅ | ✅ | ✅ | ✅ |
|
| | Adaptive and token-aware file patch fitting | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Multiple models support](https://qodo-merge-docs.qodo.ai/usage-guide/changing_a_model/) | ✅ | ✅ | ✅ | ✅ |
|
| | Multiple models support | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | [Local and global metadata](https://qodo-merge-docs.qodo.ai/core-abilities/metadata/) | ✅ | ✅ | ✅ | ✅ |
|
| | Incremental PR review | ✅ | | | |
|
||||||
| | [Dynamic context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic_context/) | ✅ | ✅ | ✅ | ✅ |
|
| | [Static code analysis](./tools/analyze.md/){:target="_blank"} 💎 | ✅ | ✅ | ✅ | |
|
||||||
| | [Self reflection](https://qodo-merge-docs.qodo.ai/core-abilities/self_reflection/) | ✅ | ✅ | ✅ | ✅ |
|
| | [Multiple configuration options](./usage-guide/configuration_options.md){:target="_blank"} 💎 | ✅ | ✅ | ✅ | |
|
||||||
| | [Static code analysis](https://qodo-merge-docs.qodo.ai/core-abilities/static_code_analysis/) 💎 | ✅ | ✅ | ✅ | |
|
|
||||||
| | [Global and wiki configurations](https://pr-agent-docs.codium.ai/usage-guide/configuration_options/) 💎 | ✅ | ✅ | ✅ | |
|
|
||||||
| | [PR interactive actions](https://www.codium.ai/images/pr_agent/pr-actions.mp4) 💎 | ✅ | ✅ | | |
|
|
||||||
| | [Impact Evaluation](https://qodo-merge-docs.qodo.ai/core-abilities/impact_evaluation/) 💎 | ✅ | ✅ | | |
|
|
||||||
|
|
||||||
💎 marks a feature available only in [Qodo Merge Pro](https://www.qodo.ai/pricing/){:target="_blank"}
|
💎 marks a feature available only in [Qodo Merge Pro](https://www.codium.ai/pricing/){:target="_blank"}
|
||||||
|
|
||||||
|
|
||||||
## Example Results
|
## Example Results
|
||||||
|
@ -51,12 +51,10 @@ stages:
|
|||||||
```
|
```
|
||||||
This script will run Qodo Merge on every new merge request, with the `improve`, `review`, and `describe` commands.
|
This script will run Qodo Merge on every new merge request, with the `improve`, `review`, and `describe` commands.
|
||||||
Note that you need to export the `azure_devops__pat` and `OPENAI_KEY` variables in the Azure DevOps pipeline settings (Pipelines -> Library -> + Variable group):
|
Note that you need to export the `azure_devops__pat` and `OPENAI_KEY` variables in the Azure DevOps pipeline settings (Pipelines -> Library -> + Variable group):
|
||||||
|
|
||||||
{width=468}
|
{width=468}
|
||||||
|
|
||||||
Make sure to give pipeline permissions to the `pr_agent` variable group.
|
Make sure to give pipeline permissions to the `pr_agent` variable group.
|
||||||
|
|
||||||
> Note that Azure Pipelines lacks support for triggering workflows from PR comments. If you find a viable solution, please contribute it to our [issue tracker](https://github.com/Codium-ai/pr-agent/issues)
|
|
||||||
|
|
||||||
## Azure DevOps from CLI
|
## Azure DevOps from CLI
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
You can use the Bitbucket Pipeline system to run Qodo Merge on every pull request open or update.
|
You can use the Bitbucket Pipeline system to run Qodo Merge on every pull request open or update.
|
||||||
|
|
||||||
1. Add the following file in your repository bitbucket-pipelines.yml
|
1. Add the following file in your repository bitbucket_pipelines.yml
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
pipelines:
|
pipelines:
|
||||||
|
@ -38,41 +38,25 @@ You can also modify the `script` section to run different Qodo Merge commands, o
|
|||||||
|
|
||||||
Note that if your base branches are not protected, don't set the variables as `protected`, since the pipeline will not have access to them.
|
Note that if your base branches are not protected, don't set the variables as `protected`, since the pipeline will not have access to them.
|
||||||
|
|
||||||
> **Note**: The `$CI_SERVER_FQDN` variable is available starting from GitLab version 16.10. If you're using an earlier version, this variable will not be available. However, you can combine `$CI_SERVER_HOST` and `$CI_SERVER_PORT` to achieve the same result. Please ensure you're using a compatible version or adjust your configuration.
|
|
||||||
|
|
||||||
|
|
||||||
## Run a GitLab webhook server
|
## Run a GitLab webhook server
|
||||||
|
|
||||||
1. From the GitLab workspace or group, create an access token with "Reporter" role ("Developer" if using Pro version of the agent) and "api" scope.
|
1. From the GitLab workspace or group, create an access token. Enable the "api" scope only.
|
||||||
|
|
||||||
2. Generate a random secret for your app, and save it for later. For example, you can use:
|
2. Generate a random secret for your app, and save it for later. For example, you can use:
|
||||||
|
|
||||||
```
|
```
|
||||||
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 [here](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-app) steps 4-7.
|
||||||
|
|
||||||
3. Clone this repository:
|
4. In the secrets file, fill in the following:
|
||||||
|
- Your OpenAI key.
|
||||||
|
- In the [gitlab] section, fill in personal_access_token and shared_secret. The access token can be a personal access token, or a group or project access token.
|
||||||
|
- Set deployment_type to 'gitlab' in [configuration.toml](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml)
|
||||||
|
|
||||||
```
|
5. Create a webhook in GitLab. Set the URL to ```http[s]://<PR_AGENT_HOSTNAME>/webhook```. Set the secret token to the generated secret from step 2.
|
||||||
git clone https://github.com/Codium-ai/pr-agent.git
|
In the "Trigger" section, check the ‘comments’ and ‘merge request events’ boxes.
|
||||||
```
|
|
||||||
|
|
||||||
4. Prepare variables and secrets. Skip this step if you plan on settings these as environment variables when running the agent:
|
6. Test your installation by opening a merge request or commenting or a merge request using one of CodiumAI's commands.
|
||||||
1. In the configuration file/variables:
|
|
||||||
- Set `deployment_type` to "gitlab"
|
|
||||||
|
|
||||||
2. In the secrets file/variables:
|
|
||||||
- Set your AI model key in the respective section
|
|
||||||
- In the [gitlab] section, set `personal_access_token` (with token from step 1) and `shared_secret` (with secret from step 2)
|
|
||||||
|
|
||||||
|
|
||||||
5. Build a Docker image for the app and optionally push it to a Docker repository. We'll use Dockerhub as an example:
|
|
||||||
```
|
|
||||||
docker build . -t gitlab_pr_agent --target gitlab_webhook -f docker/Dockerfile
|
|
||||||
docker push codiumai/pr-agent:gitlab_webhook # Push to your Docker repository
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Create a webhook in GitLab. Set the URL to ```http[s]://<PR_AGENT_HOSTNAME>/webhook```, the secret token to the generated secret from step 2, and enable the triggers `push`, `comments` and `merge request events`.
|
|
||||||
|
|
||||||
7. Test your installation by opening a merge request or commenting on a merge request using one of CodiumAI's commands.
|
|
||||||
boxes
|
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
## Self-hosted Qodo Merge
|
## Self-hosted Qodo Merge
|
||||||
If you choose to host your own Qodo Merge, you first need to acquire two tokens:
|
If you choose to host your own Qodo Merge, you first need to acquire two tokens:
|
||||||
|
|
||||||
1. An OpenAI key from [here](https://platform.openai.com/api-keys){:target="_blank"}, with access to GPT-4 (or a key for other [language models](https://qodo-merge-docs.qodo.ai/usage-guide/changing_a_model/), if you prefer).
|
1. An OpenAI key from [here](https://platform.openai.com/api-keys), with access to GPT-4 (or a key for other [language models](https://qodo-merge-docs.qodo.ai/usage-guide/changing_a_model/), if you prefer).
|
||||||
2. A GitHub\GitLab\BitBucket personal access token (classic), with the repo scope. [GitHub from [here](https://github.com/settings/tokens){:target="_blank"}]
|
2. A GitHub\GitLab\BitBucket personal access token (classic), with the repo scope. [GitHub from [here](https://github.com/settings/tokens)]
|
||||||
|
|
||||||
There are several ways to use self-hosted Qodo Merge:
|
There are several ways to use self-hosted Qodo Merge:
|
||||||
|
|
||||||
|
@ -66,30 +66,7 @@ To invoke a tool (for example `review`), you can run directly from the Docker im
|
|||||||
docker run --rm -it -e CONFIG.GIT_PROVIDER=bitbucket -e OPENAI.KEY=$OPENAI_API_KEY -e BITBUCKET.BEARER_TOKEN=$BITBUCKET_BEARER_TOKEN codiumai/pr-agent:latest --pr_url=<pr_url> review
|
docker run --rm -it -e CONFIG.GIT_PROVIDER=bitbucket -e OPENAI.KEY=$OPENAI_API_KEY -e BITBUCKET.BEARER_TOKEN=$BITBUCKET_BEARER_TOKEN codiumai/pr-agent:latest --pr_url=<pr_url> review
|
||||||
```
|
```
|
||||||
|
|
||||||
For other git providers, update `CONFIG.GIT_PROVIDER` accordingly and check the `pr_agent/settings/.secrets_template.toml` file for environment variables expected names and values.
|
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.
|
||||||
The `pr_agent` uses [Dynaconf](https://www.dynaconf.com/) to load settings from configuration files.
|
|
||||||
|
|
||||||
It is also possible to provide or override the configuration by setting the corresponding environment variables.
|
|
||||||
You can define the corresponding environment variables by following this convention: `<TABLE>__<KEY>=<VALUE>` or `<TABLE>.<KEY>=<VALUE>`.
|
|
||||||
The `<TABLE>` refers to a table/section in a configuration file and `<KEY>=<VALUE>` refers to the key/value pair of a setting in the configuration file.
|
|
||||||
|
|
||||||
For example, suppose you want to run `pr_agent` that connects to a self-hosted GitLab instance similar to an example above.
|
|
||||||
You can define the environment variables in a plain text file named `.env` with the following content:
|
|
||||||
|
|
||||||
> Warning: Never commit the `.env` file to version control system as it might contains sensitive credentials!
|
|
||||||
|
|
||||||
```
|
|
||||||
CONFIG__GIT_PROVIDER="gitlab"
|
|
||||||
GITLAB__URL="<your url>"
|
|
||||||
GITLAB__PERSONAL_ACCESS_TOKEN="<your token>"
|
|
||||||
OPENAI__KEY="<your key>"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, you can run `pr_agent` using Docker with the following command:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
docker run --rm -it --env-file .env codiumai/pr-agent:latest <tool> <tool parameter>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,44 +1,31 @@
|
|||||||
Qodo Merge Pro is a versatile application compatible with GitHub, GitLab, and BitBucket, hosted by QodoAI.
|
|
||||||
|
## Getting Started with Qodo Merge Pro
|
||||||
|
|
||||||
|
Qodo Merge Pro is a versatile application compatible with GitHub, GitLab, and BitBucket, hosted by CodiumAI.
|
||||||
See [here](https://qodo-merge-docs.qodo.ai/overview/pr_agent_pro/) for more details about the benefits of using Qodo Merge Pro.
|
See [here](https://qodo-merge-docs.qodo.ai/overview/pr_agent_pro/) for more details about the benefits of using Qodo Merge Pro.
|
||||||
|
|
||||||
A complimentary two-week trial is provided to all new users. Following the trial period, user licenses (seats) are required for continued access.
|
Interested parties can subscribe to Qodo Merge Pro through the following [link](https://www.codium.ai/pricing/).
|
||||||
To purchase user licenses, please visit our [pricing page](https://www.qodo.ai/pricing/).
|
After subscribing, you are granted the ability to easily install the application across any of your repositories.
|
||||||
Once subscribed, users can seamlessly deploy the application across any of their code repositories.
|
|
||||||
|
|
||||||
## Install Qodo Merge Pro for GitHub
|
|
||||||
|
|
||||||
### GitHub Cloud
|
|
||||||
|
|
||||||
Qodo Merge Pro for GitHub cloud is available for installation through the [GitHub Marketplace](https://github.com/apps/qodo-merge-pro).
|
|
||||||
|
|
||||||
{width=468}
|
{width=468}
|
||||||
|
|
||||||
### GitHub Enterprise Server
|
Each user who wants to use Qodo Merge pro needs to buy a seat.
|
||||||
|
Initially, CodiumAI offers a two-week trial period at no cost, after which continued access requires each user to secure a personal seat.
|
||||||
|
Once a user acquires a seat, they gain the flexibility to use Qodo Merge Pro across any repository where it was enabled.
|
||||||
|
|
||||||
|
Users without a purchased seat who interact with a repository featuring Qodo Merge Pro are entitled to receive up to five complimentary feedbacks.
|
||||||
|
Beyond this limit, Qodo Merge Pro will cease to respond to their inquiries unless a seat is purchased.
|
||||||
|
|
||||||
|
## Install Qodo Merge Pro for GitHub Enterprise Server
|
||||||
|
|
||||||
To use Qodo Merge Pro application on your private GitHub Enterprise Server, you will need to contact us for starting an [Enterprise](https://www.codium.ai/pricing/) trial.
|
To use Qodo Merge Pro application on your private GitHub Enterprise Server, you will need to contact us for starting an [Enterprise](https://www.codium.ai/pricing/) trial.
|
||||||
|
|
||||||
### GitHub Open Source Projects
|
|
||||||
|
|
||||||
For open-source projects, Qodo Merge Pro is available for free usage. To install Qodo Merge Pro for your open-source repositories, use the following marketplace [link](https://github.com/apps/qodo-merge-pro-for-open-source).
|
|
||||||
|
|
||||||
## Install Qodo Merge Pro for Bitbucket
|
|
||||||
|
|
||||||
### Bitbucket Cloud
|
|
||||||
|
|
||||||
Qodo Merge Pro for Bitbucket Cloud is available for installation through the following [link](https://bitbucket.org/site/addons/authorize?addon_key=d6df813252c37258)
|
|
||||||
|
|
||||||
{width=468}
|
|
||||||
|
|
||||||
### Bitbucket Server
|
|
||||||
|
|
||||||
To use Qodo Merge Pro application on your private Bitbucket Server, you will need to contact us for starting an [Enterprise](https://www.codium.ai/pricing/) trial.
|
|
||||||
|
|
||||||
|
|
||||||
## Install Qodo Merge Pro for GitLab (Teams & Enterprise)
|
## Install Qodo Merge Pro for GitLab (Teams & Enterprise)
|
||||||
|
|
||||||
Since GitLab platform does not support apps, installing Qodo Merge Pro for GitLab is a bit more involved, and requires the following steps:
|
Since GitLab platform does not support apps, installing Qodo Merge Pro for GitLab is a bit more involved, and requires the following steps:
|
||||||
|
|
||||||
#### Step 1
|
### Step 1
|
||||||
|
|
||||||
Acquire a personal, project or group level access token. Enable the “api” scope in order to allow Qodo Merge to read pull requests, comment and respond to requests.
|
Acquire a personal, project or group level access token. Enable the “api” scope in order to allow Qodo Merge to read pull requests, comment and respond to requests.
|
||||||
|
|
||||||
@ -48,14 +35,14 @@ Acquire a personal, project or group level access token. Enable the “api” sc
|
|||||||
|
|
||||||
Store the token in a safe place, you won’t be able to access it again after it was generated.
|
Store the token in a safe place, you won’t be able to access it again after it was generated.
|
||||||
|
|
||||||
#### Step 2
|
### Step 2
|
||||||
|
|
||||||
Generate a shared secret and link it to the access token. Browse to [https://register.gitlab.pr-agent.codium.ai](https://register.gitlab.pr-agent.codium.ai).
|
Generate a shared secret and link it to the access token. Browse to [https://register.gitlab.pr-agent.codium.ai](https://register.gitlab.pr-agent.codium.ai).
|
||||||
Fill in your generated GitLab token and your company or personal name in the appropriate fields and click "Submit".
|
Fill in your generated GitLab token and your company or personal name in the appropriate fields and click "Submit".
|
||||||
|
|
||||||
You should see "Success!" displayed above the Submit button, and a shared secret will be generated. Store it in a safe place, you won’t be able to access it again after it was generated.
|
You should see "Success!" displayed above the Submit button, and a shared secret will be generated. Store it in a safe place, you won’t be able to access it again after it was generated.
|
||||||
|
|
||||||
#### Step 3
|
### Step 3
|
||||||
|
|
||||||
Install a webhook for your repository or groups, by clicking “webhooks” on the settings menu. Click the “Add new webhook” button.
|
Install a webhook for your repository or groups, by clicking “webhooks” on the settings menu. Click the “Add new webhook” button.
|
||||||
|
|
||||||
@ -66,7 +53,7 @@ Install a webhook for your repository or groups, by clicking “webhooks” on t
|
|||||||
In the webhook definition form, fill in the following fields:
|
In the webhook definition form, fill in the following fields:
|
||||||
URL: https://pro.gitlab.pr-agent.codium.ai/webhook
|
URL: https://pro.gitlab.pr-agent.codium.ai/webhook
|
||||||
|
|
||||||
Secret token: Your QodoAI key
|
Secret token: Your CodiumAI key
|
||||||
Trigger: Check the ‘comments’ and ‘merge request events’ boxes.
|
Trigger: Check the ‘comments’ and ‘merge request events’ boxes.
|
||||||
Enable SSL verification: Check the box.
|
Enable SSL verification: Check the box.
|
||||||
|
|
||||||
@ -74,7 +61,7 @@ Enable SSL verification: Check the box.
|
|||||||
{width=750}
|
{width=750}
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
#### Step 4
|
### Step 4
|
||||||
|
|
||||||
You’re all set!
|
You’re all set!
|
||||||
|
|
||||||
|
@ -41,8 +41,6 @@ Qodo Merge offers extensive pull request functionalities across various git prov
|
|||||||
| | [Add PR Documentation](./tools/documentation.md){:target="_blank"} 💎 | ✅ | ✅ | | ✅ |
|
| | [Add PR Documentation](./tools/documentation.md){:target="_blank"} 💎 | ✅ | ✅ | | ✅ |
|
||||||
| | [Generate Custom Labels](./tools/describe.md#handle-custom-labels-from-the-repos-labels-page-💎){:target="_blank"} 💎 | ✅ | ✅ | | ✅ |
|
| | [Generate Custom Labels](./tools/describe.md#handle-custom-labels-from-the-repos-labels-page-💎){:target="_blank"} 💎 | ✅ | ✅ | | ✅ |
|
||||||
| | [Analyze PR Components](./tools/analyze.md){:target="_blank"} 💎 | ✅ | ✅ | | ✅ |
|
| | [Analyze PR Components](./tools/analyze.md){:target="_blank"} 💎 | ✅ | ✅ | | ✅ |
|
||||||
| | [Test](https://pr-agent-docs.codium.ai/tools/test/) 💎 | ✅ | ✅ | | |
|
|
||||||
| | [Implement](https://pr-agent-docs.codium.ai/tools/implement/) 💎 | ✅ | ✅ | ✅ | |
|
|
||||||
| | | | | | ️ |
|
| | | | | | ️ |
|
||||||
| USAGE | CLI | ✅ | ✅ | ✅ | ✅ |
|
| USAGE | CLI | ✅ | ✅ | ✅ | ✅ |
|
||||||
| | App / webhook | ✅ | ✅ | ✅ | ✅ |
|
| | App / webhook | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
### Overview
|
### Overview
|
||||||
|
|
||||||
[Qodo Merge Pro](https://www.codium.ai/pricing/){:target="_blank"} is a hosted version of open-source [Qodo Merge (PR-Agent)](https://github.com/Codium-ai/pr-agent){:target="_blank"}. A complimentary two-week trial is offered, followed by a monthly subscription fee.
|
[Qodo Merge Pro](https://www.codium.ai/pricing/) is a hosted version of Qodo Merge, provided by Qodo. A complimentary two-week trial is offered, followed by a monthly subscription fee.
|
||||||
Qodo Merge Pro is designed for companies and teams that require additional features and capabilities. It provides the following benefits:
|
Qodo Merge Pro is designed for companies and teams that require additional features and capabilities. It provides the following benefits:
|
||||||
|
|
||||||
1. **Fully managed** - We take care of everything for you - hosting, models, regular updates, and more. Installation is as simple as signing up and adding the Qodo Merge app to your GitHub\GitLab\BitBucket repo.
|
1. **Fully managed** - We take care of everything for you - hosting, models, regular updates, and more. Installation is as simple as signing up and adding the Qodo Merge app to your GitHub\GitLab\BitBucket repo.
|
||||||
@ -34,14 +34,13 @@ Here are some of the additional features and capabilities that Qodo Merge Pro of
|
|||||||
Here are additional tools that are available only for Qodo Merge Pro users:
|
Here are additional tools that are available only for Qodo Merge Pro users:
|
||||||
|
|
||||||
| Feature | Description |
|
| Feature | Description |
|
||||||
|---------------------------------------------------------------------------------------|-------------|
|
|---------|-------------|
|
||||||
| [**Custom Prompt Suggestions**](https://qodo-merge-docs.qodo.ai/tools/custom_prompt/) | Generate code suggestions based on custom prompts from the user |
|
| [**Custom Prompt Suggestions**](https://qodo-merge-docs.qodo.ai/tools/custom_prompt/) | Generate code suggestions based on custom prompts from the user |
|
||||||
| [**Analyze PR components**](https://qodo-merge-docs.qodo.ai/tools/analyze/) | Identify the components that changed in the PR, and enable to interactively apply different tools to them |
|
| [**Analyze PR components**](https://qodo-merge-docs.qodo.ai/tools/analyze/) | Identify the components that changed in the PR, and enable to interactively apply different tools to them |
|
||||||
| [**Tests**](https://qodo-merge-docs.qodo.ai/tools/test/) | Generate tests for code components that changed in the PR |
|
| [**Tests**](https://qodo-merge-docs.qodo.ai/tools/test/) | Generate tests for code components that changed in the PR |
|
||||||
| [**PR documentation**](https://qodo-merge-docs.qodo.ai/tools/documentation/) | Generate docstring for code components that changed in the PR |
|
| [**PR documentation**](https://qodo-merge-docs.qodo.ai/tools/documentation/) | Generate docstring for code components that changed in the PR |
|
||||||
| [**Improve Component**](https://qodo-merge-docs.qodo.ai/tools/improve_component/) | Generate code suggestions for code components that changed in the PR |
|
| [**Improve Component**](https://qodo-merge-docs.qodo.ai/tools/improve_component/) | Generate code suggestions for code components that changed in the PR |
|
||||||
| [**Similar code search**](https://qodo-merge-docs.qodo.ai/tools/similar_code/) | Search for similar code in the repository, organization, or entire GitHub |
|
| [**Similar code search**](https://qodo-merge-docs.qodo.ai/tools/similar_code/) | Search for similar code in the repository, organization, or entire GitHub |
|
||||||
| [**Code implementation**](https://qodo-merge-docs.qodo.ai/tools/implement/) | Generates implementation code from review suggestions |
|
|
||||||
|
|
||||||
|
|
||||||
### Supported languages
|
### Supported languages
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
## Overview
|
|
||||||
|
|
||||||
The `implement` tool converts human code review discussions and feedback into ready-to-commit code changes.
|
|
||||||
It leverages LLM technology to transform PR comments and review suggestions into concrete implementation code, helping developers quickly turn feedback into working solutions.
|
|
||||||
|
|
||||||
## Usage Scenarios
|
|
||||||
|
|
||||||
|
|
||||||
### For Reviewers
|
|
||||||
|
|
||||||
Reviewers can request code changes by: <br>
|
|
||||||
1. Selecting the code block to be modified. <br>
|
|
||||||
2. Adding a comment with the syntax:
|
|
||||||
```
|
|
||||||
/implement <code-change-description>
|
|
||||||
```
|
|
||||||
|
|
||||||
{width=640}
|
|
||||||
|
|
||||||
|
|
||||||
### For PR Authors
|
|
||||||
|
|
||||||
PR authors can implement suggested changes by replying to a review comment using either: <br>
|
|
||||||
1. Add specific implementation details as described above
|
|
||||||
```
|
|
||||||
/implement <code-change-description>
|
|
||||||
```
|
|
||||||
2. Use the original review comment as instructions
|
|
||||||
```
|
|
||||||
/implement
|
|
||||||
```
|
|
||||||
|
|
||||||
{width=640}
|
|
||||||
|
|
||||||
### For Referencing Comments
|
|
||||||
|
|
||||||
You can reference and implement changes from any comment by:
|
|
||||||
```
|
|
||||||
/implement <link-to-review-comment>
|
|
||||||
```
|
|
||||||
|
|
||||||
{width=640}
|
|
||||||
|
|
||||||
Note that the implementation will occur within the review discussion thread.
|
|
||||||
|
|
||||||
|
|
||||||
**Configuration options** <br>
|
|
||||||
- Use `/implement` to implement code change within and based on the review discussion. <br>
|
|
||||||
- Use `/implement <code-change-description>` inside a review discussion to implement specific instructions. <br>
|
|
||||||
- Use `/implement <link-to-review-comment>` to indirectly call the tool from any comment. <br>
|
|
@ -95,112 +95,6 @@ This feature is controlled by a boolean configuration parameter: `pr_code_sugges
|
|||||||
|
|
||||||
Instead, we leverage a dedicated private page, within your repository wiki, to track suggestions. This approach offers convenient secure suggestion tracking while avoiding pull requests or any noise to the main repository.
|
Instead, we leverage a dedicated private page, within your repository wiki, to track suggestions. This approach offers convenient secure suggestion tracking while avoiding pull requests or any noise to the main repository.
|
||||||
|
|
||||||
## `Extra instructions` and `best practices`
|
|
||||||
|
|
||||||
The `improve` tool can be further customized by providing additional instructions and best practices to the AI model.
|
|
||||||
|
|
||||||
### Extra instructions
|
|
||||||
|
|
||||||
>`Platforms supported: GitHub, GitLab, Bitbucket, Azure DevOps`
|
|
||||||
|
|
||||||
You can use the `extra_instructions` configuration option to give the AI model additional instructions for the `improve` tool.
|
|
||||||
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter.
|
|
||||||
|
|
||||||
Examples for possible instructions:
|
|
||||||
```toml
|
|
||||||
[pr_code_suggestions]
|
|
||||||
extra_instructions="""\
|
|
||||||
(1) Answer in japanese
|
|
||||||
(2) Don't suggest to add try-except block
|
|
||||||
(3) Ignore changes in toml files
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
```
|
|
||||||
Use triple quotes to write multi-line instructions. Use bullet points or numbers to make the instructions more readable.
|
|
||||||
|
|
||||||
### Best practices 💎
|
|
||||||
|
|
||||||
>`Platforms supported: GitHub, GitLab, Bitbucket`
|
|
||||||
|
|
||||||
Another option to give additional guidance to the AI model is by creating a `best_practices.md` file, either in your repository's root directory or as a [**wiki page**](https://github.com/Codium-ai/pr-agent/wiki) (we recommend the wiki page, as editing and maintaining it over time is easier).
|
|
||||||
This page can contain a list of best practices, coding standards, and guidelines that are specific to your repo/organization.
|
|
||||||
|
|
||||||
The AI model will use this wiki page as a reference, and in case the PR code violates any of the guidelines, it will create additional suggestions, with a dedicated label: `Organization
|
|
||||||
best practice`.
|
|
||||||
|
|
||||||
Example for a python `best_practices.md` content:
|
|
||||||
```markdown
|
|
||||||
## Project best practices
|
|
||||||
- Make sure that I/O operations are encapsulated in a try-except block
|
|
||||||
- Use the `logging` module for logging instead of `print` statements
|
|
||||||
- Use `is` and `is not` to compare with `None`
|
|
||||||
- Use `if __name__ == '__main__':` to run the code only when the script is executed
|
|
||||||
- Use `with` statement to open files
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Tips for writing an effective `best_practices.md` file:
|
|
||||||
|
|
||||||
- Write clearly and concisely
|
|
||||||
- Include brief code examples when helpful
|
|
||||||
- Focus on project-specific guidelines, that will result in relevant suggestions you actually want to get
|
|
||||||
- Keep the file relatively short, under 800 lines, since:
|
|
||||||
- AI models may not process effectively very long documents
|
|
||||||
- Long files tend to contain generic guidelines already known to AI
|
|
||||||
|
|
||||||
#### Local and global best practices
|
|
||||||
By default, Qodo Merge will look for a local `best_practices.md` wiki file in the root of the relevant local repo.
|
|
||||||
|
|
||||||
If you want to enable also a global `best_practices.md` wiki file, set first in the global configuration file:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[best_practices]
|
|
||||||
enable_global_best_practices = true
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, create a `best_practices.md` wiki file in the root of [global](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#global-configuration-file) configuration repository, `pr-agent-settings`.
|
|
||||||
|
|
||||||
#### Best practices for multiple languages
|
|
||||||
For a git organization working with multiple programming languages, you can maintain a centralized global `best_practices.md` file containing language-specific guidelines.
|
|
||||||
When reviewing pull requests, Qodo Merge automatically identifies the programming language and applies the relevant best practices from this file.
|
|
||||||
|
|
||||||
To do this, structure your `best_practices.md` file using the following format:
|
|
||||||
|
|
||||||
```
|
|
||||||
# [Python]
|
|
||||||
...
|
|
||||||
# [Java]
|
|
||||||
...
|
|
||||||
# [JavaScript]
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Dedicated label for best practices suggestions
|
|
||||||
Best practice suggestions are labeled as `Organization best practice` by default.
|
|
||||||
To customize this label, modify it in your configuration file:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[best_practices]
|
|
||||||
organization_name = "..."
|
|
||||||
```
|
|
||||||
|
|
||||||
And the label will be: `{organization_name} best practice`.
|
|
||||||
|
|
||||||
|
|
||||||
#### Example results
|
|
||||||
|
|
||||||
{width=512}
|
|
||||||
|
|
||||||
|
|
||||||
### How to combine `extra instructions` and `best practices`
|
|
||||||
|
|
||||||
The `extra instructions` configuration is more related to the `improve` tool prompt. It can be used, for example, to avoid specific suggestions ("Don't suggest to add try-except block", "Ignore changes in toml files", ...) or to emphasize specific aspects or formats ("Answer in Japanese", "Give only short suggestions", ...)
|
|
||||||
|
|
||||||
In contrast, the `best_practices.md` file is a general guideline for the way code should be written in the repo.
|
|
||||||
|
|
||||||
Using a combination of both can help the AI model to provide relevant and tailored suggestions.
|
|
||||||
|
|
||||||
|
|
||||||
## Usage Tips
|
## Usage Tips
|
||||||
|
|
||||||
### Implementing the proposed code suggestions
|
### Implementing the proposed code suggestions
|
||||||
@ -297,6 +191,73 @@ This approach has two main benefits:
|
|||||||
Note: Chunking is primarily relevant for large PRs. For most PRs (up to 500 lines of code), Qodo Merge will be able to process the entire code in a single call.
|
Note: Chunking is primarily relevant for large PRs. For most PRs (up to 500 lines of code), Qodo Merge will be able to process the entire code in a single call.
|
||||||
|
|
||||||
|
|
||||||
|
### 'Extra instructions' and 'best practices'
|
||||||
|
|
||||||
|
#### Extra instructions
|
||||||
|
|
||||||
|
>`Platforms supported: GitHub, GitLab, Bitbucket`
|
||||||
|
|
||||||
|
You can use the `extra_instructions` configuration option to give the AI model additional instructions for the `improve` tool.
|
||||||
|
Be specific, clear, and concise in the instructions. With extra instructions, you are the prompter. Specify relevant aspects that you want the model to focus on.
|
||||||
|
|
||||||
|
Examples for possible instructions:
|
||||||
|
```toml
|
||||||
|
[pr_code_suggestions]
|
||||||
|
extra_instructions="""\
|
||||||
|
(1) Answer in japanese
|
||||||
|
(2) Don't suggest to add try-excpet block
|
||||||
|
(3) Ignore changes in toml files
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
Use triple quotes to write multi-line instructions. Use bullet points or numbers to make the instructions more readable.
|
||||||
|
|
||||||
|
#### Best practices 💎
|
||||||
|
|
||||||
|
>`Platforms supported: GitHub, GitLab`
|
||||||
|
|
||||||
|
Another option to give additional guidance to the AI model is by creating a dedicated [**wiki page**](https://github.com/Codium-ai/pr-agent/wiki) called `best_practices.md`.
|
||||||
|
This page can contain a list of best practices, coding standards, and guidelines that are specific to your repo/organization.
|
||||||
|
|
||||||
|
The AI model will use this wiki page as a reference, and in case the PR code violates any of the guidelines, it will suggest improvements accordingly, with a dedicated label: `Organization
|
||||||
|
best practice`.
|
||||||
|
|
||||||
|
Example for a `best_practices.md` content can be found [here](https://github.com/Codium-ai/pr-agent/blob/main/docs/docs/usage-guide/EXAMPLE_BEST_PRACTICE.md) (adapted from Google's [pyguide](https://google.github.io/styleguide/pyguide.html)).
|
||||||
|
This file is only an example. Since it is used as a prompt for an AI model, we want to emphasize the following:
|
||||||
|
|
||||||
|
- It should be written in a clear and concise manner
|
||||||
|
- If needed, it should give short relevant code snippets as examples
|
||||||
|
- Recommended to limit the text to 800 lines or fewer. Here’s why:
|
||||||
|
|
||||||
|
1) Extremely long best practices documents may not be fully processed by the AI model.
|
||||||
|
|
||||||
|
2) A lengthy file probably represent a more "**generic**" set of guidelines, which the AI model is already familiar with. The objective is to focus on a more targeted set of guidelines tailored to the specific needs of this project.
|
||||||
|
|
||||||
|
##### Local and global best practices
|
||||||
|
By default, Qodo Merge will look for a local `best_practices.md` wiki file in the root of the relevant local repo.
|
||||||
|
|
||||||
|
If you want to enable also a global `best_practices.md` wiki file, set first in the global configuration file:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[best_practices]
|
||||||
|
enable_global_best_practices = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, create a `best_practices.md` wiki file in the root of [global](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#global-configuration-file) configuration repository, `pr-agent-settings`.
|
||||||
|
|
||||||
|
##### Example results
|
||||||
|
|
||||||
|
{width=512}
|
||||||
|
|
||||||
|
|
||||||
|
#### How to combine `extra instructions` and `best practices`
|
||||||
|
|
||||||
|
The `extra instructions` configuration is more related to the `improve` tool prompt. It can be used, for example, to avoid specific suggestions ("Don't suggest to add try-except block", "Ignore changes in toml files", ...) or to emphasize specific aspects or formats ("Answer in Japanese", "Give only short suggestions", ...)
|
||||||
|
|
||||||
|
In contrast, the `best_practices.md` file is a general guideline for the way code should be written in the repo.
|
||||||
|
|
||||||
|
Using a combination of both can help the AI model to provide relevant and tailored suggestions.
|
||||||
|
|
||||||
## Configuration options
|
## Configuration options
|
||||||
|
|
||||||
??? example "General options"
|
??? example "General options"
|
||||||
@ -314,14 +275,14 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 500 line
|
|||||||
<td><b>dual_publishing_score_threshold</b></td>
|
<td><b>dual_publishing_score_threshold</b></td>
|
||||||
<td>Minimum score threshold for suggestions to be presented as commitable PR comments in addition to the table. Default is -1 (disabled).</td>
|
<td>Minimum score threshold for suggestions to be presented as commitable PR comments in addition to the table. Default is -1 (disabled).</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><b>focus_only_on_problems</b></td>
|
|
||||||
<td>If set to true, suggestions will focus primarily on identifying and fixing code problems, and less on style considerations like best practices, maintainability, or readability. Default is true.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>persistent_comment</b></td>
|
<td><b>persistent_comment</b></td>
|
||||||
<td>If set to true, the improve comment will be persistent, meaning that every new improve request will edit the previous one. Default is false.</td>
|
<td>If set to true, the improve comment will be persistent, meaning that every new improve request will edit the previous one. Default is false.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>self_reflect_on_suggestions</b></td>
|
||||||
|
<td>If set to true, the improve tool will calculate an importance score for each suggestion [1-10], and sort the suggestion labels group based on this score. Default is true.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>suggestions_score_threshold</b></td>
|
<td><b>suggestions_score_threshold</b></td>
|
||||||
<td> Any suggestion with importance score less than this threshold will be removed. Default is 0. Highly recommend not to set this value above 7-8, since above it may clip relevant suggestions that can be useful. </td>
|
<td> Any suggestion with importance score less than this threshold will be removed. Default is 0. Highly recommend not to set this value above 7-8, since above it may clip relevant suggestions that can be useful. </td>
|
||||||
@ -342,10 +303,6 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 500 line
|
|||||||
<td><b>wiki_page_accepted_suggestions</b></td>
|
<td><b>wiki_page_accepted_suggestions</b></td>
|
||||||
<td>If set to true, the tool will automatically track accepted suggestions in a dedicated wiki page called `.pr_agent_accepted_suggestions`. Default is true.</td>
|
<td>If set to true, the tool will automatically track accepted suggestions in a dedicated wiki page called `.pr_agent_accepted_suggestions`. Default is true.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><b>allow_thumbs_up_down</b></td>
|
|
||||||
<td>If set to true, all code suggestions will have thumbs up and thumbs down buttons, to encourage users to provide feedback on the suggestions. Default is false.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
??? example "Params for number of suggestions and AI calls"
|
??? example "Params for number of suggestions and AI calls"
|
||||||
@ -363,6 +320,10 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 500 line
|
|||||||
<td><b>max_number_of_calls</b></td>
|
<td><b>max_number_of_calls</b></td>
|
||||||
<td>Maximum number of chunks. Default is 3.</td>
|
<td>Maximum number of chunks. Default is 3.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>rank_extended_suggestions</b></td>
|
||||||
|
<td>If set to true, the tool will rank the suggestions, based on importance. Default is true.</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## A note on code suggestions quality
|
## A note on code suggestions quality
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
Here is a list of Qodo Merge tools, each with a dedicated page that explains how to use it:
|
Here is a list of Qodo Merge tools, each with a dedicated page that explains how to use it:
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| **[PR Description (`/describe`](./describe.md))** | Automatically generating PR description - title, type, summary, code walkthrough and labels |
|
| **[PR Description (`/describe`](./describe.md))** | Automatically generating PR description - title, type, summary, code walkthrough and labels |
|
||||||
| **[PR Review (`/review`](./review.md))** | Adjustable feedback about the PR, possible issues, security concerns, review effort and more |
|
| **[PR Review (`/review`](./review.md))** | Adjustable feedback about the PR, possible issues, security concerns, review effort and more |
|
||||||
| **[Code Suggestions (`/improve`](./improve.md))** | Code suggestions for improving the PR |
|
| **[Code Suggestions (`/improve`](./improve.md))** | Code suggestions for improving the PR |
|
||||||
@ -14,10 +14,9 @@ Here is a list of Qodo Merge tools, each with a dedicated page that explains how
|
|||||||
| **💎 [Add Documentation (`/add_docs`](./documentation.md))** | Generates documentation to methods/functions/classes that changed in the PR |
|
| **💎 [Add Documentation (`/add_docs`](./documentation.md))** | Generates documentation to methods/functions/classes that changed in the PR |
|
||||||
| **💎 [Generate Custom Labels (`/generate_labels`](./custom_labels.md))** | Generates custom labels for the PR, based on specific guidelines defined by the user |
|
| **💎 [Generate Custom Labels (`/generate_labels`](./custom_labels.md))** | Generates custom labels for the PR, based on specific guidelines defined by the user |
|
||||||
| **💎 [Analyze (`/analyze`](./analyze.md))** | Identify code components that changed in the PR, and enables to interactively generate tests, docs, and code suggestions for each component |
|
| **💎 [Analyze (`/analyze`](./analyze.md))** | Identify code components that changed in the PR, and enables to interactively generate tests, docs, and code suggestions for each component |
|
||||||
| **💎 [Test (`/test`](./test.md))** | generate tests for a selected component, based on the PR code changes |
|
|
||||||
| **💎 [Custom Prompt (`/custom_prompt`](./custom_prompt.md))** | Automatically generates custom suggestions for improving the PR code, based on specific guidelines defined by the user |
|
| **💎 [Custom Prompt (`/custom_prompt`](./custom_prompt.md))** | Automatically generates custom suggestions for improving the PR code, based on specific guidelines defined by the user |
|
||||||
| **💎 [Generate Tests (`/test component_name`](./test.md))** | Automatically generates unit tests for a selected component, based on the PR code changes |
|
| **💎 [Generate Tests (`/test component_name`](./test.md))** | Automatically generates unit tests for a selected component, based on the PR code changes |
|
||||||
| **💎 [Improve Component (`/improve_component component_name`](./improve_component.md))** | Generates code suggestions for a specific code component that changed in the PR |
|
| **💎 [Improve Component (`/improve_component component_name`](./improve_component.md))** | Generates code suggestions for a specific code component that changed in the PR |
|
||||||
| **💎 [CI Feedback (`/checks ci_job`](./ci_feedback.md))** | Automatically generates feedback and analysis for a failed CI job |
|
| **💎 [CI Feedback (`/checks ci_job`](./ci_feedback.md))** | Automatically generates feedback and analysis for a failed CI job |
|
||||||
| **💎 [Implement (`/implement`](./implement.md))** | Generates implementation code from review suggestions |
|
|
||||||
Note that the tools marked with 💎 are available only for Qodo Merge Pro users.
|
Note that the tools marked with 💎 are available only for Qodo Merge Pro users.
|
@ -39,13 +39,54 @@ pr_commands = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[pr_reviewer]
|
[pr_reviewer]
|
||||||
extra_instructions = "..."
|
num_code_suggestions = ...
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
- The `pr_commands` lists commands that will be executed automatically when a PR is opened.
|
- The `pr_commands` lists commands that will be executed automatically when a PR is opened.
|
||||||
- The `[pr_reviewer]` section contains the configurations for the `review` tool you want to edit (if any).
|
- The `[pr_reviewer]` section contains the configurations for the `review` tool you want to edit (if any).
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
[//]: # (### Incremental Mode)
|
||||||
|
|
||||||
|
[//]: # (Incremental review only considers changes since the last Qodo Merge review. This can be useful when working on the PR in an iterative manner, and you want to focus on the changes since the last review instead of reviewing the entire PR again.)
|
||||||
|
|
||||||
|
[//]: # (For invoking the incremental mode, the following command can be used:)
|
||||||
|
|
||||||
|
[//]: # (```)
|
||||||
|
|
||||||
|
[//]: # (/review -i)
|
||||||
|
|
||||||
|
[//]: # (```)
|
||||||
|
|
||||||
|
[//]: # (Note that the incremental mode is only available for GitHub.)
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
[//]: # ({width=512})
|
||||||
|
|
||||||
|
[//]: # (### PR Reflection)
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
[//]: # (By invoking:)
|
||||||
|
|
||||||
|
[//]: # (```)
|
||||||
|
|
||||||
|
[//]: # (/reflect_and_review)
|
||||||
|
|
||||||
|
[//]: # (```)
|
||||||
|
|
||||||
|
[//]: # (The tool will first ask the author questions about the PR, and will guide the review based on their answers.)
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
[//]: # ({width=512})
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
[//]: # ({width=512})
|
||||||
|
|
||||||
|
[//]: # ()
|
||||||
|
[//]: # ({width=512})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration options
|
## Configuration options
|
||||||
|
|
||||||
@ -53,12 +94,16 @@ extra_instructions = "..."
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>persistent_comment</b></td>
|
<td><b>num_code_suggestions</b></td>
|
||||||
<td>If set to true, the review comment will be persistent, meaning that every new review request will edit the previous one. Default is true.</td>
|
<td>Number of code suggestions provided by the 'review' tool. Default is 0, meaning no code suggestions will be provided by the `review` tool.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>final_update_message</b></td>
|
<td><b>inline_code_comments</b></td>
|
||||||
<td>When set to true, updating a persistent review comment during online commenting will automatically add a short comment with a link to the updated review in the pull request .Default is true.</td>
|
<td>If set to true, the tool will publish the code suggestions as comments on the code diff. Default is false. Note that you need to set `num_code_suggestions`>0 to get code suggestions </td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>persistent_comment</b></td>
|
||||||
|
<td>If set to true, the review comment will be persistent, meaning that every new review request will edit the previous one. Default is true.</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>extra_instructions</b></td>
|
<td><b>extra_instructions</b></td>
|
||||||
@ -95,7 +140,7 @@ extra_instructions = "..."
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>require_ticket_analysis_review</b></td>
|
<td><b>require_ticket_analysis_review</b></td>
|
||||||
<td>If set to true, and the PR contains a GitHub or Jira ticket link, the tool will add a section that checks if the PR in fact fulfilled the ticket requirements. Default is true.</td>
|
<td>If set to true, and the PR contains a GitHub ticket number, the tool will add a section that checks if the PR in fact fulfilled the ticket requirements. Default is true.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@ -144,9 +189,9 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
|
|||||||
!!! tip "Automation"
|
!!! tip "Automation"
|
||||||
When you first install Qodo Merge app, the [default mode](../usage-guide/automations_and_usage.md#github-app-automatic-tools-when-a-new-pr-is-opened) for the `review` tool is:
|
When you first install Qodo Merge app, the [default mode](../usage-guide/automations_and_usage.md#github-app-automatic-tools-when-a-new-pr-is-opened) for the `review` tool is:
|
||||||
```
|
```
|
||||||
pr_commands = ["/review", ...]
|
pr_commands = ["/review --pr_reviewer.num_code_suggestions=0", ...]
|
||||||
```
|
```
|
||||||
Meaning the `review` tool will run automatically on every PR, without any additional configurations.
|
Meaning the `review` tool will run automatically on every PR, without providing code suggestions.
|
||||||
Edit this field to enable/disable the tool, or to change the configurations used.
|
Edit this field to enable/disable the tool, or to change the configurations used.
|
||||||
|
|
||||||
!!! tip "Possible labels from the review tool"
|
!!! tip "Possible labels from the review tool"
|
||||||
@ -204,8 +249,13 @@ If enabled, the `review` tool can approve a PR when a specific comment, `/review
|
|||||||
maximal_review_effort = 5
|
maximal_review_effort = 5
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! tip "Code suggestions"
|
[//]: # (!!! tip "Code suggestions")
|
||||||
|
|
||||||
The `review` tool previously included a legacy feature for providing code suggestions (controlled by `--pr_reviewer.num_code_suggestion`). This functionality has been deprecated and replaced by the [`improve`](./improve.md) tool, which offers higher quality and more actionable code suggestions.
|
[//]: # ()
|
||||||
|
[//]: # ( If you set `num_code_suggestions`>0 , the `review` tool will also provide code suggestions.)
|
||||||
|
|
||||||
|
[//]: # ( )
|
||||||
|
[//]: # ( Notice If you are interested **only** in the code suggestions, it is recommended to use the [`improve`](./improve.md) feature instead, since it is a dedicated only to code suggestions, and usually gives better results.)
|
||||||
|
|
||||||
|
[//]: # ( Use the `review` tool if you want to get more comprehensive feedback, which includes code suggestions as well.)
|
||||||
|
|
||||||
|
@ -49,10 +49,9 @@ It can be invoked automatically from the analyze table, can be accessed by:
|
|||||||
/analyze
|
/analyze
|
||||||
```
|
```
|
||||||
Choose the components you want to find similar code for, and click on the `similar` checkbox.
|
Choose the components you want to find similar code for, and click on the `similar` checkbox.
|
||||||
|
|
||||||
{width=768}
|
{width=768}
|
||||||
|
|
||||||
You can search for similar code either within the organization's codebase or globally, which includes open-source repositories. Each result will include the relevant code components along with their associated license details.
|
If you are looking to search for similar code in the organization's codebase, you can click on the `Organization` checkbox, and it will invoke a new search command just for the organization's codebase.
|
||||||
|
|
||||||
{width=768}
|
{width=768}
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ The tool will generate tests for the selected component (if no component is stat
|
|||||||
|
|
||||||
(Example taken from [here](https://github.com/Codium-ai/pr-agent/pull/598#issuecomment-1913679429)):
|
(Example taken from [here](https://github.com/Codium-ai/pr-agent/pull/598#issuecomment-1913679429)):
|
||||||
|
|
||||||
**Notes** <br>
|
**Notes**
|
||||||
- The following languages are currently supported: Python, Java, C++, JavaScript, TypeScript, C#. <br>
|
- Language that are currently supported by the tool: Python, Java, C++, JavaScript, TypeScript, C#.
|
||||||
- This tool can also be triggered interactively by using the [`analyze`](./analyze.md) tool.
|
- This tool can also be triggered interactively by using the [`analyze`](./analyze.md) tool.
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,4 +17,3 @@ Under the section `pr_update_changelog`, the [configuration file](https://github
|
|||||||
|
|
||||||
- `push_changelog_changes`: whether to push the changes to CHANGELOG.md, or just print them. Default is false (print only).
|
- `push_changelog_changes`: whether to push the changes to CHANGELOG.md, or just print them. Default is false (print only).
|
||||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...
|
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...
|
||||||
- `add_pr_link`: whether the model should try to add a link to the PR in the changelog. Default is true.
|
|
@ -1,5 +1,5 @@
|
|||||||
## Show possible configurations
|
## Show possible configurations
|
||||||
The possible configurations of Qodo Merge are stored in [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml){:target="_blank"}.
|
The possible configurations of Qodo Merge are stored in [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml).
|
||||||
In the [tools](https://qodo-merge-docs.qodo.ai/tools/) page you can find explanations on how to use these configurations for each tool.
|
In the [tools](https://qodo-merge-docs.qodo.ai/tools/) page you can find explanations on how to use these configurations for each tool.
|
||||||
|
|
||||||
To print all the available configurations as a comment on your PR, you can use the following command:
|
To print all the available configurations as a comment on your PR, you can use the following command:
|
||||||
@ -138,17 +138,7 @@ LANGSMITH_BASE_URL=<url>
|
|||||||
|
|
||||||
## Ignoring automatic commands in PRs
|
## Ignoring automatic commands in PRs
|
||||||
|
|
||||||
Qodo Merge allows you to automatically ignore certain PRs based on various criteria:
|
In some cases, you may want to automatically ignore specific PRs . Qodo Merge enables you to ignore PR with a specific title, or from/to specific branches (regex matching).
|
||||||
|
|
||||||
- PRs with specific titles (using regex matching)
|
|
||||||
- PRs between specific branches (using regex matching)
|
|
||||||
- PRs that don't include changes from specific folders (using regex matching)
|
|
||||||
- PRs containing specific labels
|
|
||||||
- PRs opened by specific users
|
|
||||||
|
|
||||||
### Example usage
|
|
||||||
|
|
||||||
#### Ignoring PRs with specific titles
|
|
||||||
|
|
||||||
To ignore PRs with a specific title such as "[Bump]: ...", you can add the following to your `configuration.toml` file:
|
To ignore PRs with a specific title such as "[Bump]: ...", you can add the following to your `configuration.toml` file:
|
||||||
|
|
||||||
@ -159,7 +149,6 @@ ignore_pr_title = ["\\[Bump\\]"]
|
|||||||
|
|
||||||
Where the `ignore_pr_title` is a list of regex patterns to match the PR title you want to ignore. Default is `ignore_pr_title = ["^\\[Auto\\]", "^Auto"]`.
|
Where the `ignore_pr_title` is a list of regex patterns to match the PR title you want to ignore. Default is `ignore_pr_title = ["^\\[Auto\\]", "^Auto"]`.
|
||||||
|
|
||||||
#### Ignoring PRs between specific branches
|
|
||||||
|
|
||||||
To ignore PRs from specific source or target branches, you can add the following to your `configuration.toml` file:
|
To ignore PRs from specific source or target branches, you can add the following to your `configuration.toml` file:
|
||||||
|
|
||||||
@ -171,46 +160,3 @@ ignore_pr_target_branches = ["qa"]
|
|||||||
|
|
||||||
Where the `ignore_pr_source_branches` and `ignore_pr_target_branches` are lists of regex patterns to match the source and target branches you want to ignore.
|
Where the `ignore_pr_source_branches` and `ignore_pr_target_branches` are lists of regex patterns to match the source and target branches you want to ignore.
|
||||||
They are not mutually exclusive, you can use them together or separately.
|
They are not mutually exclusive, you can use them together or separately.
|
||||||
|
|
||||||
#### Ignoring PRs that don't include changes from specific folders
|
|
||||||
|
|
||||||
To allow only specific folders (often needed in large monorepos), set:
|
|
||||||
|
|
||||||
```
|
|
||||||
[config]
|
|
||||||
allow_only_specific_folders=['folder1','folder2']
|
|
||||||
```
|
|
||||||
|
|
||||||
For the configuration above, automatic feedback will only be triggered when the PR changes include files from 'folder1' or 'folder2'
|
|
||||||
|
|
||||||
#### Ignoring PRs containg specific labels
|
|
||||||
|
|
||||||
To ignore PRs containg specific labels, you can add the following to your `configuration.toml` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
[config]
|
|
||||||
ignore_pr_labels = ["do-not-merge"]
|
|
||||||
```
|
|
||||||
|
|
||||||
Where the `ignore_pr_labels` is a list of labels that when present in the PR, the PR will be ignored.
|
|
||||||
|
|
||||||
#### Ignoring PRs from specific users
|
|
||||||
|
|
||||||
Qodo Merge automatically identifies and ignores pull requests created by bots using:
|
|
||||||
|
|
||||||
- GitHub's native bot detection system
|
|
||||||
- Name-based pattern matching
|
|
||||||
|
|
||||||
While this detection is robust, it may not catch all cases, particularly when:
|
|
||||||
|
|
||||||
- Bots are registered as regular user accounts
|
|
||||||
- Bot names don't match common patterns
|
|
||||||
|
|
||||||
To supplement the automatic bot detection, you can manually specify users to ignore. Add the following to your `configuration.toml` file to ignore PRs from specific users:
|
|
||||||
```
|
|
||||||
[config]
|
|
||||||
ignore_pr_authors = ["my-special-bot-user", ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
Where the `ignore_pr_authors` is a list of usernames that you want to ignore.
|
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
## Local repo (CLI)
|
## Local repo (CLI)
|
||||||
|
|
||||||
When running from your locally cloned Qodo Merge repo (CLI), your local configuration file will be used.
|
When running from your locally cloned Qodo Merge repo (CLI), your local configuration file will be used.
|
||||||
Examples of invoking the different tools via the CLI:
|
Examples of invoking the different tools via the CLI:
|
||||||
|
|
||||||
@ -36,29 +35,9 @@ This is useful for debugging or experimenting with different tools.
|
|||||||
|
|
||||||
Default is "github".
|
Default is "github".
|
||||||
|
|
||||||
### CLI Health Check
|
|
||||||
To verify that Qodo Merge has been configured correctly, you can run this health check command from the repository root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m tests.health_test.main
|
|
||||||
```
|
|
||||||
|
|
||||||
If the health check passes, you will see the following output:
|
### Online usage
|
||||||
|
|
||||||
```
|
|
||||||
========
|
|
||||||
Health test passed successfully
|
|
||||||
========
|
|
||||||
```
|
|
||||||
|
|
||||||
At the end of the run.
|
|
||||||
|
|
||||||
Before running the health check, ensure you have:
|
|
||||||
|
|
||||||
- Configured your [LLM provider](https://qodo-merge-docs.qodo.ai/usage-guide/changing_a_model/)
|
|
||||||
- Added a valid GitHub token to your configuration file
|
|
||||||
|
|
||||||
## Online usage
|
|
||||||
|
|
||||||
Online usage means invoking Qodo Merge tools by [comments](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901) on a PR.
|
Online usage means invoking Qodo Merge tools by [comments](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901) on a PR.
|
||||||
Commands for invoking the different tools via comments:
|
Commands for invoking the different tools via comments:
|
||||||
@ -79,80 +58,59 @@ For example, if you want to edit the `review` tool configurations, you can run:
|
|||||||
Any configuration value in [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) file can be similarly edited. Comment `/config` to see the list of available configurations.
|
Any configuration value in [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) file can be similarly edited. Comment `/config` to see the list of available configurations.
|
||||||
|
|
||||||
|
|
||||||
## Qodo Merge Automatic Feedback
|
## GitHub App
|
||||||
|
|
||||||
|
|
||||||
### Disabling all automatic feedback
|
|
||||||
|
|
||||||
To easily disable all automatic feedback from Qodo Merge (GitHub App, GitLab Webhook, BitBucket App, Azure DevOps Webhook), set in a configuration file:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[config]
|
|
||||||
disable_auto_feedback = true
|
|
||||||
```
|
|
||||||
|
|
||||||
When this parameter is set to `true`, Qodo Merge will not run any automatic tools (like `describe`, `review`, `improve`) when a new PR is opened, or when new code is pushed to an open PR.
|
|
||||||
|
|
||||||
### GitHub App
|
|
||||||
|
|
||||||
!!! note "Configurations for Qodo Merge Pro"
|
!!! note "Configurations for Qodo Merge Pro"
|
||||||
Qodo Merge Pro for GitHub is an App, hosted by CodiumAI. So all the instructions below are relevant also for Qodo Merge Pro users.
|
Qodo Merge Pro for GitHub is an App, hosted by CodiumAI. So all the instructions below are relevant also for Qodo Merge Pro users.
|
||||||
Same goes for [GitLab webhook](#gitlab-webhook) and [BitBucket App](#bitbucket-app) sections.
|
Same goes for [GitLab webhook](#gitlab-webhook) and [BitBucket App](#bitbucket-app) sections.
|
||||||
|
|
||||||
#### GitHub app automatic tools when a new PR is opened
|
### GitHub app automatic tools when a new PR is opened
|
||||||
|
|
||||||
The [github_app](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L220) section defines GitHub app specific configurations.
|
The [github_app](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L108) section defines GitHub app specific configurations.
|
||||||
|
|
||||||
The configuration parameter `pr_commands` defines the list of tools that will be **run automatically** when a new PR is opened:
|
The configuration parameter `pr_commands` defines the list of tools that will be **run automatically** when a new PR is opened.
|
||||||
```toml
|
```
|
||||||
[github_app]
|
[github_app]
|
||||||
pr_commands = [
|
pr_commands = [
|
||||||
"/describe",
|
"/describe --pr_description.final_update_message=false",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
"/improve",
|
"/improve",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
This means that when a new PR is opened/reopened or marked as ready for review, Qodo Merge will run the `describe`, `review` and `improve` tools.
|
This means that when a new PR is opened/reopened or marked as ready for review, Qodo Merge will run the `describe`, `review` and `improve` tools.
|
||||||
|
For the `review` tool, for example, the `num_code_suggestions` parameter will be set to 0.
|
||||||
|
|
||||||
You can override the default tool parameters by using one the three options for a [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/): **wiki**, **local**, or **global**.
|
You can override the default tool parameters by using one the three options for a [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/): **wiki**, **local**, or **global**.
|
||||||
For example, if your configuration file contains:
|
For example, if your local `.pr_agent.toml` file contains:
|
||||||
|
```
|
||||||
```toml
|
|
||||||
[pr_description]
|
[pr_description]
|
||||||
generate_ai_title = true
|
generate_ai_title = true
|
||||||
```
|
```
|
||||||
|
Every time you run the `describe` tool, including automatic runs, the PR title will be generated by the AI.
|
||||||
|
|
||||||
Every time you run the `describe` tool (including automatic runs) the PR title will be generated by the AI.
|
To cancel the automatic run of all the tools, set:
|
||||||
|
```
|
||||||
You can customize configurations specifically for automated runs by using the `--config_path=<value>` parameter.
|
|
||||||
For instance, to modify the `review` tool settings only for newly opened PRs, use:
|
|
||||||
```toml
|
|
||||||
[github_app]
|
[github_app]
|
||||||
pr_commands = [
|
pr_commands = []
|
||||||
"/describe",
|
|
||||||
"/review --pr_reviewer.extra_instructions='focus on the file: ...'",
|
|
||||||
"/improve",
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### GitHub app automatic tools for push actions (commits to an open PR)
|
### GitHub app automatic tools for push actions (commits to an open PR)
|
||||||
|
|
||||||
In addition to running automatic tools when a PR is opened, the GitHub app can also respond to new code that is pushed to an open PR.
|
In addition to running automatic tools when a PR is opened, the GitHub app can also respond to new code that is pushed to an open PR.
|
||||||
|
|
||||||
The configuration toggle `handle_push_trigger` can be used to enable this feature.
|
The configuration toggle `handle_push_trigger` can be used to enable this feature.
|
||||||
The configuration parameter `push_commands` defines the list of tools that will be **run automatically** when new code is pushed to the PR.
|
The configuration parameter `push_commands` defines the list of tools that will be **run automatically** when new code is pushed to the PR.
|
||||||
```toml
|
```
|
||||||
[github_app]
|
[github_app]
|
||||||
handle_push_trigger = true
|
handle_push_trigger = true
|
||||||
push_commands = [
|
push_commands = [
|
||||||
"/describe",
|
"/describe",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0 --pr_reviewer.final_update_message=false",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
This means that when new code is pushed to the PR, the Qodo Merge will run the `describe` and `review` tools, with the specified parameters.
|
This means that when new code is pushed to the PR, the Qodo Merge will run the `describe` and `review` tools, with the specified parameters.
|
||||||
|
|
||||||
### GitHub Action
|
## GitHub Action
|
||||||
`GitHub Action` is a different way to trigger Qodo Merge tools, and uses a different configuration mechanism than `GitHub App`.<br>
|
`GitHub Action` is a different way to trigger Qodo Merge tools, and uses a different configuration mechanism than `GitHub App`.<br>
|
||||||
You can configure settings for `GitHub Action` by adding environment variables under the env section in `.github/workflows/pr_agent.yml` file.
|
You can configure settings for `GitHub Action` by adding environment variables under the env section in `.github/workflows/pr_agent.yml` file.
|
||||||
Specifically, start by setting the following environment variables:
|
Specifically, start by setting the following environment variables:
|
||||||
@ -163,7 +121,7 @@ Specifically, start by setting the following environment variables:
|
|||||||
github_action_config.auto_review: "true" # enable\disable auto review
|
github_action_config.auto_review: "true" # enable\disable auto review
|
||||||
github_action_config.auto_describe: "true" # enable\disable auto describe
|
github_action_config.auto_describe: "true" # enable\disable auto describe
|
||||||
github_action_config.auto_improve: "true" # enable\disable auto improve
|
github_action_config.auto_improve: "true" # enable\disable auto improve
|
||||||
github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested"]'
|
github_action_config.pr_actions: ["opened", "reopened", "ready_for_review", "review_requested"]
|
||||||
```
|
```
|
||||||
`github_action_config.auto_review`, `github_action_config.auto_describe` and `github_action_config.auto_improve` are used to enable/disable automatic tools that run when a new PR is opened.
|
`github_action_config.auto_review`, `github_action_config.auto_describe` and `github_action_config.auto_improve` are used to enable/disable automatic tools that run when a new PR is opened.
|
||||||
If not set, the default configuration is for all three tools to run automatically when a new PR is opened.
|
If not set, the default configuration is for all three tools to run automatically when a new PR is opened.
|
||||||
@ -178,22 +136,19 @@ The JSON structure is equivalent to the yaml data structure defined in [pr_revie
|
|||||||
Note that you can give additional config parameters by adding environment variables to `.github/workflows/pr_agent.yml`, or by using a `.pr_agent.toml` [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#global-configuration-file) in the root of your repo
|
Note that you can give additional config parameters by adding environment variables to `.github/workflows/pr_agent.yml`, or by using a `.pr_agent.toml` [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#global-configuration-file) in the root of your repo
|
||||||
|
|
||||||
For example, you can set an environment variable: `pr_description.publish_labels=false`, or add a `.pr_agent.toml` file with the following content:
|
For example, you can set an environment variable: `pr_description.publish_labels=false`, or add a `.pr_agent.toml` file with the following content:
|
||||||
|
```
|
||||||
```toml
|
|
||||||
[pr_description]
|
[pr_description]
|
||||||
publish_labels = false
|
publish_labels = false
|
||||||
```
|
```
|
||||||
|
|
||||||
to prevent Qodo Merge from publishing labels when running the `describe` tool.
|
to prevent Qodo Merge from publishing labels when running the `describe` tool.
|
||||||
|
|
||||||
### GitLab Webhook
|
## GitLab Webhook
|
||||||
After setting up a GitLab webhook, to control which commands will run automatically when a new MR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:
|
After setting up a GitLab webhook, to control which commands will run automatically when a new MR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:
|
||||||
|
```
|
||||||
```toml
|
|
||||||
[gitlab]
|
[gitlab]
|
||||||
pr_commands = [
|
pr_commands = [
|
||||||
"/describe",
|
"/describe",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
"/improve",
|
"/improve",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
@ -201,24 +156,24 @@ pr_commands = [
|
|||||||
the GitLab webhook can also respond to new code that is pushed to an open MR.
|
the GitLab webhook can also respond to new code that is pushed to an open MR.
|
||||||
The configuration toggle `handle_push_trigger` can be used to enable this feature.
|
The configuration toggle `handle_push_trigger` can be used to enable this feature.
|
||||||
The configuration parameter `push_commands` defines the list of tools that will be **run automatically** when new code is pushed to the MR.
|
The configuration parameter `push_commands` defines the list of tools that will be **run automatically** when new code is pushed to the MR.
|
||||||
```toml
|
```
|
||||||
[gitlab]
|
[gitlab]
|
||||||
handle_push_trigger = true
|
handle_push_trigger = true
|
||||||
push_commands = [
|
push_commands = [
|
||||||
"/describe",
|
"/describe",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0 --pr_reviewer.final_update_message=false",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that to use the 'handle_push_trigger' feature, you need to give the gitlab webhook also the "Push events" scope.
|
Note that to use the 'handle_push_trigger' feature, you need to give the gitlab webhook also the "Push events" scope.
|
||||||
|
|
||||||
### BitBucket App
|
## BitBucket App
|
||||||
Similar to GitHub app, when running Qodo Merge from BitBucket App, the default [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded.
|
Similar to GitHub app, when running Qodo Merge from BitBucket App, the default [configuration file](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml) from a pre-built docker will be initially loaded.
|
||||||
|
|
||||||
By uploading a local `.pr_agent.toml` file to the root of the repo's main branch, you can edit and customize any configuration parameter. Note that you need to upload `.pr_agent.toml` prior to creating a PR, in order for the configuration to take effect.
|
By uploading a local `.pr_agent.toml` file to the root of the repo's main branch, you can edit and customize any configuration parameter. Note that you need to upload `.pr_agent.toml` prior to creating a PR, in order for the configuration to take effect.
|
||||||
|
|
||||||
For example, if your local `.pr_agent.toml` file contains:
|
For example, if your local `.pr_agent.toml` file contains:
|
||||||
```toml
|
```
|
||||||
[pr_reviewer]
|
[pr_reviewer]
|
||||||
extra_instructions = "Answer in japanese"
|
extra_instructions = "Answer in japanese"
|
||||||
```
|
```
|
||||||
@ -227,39 +182,29 @@ Each time you invoke a `/review` tool, it will use the extra instructions you se
|
|||||||
|
|
||||||
|
|
||||||
Note that among other limitations, BitBucket provides relatively low rate-limits for applications (up to 1000 requests per hour), and does not provide an API to track the actual rate-limit usage.
|
Note that among other limitations, BitBucket provides relatively low rate-limits for applications (up to 1000 requests per hour), and does not provide an API to track the actual rate-limit usage.
|
||||||
If you experience a lack of responses from Qodo Merge, you might want to set: `bitbucket_app.avoid_full_files=true` in your configuration file.
|
If you experience lack of responses from Qodo Merge, you might want to set: `bitbucket_app.avoid_full_files=true` in your configuration file.
|
||||||
This will prevent Qodo Merge from acquiring the full file content, and will only use the diff content. This will reduce the number of requests made to BitBucket, at the cost of small decrease in accuracy, as dynamic context will not be applicable.
|
This will prevent Qodo Merge from acquiring the full file content, and will only use the diff content. This will reduce the number of requests made to BitBucket, at the cost of small decrease in accuracy, as dynamic context will not be applicable.
|
||||||
|
|
||||||
|
|
||||||
#### BitBucket Self-Hosted App automatic tools
|
### BitBucket Self-Hosted App automatic tools
|
||||||
|
|
||||||
To control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file:
|
To control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file:
|
||||||
Specifically, set the following values:
|
Specifically, set the following values:
|
||||||
|
|
||||||
```toml
|
```
|
||||||
[bitbucket_app]
|
[bitbucket_app]
|
||||||
pr_commands = [
|
pr_commands = [
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
"/improve --pr_code_suggestions.commitable_code_suggestions=true --pr_code_suggestions.suggestions_score_threshold=7",
|
"/improve --pr_code_suggestions.commitable_code_suggestions=true --pr_code_suggestions.suggestions_score_threshold=7",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
Note that we set specifically for bitbucket, we recommend using: `--pr_code_suggestions.suggestions_score_threshold=7` and that is the default value we set for bitbucket.
|
Note that we set specifically for bitbucket, we recommend using: `--pr_code_suggestions.suggestions_score_threshold=7` and that is the default value we set for bitbucket.
|
||||||
Since this platform only supports inline code suggestions, we want to limit the number of suggestions, and only present a limited number.
|
Since this platform only supports inline code suggestions, we want to limit the number of suggestions, and only present a limited number.
|
||||||
|
|
||||||
To enable BitBucket app to respond to each **push** to the PR, set (for example):
|
## Azure DevOps provider
|
||||||
```toml
|
|
||||||
[bitbucket_app]
|
|
||||||
handle_push_trigger = true
|
|
||||||
push_commands = [
|
|
||||||
"/describe",
|
|
||||||
"/review",
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Azure DevOps provider
|
|
||||||
|
|
||||||
To use Azure DevOps provider use the following settings in configuration.toml:
|
To use Azure DevOps provider use the following settings in configuration.toml:
|
||||||
```toml
|
```
|
||||||
[config]
|
[config]
|
||||||
git_provider="azure"
|
git_provider="azure"
|
||||||
```
|
```
|
||||||
@ -278,14 +223,14 @@ org = "https://dev.azure.com/YOUR_ORGANIZATION/"
|
|||||||
# pat = "YOUR_PAT_TOKEN" needed only if using PAT for authentication
|
# pat = "YOUR_PAT_TOKEN" needed only if using PAT for authentication
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Azure DevOps Webhook
|
### Azure DevOps Webhook
|
||||||
|
|
||||||
To control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:
|
To control which commands will run automatically when a new PR is opened, you can set the `pr_commands` parameter in the configuration file, similar to the GitHub App:
|
||||||
```toml
|
```
|
||||||
[azure_devops_server]
|
[azure_devops_server]
|
||||||
pr_commands = [
|
pr_commands = [
|
||||||
"/describe",
|
"/describe",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
"/improve",
|
"/improve",
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
@ -5,6 +5,7 @@ To use a different model than the default (GPT-4), you need to edit in the [conf
|
|||||||
```
|
```
|
||||||
[config]
|
[config]
|
||||||
model = "..."
|
model = "..."
|
||||||
|
model_turbo = "..."
|
||||||
fallback_models = ["..."]
|
fallback_models = ["..."]
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -26,43 +27,57 @@ deployment_id = "" # The deployment name you chose when you deployed the engine
|
|||||||
and set in your configuration file:
|
and set in your configuration file:
|
||||||
```
|
```
|
||||||
[config]
|
[config]
|
||||||
model="" # the OpenAI model you've deployed on Azure (e.g. gpt-4o)
|
model="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
|
||||||
fallback_models=["..."]
|
model_turbo="" # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
|
||||||
|
fallback_models=["..."] # the OpenAI model you've deployed on Azure (e.g. gpt-3.5-turbo)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ollama
|
|
||||||
|
|
||||||
You can run 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 model locally via Ollama, set in `.secrets.toml` or in a configuration file:
|
|
||||||
```
|
|
||||||
[config]
|
|
||||||
model = "ollama/qwen2.5-coder:32b"
|
|
||||||
fallback_models=["ollama/qwen2.5-coder:32b"]
|
|
||||||
custom_model_max_tokens=128000 # set the maximal input tokens for the model
|
|
||||||
duplicate_examples=true # will duplicate the examples in the prompt, to help the model to generate structured output
|
|
||||||
|
|
||||||
[ollama]
|
|
||||||
api_base = "http://localhost:11434" # or whatever port you're running Ollama on
|
|
||||||
```
|
|
||||||
|
|
||||||
!!! note "Local models vs commercial models"
|
|
||||||
Qodo Merge is compatible with almost any AI model, but analyzing complex code repositories and pull requests requires a model specifically optimized for code analysis.
|
|
||||||
|
|
||||||
Commercial models such as GPT-4, Claude Sonnet, and Gemini have demonstrated robust capabilities in generating structured output for code analysis tasks with large input. In contrast, most open-source models currently available (as of January 2025) face challenges with these complex tasks.
|
|
||||||
|
|
||||||
Based on our testing, local open-source models are suitable for experimentation and learning purposes, but they are not suitable for production-level code analysis tasks.
|
|
||||||
|
|
||||||
Hence, for production workflows and real-world usage, we recommend using commercial models.
|
|
||||||
|
|
||||||
### Hugging Face
|
### Hugging Face
|
||||||
|
|
||||||
|
**Local**
|
||||||
|
You can run Hugging Face 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 Hugging Face model locally via Ollama, set:
|
||||||
|
```
|
||||||
|
[__init__.py]
|
||||||
|
MAX_TOKENS = {
|
||||||
|
"model-name-on-ollama": <max_tokens>
|
||||||
|
}
|
||||||
|
e.g.
|
||||||
|
MAX_TOKENS={
|
||||||
|
...,
|
||||||
|
"ollama/llama2": 4096
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[config] # in configuration.toml
|
||||||
|
model = "ollama/llama2"
|
||||||
|
model_turbo = "ollama/llama2"
|
||||||
|
fallback_models=["ollama/llama2"]
|
||||||
|
|
||||||
|
[ollama] # in .secrets.toml
|
||||||
|
api_base = ... # the base url for your Hugging Face inference endpoint
|
||||||
|
# e.g. if running Ollama locally, you may use:
|
||||||
|
api_base = "http://localhost:11434/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inference Endpoints
|
||||||
|
|
||||||
To use a new model with Hugging Face Inference Endpoints, for example, set:
|
To use a new model with Hugging Face 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
|
[config] # in configuration.toml
|
||||||
model = "huggingface/meta-llama/Llama-2-7b-chat-hf"
|
model = "huggingface/meta-llama/Llama-2-7b-chat-hf"
|
||||||
|
model_turbo = "huggingface/meta-llama/Llama-2-7b-chat-hf"
|
||||||
fallback_models=["huggingface/meta-llama/Llama-2-7b-chat-hf"]
|
fallback_models=["huggingface/meta-llama/Llama-2-7b-chat-hf"]
|
||||||
custom_model_max_tokens=... # set the maximal input tokens for the model
|
|
||||||
|
|
||||||
[huggingface] # in .secrets.toml
|
[huggingface] # in .secrets.toml
|
||||||
key = ... # your Hugging Face api key
|
key = ... # your Hugging Face api key
|
||||||
@ -76,6 +91,7 @@ To use Llama2 model with Replicate, for example, set:
|
|||||||
```
|
```
|
||||||
[config] # in configuration.toml
|
[config] # in configuration.toml
|
||||||
model = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
|
model = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
|
||||||
|
model_turbo = "replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"
|
||||||
fallback_models=["replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"]
|
fallback_models=["replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1"]
|
||||||
[replicate] # in .secrets.toml
|
[replicate] # in .secrets.toml
|
||||||
key = ...
|
key = ...
|
||||||
@ -91,6 +107,7 @@ To use Llama3 model with Groq, for example, set:
|
|||||||
```
|
```
|
||||||
[config] # in configuration.toml
|
[config] # in configuration.toml
|
||||||
model = "llama3-70b-8192"
|
model = "llama3-70b-8192"
|
||||||
|
model_turbo = "llama3-70b-8192"
|
||||||
fallback_models = ["groq/llama3-70b-8192"]
|
fallback_models = ["groq/llama3-70b-8192"]
|
||||||
[groq] # in .secrets.toml
|
[groq] # in .secrets.toml
|
||||||
key = ... # your Groq api key
|
key = ... # your Groq api key
|
||||||
@ -104,6 +121,7 @@ To use Google's Vertex AI platform and its associated models (chat-bison/codecha
|
|||||||
```
|
```
|
||||||
[config] # in configuration.toml
|
[config] # in configuration.toml
|
||||||
model = "vertex_ai/codechat-bison"
|
model = "vertex_ai/codechat-bison"
|
||||||
|
model_turbo = "vertex_ai/codechat-bison"
|
||||||
fallback_models="vertex_ai/codechat-bison"
|
fallback_models="vertex_ai/codechat-bison"
|
||||||
|
|
||||||
[vertexai] # in .secrets.toml
|
[vertexai] # in .secrets.toml
|
||||||
@ -115,28 +133,13 @@ Your [application default credentials](https://cloud.google.com/docs/authenticat
|
|||||||
|
|
||||||
If you do want to set explicit credentials, then you can use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable set to a path to a json credentials file.
|
If you do want to set explicit credentials, then you can use the `GOOGLE_APPLICATION_CREDENTIALS` environment variable set to a path to a json credentials file.
|
||||||
|
|
||||||
### Google AI Studio
|
|
||||||
|
|
||||||
To use [Google AI Studio](https://aistudio.google.com/) models, set the relevant models in the configuration section of the configuration file:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[config] # in configuration.toml
|
|
||||||
model="google_ai_studio/gemini-1.5-flash"
|
|
||||||
fallback_models=["google_ai_studio/gemini-1.5-flash"]
|
|
||||||
|
|
||||||
[google_ai_studio] # in .secrets.toml
|
|
||||||
gemini_api_key = "..."
|
|
||||||
```
|
|
||||||
|
|
||||||
If you don't want to set the API key in the .secrets.toml file, you can set the `GOOGLE_AI_STUDIO.GEMINI_API_KEY` environment variable.
|
|
||||||
|
|
||||||
### Anthropic
|
### Anthropic
|
||||||
|
|
||||||
To use Anthropic models, set the relevant models in the configuration section of the configuration file:
|
To use Anthropic models, set the relevant models in the configuration section of the configuration file:
|
||||||
|
|
||||||
```
|
```
|
||||||
[config]
|
[config]
|
||||||
model="anthropic/claude-3-opus-20240229"
|
model="anthropic/claude-3-opus-20240229"
|
||||||
|
model_turbo="anthropic/claude-3-opus-20240229"
|
||||||
fallback_models=["anthropic/claude-3-opus-20240229"]
|
fallback_models=["anthropic/claude-3-opus-20240229"]
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -153,6 +156,7 @@ To use Amazon Bedrock and its foundational models, add the below configuration:
|
|||||||
```
|
```
|
||||||
[config] # in configuration.toml
|
[config] # in configuration.toml
|
||||||
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
|
model="bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
|
||||||
|
model_turbo="bedrock/anthropic.claude-3-sonnet-20240229-v1:0"
|
||||||
fallback_models=["bedrock/anthropic.claude-v2:1"]
|
fallback_models=["bedrock/anthropic.claude-v2:1"]
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -166,25 +170,6 @@ drop_params = true
|
|||||||
|
|
||||||
AWS session is automatically authenticated from your environment, but you can also explicitly set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION_NAME` environment variables. Please refer to [this document](https://litellm.vercel.app/docs/providers/bedrock) for more details.
|
AWS session is automatically authenticated from your environment, but you can also explicitly set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION_NAME` environment variables. Please refer to [this document](https://litellm.vercel.app/docs/providers/bedrock) for more details.
|
||||||
|
|
||||||
### DeepSeek
|
|
||||||
|
|
||||||
To use deepseek-chat model with DeepSeek, for example, set:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[config] # in configuration.toml
|
|
||||||
model = "deepseek/deepseek-chat"
|
|
||||||
fallback_models=["deepseek/deepseek-chat"]
|
|
||||||
```
|
|
||||||
|
|
||||||
and fill up your key
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[deepseek] # in .secrets.toml
|
|
||||||
key = ...
|
|
||||||
```
|
|
||||||
|
|
||||||
(you can obtain a deepseek-chat key from [here](https://platform.deepseek.com))
|
|
||||||
|
|
||||||
### Custom models
|
### Custom models
|
||||||
|
|
||||||
If the relevant model doesn't appear [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/algo/__init__.py), you can still use it as a custom model:
|
If the relevant model doesn't appear [here](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/algo/__init__.py), you can still use it as a custom model:
|
||||||
@ -193,6 +178,7 @@ If the relevant model doesn't appear [here](https://github.com/Codium-ai/pr-agen
|
|||||||
```
|
```
|
||||||
[config]
|
[config]
|
||||||
model="custom_model_name"
|
model="custom_model_name"
|
||||||
|
model_turbo="custom_model_name"
|
||||||
fallback_models=["custom_model_name"]
|
fallback_models=["custom_model_name"]
|
||||||
```
|
```
|
||||||
(2) Set the maximal tokens for the model:
|
(2) Set the maximal tokens for the model:
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
`Supported Git Platforms: GitHub, GitLab, Bitbucket`
|
|
||||||
|
|
||||||
|
|
||||||
For optimal functionality of Qodo Merge, we recommend enabling a wiki for each repository where Qodo Merge is installed. The wiki serves several important purposes:
|
|
||||||
|
|
||||||
**Key Wiki Features:**
|
|
||||||
|
|
||||||
- Storing a [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/#wiki-configuration-file)
|
|
||||||
- Defining a [`best_practices.md`](https://qodo-merge-docs.qodo.ai/tools/improve/#best-practices) file
|
|
||||||
- Track [accepted suggestions](https://qodo-merge-docs.qodo.ai/tools/improve/#suggestion-tracking)
|
|
||||||
- Facilitates learning over time by creating an [auto_best_practices.md]() file
|
|
||||||
|
|
||||||
|
|
||||||
**Setup Instructions (GitHub):**
|
|
||||||
|
|
||||||
To enable a wiki for your repository:
|
|
||||||
|
|
||||||
1. Navigate to your repository's main page on GitHub
|
|
||||||
2. Select "Settings" from the top navigation bar
|
|
||||||
3. Locate the "Features" section
|
|
||||||
4. Enable the "Wikis" option by checking the corresponding box
|
|
||||||
5. Return to your repository's main page
|
|
||||||
6. Look for the newly added "Wiki" tab in the top navigation
|
|
||||||
7. Initialize your wiki by clicking "Create the first page" (this step is important - without creating an initial page, the wiki will not be fully functional)
|
|
@ -5,7 +5,6 @@ It includes information on how to adjust Qodo Merge configurations, define which
|
|||||||
|
|
||||||
|
|
||||||
- [Introduction](./introduction.md)
|
- [Introduction](./introduction.md)
|
||||||
- [Enabling a Wiki](./enabling_a_wiki)
|
|
||||||
- [Configuration File](./configuration_options.md)
|
- [Configuration File](./configuration_options.md)
|
||||||
- [Usage and Automation](./automations_and_usage.md)
|
- [Usage and Automation](./automations_and_usage.md)
|
||||||
- [Local Repo (CLI)](./automations_and_usage.md#local-repo-cli)
|
- [Local Repo (CLI)](./automations_and_usage.md#local-repo-cli)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
After [installation](https://qodo-merge-docs.qodo.ai/installation/), there are three basic ways to invoke Qodo Merge:
|
After [installation](https://qodo-merge-docs.qodo.ai/installation/), there are three basic ways to invoke Qodo Merge:
|
||||||
|
|
||||||
1. Locally running a CLI command
|
1. Locally running a CLI command
|
||||||
2. Online usage - by [commenting](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901){:target="_blank"} on a PR
|
2. Online usage - by [commenting](https://github.com/Codium-ai/pr-agent/pull/229#issuecomment-1695021901) on a PR
|
||||||
3. Enabling Qodo Merge tools to run automatically when a new PR is opened
|
3. Enabling Qodo Merge tools to run automatically when a new PR is opened
|
||||||
|
|
||||||
|
|
||||||
@ -10,3 +10,4 @@ Specifically, CLI commands can be issued by invoking a pre-built [docker image](
|
|||||||
|
|
||||||
For online usage, you will need to setup either a [GitHub App](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-app) or a [GitHub Action](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-action) (GitHub), a [GitLab webhook](https://qodo-merge-docs.qodo.ai/installation/gitlab/#run-a-gitlab-webhook-server) (GitLab), or a [BitBucket App](https://qodo-merge-docs.qodo.ai/installation/bitbucket/#run-using-codiumai-hosted-bitbucket-app) (BitBucket).
|
For online usage, you will need to setup either a [GitHub App](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-app) or a [GitHub Action](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-action) (GitHub), a [GitLab webhook](https://qodo-merge-docs.qodo.ai/installation/gitlab/#run-a-gitlab-webhook-server) (GitLab), or a [BitBucket App](https://qodo-merge-docs.qodo.ai/installation/bitbucket/#run-using-codiumai-hosted-bitbucket-app) (BitBucket).
|
||||||
These platforms also enable to run Qodo Merge specific tools automatically when a new PR is opened, or on each push to a branch.
|
These platforms also enable to run Qodo Merge specific tools automatically when a new PR is opened, or on each push to a branch.
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ nav:
|
|||||||
- Usage Guide:
|
- Usage Guide:
|
||||||
- 'usage-guide/index.md'
|
- 'usage-guide/index.md'
|
||||||
- Introduction: 'usage-guide/introduction.md'
|
- Introduction: 'usage-guide/introduction.md'
|
||||||
- Enabling a Wiki: 'usage-guide/enabling_a_wiki.md'
|
|
||||||
- Configuration File: 'usage-guide/configuration_options.md'
|
- Configuration File: 'usage-guide/configuration_options.md'
|
||||||
- Usage and Automation: 'usage-guide/automations_and_usage.md'
|
- Usage and Automation: 'usage-guide/automations_and_usage.md'
|
||||||
- Managing Mail Notifications: 'usage-guide/mail_notifications.md'
|
- Managing Mail Notifications: 'usage-guide/mail_notifications.md'
|
||||||
@ -42,10 +41,8 @@ nav:
|
|||||||
- 💎 Custom Prompt: 'tools/custom_prompt.md'
|
- 💎 Custom Prompt: 'tools/custom_prompt.md'
|
||||||
- 💎 CI Feedback: 'tools/ci_feedback.md'
|
- 💎 CI Feedback: 'tools/ci_feedback.md'
|
||||||
- 💎 Similar Code: 'tools/similar_code.md'
|
- 💎 Similar Code: 'tools/similar_code.md'
|
||||||
- 💎 Implement: 'tools/implement.md'
|
|
||||||
- Core Abilities:
|
- Core Abilities:
|
||||||
- 'core-abilities/index.md'
|
- 'core-abilities/index.md'
|
||||||
- Fetching ticket context: 'core-abilities/fetching_ticket_context.md'
|
|
||||||
- Local and global metadata: 'core-abilities/metadata.md'
|
- Local and global metadata: 'core-abilities/metadata.md'
|
||||||
- Dynamic context: 'core-abilities/dynamic_context.md'
|
- Dynamic context: 'core-abilities/dynamic_context.md'
|
||||||
- Self-reflection: 'core-abilities/self_reflection.md'
|
- Self-reflection: 'core-abilities/self_reflection.md'
|
||||||
|
@ -3,5 +3,5 @@
|
|||||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||||
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
})(window,document,'script','dataLayer','GTM-M6PJSFV');</script>
|
})(window,document,'script','dataLayer','GTM-5C9KZBM3');</script>
|
||||||
<!-- End Google Tag Manager -->
|
<!-- End Google Tag Manager -->
|
@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
@ -3,6 +3,7 @@ from functools import partial
|
|||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
|
|
||||||
from pr_agent.algo.utils import update_settings_from_args
|
from pr_agent.algo.utils import update_settings_from_args
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.utils import apply_repo_settings
|
from pr_agent.git_providers.utils import apply_repo_settings
|
||||||
@ -13,6 +14,7 @@ from pr_agent.tools.pr_config import PRConfig
|
|||||||
from pr_agent.tools.pr_description import PRDescription
|
from pr_agent.tools.pr_description import PRDescription
|
||||||
from pr_agent.tools.pr_generate_labels import PRGenerateLabels
|
from pr_agent.tools.pr_generate_labels import PRGenerateLabels
|
||||||
from pr_agent.tools.pr_help_message import PRHelpMessage
|
from pr_agent.tools.pr_help_message import PRHelpMessage
|
||||||
|
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
|
||||||
from pr_agent.tools.pr_line_questions import PR_LineQuestions
|
from pr_agent.tools.pr_line_questions import PR_LineQuestions
|
||||||
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
|
||||||
@ -24,6 +26,8 @@ command2class = {
|
|||||||
"answer": PRReviewer,
|
"answer": PRReviewer,
|
||||||
"review": PRReviewer,
|
"review": PRReviewer,
|
||||||
"review_pr": PRReviewer,
|
"review_pr": PRReviewer,
|
||||||
|
"reflect": PRInformationFromUser,
|
||||||
|
"reflect_and_review": PRInformationFromUser,
|
||||||
"describe": PRDescription,
|
"describe": PRDescription,
|
||||||
"describe_pr": PRDescription,
|
"describe_pr": PRDescription,
|
||||||
"improve": PRCodeSuggestions,
|
"improve": PRCodeSuggestions,
|
||||||
@ -46,6 +50,7 @@ commands = list(command2class.keys())
|
|||||||
class PRAgent:
|
class PRAgent:
|
||||||
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
|
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
|
||||||
self.ai_handler = ai_handler # will be initialized in run_action
|
self.ai_handler = ai_handler # will be initialized in run_action
|
||||||
|
self.forbidden_cli_args = ['enable_auto_approval']
|
||||||
|
|
||||||
async def handle_request(self, pr_url, request, notify=None) -> bool:
|
async def handle_request(self, pr_url, request, notify=None) -> bool:
|
||||||
# First, apply repo specific settings if exists
|
# First, apply repo specific settings if exists
|
||||||
@ -60,21 +65,10 @@ class PRAgent:
|
|||||||
else:
|
else:
|
||||||
action, *args = request
|
action, *args = request
|
||||||
|
|
||||||
forbidden_cli_args = ['enable_auto_approval', 'approve_pr_on_self_review', 'base_url', 'url', 'app_name', 'secret_provider',
|
|
||||||
'git_provider', 'skip_keys', 'openai.key', 'ANALYTICS_FOLDER', 'uri', 'app_id', 'webhook_secret',
|
|
||||||
'bearer_token', 'PERSONAL_ACCESS_TOKEN', 'override_deployment_type', 'private_key',
|
|
||||||
'local_cache_path', 'enable_local_cache', 'jira_base_url', 'api_base', 'api_type', 'api_version',
|
|
||||||
'skip_keys']
|
|
||||||
if args:
|
if args:
|
||||||
|
for forbidden_arg in self.forbidden_cli_args:
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if arg.startswith('--'):
|
if forbidden_arg in arg:
|
||||||
arg_word = arg.lower()
|
|
||||||
arg_word = arg_word.replace('__', '.') # replace double underscore with dot, e.g. --openai__key -> --openai.key
|
|
||||||
for forbidden_arg in forbidden_cli_args:
|
|
||||||
forbidden_arg_word = forbidden_arg.lower()
|
|
||||||
if '.' not in forbidden_arg_word:
|
|
||||||
forbidden_arg_word = '.' + forbidden_arg_word
|
|
||||||
if forbidden_arg_word in arg_word:
|
|
||||||
get_logger().error(
|
get_logger().error(
|
||||||
f"CLI argument for param '{forbidden_arg}' is forbidden. Use instead a configuration file."
|
f"CLI argument for param '{forbidden_arg}' is forbidden. Use instead a configuration file."
|
||||||
)
|
)
|
||||||
@ -83,10 +77,12 @@ class PRAgent:
|
|||||||
|
|
||||||
action = action.lstrip("/").lower()
|
action = action.lstrip("/").lower()
|
||||||
if action not in command2class:
|
if action not in command2class:
|
||||||
get_logger().error(f"Unknown command: {action}")
|
get_logger().debug(f"Unknown command: {action}")
|
||||||
return False
|
return False
|
||||||
with get_logger().contextualize(command=action, pr_url=pr_url):
|
with get_logger().contextualize(command=action, pr_url=pr_url):
|
||||||
get_logger().info("PR-Agent request handler started", analytics=True)
|
get_logger().info("PR-Agent request handler started", analytics=True)
|
||||||
|
if action == "reflect_and_review":
|
||||||
|
get_settings().pr_reviewer.ask_and_reflect = True
|
||||||
if action == "answer":
|
if action == "answer":
|
||||||
if notify:
|
if notify:
|
||||||
notify()
|
notify()
|
||||||
|
@ -19,34 +19,25 @@ MAX_TOKENS = {
|
|||||||
'gpt-4o-mini': 128000, # 128K, but may be limited by config.max_model_tokens
|
'gpt-4o-mini': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||||
'gpt-4o-mini-2024-07-18': 128000, # 128K, but may be limited by config.max_model_tokens
|
'gpt-4o-mini-2024-07-18': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||||
'gpt-4o-2024-08-06': 128000, # 128K, but may be limited by config.max_model_tokens
|
'gpt-4o-2024-08-06': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||||
'gpt-4o-2024-11-20': 128000, # 128K, but may be limited by config.max_model_tokens
|
|
||||||
'o1-mini': 128000, # 128K, but may be limited by config.max_model_tokens
|
'o1-mini': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||||
'o1-mini-2024-09-12': 128000, # 128K, but may be limited by config.max_model_tokens
|
'o1-mini-2024-09-12': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||||
'o1-preview': 128000, # 128K, but may be limited by config.max_model_tokens
|
'o1-preview': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||||
'o1-preview-2024-09-12': 128000, # 128K, but may be limited by config.max_model_tokens
|
'o1-preview-2024-09-12': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||||
'o1-2024-12-17': 204800, # 200K, but may be limited by config.max_model_tokens
|
|
||||||
'o1': 204800, # 200K, but may be limited by config.max_model_tokens
|
|
||||||
'claude-instant-1': 100000,
|
'claude-instant-1': 100000,
|
||||||
'claude-2': 100000,
|
'claude-2': 100000,
|
||||||
'command-nightly': 4096,
|
'command-nightly': 4096,
|
||||||
'deepseek/deepseek-chat': 128000, # 128K, but may be limited by config.max_model_tokens
|
|
||||||
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
|
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
|
||||||
'meta-llama/Llama-2-7b-chat-hf': 4096,
|
'meta-llama/Llama-2-7b-chat-hf': 4096,
|
||||||
'vertex_ai/codechat-bison': 6144,
|
'vertex_ai/codechat-bison': 6144,
|
||||||
'vertex_ai/codechat-bison-32k': 32000,
|
'vertex_ai/codechat-bison-32k': 32000,
|
||||||
'vertex_ai/claude-3-haiku@20240307': 100000,
|
'vertex_ai/claude-3-haiku@20240307': 100000,
|
||||||
'vertex_ai/claude-3-5-haiku@20241022': 100000,
|
|
||||||
'vertex_ai/claude-3-sonnet@20240229': 100000,
|
'vertex_ai/claude-3-sonnet@20240229': 100000,
|
||||||
'vertex_ai/claude-3-opus@20240229': 100000,
|
'vertex_ai/claude-3-opus@20240229': 100000,
|
||||||
'vertex_ai/claude-3-5-sonnet@20240620': 100000,
|
'vertex_ai/claude-3-5-sonnet@20240620': 100000,
|
||||||
'vertex_ai/claude-3-5-sonnet-v2@20241022': 100000,
|
'vertex_ai/claude-3-5-sonnet-v2@20241022': 100000,
|
||||||
'vertex_ai/gemini-1.5-pro': 1048576,
|
'vertex_ai/gemini-1.5-pro': 1048576,
|
||||||
'vertex_ai/gemini-1.5-flash': 1048576,
|
'vertex_ai/gemini-1.5-flash': 1048576,
|
||||||
'vertex_ai/gemini-2.0-flash-exp': 1048576,
|
|
||||||
'vertex_ai/gemma2': 8200,
|
'vertex_ai/gemma2': 8200,
|
||||||
'gemini/gemini-1.5-pro': 1048576,
|
|
||||||
'gemini/gemini-1.5-flash': 1048576,
|
|
||||||
'gemini/gemini-2.0-flash-exp': 1048576,
|
|
||||||
'codechat-bison': 6144,
|
'codechat-bison': 6144,
|
||||||
'codechat-bison-32k': 32000,
|
'codechat-bison-32k': 32000,
|
||||||
'anthropic.claude-instant-v1': 100000,
|
'anthropic.claude-instant-v1': 100000,
|
||||||
@ -55,23 +46,20 @@ MAX_TOKENS = {
|
|||||||
'anthropic/claude-3-opus-20240229': 100000,
|
'anthropic/claude-3-opus-20240229': 100000,
|
||||||
'anthropic/claude-3-5-sonnet-20240620': 100000,
|
'anthropic/claude-3-5-sonnet-20240620': 100000,
|
||||||
'anthropic/claude-3-5-sonnet-20241022': 100000,
|
'anthropic/claude-3-5-sonnet-20241022': 100000,
|
||||||
'anthropic/claude-3-5-haiku-20241022': 100000,
|
|
||||||
'bedrock/anthropic.claude-instant-v1': 100000,
|
'bedrock/anthropic.claude-instant-v1': 100000,
|
||||||
'bedrock/anthropic.claude-v2': 100000,
|
'bedrock/anthropic.claude-v2': 100000,
|
||||||
'bedrock/anthropic.claude-v2:1': 100000,
|
'bedrock/anthropic.claude-v2:1': 100000,
|
||||||
'bedrock/anthropic.claude-3-sonnet-20240229-v1:0': 100000,
|
'bedrock/anthropic.claude-3-sonnet-20240229-v1:0': 100000,
|
||||||
'bedrock/anthropic.claude-3-haiku-20240307-v1:0': 100000,
|
'bedrock/anthropic.claude-3-haiku-20240307-v1:0': 100000,
|
||||||
'bedrock/anthropic.claude-3-5-haiku-20241022-v1:0': 100000,
|
|
||||||
'bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0': 100000,
|
'bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0': 100000,
|
||||||
'bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0': 100000,
|
'bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0': 100000,
|
||||||
"bedrock/us.anthropic.claude-3-5-sonnet-20241022-v2:0": 100000,
|
|
||||||
'claude-3-5-sonnet': 100000,
|
'claude-3-5-sonnet': 100000,
|
||||||
'groq/llama3-8b-8192': 8192,
|
'groq/llama3-8b-8192': 8192,
|
||||||
'groq/llama3-70b-8192': 8192,
|
'groq/llama3-70b-8192': 8192,
|
||||||
'groq/llama-3.1-8b-instant': 8192,
|
|
||||||
'groq/llama-3.3-70b-versatile': 128000,
|
|
||||||
'groq/mixtral-8x7b-32768': 32768,
|
'groq/mixtral-8x7b-32768': 32768,
|
||||||
'groq/gemma2-9b-it': 8192,
|
'groq/llama-3.1-8b-instant': 131072,
|
||||||
|
'groq/llama-3.1-70b-versatile': 131072,
|
||||||
|
'groq/llama-3.1-405b-reasoning': 131072,
|
||||||
'ollama/llama3': 4096,
|
'ollama/llama3': 4096,
|
||||||
'watsonx/meta-llama/llama-3-8b-instruct': 4096,
|
'watsonx/meta-llama/llama-3-8b-instruct': 4096,
|
||||||
"watsonx/meta-llama/llama-3-70b-instruct": 4096,
|
"watsonx/meta-llama/llama-3-70b-instruct": 4096,
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
try:
|
try:
|
||||||
from langchain_core.messages import HumanMessage, SystemMessage
|
from langchain_openai import ChatOpenAI, AzureChatOpenAI
|
||||||
from langchain_openai import AzureChatOpenAI, ChatOpenAI
|
from langchain_core.messages import SystemMessage, HumanMessage
|
||||||
except: # we don't enforce langchain as a dependency, so if it's not installed, just move on
|
except: # we don't enforce langchain as a dependency, so if it's not installed, just move on
|
||||||
pass
|
pass
|
||||||
|
|
||||||
import functools
|
|
||||||
|
|
||||||
from openai import APIError, RateLimitError, Timeout
|
|
||||||
from retry import retry
|
|
||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
from openai import APIError, RateLimitError, Timeout
|
||||||
|
from retry import retry
|
||||||
|
import functools
|
||||||
|
|
||||||
OPENAI_RETRIES = 5
|
OPENAI_RETRIES = 5
|
||||||
|
|
||||||
|
|
||||||
@ -74,3 +73,4 @@ class LangChainOpenAIHandler(BaseAiHandler):
|
|||||||
raise ValueError(f"OpenAI {e.name} is required") from e
|
raise ValueError(f"OpenAI {e.name} is required") from e
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
|
import requests
|
||||||
import litellm
|
import litellm
|
||||||
import openai
|
import openai
|
||||||
import requests
|
|
||||||
from litellm import acompletion
|
from litellm import acompletion
|
||||||
from tenacity import retry, retry_if_exception_type, stop_after_attempt
|
from tenacity import retry, retry_if_exception_type, stop_after_attempt
|
||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
from pr_agent.algo.utils import get_version
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
@ -85,15 +83,6 @@ class LiteLLMAIHandler(BaseAiHandler):
|
|||||||
litellm.vertex_location = get_settings().get(
|
litellm.vertex_location = get_settings().get(
|
||||||
"VERTEXAI.VERTEX_LOCATION", None
|
"VERTEXAI.VERTEX_LOCATION", None
|
||||||
)
|
)
|
||||||
# Google AI Studio
|
|
||||||
# SEE https://docs.litellm.ai/docs/providers/gemini
|
|
||||||
if get_settings().get("GOOGLE_AI_STUDIO.GEMINI_API_KEY", None):
|
|
||||||
os.environ["GEMINI_API_KEY"] = get_settings().google_ai_studio.gemini_api_key
|
|
||||||
|
|
||||||
# Support deepseek models
|
|
||||||
if get_settings().get("DEEPSEEK.KEY", None):
|
|
||||||
os.environ['DEEPSEEK_API_KEY'] = get_settings().get("DEEPSEEK.KEY")
|
|
||||||
|
|
||||||
def prepare_logs(self, response, system, user, resp, finish_reason):
|
def prepare_logs(self, response, system, user, resp, finish_reason):
|
||||||
response_log = response.dict().copy()
|
response_log = response.dict().copy()
|
||||||
response_log['system'] = system
|
response_log['system'] = system
|
||||||
@ -137,7 +126,7 @@ class LiteLLMAIHandler(BaseAiHandler):
|
|||||||
if "langfuse" in callbacks:
|
if "langfuse" in callbacks:
|
||||||
metadata.update({
|
metadata.update({
|
||||||
"trace_name": command,
|
"trace_name": command,
|
||||||
"tags": [git_provider, command, f'version:{get_version()}'],
|
"tags": [git_provider, command],
|
||||||
"trace_metadata": {
|
"trace_metadata": {
|
||||||
"command": command,
|
"command": command,
|
||||||
"pr_url": pr_url,
|
"pr_url": pr_url,
|
||||||
@ -146,7 +135,7 @@ class LiteLLMAIHandler(BaseAiHandler):
|
|||||||
if "langsmith" in callbacks:
|
if "langsmith" in callbacks:
|
||||||
metadata.update({
|
metadata.update({
|
||||||
"run_name": command,
|
"run_name": command,
|
||||||
"tags": [git_provider, command, f'version:{get_version()}'],
|
"tags": [git_provider, command],
|
||||||
"extra": {
|
"extra": {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"command": command,
|
"command": command,
|
||||||
@ -197,8 +186,8 @@ class LiteLLMAIHandler(BaseAiHandler):
|
|||||||
messages[1]["content"] = [{"type": "text", "text": messages[1]["content"]},
|
messages[1]["content"] = [{"type": "text", "text": messages[1]["content"]},
|
||||||
{"type": "image_url", "image_url": {"url": img_path}}]
|
{"type": "image_url", "image_url": {"url": img_path}}]
|
||||||
|
|
||||||
# Currently, model OpenAI o1 series does not support a separate system and user prompts
|
# Currently O1 does not support separate system and user prompts
|
||||||
O1_MODEL_PREFIX = 'o1'
|
O1_MODEL_PREFIX = 'o1-'
|
||||||
model_type = model.split('/')[-1] if '/' in model else model
|
model_type = model.split('/')[-1] if '/' in model else model
|
||||||
if model_type.startswith(O1_MODEL_PREFIX):
|
if model_type.startswith(O1_MODEL_PREFIX):
|
||||||
user = f"{system}\n\n\n{user}"
|
user = f"{system}\n\n\n{user}"
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
from os import environ
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
import openai
|
import openai
|
||||||
from openai import APIError, AsyncOpenAI, RateLimitError, Timeout
|
from openai.error import APIError, RateLimitError, Timeout, TryAgain
|
||||||
from retry import retry
|
from retry import retry
|
||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
@ -16,7 +14,7 @@ class OpenAIHandler(BaseAiHandler):
|
|||||||
# Initialize OpenAIHandler specific attributes here
|
# Initialize OpenAIHandler specific attributes here
|
||||||
try:
|
try:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
environ["OPENAI_API_KEY"] = get_settings().openai.key
|
openai.api_key = get_settings().openai.key
|
||||||
if get_settings().get("OPENAI.ORG", None):
|
if get_settings().get("OPENAI.ORG", None):
|
||||||
openai.organization = get_settings().openai.org
|
openai.organization = get_settings().openai.org
|
||||||
if get_settings().get("OPENAI.API_TYPE", None):
|
if get_settings().get("OPENAI.API_TYPE", None):
|
||||||
@ -26,7 +24,7 @@ class OpenAIHandler(BaseAiHandler):
|
|||||||
if get_settings().get("OPENAI.API_VERSION", None):
|
if get_settings().get("OPENAI.API_VERSION", None):
|
||||||
openai.api_version = get_settings().openai.api_version
|
openai.api_version = get_settings().openai.api_version
|
||||||
if get_settings().get("OPENAI.API_BASE", None):
|
if get_settings().get("OPENAI.API_BASE", None):
|
||||||
environ["OPENAI_BASE_URL"] = get_settings().openai.api_base
|
openai.api_base = get_settings().openai.api_base
|
||||||
|
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
raise ValueError("OpenAI key is required") from e
|
raise ValueError("OpenAI key is required") from e
|
||||||
@ -38,26 +36,28 @@ class OpenAIHandler(BaseAiHandler):
|
|||||||
"""
|
"""
|
||||||
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
|
return get_settings().get("OPENAI.DEPLOYMENT_ID", None)
|
||||||
|
|
||||||
@retry(exceptions=(APIError, Timeout, AttributeError, RateLimitError),
|
@retry(exceptions=(APIError, Timeout, TryAgain, AttributeError, RateLimitError),
|
||||||
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
|
tries=OPENAI_RETRIES, delay=2, backoff=2, jitter=(1, 3))
|
||||||
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
|
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2):
|
||||||
try:
|
try:
|
||||||
|
deployment_id = self.deployment_id
|
||||||
get_logger().info("System: ", system)
|
get_logger().info("System: ", system)
|
||||||
get_logger().info("User: ", user)
|
get_logger().info("User: ", user)
|
||||||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
||||||
client = AsyncOpenAI()
|
|
||||||
chat_completion = await client.chat.completions.create(
|
chat_completion = await openai.ChatCompletion.acreate(
|
||||||
model=model,
|
model=model,
|
||||||
|
deployment_id=deployment_id,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
temperature=temperature,
|
temperature=temperature,
|
||||||
)
|
)
|
||||||
resp = chat_completion.choices[0].message.content
|
resp = chat_completion["choices"][0]['message']['content']
|
||||||
finish_reason = chat_completion.choices[0].finish_reason
|
finish_reason = chat_completion["choices"][0]["finish_reason"]
|
||||||
usage = chat_completion.usage
|
usage = chat_completion.get("usage")
|
||||||
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
|
get_logger().info("AI response", response=resp, messages=messages, finish_reason=finish_reason,
|
||||||
model=model, usage=usage)
|
model=model, usage=usage)
|
||||||
return resp, finish_reason
|
return resp, finish_reason
|
||||||
except (APIError, Timeout) as e:
|
except (APIError, Timeout, TryAgain) as e:
|
||||||
get_logger().error("Error during OpenAI inference: ", e)
|
get_logger().error("Error during OpenAI inference: ", e)
|
||||||
raise
|
raise
|
||||||
except (RateLimitError) as e:
|
except (RateLimitError) as e:
|
||||||
@ -65,4 +65,4 @@ class OpenAIHandler(BaseAiHandler):
|
|||||||
raise
|
raise
|
||||||
except (Exception) as e:
|
except (Exception) as e:
|
||||||
get_logger().error("Unknown error during OpenAI inference: ", e)
|
get_logger().error("Unknown error during OpenAI inference: ", e)
|
||||||
raise
|
raise TryAgain from e
|
||||||
|
@ -3,8 +3,8 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ def extend_patch(original_file_str, patch_str, patch_extra_lines_before=0,
|
|||||||
|
|
||||||
|
|
||||||
def decode_if_bytes(original_file_str):
|
def decode_if_bytes(original_file_str):
|
||||||
if isinstance(original_file_str, (bytes, bytearray)):
|
if isinstance(original_file_str, bytes):
|
||||||
try:
|
try:
|
||||||
return original_file_str.decode('utf-8')
|
return original_file_str.decode('utf-8')
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
@ -61,26 +61,23 @@ def process_patch_lines(patch_str, original_file_str, patch_extra_lines_before,
|
|||||||
patch_lines = patch_str.splitlines()
|
patch_lines = patch_str.splitlines()
|
||||||
extended_patch_lines = []
|
extended_patch_lines = []
|
||||||
|
|
||||||
is_valid_hunk = True
|
|
||||||
start1, size1, start2, size2 = -1, -1, -1, -1
|
start1, size1, start2, size2 = -1, -1, -1, -1
|
||||||
RE_HUNK_HEADER = re.compile(
|
RE_HUNK_HEADER = re.compile(
|
||||||
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
||||||
try:
|
try:
|
||||||
for i,line in enumerate(patch_lines):
|
for line in patch_lines:
|
||||||
if line.startswith('@@'):
|
if line.startswith('@@'):
|
||||||
match = RE_HUNK_HEADER.match(line)
|
match = RE_HUNK_HEADER.match(line)
|
||||||
# identify hunk header
|
# identify hunk header
|
||||||
if match:
|
if match:
|
||||||
# finish processing previous hunk
|
# finish processing previous hunk
|
||||||
if is_valid_hunk and (start1 != -1 and patch_extra_lines_after > 0):
|
if start1 != -1 and patch_extra_lines_after > 0:
|
||||||
delta_lines = [f' {line}' for line in original_lines[start1 + size1 - 1:start1 + size1 - 1 + patch_extra_lines_after]]
|
delta_lines = [f' {line}' for line in original_lines[start1 + size1 - 1:start1 + size1 - 1 + patch_extra_lines_after]]
|
||||||
extended_patch_lines.extend(delta_lines)
|
extended_patch_lines.extend(delta_lines)
|
||||||
|
|
||||||
section_header, size1, size2, start1, start2 = extract_hunk_headers(match)
|
section_header, size1, size2, start1, start2 = extract_hunk_headers(match)
|
||||||
|
|
||||||
is_valid_hunk = check_if_hunk_lines_matches_to_file(i, original_lines, patch_lines, start1)
|
if patch_extra_lines_before > 0 or patch_extra_lines_after > 0:
|
||||||
|
|
||||||
if is_valid_hunk and (patch_extra_lines_before > 0 or patch_extra_lines_after > 0):
|
|
||||||
def _calc_context_limits(patch_lines_before):
|
def _calc_context_limits(patch_lines_before):
|
||||||
extended_start1 = max(1, start1 - patch_lines_before)
|
extended_start1 = max(1, start1 - patch_lines_before)
|
||||||
extended_size1 = size1 + (start1 - extended_start1) + patch_extra_lines_after
|
extended_size1 = size1 + (start1 - extended_start1) + patch_extra_lines_after
|
||||||
@ -141,7 +138,7 @@ def process_patch_lines(patch_str, original_file_str, patch_extra_lines_before,
|
|||||||
return patch_str
|
return patch_str
|
||||||
|
|
||||||
# finish processing last hunk
|
# finish processing last hunk
|
||||||
if start1 != -1 and patch_extra_lines_after > 0 and is_valid_hunk:
|
if start1 != -1 and patch_extra_lines_after > 0:
|
||||||
delta_lines = original_lines[start1 + size1 - 1:start1 + size1 - 1 + patch_extra_lines_after]
|
delta_lines = original_lines[start1 + size1 - 1:start1 + size1 - 1 + patch_extra_lines_after]
|
||||||
# add space at the beginning of each extra line
|
# add space at the beginning of each extra line
|
||||||
delta_lines = [f' {line}' for line in delta_lines]
|
delta_lines = [f' {line}' for line in delta_lines]
|
||||||
@ -151,23 +148,6 @@ def process_patch_lines(patch_str, original_file_str, patch_extra_lines_before,
|
|||||||
return extended_patch_str
|
return extended_patch_str
|
||||||
|
|
||||||
|
|
||||||
def check_if_hunk_lines_matches_to_file(i, original_lines, patch_lines, start1):
|
|
||||||
"""
|
|
||||||
Check if the hunk lines match the original file content. We saw cases where the hunk header line doesn't match the original file content, and then
|
|
||||||
extending the hunk with extra lines before the hunk header can cause the hunk to be invalid.
|
|
||||||
"""
|
|
||||||
is_valid_hunk = True
|
|
||||||
try:
|
|
||||||
if i + 1 < len(patch_lines) and patch_lines[i + 1][0] == ' ': # an existing line in the file
|
|
||||||
if patch_lines[i + 1].strip() != original_lines[start1 - 1].strip():
|
|
||||||
is_valid_hunk = False
|
|
||||||
get_logger().error(
|
|
||||||
f"Invalid hunk in PR, line {start1} in hunk header doesn't match the original file content")
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return is_valid_hunk
|
|
||||||
|
|
||||||
|
|
||||||
def extract_hunk_headers(match):
|
def extract_hunk_headers(match):
|
||||||
res = list(match.groups())
|
res = list(match.groups())
|
||||||
for i in range(len(res)):
|
for i in range(len(res)):
|
||||||
@ -364,7 +344,7 @@ __old hunk__
|
|||||||
|
|
||||||
|
|
||||||
def extract_hunk_lines_from_patch(patch: str, file_name, line_start, line_end, side) -> tuple[str, str]:
|
def extract_hunk_lines_from_patch(patch: str, file_name, line_start, line_end, side) -> tuple[str, str]:
|
||||||
try:
|
|
||||||
patch_with_lines_str = f"\n\n## File: '{file_name.strip()}'\n\n"
|
patch_with_lines_str = f"\n\n## File: '{file_name.strip()}'\n\n"
|
||||||
selected_lines = ""
|
selected_lines = ""
|
||||||
patch_lines = patch.splitlines()
|
patch_lines = patch.splitlines()
|
||||||
@ -407,8 +387,5 @@ def extract_hunk_lines_from_patch(patch: str, file_name, line_start, line_end, s
|
|||||||
patch_with_lines_str += line + '\n'
|
patch_with_lines_str += line + '\n'
|
||||||
if not line.startswith('-'): # currently we don't support /ask line for deleted lines
|
if not line.startswith('-'): # currently we don't support /ask line for deleted lines
|
||||||
selected_lines_num += 1
|
selected_lines_num += 1
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to extract hunk lines from patch: {e}", artifact={"traceback": traceback.format_exc()})
|
|
||||||
return "", ""
|
|
||||||
|
|
||||||
return patch_with_lines_str.rstrip(), selected_lines.rstrip()
|
return patch_with_lines_str.rstrip(), selected_lines.rstrip()
|
@ -4,6 +4,8 @@ from typing import Dict
|
|||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def filter_bad_extensions(files):
|
def filter_bad_extensions(files):
|
||||||
# Bad Extensions, source: https://github.com/EleutherAI/github-downloader/blob/345e7c4cbb9e0dc8a0615fd995a08bf9d73b3fe6/download_repo_text.py # noqa: E501
|
# Bad Extensions, source: https://github.com/EleutherAI/github-downloader/blob/345e7c4cbb9e0dc8a0615fd995a08bf9d73b3fe6/download_repo_text.py # noqa: E501
|
||||||
bad_extensions = get_settings().bad_extensions.default
|
bad_extensions = get_settings().bad_extensions.default
|
||||||
|
@ -5,15 +5,14 @@ from typing import Callable, List, Tuple
|
|||||||
|
|
||||||
from github import RateLimitExceededException
|
from github import RateLimitExceededException
|
||||||
|
|
||||||
from pr_agent.algo.file_filter import filter_ignored
|
from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions
|
||||||
from pr_agent.algo.git_patch_processing import (
|
|
||||||
convert_to_hunks_with_lines_numbers, extend_patch, handle_patch_deletions)
|
|
||||||
from pr_agent.algo.language_handler import sort_files_by_main_languages
|
from pr_agent.algo.language_handler import sort_files_by_main_languages
|
||||||
|
from pr_agent.algo.file_filter import filter_ignored
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
from pr_agent.algo.utils import get_max_tokens, clip_tokens, ModelType
|
||||||
from pr_agent.algo.utils import ModelType, clip_tokens, get_max_tokens, get_weak_model
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.git_provider import GitProvider
|
from pr_agent.git_providers.git_provider import GitProvider
|
||||||
|
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
DELETED_FILES_ = "Deleted files:\n"
|
DELETED_FILES_ = "Deleted files:\n"
|
||||||
@ -205,11 +204,10 @@ def pr_generate_extended_diff(pr_languages: list,
|
|||||||
if not extended_patch:
|
if not extended_patch:
|
||||||
get_logger().warning(f"Failed to extend patch for file: {file.filename}")
|
get_logger().warning(f"Failed to extend patch for file: {file.filename}")
|
||||||
continue
|
continue
|
||||||
|
full_extended_patch = f"\n\n## {file.filename}\n{extended_patch.rstrip()}\n"
|
||||||
|
|
||||||
if add_line_numbers_to_hunks:
|
if add_line_numbers_to_hunks:
|
||||||
full_extended_patch = convert_to_hunks_with_lines_numbers(extended_patch, file)
|
full_extended_patch = convert_to_hunks_with_lines_numbers(extended_patch, file)
|
||||||
else:
|
|
||||||
full_extended_patch = f"\n\n## File: '{file.filename.strip()}'\n{extended_patch.rstrip()}\n"
|
|
||||||
|
|
||||||
# add AI-summary metadata to the patch
|
# add AI-summary metadata to the patch
|
||||||
if file.ai_file_summary and get_settings().get("config.enable_ai_metadata", False):
|
if file.ai_file_summary and get_settings().get("config.enable_ai_metadata", False):
|
||||||
@ -317,13 +315,13 @@ def generate_full_patch(convert_hunks_to_line_numbers, file_dict, max_tokens_mod
|
|||||||
# TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens
|
# TODO: Option for alternative logic to remove hunks from the patch to reduce the number of tokens
|
||||||
# until we meet the requirements
|
# until we meet the requirements
|
||||||
if get_settings().config.verbosity_level >= 2:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().warning(f"Patch too large, skipping it: '{filename}'")
|
get_logger().warning(f"Patch too large, skipping it, {filename}")
|
||||||
remaining_files_list_new.append(filename)
|
remaining_files_list_new.append(filename)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if patch:
|
if patch:
|
||||||
if not convert_hunks_to_line_numbers:
|
if not convert_hunks_to_line_numbers:
|
||||||
patch_final = f"\n\n## File: '{filename.strip()}'\n\n{patch.strip()}\n"
|
patch_final = f"\n\n## File: '{filename.strip()}\n\n{patch.strip()}\n'"
|
||||||
else:
|
else:
|
||||||
patch_final = "\n\n" + patch.strip()
|
patch_final = "\n\n" + patch.strip()
|
||||||
patches.append(patch_final)
|
patches.append(patch_final)
|
||||||
@ -355,8 +353,8 @@ async def retry_with_fallback_models(f: Callable, model_type: ModelType = ModelT
|
|||||||
|
|
||||||
|
|
||||||
def _get_all_models(model_type: ModelType = ModelType.REGULAR) -> List[str]:
|
def _get_all_models(model_type: ModelType = ModelType.REGULAR) -> List[str]:
|
||||||
if model_type == ModelType.WEAK:
|
if model_type == ModelType.TURBO:
|
||||||
model = get_weak_model()
|
model = get_settings().config.model_turbo
|
||||||
else:
|
else:
|
||||||
model = get_settings().config.model
|
model = get_settings().config.model
|
||||||
fallback_models = get_settings().config.fallback_models
|
fallback_models = get_settings().config.fallback_models
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from threading import Lock
|
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
from tiktoken import encoding_for_model, get_encoding
|
from tiktoken import encoding_for_model, get_encoding
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class EDIT_TYPE(Enum):
|
class EDIT_TYPE(Enum):
|
||||||
@ -22,5 +21,4 @@ class FilePatchInfo:
|
|||||||
old_filename: str = None
|
old_filename: str = None
|
||||||
num_plus_lines: int = -1
|
num_plus_lines: int = -1
|
||||||
num_minus_lines: int = -1
|
num_minus_lines: int = -1
|
||||||
language: Optional[str] = None
|
|
||||||
ai_file_summary: str = None
|
ai_file_summary: str = None
|
||||||
|
@ -7,15 +7,14 @@ import html
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
import textwrap
|
import textwrap
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from importlib.metadata import PackageNotFoundError, version
|
|
||||||
from typing import Any, List, Tuple
|
from typing import Any, List, Tuple
|
||||||
|
|
||||||
|
|
||||||
import html2text
|
import html2text
|
||||||
import requests
|
import requests
|
||||||
import yaml
|
import yaml
|
||||||
@ -23,19 +22,11 @@ from pydantic import BaseModel
|
|||||||
from starlette_context import context
|
from starlette_context import context
|
||||||
|
|
||||||
from pr_agent.algo import MAX_TOKENS
|
from pr_agent.algo import MAX_TOKENS
|
||||||
from pr_agent.algo.git_patch_processing import extract_hunk_lines_from_patch
|
|
||||||
from pr_agent.algo.token_handler import TokenEncoder
|
from pr_agent.algo.token_handler import TokenEncoder
|
||||||
from pr_agent.algo.types import FilePatchInfo
|
|
||||||
from pr_agent.config_loader import get_settings, global_settings
|
from pr_agent.config_loader import get_settings, global_settings
|
||||||
|
from pr_agent.algo.types import FilePatchInfo
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
|
||||||
def get_weak_model() -> str:
|
|
||||||
if get_settings().get("config.model_weak"):
|
|
||||||
return get_settings().config.model_weak
|
|
||||||
return get_settings().config.model
|
|
||||||
|
|
||||||
|
|
||||||
class Range(BaseModel):
|
class Range(BaseModel):
|
||||||
line_start: int # should be 0-indexed
|
line_start: int # should be 0-indexed
|
||||||
line_end: int
|
line_end: int
|
||||||
@ -44,17 +35,14 @@ class Range(BaseModel):
|
|||||||
|
|
||||||
class ModelType(str, Enum):
|
class ModelType(str, Enum):
|
||||||
REGULAR = "regular"
|
REGULAR = "regular"
|
||||||
WEAK = "weak"
|
TURBO = "turbo"
|
||||||
|
|
||||||
|
|
||||||
class PRReviewHeader(str, Enum):
|
class PRReviewHeader(str, Enum):
|
||||||
REGULAR = "## PR Reviewer Guide"
|
REGULAR = "## PR Reviewer Guide"
|
||||||
INCREMENTAL = "## Incremental PR Reviewer Guide"
|
INCREMENTAL = "## Incremental PR Reviewer Guide"
|
||||||
|
|
||||||
|
|
||||||
class PRDescriptionHeader(str, Enum):
|
|
||||||
CHANGES_WALKTHROUGH = "### **Changes walkthrough** 📝"
|
|
||||||
|
|
||||||
|
|
||||||
def get_setting(key: str) -> Any:
|
def get_setting(key: str) -> Any:
|
||||||
try:
|
try:
|
||||||
key = key.upper()
|
key = key.upper()
|
||||||
@ -105,8 +93,7 @@ def unique_strings(input_list: List[str]) -> List[str]:
|
|||||||
def convert_to_markdown_v2(output_data: dict,
|
def convert_to_markdown_v2(output_data: dict,
|
||||||
gfm_supported: bool = True,
|
gfm_supported: bool = True,
|
||||||
incremental_review=None,
|
incremental_review=None,
|
||||||
git_provider=None,
|
git_provider=None) -> str:
|
||||||
files=None) -> str:
|
|
||||||
"""
|
"""
|
||||||
Convert a dictionary of data into markdown format.
|
Convert a dictionary of data into markdown format.
|
||||||
Args:
|
Args:
|
||||||
@ -182,7 +169,7 @@ def convert_to_markdown_v2(output_data: dict,
|
|||||||
if is_value_no(value):
|
if is_value_no(value):
|
||||||
markdown_text += f'### {emoji} No relevant tests\n\n'
|
markdown_text += f'### {emoji} No relevant tests\n\n'
|
||||||
else:
|
else:
|
||||||
markdown_text += f"### {emoji} PR contains tests\n\n"
|
markdown_text += f"### PR contains tests\n\n"
|
||||||
elif 'ticket compliance check' in key_nice.lower():
|
elif 'ticket compliance check' in key_nice.lower():
|
||||||
markdown_text = ticket_markdown_logic(emoji, markdown_text, value, gfm_supported)
|
markdown_text = ticket_markdown_logic(emoji, markdown_text, value, gfm_supported)
|
||||||
elif 'security concerns' in key_nice.lower():
|
elif 'security concerns' in key_nice.lower():
|
||||||
@ -230,31 +217,15 @@ def convert_to_markdown_v2(output_data: dict,
|
|||||||
continue
|
continue
|
||||||
relevant_file = issue.get('relevant_file', '').strip()
|
relevant_file = issue.get('relevant_file', '').strip()
|
||||||
issue_header = issue.get('issue_header', '').strip()
|
issue_header = issue.get('issue_header', '').strip()
|
||||||
if issue_header.lower() == 'possible bug':
|
|
||||||
issue_header = 'Possible Issue' # Make the header less frightening
|
|
||||||
issue_content = issue.get('issue_content', '').strip()
|
issue_content = issue.get('issue_content', '').strip()
|
||||||
start_line = int(str(issue.get('start_line', 0)).strip())
|
start_line = int(str(issue.get('start_line', 0)).strip())
|
||||||
end_line = int(str(issue.get('end_line', 0)).strip())
|
end_line = int(str(issue.get('end_line', 0)).strip())
|
||||||
|
|
||||||
relevant_lines_str = extract_relevant_lines_str(end_line, files, relevant_file, start_line, dedent=True)
|
|
||||||
if git_provider:
|
|
||||||
reference_link = git_provider.get_line_link(relevant_file, start_line, end_line)
|
reference_link = git_provider.get_line_link(relevant_file, start_line, end_line)
|
||||||
else:
|
|
||||||
reference_link = None
|
|
||||||
|
|
||||||
if gfm_supported:
|
if gfm_supported:
|
||||||
if reference_link is not None and len(reference_link) > 0:
|
|
||||||
if relevant_lines_str:
|
|
||||||
issue_str = f"<details><summary><a href='{reference_link}'><strong>{issue_header}</strong></a>\n\n{issue_content}</summary>\n\n{relevant_lines_str}\n\n</details>"
|
|
||||||
else:
|
|
||||||
issue_str = f"<a href='{reference_link}'><strong>{issue_header}</strong></a><br>{issue_content}"
|
issue_str = f"<a href='{reference_link}'><strong>{issue_header}</strong></a><br>{issue_content}"
|
||||||
else:
|
else:
|
||||||
issue_str = f"<strong>{issue_header}</strong><br>{issue_content}"
|
|
||||||
else:
|
|
||||||
if reference_link is not None and len(reference_link) > 0:
|
|
||||||
issue_str = f"[**{issue_header}**]({reference_link})\n\n{issue_content}\n\n"
|
issue_str = f"[**{issue_header}**]({reference_link})\n\n{issue_content}\n\n"
|
||||||
else:
|
|
||||||
issue_str = f"**{issue_header}**\n\n{issue_content}\n\n"
|
|
||||||
markdown_text += f"{issue_str}\n\n"
|
markdown_text += f"{issue_str}\n\n"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to process 'Recommended focus areas for review': {e}")
|
get_logger().exception(f"Failed to process 'Recommended focus areas for review': {e}")
|
||||||
@ -271,47 +242,23 @@ def convert_to_markdown_v2(output_data: dict,
|
|||||||
if gfm_supported:
|
if gfm_supported:
|
||||||
markdown_text += "</table>\n"
|
markdown_text += "</table>\n"
|
||||||
|
|
||||||
return markdown_text
|
if 'code_feedback' in output_data:
|
||||||
|
if gfm_supported:
|
||||||
|
markdown_text += f"\n\n"
|
||||||
def extract_relevant_lines_str(end_line, files, relevant_file, start_line, dedent=False) -> str:
|
markdown_text += f"<details><summary> <strong>Code feedback:</strong></summary>\n\n"
|
||||||
"""
|
markdown_text += "<hr>"
|
||||||
Finds 'relevant_file' in 'files', and extracts the lines from 'start_line' to 'end_line' string from the file content.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
relevant_lines_str = ""
|
|
||||||
if files:
|
|
||||||
files = set_file_languages(files)
|
|
||||||
for file in files:
|
|
||||||
if file.filename.strip() == relevant_file:
|
|
||||||
if not file.head_file:
|
|
||||||
# as a fallback, extract relevant lines directly from patch
|
|
||||||
patch = file.patch
|
|
||||||
get_logger().info(f"No content found in file: '{file.filename}' for 'extract_relevant_lines_str'. Using patch instead")
|
|
||||||
_, selected_lines = extract_hunk_lines_from_patch(patch, file.filename, start_line, end_line,side='right')
|
|
||||||
if not selected_lines:
|
|
||||||
get_logger().error(f"Failed to extract relevant lines from patch: {file.filename}")
|
|
||||||
return ""
|
|
||||||
# filter out '-' lines
|
|
||||||
relevant_lines_str = ""
|
|
||||||
for line in selected_lines.splitlines():
|
|
||||||
if line.startswith('-'):
|
|
||||||
continue
|
|
||||||
relevant_lines_str += line[1:] + '\n'
|
|
||||||
else:
|
else:
|
||||||
relevant_file_lines = file.head_file.splitlines()
|
markdown_text += f"\n\n### Code feedback:\n\n"
|
||||||
relevant_lines_str = "\n".join(relevant_file_lines[start_line - 1:end_line])
|
for i, value in enumerate(output_data['code_feedback']):
|
||||||
|
if value is None or value == '' or value == {} or value == []:
|
||||||
|
continue
|
||||||
|
markdown_text += parse_code_suggestion(value, i, gfm_supported)+"\n\n"
|
||||||
|
if markdown_text.endswith('<hr>'):
|
||||||
|
markdown_text= markdown_text[:-4]
|
||||||
|
if gfm_supported:
|
||||||
|
markdown_text += f"</details>"
|
||||||
|
|
||||||
if dedent and relevant_lines_str:
|
return markdown_text
|
||||||
# Remove the longest leading string of spaces and tabs common to all lines.
|
|
||||||
relevant_lines_str = textwrap.dedent(relevant_lines_str)
|
|
||||||
relevant_lines_str = f"```{file.language}\n{relevant_lines_str}\n```"
|
|
||||||
break
|
|
||||||
|
|
||||||
return relevant_lines_str
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().exception(f"Failed to extract relevant lines: {e}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def ticket_markdown_logic(emoji, markdown_text, value, gfm_supported) -> str:
|
def ticket_markdown_logic(emoji, markdown_text, value, gfm_supported) -> str:
|
||||||
@ -583,22 +530,27 @@ def load_large_diff(filename, new_file_content_str: str, original_file_content_s
|
|||||||
"""
|
"""
|
||||||
Generate a patch for a modified file by comparing the original content of the file with the new content provided as
|
Generate a patch for a modified file by comparing the original content of the file with the new content provided as
|
||||||
input.
|
input.
|
||||||
"""
|
|
||||||
if not original_file_content_str and not new_file_content_str:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_file_content_str: The new content of the file as a string.
|
||||||
|
original_file_content_str: The original content of the file as a string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The generated or provided patch string.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
None.
|
||||||
|
"""
|
||||||
|
patch = ""
|
||||||
try:
|
try:
|
||||||
original_file_content_str = (original_file_content_str or "").rstrip() + "\n"
|
|
||||||
new_file_content_str = (new_file_content_str or "").rstrip() + "\n"
|
|
||||||
diff = difflib.unified_diff(original_file_content_str.splitlines(keepends=True),
|
diff = difflib.unified_diff(original_file_content_str.splitlines(keepends=True),
|
||||||
new_file_content_str.splitlines(keepends=True))
|
new_file_content_str.splitlines(keepends=True))
|
||||||
if get_settings().config.verbosity_level >= 2 and show_warning:
|
if get_settings().config.verbosity_level >= 2 and show_warning:
|
||||||
get_logger().info(f"File was modified, but no patch was found. Manually creating patch: {filename}.")
|
get_logger().warning(f"File was modified, but no patch was found. Manually creating patch: {filename}.")
|
||||||
patch = ''.join(diff)
|
patch = ''.join(diff)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return patch
|
return patch
|
||||||
except Exception as e:
|
|
||||||
get_logger().exception(f"Failed to generate patch for file: {filename}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
def update_settings_from_args(args: List[str]) -> List[str]:
|
def update_settings_from_args(args: List[str]) -> List[str]:
|
||||||
@ -1072,7 +1024,8 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
|||||||
if not description_full:
|
if not description_full:
|
||||||
return "", []
|
return "", []
|
||||||
|
|
||||||
description_split = description_full.split(PRDescriptionHeader.CHANGES_WALKTHROUGH.value)
|
split_str = "### **Changes walkthrough** 📝"
|
||||||
|
description_split = description_full.split(split_str)
|
||||||
base_description_str = description_split[0]
|
base_description_str = description_split[0]
|
||||||
changes_walkthrough_str = ""
|
changes_walkthrough_str = ""
|
||||||
files = []
|
files = []
|
||||||
@ -1107,9 +1060,6 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
|||||||
if not res or res.lastindex != 4:
|
if not res or res.lastindex != 4:
|
||||||
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong><dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\n\n\s*(.*?)</details>'
|
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong><dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\n\n\s*(.*?)</details>'
|
||||||
res = re.search(pattern_back, file_data, re.DOTALL)
|
res = re.search(pattern_back, file_data, re.DOTALL)
|
||||||
if not res or res.lastindex != 4:
|
|
||||||
pattern_back = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*-\s*(.*?)\s*</details>' # looking for hypen ('- ')
|
|
||||||
res = re.search(pattern_back, file_data, re.DOTALL)
|
|
||||||
if res and res.lastindex == 4:
|
if res and res.lastindex == 4:
|
||||||
short_filename = res.group(1).strip()
|
short_filename = res.group(1).strip()
|
||||||
short_summary = res.group(2).strip()
|
short_summary = res.group(2).strip()
|
||||||
@ -1141,48 +1091,3 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
|||||||
get_logger().exception(f"Failed to process description: {e}")
|
get_logger().exception(f"Failed to process description: {e}")
|
||||||
|
|
||||||
return base_description_str, files
|
return base_description_str, files
|
||||||
|
|
||||||
def get_version() -> str:
|
|
||||||
# First check pyproject.toml if running directly out of repository
|
|
||||||
if os.path.exists("pyproject.toml"):
|
|
||||||
if sys.version_info >= (3, 11):
|
|
||||||
import tomllib
|
|
||||||
with open("pyproject.toml", "rb") as f:
|
|
||||||
data = tomllib.load(f)
|
|
||||||
if "project" in data and "version" in data["project"]:
|
|
||||||
return data["project"]["version"]
|
|
||||||
else:
|
|
||||||
get_logger().warning("Version not found in pyproject.toml")
|
|
||||||
else:
|
|
||||||
get_logger().warning("Unable to determine local version from pyproject.toml")
|
|
||||||
|
|
||||||
# Otherwise get the installed pip package version
|
|
||||||
try:
|
|
||||||
return version('pr-agent')
|
|
||||||
except PackageNotFoundError:
|
|
||||||
get_logger().warning("Unable to find package named 'pr-agent'")
|
|
||||||
return "unknown"
|
|
||||||
|
|
||||||
|
|
||||||
def set_file_languages(diff_files) -> List[FilePatchInfo]:
|
|
||||||
try:
|
|
||||||
# if the language is already set, do not change it
|
|
||||||
if hasattr(diff_files[0], 'language') and diff_files[0].language:
|
|
||||||
return diff_files
|
|
||||||
|
|
||||||
# map file extensions to programming languages
|
|
||||||
language_extension_map_org = get_settings().language_extension_map_org
|
|
||||||
extension_to_language = {}
|
|
||||||
for language, extensions in language_extension_map_org.items():
|
|
||||||
for ext in extensions:
|
|
||||||
extension_to_language[ext] = language
|
|
||||||
for file in diff_files:
|
|
||||||
extension_s = '.' + file.filename.rsplit('.')[-1]
|
|
||||||
language_name = "txt"
|
|
||||||
if extension_s and (extension_s in extension_to_language):
|
|
||||||
language_name = extension_to_language[extension_s]
|
|
||||||
file.language = language_name.lower()
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().exception(f"Failed to set file languages: {e}")
|
|
||||||
|
|
||||||
return diff_files
|
|
||||||
|
@ -3,9 +3,8 @@ import asyncio
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent, commands
|
from pr_agent.agent.pr_agent import PRAgent, commands
|
||||||
from pr_agent.algo.utils import get_version
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import get_logger, setup_logger
|
from pr_agent.log import setup_logger, get_logger
|
||||||
|
|
||||||
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
||||||
setup_logger(log_level)
|
setup_logger(log_level)
|
||||||
@ -46,7 +45,6 @@ def set_parser():
|
|||||||
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('--version', action='version', version=f'pr-agent {get_version()}')
|
|
||||||
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', default=None)
|
||||||
parser.add_argument('--issue_url', type=str, help='The URL of the Issue to review', default=None)
|
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')
|
||||||
@ -77,8 +75,6 @@ def run(inargs=None, args=None):
|
|||||||
async def inner():
|
async def inner():
|
||||||
if args.issue_url:
|
if args.issue_url:
|
||||||
result = await asyncio.create_task(PRAgent().handle_request(args.issue_url, [command] + args.rest))
|
result = await asyncio.create_task(PRAgent().handle_request(args.issue_url, [command] + args.rest))
|
||||||
elif args.repo_url:
|
|
||||||
result = await asyncio.create_task(PRAgent().handle_request(args.repo_url, [command] + args.rest))
|
|
||||||
else:
|
else:
|
||||||
result = await asyncio.create_task(PRAgent().handle_request(args.pr_url, [command] + args.rest))
|
result = await asyncio.create_task(PRAgent().handle_request(args.pr_url, [command] + args.rest))
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ global_settings = Dynaconf(
|
|||||||
envvar_prefix=False,
|
envvar_prefix=False,
|
||||||
merge_enabled=True,
|
merge_enabled=True,
|
||||||
settings_files=[join(current_dir, f) for f in [
|
settings_files=[join(current_dir, f) for f in [
|
||||||
|
"settings/.secrets.toml",
|
||||||
"settings/configuration.toml",
|
"settings/configuration.toml",
|
||||||
"settings/ignore.toml",
|
"settings/ignore.toml",
|
||||||
"settings/language_extensions.toml",
|
"settings/language_extensions.toml",
|
||||||
@ -28,7 +29,6 @@ global_settings = Dynaconf(
|
|||||||
"settings/pr_add_docs.toml",
|
"settings/pr_add_docs.toml",
|
||||||
"settings/custom_labels.toml",
|
"settings/custom_labels.toml",
|
||||||
"settings/pr_help_prompts.toml",
|
"settings/pr_help_prompts.toml",
|
||||||
"settings/.secrets.toml",
|
|
||||||
"settings_prod/.secrets.toml",
|
"settings_prod/.secrets.toml",
|
||||||
]]
|
]]
|
||||||
)
|
)
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
from starlette_context import context
|
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider
|
|
||||||
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
|
from pr_agent.git_providers.bitbucket_provider import BitbucketProvider
|
||||||
from pr_agent.git_providers.bitbucket_server_provider import \
|
from pr_agent.git_providers.bitbucket_server_provider import BitbucketServerProvider
|
||||||
BitbucketServerProvider
|
|
||||||
from pr_agent.git_providers.codecommit_provider import CodeCommitProvider
|
from pr_agent.git_providers.codecommit_provider import CodeCommitProvider
|
||||||
from pr_agent.git_providers.gerrit_provider import GerritProvider
|
|
||||||
from pr_agent.git_providers.git_provider import GitProvider
|
from pr_agent.git_providers.git_provider import GitProvider
|
||||||
from pr_agent.git_providers.github_provider import GithubProvider
|
from pr_agent.git_providers.github_provider import GithubProvider
|
||||||
from pr_agent.git_providers.gitlab_provider import GitLabProvider
|
from pr_agent.git_providers.gitlab_provider import GitLabProvider
|
||||||
from pr_agent.git_providers.local_git_provider import LocalGitProvider
|
from pr_agent.git_providers.local_git_provider import LocalGitProvider
|
||||||
|
from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider
|
||||||
|
from pr_agent.git_providers.gerrit_provider import GerritProvider
|
||||||
|
from starlette_context import context
|
||||||
|
|
||||||
_GIT_PROVIDERS = {
|
_GIT_PROVIDERS = {
|
||||||
'github': GithubProvider,
|
'github': GithubProvider,
|
||||||
|
@ -2,16 +2,13 @@ import os
|
|||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
|
||||||
|
|
||||||
from ..algo.file_filter import filter_ignored
|
from ..algo.file_filter import filter_ignored
|
||||||
from ..algo.language_handler import is_valid_file
|
|
||||||
from ..algo.utils import (PRDescriptionHeader, clip_tokens,
|
|
||||||
find_line_number_of_relevant_line_in_file,
|
|
||||||
load_large_diff)
|
|
||||||
from ..config_loader import get_settings
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
from ..algo.language_handler import is_valid_file
|
||||||
|
from ..algo.utils import clip_tokens, find_line_number_of_relevant_line_in_file, load_large_diff
|
||||||
|
from ..config_loader import get_settings
|
||||||
from .git_provider import GitProvider
|
from .git_provider import GitProvider
|
||||||
|
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
|
|
||||||
AZURE_DEVOPS_AVAILABLE = True
|
AZURE_DEVOPS_AVAILABLE = True
|
||||||
ADO_APP_CLIENT_DEFAULT_ID = "499b84ac-1321-427f-aa17-267ca6975798/.default"
|
ADO_APP_CLIENT_DEFAULT_ID = "499b84ac-1321-427f-aa17-267ca6975798/.default"
|
||||||
@ -19,16 +16,19 @@ MAX_PR_DESCRIPTION_AZURE_LENGTH = 4000-1
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
|
from msrest.authentication import BasicAuthentication
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from azure.devops.connection import Connection
|
from azure.devops.connection import Connection
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from azure.devops.v7_1.git.models import (Comment, CommentThread,
|
from azure.identity import DefaultAzureCredential
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
from azure.devops.v7_1.git.models import (
|
||||||
|
Comment,
|
||||||
|
CommentThread,
|
||||||
|
GitVersionDescriptor,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
GitPullRequestIterationChanges,
|
GitPullRequestIterationChanges,
|
||||||
GitVersionDescriptor)
|
)
|
||||||
# noinspection PyUnresolvedReferences
|
|
||||||
from azure.identity import DefaultAzureCredential
|
|
||||||
from msrest.authentication import BasicAuthentication
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
AZURE_DEVOPS_AVAILABLE = False
|
AZURE_DEVOPS_AVAILABLE = False
|
||||||
|
|
||||||
@ -67,12 +67,14 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
relevant_lines_end = suggestion['relevant_lines_end']
|
relevant_lines_end = suggestion['relevant_lines_end']
|
||||||
|
|
||||||
if not relevant_lines_start or relevant_lines_start == -1:
|
if not relevant_lines_start or relevant_lines_start == -1:
|
||||||
get_logger().warning(
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().exception(
|
||||||
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
|
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if relevant_lines_end < relevant_lines_start:
|
if relevant_lines_end < relevant_lines_start:
|
||||||
get_logger().warning(f"Failed to publish code suggestion, "
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().exception(f"Failed to publish code suggestion, "
|
||||||
f"relevant_lines_end is {relevant_lines_end} and "
|
f"relevant_lines_end is {relevant_lines_end} and "
|
||||||
f"relevant_lines_start is {relevant_lines_start}")
|
f"relevant_lines_start is {relevant_lines_start}")
|
||||||
continue
|
continue
|
||||||
@ -93,11 +95,9 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
"side": "RIGHT",
|
"side": "RIGHT",
|
||||||
}
|
}
|
||||||
post_parameters_list.append(post_parameters)
|
post_parameters_list.append(post_parameters)
|
||||||
if not post_parameters_list:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for post_parameters in post_parameters_list:
|
|
||||||
try:
|
try:
|
||||||
|
for post_parameters in post_parameters_list:
|
||||||
comment = Comment(content=post_parameters["body"], comment_type=1)
|
comment = Comment(content=post_parameters["body"], comment_type=1)
|
||||||
thread = CommentThread(comments=[comment],
|
thread = CommentThread(comments=[comment],
|
||||||
thread_context={
|
thread_context={
|
||||||
@ -117,11 +117,15 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
repository_id=self.repo_slug,
|
repository_id=self.repo_slug,
|
||||||
pull_request_id=self.pr_num
|
pull_request_id=self.pr_num
|
||||||
)
|
)
|
||||||
except Exception as e:
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().warning(f"Azure failed to publish code suggestion, error: {e}")
|
get_logger().info(
|
||||||
|
f"Published code suggestion on {self.pr_num} at {post_parameters['path']}"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().error(f"Failed to publish code suggestion, error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def get_pr_description_full(self) -> str:
|
def get_pr_description_full(self) -> str:
|
||||||
return self.pr.description
|
return self.pr.description
|
||||||
@ -326,13 +330,13 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
edit_type = EDIT_TYPE.ADDED
|
edit_type = EDIT_TYPE.ADDED
|
||||||
elif diff_types[file] == "delete":
|
elif diff_types[file] == "delete":
|
||||||
edit_type = EDIT_TYPE.DELETED
|
edit_type = EDIT_TYPE.DELETED
|
||||||
elif "rename" in diff_types[file]: # diff_type can be `rename` | `edit, rename`
|
elif diff_types[file] == "rename":
|
||||||
edit_type = EDIT_TYPE.RENAMED
|
edit_type = EDIT_TYPE.RENAMED
|
||||||
|
|
||||||
version = GitVersionDescriptor(
|
version = GitVersionDescriptor(
|
||||||
version=base_sha.commit_id, version_type="commit"
|
version=base_sha.commit_id, version_type="commit"
|
||||||
)
|
)
|
||||||
if edit_type == EDIT_TYPE.ADDED or edit_type == EDIT_TYPE.RENAMED:
|
if edit_type == EDIT_TYPE.ADDED:
|
||||||
original_file_content_str = ""
|
original_file_content_str = ""
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@ -378,9 +382,6 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False, thread_context=None):
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False, thread_context=None):
|
||||||
if is_temporary and not get_settings().config.publish_output_progress:
|
|
||||||
get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}")
|
|
||||||
return None
|
|
||||||
comment = Comment(content=pr_comment)
|
comment = Comment(content=pr_comment)
|
||||||
thread = CommentThread(comments=[comment], thread_context=thread_context, status=5)
|
thread = CommentThread(comments=[comment], thread_context=thread_context, status=5)
|
||||||
thread_response = self.azure_devops_client.create_thread(
|
thread_response = self.azure_devops_client.create_thread(
|
||||||
@ -403,7 +404,7 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
pr_body = pr_body[:ind]
|
pr_body = pr_body[:ind]
|
||||||
|
|
||||||
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
|
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
|
||||||
changes_walkthrough_text = PRDescriptionHeader.CHANGES_WALKTHROUGH.value
|
changes_walkthrough_text = '## **Changes walkthrough**'
|
||||||
ind = pr_body.find(changes_walkthrough_text)
|
ind = pr_body.find(changes_walkthrough_text)
|
||||||
if ind != -1:
|
if ind != -1:
|
||||||
pr_body = pr_body[:ind]
|
pr_body = pr_body[:ind]
|
||||||
@ -619,3 +620,4 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
|
|
||||||
def publish_file_comments(self, file_comments: list) -> bool:
|
def publish_file_comments(self, file_comments: list) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import difflib
|
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -8,14 +6,13 @@ import requests
|
|||||||
from atlassian.bitbucket import Cloud
|
from atlassian.bitbucket import Cloud
|
||||||
from starlette_context import context
|
from starlette_context import context
|
||||||
|
|
||||||
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
from pr_agent.algo.types import FilePatchInfo, EDIT_TYPE
|
||||||
|
|
||||||
from ..algo.file_filter import filter_ignored
|
from ..algo.file_filter import filter_ignored
|
||||||
from ..algo.language_handler import is_valid_file
|
from ..algo.language_handler import is_valid_file
|
||||||
from ..algo.utils import find_line_number_of_relevant_line_in_file
|
from ..algo.utils import find_line_number_of_relevant_line_in_file
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .git_provider import MAX_FILES_ALLOWED_FULL, GitProvider
|
from .git_provider import GitProvider, MAX_FILES_ALLOWED_FULL
|
||||||
|
|
||||||
|
|
||||||
def _gef_filename(diff):
|
def _gef_filename(diff):
|
||||||
@ -74,33 +71,19 @@ class BitbucketProvider(GitProvider):
|
|||||||
post_parameters_list = []
|
post_parameters_list = []
|
||||||
for suggestion in code_suggestions:
|
for suggestion in code_suggestions:
|
||||||
body = suggestion["body"]
|
body = suggestion["body"]
|
||||||
original_suggestion = suggestion.get('original_suggestion', None) # needed for diff code
|
|
||||||
if original_suggestion:
|
|
||||||
try:
|
|
||||||
existing_code = original_suggestion['existing_code'].rstrip() + "\n"
|
|
||||||
improved_code = original_suggestion['improved_code'].rstrip() + "\n"
|
|
||||||
diff = difflib.unified_diff(existing_code.split('\n'),
|
|
||||||
improved_code.split('\n'), n=999)
|
|
||||||
patch_orig = "\n".join(diff)
|
|
||||||
patch = "\n".join(patch_orig.splitlines()[5:]).strip('\n')
|
|
||||||
diff_code = f"\n\n```diff\n{patch.rstrip()}\n```"
|
|
||||||
# replace ```suggestion ... ``` with diff_code, using regex:
|
|
||||||
body = re.sub(r'```suggestion.*?```', diff_code, body, flags=re.DOTALL)
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().exception(f"Bitbucket failed to get diff code for publishing, error: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
relevant_file = suggestion["relevant_file"]
|
relevant_file = suggestion["relevant_file"]
|
||||||
relevant_lines_start = suggestion["relevant_lines_start"]
|
relevant_lines_start = suggestion["relevant_lines_start"]
|
||||||
relevant_lines_end = suggestion["relevant_lines_end"]
|
relevant_lines_end = suggestion["relevant_lines_end"]
|
||||||
|
|
||||||
if not relevant_lines_start or relevant_lines_start == -1:
|
if not relevant_lines_start or relevant_lines_start == -1:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().exception(
|
get_logger().exception(
|
||||||
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
|
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if relevant_lines_end < relevant_lines_start:
|
if relevant_lines_end < relevant_lines_start:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().exception(
|
get_logger().exception(
|
||||||
f"Failed to publish code suggestion, "
|
f"Failed to publish code suggestion, "
|
||||||
f"relevant_lines_end is {relevant_lines_end} and "
|
f"relevant_lines_end is {relevant_lines_end} and "
|
||||||
@ -129,7 +112,8 @@ class BitbucketProvider(GitProvider):
|
|||||||
self.publish_inline_comments(post_parameters_list)
|
self.publish_inline_comments(post_parameters_list)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Bitbucket failed to publish code suggestion, error: {e}")
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().error(f"Failed to publish code suggestion, error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def publish_file_comments(self, file_comments: list) -> bool:
|
def publish_file_comments(self, file_comments: list) -> bool:
|
||||||
@ -325,9 +309,6 @@ class BitbucketProvider(GitProvider):
|
|||||||
self.publish_comment(pr_comment)
|
self.publish_comment(pr_comment)
|
||||||
|
|
||||||
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
|
||||||
if is_temporary and not get_settings().config.publish_output_progress:
|
|
||||||
get_logger().debug(f"Skipping publish_comment for temporary comment: {pr_comment}")
|
|
||||||
return None
|
|
||||||
pr_comment = self.limit_output_characters(pr_comment, self.max_comment_length)
|
pr_comment = self.limit_output_characters(pr_comment, self.max_comment_length)
|
||||||
comment = self.pr.comment(pr_comment)
|
comment = self.pr.comment(pr_comment)
|
||||||
if is_temporary:
|
if is_temporary:
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
import difflib
|
from distutils.version import LooseVersion
|
||||||
import re
|
from requests.exceptions import HTTPError
|
||||||
|
|
||||||
from packaging.version import parse as parse_version
|
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import quote_plus, urlparse
|
from urllib.parse import quote_plus, urlparse
|
||||||
|
|
||||||
from atlassian.bitbucket import Bitbucket
|
from atlassian.bitbucket import Bitbucket
|
||||||
from requests.exceptions import HTTPError
|
|
||||||
|
|
||||||
from ..algo.git_patch_processing import decode_if_bytes
|
from .git_provider import GitProvider
|
||||||
from ..algo.language_handler import is_valid_file
|
|
||||||
from ..algo.types import EDIT_TYPE, FilePatchInfo
|
from ..algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
from ..algo.utils import (find_line_number_of_relevant_line_in_file,
|
from ..algo.language_handler import is_valid_file
|
||||||
load_large_diff)
|
from ..algo.utils import load_large_diff, find_line_number_of_relevant_line_in_file
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .git_provider import GitProvider
|
|
||||||
|
|
||||||
|
|
||||||
class BitbucketServerProvider(GitProvider):
|
class BitbucketServerProvider(GitProvider):
|
||||||
@ -40,7 +35,7 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
token=get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN",
|
token=get_settings().get("BITBUCKET_SERVER.BEARER_TOKEN",
|
||||||
None))
|
None))
|
||||||
try:
|
try:
|
||||||
self.bitbucket_api_version = parse_version(self.bitbucket_client.get("rest/api/1.0/application-properties").get('version'))
|
self.bitbucket_api_version = LooseVersion(self.bitbucket_client.get("rest/api/1.0/application-properties").get('version'))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.bitbucket_api_version = None
|
self.bitbucket_api_version = None
|
||||||
|
|
||||||
@ -70,33 +65,20 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
post_parameters_list = []
|
post_parameters_list = []
|
||||||
for suggestion in code_suggestions:
|
for suggestion in code_suggestions:
|
||||||
body = suggestion["body"]
|
body = suggestion["body"]
|
||||||
original_suggestion = suggestion.get('original_suggestion', None) # needed for diff code
|
|
||||||
if original_suggestion:
|
|
||||||
try:
|
|
||||||
existing_code = original_suggestion['existing_code'].rstrip() + "\n"
|
|
||||||
improved_code = original_suggestion['improved_code'].rstrip() + "\n"
|
|
||||||
diff = difflib.unified_diff(existing_code.split('\n'),
|
|
||||||
improved_code.split('\n'), n=999)
|
|
||||||
patch_orig = "\n".join(diff)
|
|
||||||
patch = "\n".join(patch_orig.splitlines()[5:]).strip('\n')
|
|
||||||
diff_code = f"\n\n```diff\n{patch.rstrip()}\n```"
|
|
||||||
# replace ```suggestion ... ``` with diff_code, using regex:
|
|
||||||
body = re.sub(r'```suggestion.*?```', diff_code, body, flags=re.DOTALL)
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().exception(f"Bitbucket failed to get diff code for publishing, error: {e}")
|
|
||||||
continue
|
|
||||||
relevant_file = suggestion["relevant_file"]
|
relevant_file = suggestion["relevant_file"]
|
||||||
relevant_lines_start = suggestion["relevant_lines_start"]
|
relevant_lines_start = suggestion["relevant_lines_start"]
|
||||||
relevant_lines_end = suggestion["relevant_lines_end"]
|
relevant_lines_end = suggestion["relevant_lines_end"]
|
||||||
|
|
||||||
if not relevant_lines_start or relevant_lines_start == -1:
|
if not relevant_lines_start or relevant_lines_start == -1:
|
||||||
get_logger().warning(
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().exception(
|
||||||
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
|
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if relevant_lines_end < relevant_lines_start:
|
if relevant_lines_end < relevant_lines_start:
|
||||||
get_logger().warning(
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().exception(
|
||||||
f"Failed to publish code suggestion, "
|
f"Failed to publish code suggestion, "
|
||||||
f"relevant_lines_end is {relevant_lines_end} and "
|
f"relevant_lines_end is {relevant_lines_end} and "
|
||||||
f"relevant_lines_start is {relevant_lines_start}"
|
f"relevant_lines_start is {relevant_lines_start}"
|
||||||
@ -177,7 +159,7 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
head_sha = self.pr.fromRef['latestCommit']
|
head_sha = self.pr.fromRef['latestCommit']
|
||||||
|
|
||||||
# if Bitbucket api version is >= 8.16 then use the merge-base api for 2-way diff calculation
|
# if Bitbucket api version is >= 8.16 then use the merge-base api for 2-way diff calculation
|
||||||
if self.bitbucket_api_version is not None and self.bitbucket_api_version >= parse_version("8.16"):
|
if self.bitbucket_api_version is not None and self.bitbucket_api_version >= LooseVersion("8.16"):
|
||||||
try:
|
try:
|
||||||
base_sha = self.bitbucket_client.get(self._get_merge_base())['id']
|
base_sha = self.bitbucket_client.get(self._get_merge_base())['id']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -192,7 +174,7 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
# if Bitbucket api version is None or < 7.0 then do a simple diff with a guaranteed common ancestor
|
# if Bitbucket api version is None or < 7.0 then do a simple diff with a guaranteed common ancestor
|
||||||
base_sha = source_commits_list[-1]['parents'][0]['id']
|
base_sha = source_commits_list[-1]['parents'][0]['id']
|
||||||
# if Bitbucket api version is 7.0-8.15 then use 2-way diff functionality for the base_sha
|
# if Bitbucket api version is 7.0-8.15 then use 2-way diff functionality for the base_sha
|
||||||
if self.bitbucket_api_version is not None and self.bitbucket_api_version >= parse_version("7.0"):
|
if self.bitbucket_api_version is not None and self.bitbucket_api_version >= LooseVersion("7.0"):
|
||||||
try:
|
try:
|
||||||
destination_commits = list(
|
destination_commits = list(
|
||||||
self.bitbucket_client.get_commits(self.workspace_slug, self.repo_slug, base_sha,
|
self.bitbucket_client.get_commits(self.workspace_slug, self.repo_slug, base_sha,
|
||||||
@ -218,21 +200,25 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
case 'ADD':
|
case 'ADD':
|
||||||
edit_type = EDIT_TYPE.ADDED
|
edit_type = EDIT_TYPE.ADDED
|
||||||
new_file_content_str = self.get_file(file_path, head_sha)
|
new_file_content_str = self.get_file(file_path, head_sha)
|
||||||
new_file_content_str = decode_if_bytes(new_file_content_str)
|
if isinstance(new_file_content_str, (bytes, bytearray)):
|
||||||
|
new_file_content_str = new_file_content_str.decode("utf-8")
|
||||||
original_file_content_str = ""
|
original_file_content_str = ""
|
||||||
case 'DELETE':
|
case 'DELETE':
|
||||||
edit_type = EDIT_TYPE.DELETED
|
edit_type = EDIT_TYPE.DELETED
|
||||||
new_file_content_str = ""
|
new_file_content_str = ""
|
||||||
original_file_content_str = self.get_file(file_path, base_sha)
|
original_file_content_str = self.get_file(file_path, base_sha)
|
||||||
original_file_content_str = decode_if_bytes(original_file_content_str)
|
if isinstance(original_file_content_str, (bytes, bytearray)):
|
||||||
|
original_file_content_str = original_file_content_str.decode("utf-8")
|
||||||
case 'RENAME':
|
case 'RENAME':
|
||||||
edit_type = EDIT_TYPE.RENAMED
|
edit_type = EDIT_TYPE.RENAMED
|
||||||
case _:
|
case _:
|
||||||
edit_type = EDIT_TYPE.MODIFIED
|
edit_type = EDIT_TYPE.MODIFIED
|
||||||
original_file_content_str = self.get_file(file_path, base_sha)
|
original_file_content_str = self.get_file(file_path, base_sha)
|
||||||
original_file_content_str = decode_if_bytes(original_file_content_str)
|
if isinstance(original_file_content_str, (bytes, bytearray)):
|
||||||
|
original_file_content_str = original_file_content_str.decode("utf-8")
|
||||||
new_file_content_str = self.get_file(file_path, head_sha)
|
new_file_content_str = self.get_file(file_path, head_sha)
|
||||||
new_file_content_str = decode_if_bytes(new_file_content_str)
|
if isinstance(new_file_content_str, (bytes, bytearray)):
|
||||||
|
new_file_content_str = new_file_content_str.decode("utf-8")
|
||||||
|
|
||||||
patch = load_large_diff(file_path, new_file_content_str, original_file_content_str)
|
patch = load_large_diff(file_path, new_file_content_str, original_file_content_str)
|
||||||
|
|
||||||
@ -402,21 +388,10 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
projects_index = path_parts.index("projects")
|
projects_index = path_parts.index("projects")
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
projects_index = -1
|
|
||||||
|
|
||||||
try:
|
|
||||||
users_index = path_parts.index("users")
|
|
||||||
except ValueError:
|
|
||||||
users_index = -1
|
|
||||||
|
|
||||||
if projects_index == -1 and users_index == -1:
|
|
||||||
raise ValueError(f"The provided URL '{pr_url}' does not appear to be a Bitbucket PR URL")
|
raise ValueError(f"The provided URL '{pr_url}' does not appear to be a Bitbucket PR URL")
|
||||||
|
|
||||||
if projects_index != -1:
|
|
||||||
path_parts = path_parts[projects_index:]
|
path_parts = path_parts[projects_index:]
|
||||||
else:
|
|
||||||
path_parts = path_parts[users_index:]
|
|
||||||
|
|
||||||
if len(path_parts) < 6 or path_parts[2] != "repos" or path_parts[4] != "pull-requests":
|
if len(path_parts) < 6 or path_parts[2] != "repos" or path_parts[4] != "pull-requests":
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -424,8 +399,6 @@ class BitbucketServerProvider(GitProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
workspace_slug = path_parts[1]
|
workspace_slug = path_parts[1]
|
||||||
if users_index != -1:
|
|
||||||
workspace_slug = f"~{workspace_slug}"
|
|
||||||
repo_slug = path_parts[3]
|
repo_slug = path_parts[3]
|
||||||
try:
|
try:
|
||||||
pr_number = int(path_parts[5])
|
pr_number = int(path_parts[5])
|
||||||
|
@ -4,15 +4,13 @@ from collections import Counter
|
|||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from pr_agent.algo.language_handler import is_valid_file
|
|
||||||
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
|
||||||
from pr_agent.git_providers.codecommit_client import CodeCommitClient
|
from pr_agent.git_providers.codecommit_client import CodeCommitClient
|
||||||
|
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
from ..algo.utils import load_large_diff
|
from ..algo.utils import load_large_diff
|
||||||
|
from .git_provider import GitProvider
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .git_provider import GitProvider
|
from pr_agent.algo.language_handler import is_valid_file
|
||||||
|
|
||||||
|
|
||||||
class PullRequestCCMimic:
|
class PullRequestCCMimic:
|
||||||
"""
|
"""
|
||||||
|
@ -12,9 +12,9 @@ import requests
|
|||||||
import urllib3.util
|
import urllib3.util
|
||||||
from git import Repo
|
from git import Repo
|
||||||
|
|
||||||
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.git_provider import GitProvider
|
from pr_agent.git_providers.git_provider import GitProvider
|
||||||
|
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
from pr_agent.git_providers.local_git_provider import PullRequestMimic
|
from pr_agent.git_providers.local_git_provider import PullRequestMimic
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
|
# enum EDIT_TYPE (ADDED, DELETED, MODIFIED, RENAMED)
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pr_agent.algo.types import FilePatchInfo
|
|
||||||
from pr_agent.algo.utils import Range, process_description
|
from pr_agent.algo.utils import Range, process_description
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.algo.types import FilePatchInfo
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
MAX_FILES_ALLOWED_FULL = 50
|
MAX_FILES_ALLOWED_FULL = 50
|
||||||
|
|
||||||
class GitProvider(ABC):
|
class GitProvider(ABC):
|
||||||
@ -62,8 +62,8 @@ class GitProvider(ABC):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def get_pr_description(self, full: bool = True, split_changes_walkthrough=False) -> str or tuple:
|
def get_pr_description(self, full: bool = True, split_changes_walkthrough=False) -> str or tuple:
|
||||||
from pr_agent.algo.utils import clip_tokens
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.algo.utils import clip_tokens
|
||||||
max_tokens_description = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
|
max_tokens_description = get_settings().get("CONFIG.MAX_DESCRIPTION_TOKENS", None)
|
||||||
description = self.get_pr_description_full() if full else self.get_user_description()
|
description = self.get_pr_description_full() if full else self.get_user_description()
|
||||||
if split_changes_walkthrough:
|
if split_changes_walkthrough:
|
||||||
|
@ -1,30 +1,22 @@
|
|||||||
import copy
|
|
||||||
import difflib
|
|
||||||
import hashlib
|
|
||||||
import itertools
|
import itertools
|
||||||
import re
|
|
||||||
import time
|
import time
|
||||||
|
import hashlib
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from github import AppAuthentication, Auth, Github
|
from github import AppAuthentication, Auth, Github
|
||||||
from retry import retry
|
from retry import retry
|
||||||
from starlette_context import context
|
from starlette_context import context
|
||||||
|
|
||||||
from ..algo.file_filter import filter_ignored
|
from ..algo.file_filter import filter_ignored
|
||||||
from ..algo.git_patch_processing import extract_hunk_headers
|
|
||||||
from ..algo.language_handler import is_valid_file
|
from ..algo.language_handler import is_valid_file
|
||||||
from ..algo.types import EDIT_TYPE
|
from ..algo.types import EDIT_TYPE
|
||||||
from ..algo.utils import (PRReviewHeader, Range, clip_tokens,
|
from ..algo.utils import PRReviewHeader, load_large_diff, clip_tokens, find_line_number_of_relevant_line_in_file, Range
|
||||||
find_line_number_of_relevant_line_in_file,
|
|
||||||
load_large_diff, set_file_languages)
|
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ..servers.utils import RateLimitExceeded
|
from ..servers.utils import RateLimitExceeded
|
||||||
from .git_provider import (MAX_FILES_ALLOWED_FULL, FilePatchInfo, GitProvider,
|
from .git_provider import FilePatchInfo, GitProvider, IncrementalPR, MAX_FILES_ALLOWED_FULL
|
||||||
IncrementalPR)
|
|
||||||
|
|
||||||
|
|
||||||
class GithubProvider(GitProvider):
|
class GithubProvider(GitProvider):
|
||||||
@ -174,24 +166,6 @@ class GithubProvider(GitProvider):
|
|||||||
|
|
||||||
diff_files = []
|
diff_files = []
|
||||||
invalid_files_names = []
|
invalid_files_names = []
|
||||||
is_close_to_rate_limit = False
|
|
||||||
|
|
||||||
# The base.sha will point to the current state of the base branch (including parallel merges), not the original base commit when the PR was created
|
|
||||||
# We can fix this by finding the merge base commit between the PR head and base branches
|
|
||||||
# Note that The pr.head.sha is actually correct as is - it points to the latest commit in your PR branch.
|
|
||||||
# This SHA isn't affected by parallel merges to the base branch since it's specific to your PR's branch.
|
|
||||||
repo = self.repo_obj
|
|
||||||
pr = self.pr
|
|
||||||
try:
|
|
||||||
compare = repo.compare(pr.base.sha, pr.head.sha) # communication with GitHub
|
|
||||||
merge_base_commit = compare.merge_base_commit
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to get merge base commit: {e}")
|
|
||||||
merge_base_commit = pr.base
|
|
||||||
if merge_base_commit.sha != pr.base.sha:
|
|
||||||
get_logger().info(
|
|
||||||
f"Using merge base commit {merge_base_commit.sha} instead of base commit ")
|
|
||||||
|
|
||||||
counter_valid = 0
|
counter_valid = 0
|
||||||
for file in files:
|
for file in files:
|
||||||
if not is_valid_file(file.filename):
|
if not is_valid_file(file.filename):
|
||||||
@ -199,10 +173,7 @@ class GithubProvider(GitProvider):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
patch = file.patch
|
patch = file.patch
|
||||||
if is_close_to_rate_limit:
|
|
||||||
new_file_content_str = ""
|
|
||||||
original_file_content_str = ""
|
|
||||||
else:
|
|
||||||
# allow only a limited number of files to be fully loaded. We can manage the rest with diffs only
|
# allow only a limited number of files to be fully loaded. We can manage the rest with diffs only
|
||||||
counter_valid += 1
|
counter_valid += 1
|
||||||
avoid_load = False
|
avoid_load = False
|
||||||
@ -224,12 +195,10 @@ class GithubProvider(GitProvider):
|
|||||||
if avoid_load:
|
if avoid_load:
|
||||||
original_file_content_str = ""
|
original_file_content_str = ""
|
||||||
else:
|
else:
|
||||||
original_file_content_str = self._get_pr_file_content(file, merge_base_commit.sha)
|
original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha)
|
||||||
# original_file_content_str = self._get_pr_file_content(file, self.pr.base.sha)
|
|
||||||
if not patch:
|
if not patch:
|
||||||
patch = load_large_diff(file.filename, new_file_content_str, original_file_content_str)
|
patch = load_large_diff(file.filename, new_file_content_str, original_file_content_str)
|
||||||
|
|
||||||
|
|
||||||
if file.status == 'added':
|
if file.status == 'added':
|
||||||
edit_type = EDIT_TYPE.ADDED
|
edit_type = EDIT_TYPE.ADDED
|
||||||
elif file.status == 'removed':
|
elif file.status == 'removed':
|
||||||
@ -243,14 +212,9 @@ class GithubProvider(GitProvider):
|
|||||||
edit_type = EDIT_TYPE.UNKNOWN
|
edit_type = EDIT_TYPE.UNKNOWN
|
||||||
|
|
||||||
# count number of lines added and removed
|
# count number of lines added and removed
|
||||||
if hasattr(file, 'additions') and hasattr(file, 'deletions'):
|
|
||||||
num_plus_lines = file.additions
|
|
||||||
num_minus_lines = file.deletions
|
|
||||||
else:
|
|
||||||
patch_lines = patch.splitlines(keepends=True)
|
patch_lines = patch.splitlines(keepends=True)
|
||||||
num_plus_lines = len([line for line in patch_lines if line.startswith('+')])
|
num_plus_lines = len([line for line in patch_lines if line.startswith('+')])
|
||||||
num_minus_lines = len([line for line in patch_lines if line.startswith('-')])
|
num_minus_lines = len([line for line in patch_lines if line.startswith('-')])
|
||||||
|
|
||||||
file_patch_canonical_structure = FilePatchInfo(original_file_content_str, new_file_content_str, patch,
|
file_patch_canonical_structure = FilePatchInfo(original_file_content_str, new_file_content_str, patch,
|
||||||
file.filename, edit_type=edit_type,
|
file.filename, edit_type=edit_type,
|
||||||
num_plus_lines=num_plus_lines,
|
num_plus_lines=num_plus_lines,
|
||||||
@ -315,6 +279,7 @@ class GithubProvider(GitProvider):
|
|||||||
relevant_line_in_file,
|
relevant_line_in_file,
|
||||||
absolute_position)
|
absolute_position)
|
||||||
if position == -1:
|
if position == -1:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
get_logger().info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
||||||
subject_type = "FILE"
|
subject_type = "FILE"
|
||||||
else:
|
else:
|
||||||
@ -327,9 +292,11 @@ class GithubProvider(GitProvider):
|
|||||||
# publish all comments in a single message
|
# publish all comments in a single message
|
||||||
self.pr.create_review(commit=self.last_commit_id, comments=comments)
|
self.pr.create_review(commit=self.last_commit_id, comments=comments)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().info(f"Initially failed to publish inline comments as committable")
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().error(f"Failed to publish inline comments")
|
||||||
|
|
||||||
if (getattr(e, "status", None) == 422 and not disable_fallback):
|
if (getattr(e, "status", None) == 422
|
||||||
|
and get_settings().github.publish_inline_comments_fallback_with_verification and not disable_fallback):
|
||||||
pass # continue to try _publish_inline_comments_fallback_with_verification
|
pass # continue to try _publish_inline_comments_fallback_with_verification
|
||||||
else:
|
else:
|
||||||
raise e # will end up with publishing the comments one by one
|
raise e # will end up with publishing the comments one by one
|
||||||
@ -337,6 +304,7 @@ class GithubProvider(GitProvider):
|
|||||||
try:
|
try:
|
||||||
self._publish_inline_comments_fallback_with_verification(comments)
|
self._publish_inline_comments_fallback_with_verification(comments)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().error(f"Failed to publish inline code comments fallback, error: {e}")
|
get_logger().error(f"Failed to publish inline code comments fallback, error: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@ -362,8 +330,10 @@ class GithubProvider(GitProvider):
|
|||||||
for comment in fixed_comments_as_one_liner:
|
for comment in fixed_comments_as_one_liner:
|
||||||
try:
|
try:
|
||||||
self.publish_inline_comments([comment], disable_fallback=True)
|
self.publish_inline_comments([comment], disable_fallback=True)
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Published invalid comment as a single line comment: {comment}")
|
get_logger().info(f"Published invalid comment as a single line comment: {comment}")
|
||||||
except:
|
except:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().error(f"Failed to publish invalid comment as a single line comment: {comment}")
|
get_logger().error(f"Failed to publish invalid comment as a single line comment: {comment}")
|
||||||
|
|
||||||
def _verify_code_comment(self, comment: dict):
|
def _verify_code_comment(self, comment: dict):
|
||||||
@ -422,6 +392,7 @@ class GithubProvider(GitProvider):
|
|||||||
if fixed_comment != comment:
|
if fixed_comment != comment:
|
||||||
fixed_comments.append(fixed_comment)
|
fixed_comments.append(fixed_comment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().error(f"Failed to fix inline comment, error: {e}")
|
get_logger().error(f"Failed to fix inline comment, error: {e}")
|
||||||
return fixed_comments
|
return fixed_comments
|
||||||
|
|
||||||
@ -430,21 +401,20 @@ class GithubProvider(GitProvider):
|
|||||||
Publishes code suggestions as comments on the PR.
|
Publishes code suggestions as comments on the PR.
|
||||||
"""
|
"""
|
||||||
post_parameters_list = []
|
post_parameters_list = []
|
||||||
|
for suggestion in code_suggestions:
|
||||||
code_suggestions_validated = self.validate_comments_inside_hunks(code_suggestions)
|
|
||||||
|
|
||||||
for suggestion in code_suggestions_validated:
|
|
||||||
body = suggestion['body']
|
body = suggestion['body']
|
||||||
relevant_file = suggestion['relevant_file']
|
relevant_file = suggestion['relevant_file']
|
||||||
relevant_lines_start = suggestion['relevant_lines_start']
|
relevant_lines_start = suggestion['relevant_lines_start']
|
||||||
relevant_lines_end = suggestion['relevant_lines_end']
|
relevant_lines_end = suggestion['relevant_lines_end']
|
||||||
|
|
||||||
if not relevant_lines_start or relevant_lines_start == -1:
|
if not relevant_lines_start or relevant_lines_start == -1:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().exception(
|
get_logger().exception(
|
||||||
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
|
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if relevant_lines_end < relevant_lines_start:
|
if relevant_lines_end < relevant_lines_start:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().exception(f"Failed to publish code suggestion, "
|
get_logger().exception(f"Failed to publish code suggestion, "
|
||||||
f"relevant_lines_end is {relevant_lines_end} and "
|
f"relevant_lines_end is {relevant_lines_end} and "
|
||||||
f"relevant_lines_start is {relevant_lines_start}")
|
f"relevant_lines_start is {relevant_lines_start}")
|
||||||
@ -471,6 +441,7 @@ class GithubProvider(GitProvider):
|
|||||||
self.publish_inline_comments(post_parameters_list)
|
self.publish_inline_comments(post_parameters_list)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().error(f"Failed to publish code suggestion, error: {e}")
|
get_logger().error(f"Failed to publish code suggestion, error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -530,7 +501,6 @@ class GithubProvider(GitProvider):
|
|||||||
elif self.deployment_type == 'user':
|
elif self.deployment_type == 'user':
|
||||||
same_comment_creator = self.github_user_id == existing_comment['user']['login']
|
same_comment_creator = self.github_user_id == existing_comment['user']['login']
|
||||||
if existing_comment['subject_type'] == 'file' and comment['path'] == existing_comment['path'] and same_comment_creator:
|
if existing_comment['subject_type'] == 'file' and comment['path'] == existing_comment['path'] and same_comment_creator:
|
||||||
|
|
||||||
headers, data_patch = self.pr._requester.requestJsonAndCheck(
|
headers, data_patch = self.pr._requester.requestJsonAndCheck(
|
||||||
"PATCH", f"{self.base_url}/repos/{self.repo}/pulls/comments/{existing_comment['id']}", input={"body":comment['body']}
|
"PATCH", f"{self.base_url}/repos/{self.repo}/pulls/comments/{existing_comment['id']}", input={"body":comment['body']}
|
||||||
)
|
)
|
||||||
@ -542,6 +512,7 @@ class GithubProvider(GitProvider):
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().error(f"Failed to publish diffview file summary, error: {e}")
|
get_logger().error(f"Failed to publish diffview file summary, error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -830,6 +801,7 @@ class GithubProvider(GitProvider):
|
|||||||
link = f"{self.base_url_html}/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{absolute_position}"
|
link = f"{self.base_url_html}/{self.repo}/pull/{self.pr_num}/files#diff-{sha_file}R{absolute_position}"
|
||||||
return link
|
return link
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
get_logger().info(f"Failed adding line link, error: {e}")
|
get_logger().info(f"Failed adding line link, error: {e}")
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
@ -890,89 +862,3 @@ class GithubProvider(GitProvider):
|
|||||||
|
|
||||||
def calc_pr_statistics(self, pull_request_data: dict):
|
def calc_pr_statistics(self, pull_request_data: dict):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def validate_comments_inside_hunks(self, code_suggestions):
|
|
||||||
"""
|
|
||||||
validate that all committable comments are inside PR hunks - this is a must for committable comments in GitHub
|
|
||||||
"""
|
|
||||||
code_suggestions_copy = copy.deepcopy(code_suggestions)
|
|
||||||
diff_files = self.get_diff_files()
|
|
||||||
RE_HUNK_HEADER = re.compile(
|
|
||||||
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")
|
|
||||||
|
|
||||||
diff_files = set_file_languages(diff_files)
|
|
||||||
|
|
||||||
for suggestion in code_suggestions_copy:
|
|
||||||
try:
|
|
||||||
relevant_file_path = suggestion['relevant_file']
|
|
||||||
for file in diff_files:
|
|
||||||
if file.filename == relevant_file_path:
|
|
||||||
|
|
||||||
# generate on-demand the patches range for the relevant file
|
|
||||||
patch_str = file.patch
|
|
||||||
if not hasattr(file, 'patches_range'):
|
|
||||||
file.patches_range = []
|
|
||||||
patch_lines = patch_str.splitlines()
|
|
||||||
for i, line in enumerate(patch_lines):
|
|
||||||
if line.startswith('@@'):
|
|
||||||
match = RE_HUNK_HEADER.match(line)
|
|
||||||
# identify hunk header
|
|
||||||
if match:
|
|
||||||
section_header, size1, size2, start1, start2 = extract_hunk_headers(match)
|
|
||||||
file.patches_range.append({'start': start2, 'end': start2 + size2 - 1})
|
|
||||||
|
|
||||||
patches_range = file.patches_range
|
|
||||||
comment_start_line = suggestion.get('relevant_lines_start', None)
|
|
||||||
comment_end_line = suggestion.get('relevant_lines_end', None)
|
|
||||||
original_suggestion = suggestion.get('original_suggestion', None) # needed for diff code
|
|
||||||
if not comment_start_line or not comment_end_line or not original_suggestion:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# check if the comment is inside a valid hunk
|
|
||||||
is_valid_hunk = False
|
|
||||||
min_distance = float('inf')
|
|
||||||
patch_range_min = None
|
|
||||||
# find the hunk that contains the comment, or the closest one
|
|
||||||
for i, patch_range in enumerate(patches_range):
|
|
||||||
d1 = comment_start_line - patch_range['start']
|
|
||||||
d2 = patch_range['end'] - comment_end_line
|
|
||||||
if d1 >= 0 and d2 >= 0: # found a valid hunk
|
|
||||||
is_valid_hunk = True
|
|
||||||
min_distance = 0
|
|
||||||
patch_range_min = patch_range
|
|
||||||
break
|
|
||||||
elif d1 * d2 <= 0: # comment is possibly inside the hunk
|
|
||||||
d1_clip = abs(min(0, d1))
|
|
||||||
d2_clip = abs(min(0, d2))
|
|
||||||
d = max(d1_clip, d2_clip)
|
|
||||||
if d < min_distance:
|
|
||||||
patch_range_min = patch_range
|
|
||||||
min_distance = min(min_distance, d)
|
|
||||||
if not is_valid_hunk:
|
|
||||||
if min_distance < 10: # 10 lines - a reasonable distance to consider the comment inside the hunk
|
|
||||||
# make the suggestion non-committable, yet multi line
|
|
||||||
suggestion['relevant_lines_start'] = max(suggestion['relevant_lines_start'], patch_range_min['start'])
|
|
||||||
suggestion['relevant_lines_end'] = min(suggestion['relevant_lines_end'], patch_range_min['end'])
|
|
||||||
body = suggestion['body'].strip()
|
|
||||||
|
|
||||||
# present new diff code in collapsible
|
|
||||||
existing_code = original_suggestion['existing_code'].rstrip() + "\n"
|
|
||||||
improved_code = original_suggestion['improved_code'].rstrip() + "\n"
|
|
||||||
diff = difflib.unified_diff(existing_code.split('\n'),
|
|
||||||
improved_code.split('\n'), n=999)
|
|
||||||
patch_orig = "\n".join(diff)
|
|
||||||
patch = "\n".join(patch_orig.splitlines()[5:]).strip('\n')
|
|
||||||
diff_code = f"\n\n<details><summary>New proposed code:</summary>\n\n```diff\n{patch.rstrip()}\n```"
|
|
||||||
# replace ```suggestion ... ``` with diff_code, using regex:
|
|
||||||
body = re.sub(r'```suggestion.*?```', diff_code, body, flags=re.DOTALL)
|
|
||||||
body += "\n\n</details>"
|
|
||||||
suggestion['body'] = body
|
|
||||||
get_logger().info(f"Comment was moved to a valid hunk, "
|
|
||||||
f"start_line={suggestion['relevant_lines_start']}, end_line={suggestion['relevant_lines_end']}, file={file.filename}")
|
|
||||||
else:
|
|
||||||
get_logger().error(f"Comment is not inside a valid hunk, "
|
|
||||||
f"start_line={suggestion['relevant_lines_start']}, end_line={suggestion['relevant_lines_end']}, file={file.filename}")
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to process patch for committable comment, error: {e}")
|
|
||||||
return code_suggestions_copy
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import difflib
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
@ -8,16 +7,13 @@ import gitlab
|
|||||||
import requests
|
import requests
|
||||||
from gitlab import GitlabGetError
|
from gitlab import GitlabGetError
|
||||||
|
|
||||||
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
|
||||||
|
|
||||||
from ..algo.file_filter import filter_ignored
|
from ..algo.file_filter import filter_ignored
|
||||||
from ..algo.language_handler import is_valid_file
|
from ..algo.language_handler import is_valid_file
|
||||||
from ..algo.utils import (clip_tokens,
|
from ..algo.utils import load_large_diff, clip_tokens, find_line_number_of_relevant_line_in_file
|
||||||
find_line_number_of_relevant_line_in_file,
|
|
||||||
load_large_diff)
|
|
||||||
from ..config_loader import get_settings
|
from ..config_loader import get_settings
|
||||||
|
from .git_provider import GitProvider, MAX_FILES_ALLOWED_FULL
|
||||||
|
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .git_provider import MAX_FILES_ALLOWED_FULL, GitProvider
|
|
||||||
|
|
||||||
|
|
||||||
class DiffNotFoundError(Exception):
|
class DiffNotFoundError(Exception):
|
||||||
@ -194,9 +190,6 @@ class GitLabProvider(GitProvider):
|
|||||||
self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message)
|
self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message)
|
||||||
|
|
||||||
def publish_comment(self, mr_comment: str, is_temporary: bool = False):
|
def publish_comment(self, mr_comment: str, is_temporary: bool = False):
|
||||||
if is_temporary and not get_settings().config.publish_output_progress:
|
|
||||||
get_logger().debug(f"Skipping publish_comment for temporary comment: {mr_comment}")
|
|
||||||
return None
|
|
||||||
mr_comment = self.limit_output_characters(mr_comment, self.max_comment_chars)
|
mr_comment = self.limit_output_characters(mr_comment, self.max_comment_chars)
|
||||||
comment = self.mr.notes.create({'body': mr_comment})
|
comment = self.mr.notes.create({'body': mr_comment})
|
||||||
if is_temporary:
|
if is_temporary:
|
||||||
@ -282,23 +275,20 @@ class GitLabProvider(GitProvider):
|
|||||||
new_code_snippet = original_suggestion['improved_code']
|
new_code_snippet = original_suggestion['improved_code']
|
||||||
content = original_suggestion['suggestion_content']
|
content = original_suggestion['suggestion_content']
|
||||||
label = original_suggestion['label']
|
label = original_suggestion['label']
|
||||||
score = original_suggestion.get('score', 7)
|
if 'score' in original_suggestion:
|
||||||
|
score = original_suggestion['score']
|
||||||
|
else:
|
||||||
|
score = 7
|
||||||
|
|
||||||
if hasattr(self, 'main_language'):
|
if hasattr(self, 'main_language'):
|
||||||
language = self.main_language
|
language = self.main_language
|
||||||
else:
|
else:
|
||||||
language = ''
|
language = ''
|
||||||
link = self.get_line_link(relevant_file, line_start, line_end)
|
link = self.get_line_link(relevant_file, line_start, line_end)
|
||||||
body_fallback =f"**Suggestion:** {content} [{label}, importance: {score}]\n\n"
|
body_fallback =f"**Suggestion:** {content} [{label}, importance: {score}]\n___\n"
|
||||||
body_fallback +=f"\n\n<details><summary>[{target_file.filename} [{line_start}-{line_end}]]({link}):</summary>\n\n"
|
body_fallback +=f"\n\nReplace lines ([{line_start}-{line_end}]({link}))\n\n```{language}\n{old_code_snippet}\n````\n\n"
|
||||||
body_fallback += f"\n\n___\n\n`(Cannot implement directly - GitLab API allows committable suggestions strictly on MR diff lines)`"
|
body_fallback +=f"with\n\n```{language}\n{new_code_snippet}\n````"
|
||||||
body_fallback+="</details>\n\n"
|
body_fallback += f"\n\n___\n\n`(Cannot implement this suggestion directly, as gitlab API does not enable committing to a non -+ line in a PR)`"
|
||||||
diff_patch = difflib.unified_diff(old_code_snippet.split('\n'),
|
|
||||||
new_code_snippet.split('\n'), n=999)
|
|
||||||
patch_orig = "\n".join(diff_patch)
|
|
||||||
patch = "\n".join(patch_orig.splitlines()[5:]).strip('\n')
|
|
||||||
diff_code = f"\n\n```diff\n{patch.rstrip()}\n```"
|
|
||||||
body_fallback += diff_code
|
|
||||||
|
|
||||||
# Create a general note on the file in the MR
|
# Create a general note on the file in the MR
|
||||||
self.mr.notes.create({
|
self.mr.notes.create({
|
||||||
@ -311,7 +301,6 @@ class GitLabProvider(GitProvider):
|
|||||||
'file_path': f'{target_file.filename}',
|
'file_path': f'{target_file.filename}',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
get_logger().debug(f"Created fallback comment in MR {self.id_mr} with position {pos_obj}")
|
|
||||||
|
|
||||||
# get_logger().debug(
|
# get_logger().debug(
|
||||||
# f"Failed to create comment in MR {self.id_mr} with position {pos_obj} (probably not a '+' line)")
|
# f"Failed to create comment in MR {self.id_mr} with position {pos_obj} (probably not a '+' line)")
|
||||||
|
@ -4,9 +4,9 @@ from typing import List
|
|||||||
|
|
||||||
from git import Repo
|
from git import Repo
|
||||||
|
|
||||||
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
|
||||||
from pr_agent.config_loader import _find_repository_root, get_settings
|
from pr_agent.config_loader import _find_repository_root, get_settings
|
||||||
from pr_agent.git_providers.git_provider import GitProvider
|
from pr_agent.git_providers.git_provider import GitProvider
|
||||||
|
from pr_agent.algo.types import EDIT_TYPE, FilePatchInfo
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
|
||||||
@ -141,18 +141,6 @@ class LocalGitProvider(GitProvider):
|
|||||||
def remove_comment(self, comment):
|
def remove_comment(self, comment):
|
||||||
pass # Not applicable to the local git provider, but required by the interface
|
pass # Not applicable to the local git provider, but required by the interface
|
||||||
|
|
||||||
def add_eyes_reaction(self, comment):
|
|
||||||
pass # Not applicable to the local git provider, but required by the interface
|
|
||||||
|
|
||||||
def get_commit_messages(self):
|
|
||||||
pass # Not applicable to the local git provider, but required by the interface
|
|
||||||
|
|
||||||
def get_repo_settings(self):
|
|
||||||
pass # Not applicable to the local git provider, but required by the interface
|
|
||||||
|
|
||||||
def remove_reaction(self, comment):
|
|
||||||
pass # Not applicable to the local git provider, but required by the interface
|
|
||||||
|
|
||||||
def get_languages(self):
|
def get_languages(self):
|
||||||
"""
|
"""
|
||||||
Calculate percentage of languages in repository. Used for hunk prioritisation.
|
Calculate percentage of languages in repository. Used for hunk prioritisation.
|
||||||
|
@ -3,12 +3,11 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from dynaconf import Dynaconf
|
from dynaconf import Dynaconf
|
||||||
from starlette_context import context
|
|
||||||
|
|
||||||
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, get_git_provider_with_context
|
||||||
get_git_provider_with_context)
|
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
from starlette_context import context
|
||||||
|
|
||||||
|
|
||||||
def apply_repo_settings(pr_url):
|
def apply_repo_settings(pr_url):
|
||||||
@ -99,5 +98,5 @@ def set_claude_model():
|
|||||||
"""
|
"""
|
||||||
model_claude = "bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0"
|
model_claude = "bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0"
|
||||||
get_settings().set('config.model', model_claude)
|
get_settings().set('config.model', model_claude)
|
||||||
get_settings().set('config.model_weak', model_claude)
|
get_settings().set('config.model_turbo', model_claude)
|
||||||
get_settings().set('config.fallback_models', [model_claude])
|
get_settings().set('config.fallback_models', [model_claude])
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.identity_providers.default_identity_provider import \
|
from pr_agent.identity_providers.default_identity_provider import DefaultIdentityProvider
|
||||||
DefaultIdentityProvider
|
|
||||||
|
|
||||||
_IDENTITY_PROVIDERS = {
|
_IDENTITY_PROVIDERS = {
|
||||||
'default': DefaultIdentityProvider
|
'default': DefaultIdentityProvider
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
from pr_agent.identity_providers.identity_provider import (Eligibility,
|
from pr_agent.identity_providers.identity_provider import Eligibility, IdentityProvider
|
||||||
IdentityProvider)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultIdentityProvider(IdentityProvider):
|
class DefaultIdentityProvider(IdentityProvider):
|
||||||
|
@ -8,10 +8,12 @@ def get_secret_provider():
|
|||||||
provider_id = get_settings().config.secret_provider
|
provider_id = get_settings().config.secret_provider
|
||||||
if provider_id == 'google_cloud_storage':
|
if provider_id == 'google_cloud_storage':
|
||||||
try:
|
try:
|
||||||
from pr_agent.secret_providers.google_cloud_storage_secret_provider import \
|
from pr_agent.secret_providers.google_cloud_storage_secret_provider import GoogleCloudStorageSecretProvider
|
||||||
GoogleCloudStorageSecretProvider
|
|
||||||
return GoogleCloudStorageSecretProvider()
|
return GoogleCloudStorageSecretProvider()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Failed to initialize google_cloud_storage secret provider {provider_id}") from e
|
raise ValueError(f"Failed to initialize google_cloud_storage secret provider {provider_id}") from e
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown SECRET_PROVIDER")
|
raise ValueError("Unknown SECRET_PROVIDER")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,9 +9,9 @@ import secrets
|
|||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
|
from fastapi import APIRouter, Depends, FastAPI, HTTPException
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
from starlette import status
|
from starlette import status
|
||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
from starlette.middleware import Middleware
|
from starlette.middleware import Middleware
|
||||||
@ -23,6 +23,9 @@ from pr_agent.agent.pr_agent import PRAgent, command2class
|
|||||||
from pr_agent.algo.utils import update_settings_from_args
|
from pr_agent.algo.utils import update_settings_from_args
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.utils import apply_repo_settings
|
from pr_agent.git_providers.utils import apply_repo_settings
|
||||||
|
from pr_agent.log import get_logger
|
||||||
|
from fastapi import Request, Depends
|
||||||
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
||||||
|
|
||||||
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
|
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
|
||||||
@ -64,9 +67,6 @@ def authorize(credentials: HTTPBasicCredentials = Depends(security)):
|
|||||||
|
|
||||||
async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict):
|
async def _perform_commands_azure(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict):
|
||||||
apply_repo_settings(api_url)
|
apply_repo_settings(api_url)
|
||||||
if commands_conf == "pr_commands" and get_settings().config.disable_auto_feedback: # auto commands for PR, and auto feedback is disabled
|
|
||||||
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {api_url=}", **log_context)
|
|
||||||
return
|
|
||||||
commands = get_settings().get(f"azure_devops_server.{commands_conf}")
|
commands = get_settings().get(f"azure_devops_server.{commands_conf}")
|
||||||
get_settings().set("config.is_auto_command", True)
|
get_settings().set("config.is_auto_command", True)
|
||||||
for command in commands:
|
for command in commands:
|
||||||
|
@ -24,6 +24,10 @@ from pr_agent.identity_providers import get_identity_provider
|
|||||||
from pr_agent.identity_providers.identity_provider import Eligibility
|
from pr_agent.identity_providers.identity_provider import Eligibility
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
||||||
from pr_agent.secret_providers import get_secret_provider
|
from pr_agent.secret_providers import get_secret_provider
|
||||||
|
from pr_agent.servers.github_action_runner import get_setting_or_env, is_true
|
||||||
|
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
||||||
|
from pr_agent.tools.pr_description import PRDescription
|
||||||
|
from pr_agent.tools.pr_reviewer import PRReviewer
|
||||||
|
|
||||||
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
|
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@ -71,23 +75,8 @@ async def handle_manifest(request: Request, response: Response):
|
|||||||
return JSONResponse(manifest_obj)
|
return JSONResponse(manifest_obj)
|
||||||
|
|
||||||
|
|
||||||
def _get_username(data):
|
|
||||||
actor = data.get("data", {}).get("actor", {})
|
|
||||||
if actor:
|
|
||||||
if "username" in actor:
|
|
||||||
return actor["username"]
|
|
||||||
elif "display_name" in actor:
|
|
||||||
return actor["display_name"]
|
|
||||||
elif "nickname" in actor:
|
|
||||||
return actor["nickname"]
|
|
||||||
return ""
|
|
||||||
|
|
||||||
|
|
||||||
async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict, data: dict):
|
async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_url: str, log_context: dict, data: dict):
|
||||||
apply_repo_settings(api_url)
|
apply_repo_settings(api_url)
|
||||||
if commands_conf == "pr_commands" and get_settings().config.disable_auto_feedback: # auto commands for PR, and auto feedback is disabled
|
|
||||||
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {api_url=}")
|
|
||||||
return
|
|
||||||
if data.get("event", "") == "pullrequest:created":
|
if data.get("event", "") == "pullrequest:created":
|
||||||
if not should_process_pr_logic(data):
|
if not should_process_pr_logic(data):
|
||||||
return
|
return
|
||||||
@ -109,14 +98,11 @@ async def _perform_commands_bitbucket(commands_conf: str, agent: PRAgent, api_ur
|
|||||||
|
|
||||||
def is_bot_user(data) -> bool:
|
def is_bot_user(data) -> bool:
|
||||||
try:
|
try:
|
||||||
actor = data.get("data", {}).get("actor", {})
|
if data["data"]["actor"]["type"] != "user":
|
||||||
# allow actor type: user . if it's "AppUser" or "team" then it is a bot user
|
get_logger().info(f"BitBucket actor type is not 'user': {data['data']['actor']['type']}")
|
||||||
allowed_actor_types = {"user"}
|
|
||||||
if actor and actor["type"].lower() not in allowed_actor_types:
|
|
||||||
get_logger().info(f"BitBucket actor type is not 'user', skipping: {actor}")
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed 'is_bot_user' logic: {e}")
|
get_logger().error("Failed 'is_bot_user' logic: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -126,14 +112,6 @@ def should_process_pr_logic(data) -> bool:
|
|||||||
title = pr_data.get("title", "")
|
title = pr_data.get("title", "")
|
||||||
source_branch = pr_data.get("source", {}).get("branch", {}).get("name", "")
|
source_branch = pr_data.get("source", {}).get("branch", {}).get("name", "")
|
||||||
target_branch = pr_data.get("destination", {}).get("branch", {}).get("name", "")
|
target_branch = pr_data.get("destination", {}).get("branch", {}).get("name", "")
|
||||||
sender = _get_username(data)
|
|
||||||
|
|
||||||
# logic to ignore PRs from specific users
|
|
||||||
ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", [])
|
|
||||||
if ignore_pr_users and sender:
|
|
||||||
if sender in ignore_pr_users:
|
|
||||||
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' setting")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# logic to ignore PRs with specific titles
|
# logic to ignore PRs with specific titles
|
||||||
if title:
|
if title:
|
||||||
@ -183,9 +161,16 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
|
|||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
# Get the username of the sender
|
# Get the username of the sender
|
||||||
log_context["sender"] = _get_username(data)
|
try:
|
||||||
|
username = data["data"]["actor"]["username"]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
username = data["data"]["actor"]["display_name"]
|
||||||
|
except KeyError:
|
||||||
|
username = data["data"]["actor"]["nickname"]
|
||||||
|
log_context["sender"] = username
|
||||||
|
|
||||||
sender_id = data.get("data", {}).get("actor", {}).get("account_id", "")
|
sender_id = data["data"]["actor"]["account_id"]
|
||||||
log_context["sender_id"] = sender_id
|
log_context["sender_id"] = sender_id
|
||||||
jwt_parts = input_jwt.split(".")
|
jwt_parts = input_jwt.split(".")
|
||||||
claim_part = jwt_parts[1]
|
claim_part = jwt_parts[1]
|
||||||
|
@ -6,20 +6,20 @@ from typing import List
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import APIRouter, FastAPI
|
from fastapi import APIRouter, FastAPI
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
from starlette import status
|
from starlette import status
|
||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
from starlette.middleware import Middleware
|
from starlette.middleware import Middleware
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
from starlette_context.middleware import RawContextMiddleware
|
from starlette_context.middleware import RawContextMiddleware
|
||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.algo.utils import update_settings_from_args
|
from pr_agent.algo.utils import update_settings_from_args
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.utils import apply_repo_settings
|
from pr_agent.git_providers.utils import apply_repo_settings
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
||||||
from pr_agent.servers.utils import verify_signature
|
from pr_agent.servers.utils import verify_signature
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
|
|
||||||
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
|
setup_logger(fmt=LoggingFormat.JSON, level="DEBUG")
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@ -72,11 +72,6 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
|
|||||||
commands_to_run = []
|
commands_to_run = []
|
||||||
|
|
||||||
if data["eventKey"] == "pr:opened":
|
if data["eventKey"] == "pr:opened":
|
||||||
apply_repo_settings(pr_url)
|
|
||||||
if get_settings().config.disable_auto_feedback: # auto commands for PR, and auto feedback is disabled
|
|
||||||
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {pr_url}", **log_context)
|
|
||||||
return
|
|
||||||
get_settings().set("config.is_auto_command", True)
|
|
||||||
commands_to_run.extend(_get_commands_list_from_settings('BITBUCKET_SERVER.PR_COMMANDS'))
|
commands_to_run.extend(_get_commands_list_from_settings('BITBUCKET_SERVER.PR_COMMANDS'))
|
||||||
elif data["eventKey"] == "pr:comment:added":
|
elif data["eventKey"] == "pr:comment:added":
|
||||||
commands_to_run.append(data["comment"]["text"])
|
commands_to_run.append(data["comment"]["text"])
|
||||||
|
@ -15,8 +15,7 @@ from starlette_context.middleware import RawContextMiddleware
|
|||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.algo.utils import update_settings_from_args
|
from pr_agent.algo.utils import update_settings_from_args
|
||||||
from pr_agent.config_loader import get_settings, global_settings
|
from pr_agent.config_loader import get_settings, global_settings
|
||||||
from pr_agent.git_providers import (get_git_provider,
|
from pr_agent.git_providers import get_git_provider, get_git_provider_with_context
|
||||||
get_git_provider_with_context)
|
|
||||||
from pr_agent.git_providers.git_provider import IncrementalPR
|
from pr_agent.git_providers.git_provider import IncrementalPR
|
||||||
from pr_agent.git_providers.utils import apply_repo_settings
|
from pr_agent.git_providers.utils import apply_repo_settings
|
||||||
from pr_agent.identity_providers import get_identity_provider
|
from pr_agent.identity_providers import get_identity_provider
|
||||||
@ -257,14 +256,6 @@ def should_process_pr_logic(body) -> bool:
|
|||||||
pr_labels = pull_request.get("labels", [])
|
pr_labels = pull_request.get("labels", [])
|
||||||
source_branch = pull_request.get("head", {}).get("ref", "")
|
source_branch = pull_request.get("head", {}).get("ref", "")
|
||||||
target_branch = pull_request.get("base", {}).get("ref", "")
|
target_branch = pull_request.get("base", {}).get("ref", "")
|
||||||
sender = body.get("sender", {}).get("login")
|
|
||||||
|
|
||||||
# logic to ignore PRs from specific users
|
|
||||||
ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", [])
|
|
||||||
if ignore_pr_users and sender:
|
|
||||||
if sender in ignore_pr_users:
|
|
||||||
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' setting")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# logic to ignore PRs with specific titles
|
# logic to ignore PRs with specific titles
|
||||||
if title:
|
if title:
|
||||||
@ -284,7 +275,6 @@ def should_process_pr_logic(body) -> bool:
|
|||||||
get_logger().info(f"Ignoring PR with labels '{labels_str}' due to config.ignore_pr_labels settings")
|
get_logger().info(f"Ignoring PR with labels '{labels_str}' due to config.ignore_pr_labels settings")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# logic to ignore PRs with specific source or target branches
|
|
||||||
ignore_pr_source_branches = get_settings().get("CONFIG.IGNORE_PR_SOURCE_BRANCHES", [])
|
ignore_pr_source_branches = get_settings().get("CONFIG.IGNORE_PR_SOURCE_BRANCHES", [])
|
||||||
ignore_pr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", [])
|
ignore_pr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", [])
|
||||||
if pull_request and (ignore_pr_source_branches or ignore_pr_target_branches):
|
if pull_request and (ignore_pr_source_branches or ignore_pr_target_branches):
|
||||||
@ -383,9 +373,6 @@ def _check_pull_request_event(action: str, body: dict, log_context: dict) -> Tup
|
|||||||
async def _perform_auto_commands_github(commands_conf: str, agent: PRAgent, body: dict, api_url: str,
|
async def _perform_auto_commands_github(commands_conf: str, agent: PRAgent, body: dict, api_url: str,
|
||||||
log_context: dict):
|
log_context: dict):
|
||||||
apply_repo_settings(api_url)
|
apply_repo_settings(api_url)
|
||||||
if commands_conf == "pr_commands" and get_settings().config.disable_auto_feedback: # auto commands for PR, and auto feedback is disabled
|
|
||||||
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {api_url=}")
|
|
||||||
return
|
|
||||||
if not should_process_pr_logic(body): # Here we already updated the configuration with the repo settings
|
if not should_process_pr_logic(body): # Here we already updated the configuration with the repo settings
|
||||||
return {}
|
return {}
|
||||||
commands = get_settings().get(f"github_app.{commands_conf}")
|
commands = get_settings().get(f"github_app.{commands_conf}")
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
import traceback
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
import time
|
||||||
import aiohttp
|
|
||||||
import requests
|
import requests
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
@ -84,7 +83,6 @@ async def is_valid_notification(notification, headers, handled_ids, session, use
|
|||||||
return False, handled_ids
|
return False, handled_ids
|
||||||
async with session.get(latest_comment, headers=headers) as comment_response:
|
async with session.get(latest_comment, headers=headers) as comment_response:
|
||||||
check_prev_comments = False
|
check_prev_comments = False
|
||||||
user_tag = "@" + user_id
|
|
||||||
if comment_response.status == 200:
|
if comment_response.status == 200:
|
||||||
comment = await comment_response.json()
|
comment = await comment_response.json()
|
||||||
if 'id' in comment:
|
if 'id' in comment:
|
||||||
@ -102,6 +100,7 @@ async def is_valid_notification(notification, headers, handled_ids, session, use
|
|||||||
get_logger().debug(f"no comment_body")
|
get_logger().debug(f"no comment_body")
|
||||||
check_prev_comments = True
|
check_prev_comments = True
|
||||||
else:
|
else:
|
||||||
|
user_tag = "@" + user_id
|
||||||
if user_tag not in comment_body:
|
if user_tag not in comment_body:
|
||||||
get_logger().debug(f"user_tag not in comment_body")
|
get_logger().debug(f"user_tag not in comment_body")
|
||||||
check_prev_comments = True
|
check_prev_comments = True
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@ -61,9 +61,6 @@ async def handle_request(api_url: str, body: str, log_context: dict, sender_id:
|
|||||||
async def _perform_commands_gitlab(commands_conf: str, agent: PRAgent, api_url: str,
|
async def _perform_commands_gitlab(commands_conf: str, agent: PRAgent, api_url: str,
|
||||||
log_context: dict, data: dict):
|
log_context: dict, data: dict):
|
||||||
apply_repo_settings(api_url)
|
apply_repo_settings(api_url)
|
||||||
if commands_conf == "pr_commands" and get_settings().config.disable_auto_feedback: # auto commands for PR, and auto feedback is disabled
|
|
||||||
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {api_url=}", **log_context)
|
|
||||||
return
|
|
||||||
if not should_process_pr_logic(data): # Here we already updated the configurations
|
if not should_process_pr_logic(data): # Here we already updated the configurations
|
||||||
return
|
return
|
||||||
commands = get_settings().get(f"gitlab.{commands_conf}", {})
|
commands = get_settings().get(f"gitlab.{commands_conf}", {})
|
||||||
@ -100,14 +97,6 @@ def should_process_pr_logic(data) -> bool:
|
|||||||
if not data.get('object_attributes', {}):
|
if not data.get('object_attributes', {}):
|
||||||
return False
|
return False
|
||||||
title = data['object_attributes'].get('title')
|
title = data['object_attributes'].get('title')
|
||||||
sender = data.get("user", {}).get("username", "")
|
|
||||||
|
|
||||||
# logic to ignore PRs from specific users
|
|
||||||
ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", [])
|
|
||||||
if ignore_pr_users and sender:
|
|
||||||
if sender in ignore_pr_users:
|
|
||||||
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' settings")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# logic to ignore MRs for titles, labels and source, target branches.
|
# logic to ignore MRs for titles, labels and source, target branches.
|
||||||
ignore_mr_title = get_settings().get("CONFIG.IGNORE_PR_TITLE", [])
|
ignore_mr_title = get_settings().get("CONFIG.IGNORE_PR_TITLE", [])
|
||||||
|
@ -5,6 +5,7 @@ from starlette_context.middleware import RawContextMiddleware
|
|||||||
|
|
||||||
from pr_agent.servers.github_app import router
|
from pr_agent.servers.github_app import router
|
||||||
|
|
||||||
|
|
||||||
middleware = [Middleware(RawContextMiddleware)]
|
middleware = [Middleware(RawContextMiddleware)]
|
||||||
app = FastAPI(middleware=middleware)
|
app = FastAPI(middleware=middleware)
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
@ -2,7 +2,7 @@ import hashlib
|
|||||||
import hmac
|
import hmac
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import Any, Callable
|
from typing import Callable, Any
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
@ -43,9 +43,6 @@ api_base = "" # the base url for your local Llama 2, Code Llama, and other model
|
|||||||
vertex_project = "" # the google cloud platform project name for your vertexai deployment
|
vertex_project = "" # the google cloud platform project name for your vertexai deployment
|
||||||
vertex_location = "" # the google cloud platform location for your vertexai deployment
|
vertex_location = "" # the google cloud platform location for your vertexai deployment
|
||||||
|
|
||||||
[google_ai_studio]
|
|
||||||
gemini_api_key = "" # the google AI Studio API key
|
|
||||||
|
|
||||||
[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.
|
||||||
@ -63,7 +60,6 @@ webhook_secret = "<WEBHOOK SECRET>" # Optional, may be commented out.
|
|||||||
[gitlab]
|
[gitlab]
|
||||||
# Gitlab personal access token
|
# Gitlab personal access token
|
||||||
personal_access_token = ""
|
personal_access_token = ""
|
||||||
shared_secret = "" # webhook secret
|
|
||||||
|
|
||||||
[bitbucket]
|
[bitbucket]
|
||||||
# For Bitbucket personal/repository bearer token
|
# For Bitbucket personal/repository bearer token
|
||||||
@ -91,6 +87,3 @@ pat = ""
|
|||||||
# Optional, uncomment if you want to use Azure devops webhooks. Value assinged when you create the webhook
|
# Optional, uncomment if you want to use Azure devops webhooks. Value assinged when you create the webhook
|
||||||
# webhook_username = "<basic auth user>"
|
# webhook_username = "<basic auth user>"
|
||||||
# webhook_password = "<basic auth password>"
|
# webhook_password = "<basic auth password>"
|
||||||
|
|
||||||
[deepseek]
|
|
||||||
key = ""
|
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
[config]
|
[config]
|
||||||
# models
|
# models
|
||||||
model="gpt-4o-2024-11-20"
|
model="gpt-4-turbo-2024-04-09"
|
||||||
fallback_models=["gpt-4o-2024-08-06"]
|
model_turbo="gpt-4o-2024-08-06"
|
||||||
#model_weak="gpt-4o-mini-2024-07-18" # optional, a weaker model to use for some easier tasks
|
fallback_models=["gpt-4o-2024-05-13"]
|
||||||
# CLI
|
# CLI
|
||||||
git_provider="github"
|
git_provider="github"
|
||||||
publish_output=true
|
publish_output=true
|
||||||
publish_output_progress=true
|
publish_output_progress=true
|
||||||
publish_output_no_suggestions=true
|
|
||||||
verbosity_level=0 # 0,1,2
|
verbosity_level=0 # 0,1,2
|
||||||
use_extra_bad_extensions=false
|
use_extra_bad_extensions=false
|
||||||
# Configurations
|
# Configurations
|
||||||
use_wiki_settings_file=true
|
use_wiki_settings_file=true
|
||||||
use_repo_settings_file=true
|
use_repo_settings_file=true
|
||||||
use_global_settings_file=true
|
use_global_settings_file=true
|
||||||
disable_auto_feedback = false
|
|
||||||
ai_timeout=120 # 2minutes
|
ai_timeout=120 # 2minutes
|
||||||
skip_keys = []
|
skip_keys = []
|
||||||
# token limits
|
# token limits
|
||||||
@ -34,7 +32,6 @@ ai_disclaimer_title="" # Pro feature, title for a collapsible disclaimer to AI
|
|||||||
ai_disclaimer="" # Pro feature, full text for the AI disclaimer
|
ai_disclaimer="" # Pro feature, full text for the AI disclaimer
|
||||||
output_relevant_configurations=false
|
output_relevant_configurations=false
|
||||||
large_patch_policy = "clip" # "clip", "skip"
|
large_patch_policy = "clip" # "clip", "skip"
|
||||||
duplicate_prompt_examples = false
|
|
||||||
# seed
|
# seed
|
||||||
seed=-1 # set positive value to fix the seed (and ensure temperature=0)
|
seed=-1 # set positive value to fix the seed (and ensure temperature=0)
|
||||||
temperature=0.2
|
temperature=0.2
|
||||||
@ -43,7 +40,6 @@ ignore_pr_title = ["^\\[Auto\\]", "^Auto"] # a list of regular expressions to ma
|
|||||||
ignore_pr_target_branches = [] # a list of regular expressions of target branches to ignore from PR agent when an PR is created
|
ignore_pr_target_branches = [] # a list of regular expressions of target branches to ignore from PR agent when an PR is created
|
||||||
ignore_pr_source_branches = [] # a list of regular expressions of source branches to ignore from PR agent when an PR is created
|
ignore_pr_source_branches = [] # a list of regular expressions of source branches to ignore from PR agent when an PR is created
|
||||||
ignore_pr_labels = [] # labels to ignore from PR agent when an PR is created
|
ignore_pr_labels = [] # labels to ignore from PR agent when an PR is created
|
||||||
ignore_pr_authors = [] # authors to ignore from PR agent when an PR is created
|
|
||||||
#
|
#
|
||||||
is_auto_command = false # will be auto-set to true if the command is triggered by an automation
|
is_auto_command = false # will be auto-set to true if the command is triggered by an automation
|
||||||
enable_ai_metadata = false # will enable adding ai metadata
|
enable_ai_metadata = false # will enable adding ai metadata
|
||||||
@ -57,6 +53,10 @@ require_can_be_split_review=false
|
|||||||
require_security_review=true
|
require_security_review=true
|
||||||
require_ticket_analysis_review=true
|
require_ticket_analysis_review=true
|
||||||
# general options
|
# general options
|
||||||
|
num_code_suggestions=0
|
||||||
|
inline_code_comments = false
|
||||||
|
ask_and_reflect=false
|
||||||
|
#automatic_review=true
|
||||||
persistent_comment=true
|
persistent_comment=true
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
final_update_message = true
|
final_update_message = true
|
||||||
@ -90,7 +90,6 @@ publish_description_as_comment_persistent=true
|
|||||||
## changes walkthrough section
|
## changes walkthrough section
|
||||||
enable_semantic_files_types=true
|
enable_semantic_files_types=true
|
||||||
collapsible_file_list='adaptive' # true, false, 'adaptive'
|
collapsible_file_list='adaptive' # true, false, 'adaptive'
|
||||||
collapsible_file_list_threshold=8
|
|
||||||
inline_file_summary=false # false, true, 'table'
|
inline_file_summary=false # false, true, 'table'
|
||||||
# markers
|
# markers
|
||||||
use_description_markers=false
|
use_description_markers=false
|
||||||
@ -99,6 +98,7 @@ include_generated_by_header=true
|
|||||||
enable_large_pr_handling=true
|
enable_large_pr_handling=true
|
||||||
max_ai_calls=4
|
max_ai_calls=4
|
||||||
async_ai_calls=true
|
async_ai_calls=true
|
||||||
|
mention_extra_files=true
|
||||||
#custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other']
|
#custom_labels = ['Bug fix', 'Tests', 'Bug fix with tests', 'Enhancement', 'Documentation', 'Other']
|
||||||
|
|
||||||
[pr_questions] # /ask #
|
[pr_questions] # /ask #
|
||||||
@ -106,13 +106,13 @@ enable_help_text=false
|
|||||||
|
|
||||||
|
|
||||||
[pr_code_suggestions] # /improve #
|
[pr_code_suggestions] # /improve #
|
||||||
max_context_tokens=16000
|
max_context_tokens=14000
|
||||||
#
|
#
|
||||||
commitable_code_suggestions = false
|
commitable_code_suggestions = false
|
||||||
dual_publishing_score_threshold=-1 # -1 to disable, [0-10] to set the threshold (>=) for publishing a code suggestion both in a table and as commitable
|
dual_publishing_score_threshold=-1 # -1 to disable, [0-10] to set the threshold (>=) for publishing a code suggestion both in a table and as commitable
|
||||||
focus_only_on_problems=true
|
|
||||||
#
|
#
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
|
rank_suggestions = false
|
||||||
enable_help_text=false
|
enable_help_text=false
|
||||||
enable_chat_text=false
|
enable_chat_text=false
|
||||||
enable_intro_text=true
|
enable_intro_text=true
|
||||||
@ -121,13 +121,14 @@ max_history_len=4
|
|||||||
# enable to apply suggestion 💎
|
# enable to apply suggestion 💎
|
||||||
apply_suggestions_checkbox=true
|
apply_suggestions_checkbox=true
|
||||||
# suggestions scoring
|
# suggestions scoring
|
||||||
|
self_reflect_on_suggestions=true
|
||||||
suggestions_score_threshold=0 # [0-10]| recommend not to set this value above 8, since above it may clip highly relevant suggestions
|
suggestions_score_threshold=0 # [0-10]| recommend not to set this value above 8, since above it may clip highly relevant suggestions
|
||||||
# params for '/improve --extended' mode
|
# params for '/improve --extended' mode
|
||||||
auto_extended_mode=true
|
auto_extended_mode=true
|
||||||
num_code_suggestions_per_chunk=4
|
num_code_suggestions_per_chunk=4
|
||||||
max_number_of_calls = 3
|
max_number_of_calls = 3
|
||||||
parallel_calls = true
|
parallel_calls = true
|
||||||
|
rank_extended_suggestions = false
|
||||||
final_clip_factor = 0.8
|
final_clip_factor = 0.8
|
||||||
# self-review checkbox
|
# self-review checkbox
|
||||||
demand_code_suggestions_self_review=false # add a checkbox for the author to self-review the code suggestions
|
demand_code_suggestions_self_review=false # add a checkbox for the author to self-review the code suggestions
|
||||||
@ -137,7 +138,6 @@ fold_suggestions_on_self_review=true # Pro feature. if true, the code suggestion
|
|||||||
# Suggestion impact 💎
|
# Suggestion impact 💎
|
||||||
publish_post_process_suggestion_impact=true
|
publish_post_process_suggestion_impact=true
|
||||||
wiki_page_accepted_suggestions=true
|
wiki_page_accepted_suggestions=true
|
||||||
allow_thumbs_up_down=false
|
|
||||||
|
|
||||||
[pr_custom_prompt] # /custom_prompt #
|
[pr_custom_prompt] # /custom_prompt #
|
||||||
prompt = """\
|
prompt = """\
|
||||||
@ -161,7 +161,6 @@ class_name = "" # in case there are several methods with the same name in
|
|||||||
[pr_update_changelog] # /update_changelog #
|
[pr_update_changelog] # /update_changelog #
|
||||||
push_changelog_changes=false
|
push_changelog_changes=false
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
add_pr_link=true
|
|
||||||
|
|
||||||
[pr_analyze] # /analyze #
|
[pr_analyze] # /analyze #
|
||||||
enable_help_text=true
|
enable_help_text=true
|
||||||
@ -218,7 +217,7 @@ override_deployment_type = true
|
|||||||
handle_pr_actions = ['opened', 'reopened', 'ready_for_review']
|
handle_pr_actions = ['opened', 'reopened', 'ready_for_review']
|
||||||
pr_commands = [
|
pr_commands = [
|
||||||
"/describe --pr_description.final_update_message=false",
|
"/describe --pr_description.final_update_message=false",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
"/improve",
|
"/improve",
|
||||||
]
|
]
|
||||||
# settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits
|
# settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits
|
||||||
@ -230,27 +229,27 @@ push_trigger_pending_tasks_backlog = true
|
|||||||
push_trigger_pending_tasks_ttl = 300
|
push_trigger_pending_tasks_ttl = 300
|
||||||
push_commands = [
|
push_commands = [
|
||||||
"/describe",
|
"/describe",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[gitlab]
|
[gitlab]
|
||||||
url = "https://gitlab.com"
|
url = "https://gitlab.com"
|
||||||
pr_commands = [
|
pr_commands = [
|
||||||
"/describe --pr_description.final_update_message=false",
|
"/describe --pr_description.final_update_message=false",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
"/improve",
|
"/improve",
|
||||||
]
|
]
|
||||||
handle_push_trigger = false
|
handle_push_trigger = false
|
||||||
push_commands = [
|
push_commands = [
|
||||||
"/describe",
|
"/describe",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[bitbucket_app]
|
[bitbucket_app]
|
||||||
pr_commands = [
|
pr_commands = [
|
||||||
"/describe --pr_description.final_update_message=false",
|
"/describe --pr_description.final_update_message=false",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
"/improve --pr_code_suggestions.commitable_code_suggestions=true",
|
"/improve --pr_code_suggestions.commitable_code_suggestions=true --pr_code_suggestions.suggestions_score_threshold=7",
|
||||||
]
|
]
|
||||||
avoid_full_files = false
|
avoid_full_files = false
|
||||||
|
|
||||||
@ -275,8 +274,8 @@ avoid_full_files = false
|
|||||||
url = ""
|
url = ""
|
||||||
pr_commands = [
|
pr_commands = [
|
||||||
"/describe --pr_description.final_update_message=false",
|
"/describe --pr_description.final_update_message=false",
|
||||||
"/review",
|
"/review --pr_reviewer.num_code_suggestions=0",
|
||||||
"/improve --pr_code_suggestions.commitable_code_suggestions=true",
|
"/improve --pr_code_suggestions.commitable_code_suggestions=true --pr_code_suggestions.suggestions_score_threshold=7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[litellm]
|
[litellm]
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
[pr_code_suggestions_prompt]
|
[pr_code_suggestions_prompt]
|
||||||
system="""You are PR-Reviewer, an AI specializing in Pull Request (PR) code analysis and suggestions.
|
system="""You are PR-Reviewer, an AI specializing in Pull Request (PR) code analysis and suggestions.
|
||||||
{%- if not focus_only_on_problems %}
|
Your task is to examine the provided code diff, focusing on new code (lines prefixed with '+'), and offer concise, actionable suggestions to fix possible bugs and problems, and enhance code quality, readability, and performance.
|
||||||
Your task is to examine the provided code diff, focusing on new code (lines prefixed with '+'), and offer concise, actionable suggestions to fix possible bugs and problems, and enhance code quality and performance.
|
|
||||||
{%- else %}
|
|
||||||
Your task is to examine the provided code diff, focusing on new code (lines prefixed with '+'), and offer concise, actionable suggestions to fix critical bugs and problems.
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
The PR code diff will be in the following structured format:
|
The PR code diff will be in the following structured format:
|
||||||
======
|
======
|
||||||
@ -17,56 +14,42 @@ The PR code diff will be in the following structured format:
|
|||||||
|
|
||||||
@@ ... @@ def func1():
|
@@ ... @@ def func1():
|
||||||
__new hunk__
|
__new hunk__
|
||||||
unchanged code line0
|
11 unchanged code line0 in the PR
|
||||||
unchanged code line1
|
12 unchanged code line1 in the PR
|
||||||
+new code line2 added
|
13 +new code line2 added in the PR
|
||||||
unchanged code line3
|
14 unchanged code line3 in the PR
|
||||||
__old hunk__
|
__old hunk__
|
||||||
unchanged code line0
|
unchanged code line0
|
||||||
unchanged code line1
|
unchanged code line1
|
||||||
-old code line2 removed
|
-old code line2 removed in the PR
|
||||||
unchanged code line3
|
unchanged code line3
|
||||||
|
|
||||||
@@ ... @@ def func2():
|
@@ ... @@ def func2():
|
||||||
__new hunk__
|
__new hunk__
|
||||||
unchanged code line4
|
unchanged code line4
|
||||||
+new code line5 removed
|
+new code line5 removed in the PR
|
||||||
unchanged code line6
|
unchanged code line6
|
||||||
|
|
||||||
## File: 'src/file2.py'
|
## File: 'src/file2.py'
|
||||||
...
|
...
|
||||||
======
|
======
|
||||||
|
|
||||||
Important notes about the structured diff format above:
|
- In the format above, the diff is organized into separate '__new hunk__' and '__old hunk__' sections for each code chunk. '__new hunk__' contains the updated code, while '__old hunk__' shows the removed code. If no code was removed in a specific chunk, the __old hunk__ section will be omitted.
|
||||||
1. Each PR code chunk is decoupled into separate '__new hunk__' and '__old hunk__' sections:
|
- Line numbers were added for the '__new hunk__' sections to help referencing specific lines in the code suggestions. These numbers are for reference only and are not part of the actual code.
|
||||||
- The '__new hunk__' section shows the code chunk AFTER the PR changes.
|
- Code lines are prefixed with symbols: '+' for new code added in the PR, '-' for code removed, and ' ' for unchanged code.
|
||||||
- The '__old hunk__' section shows the code chunk BEFORE the PR changes. If no code was removed from the chunk, the '__old hunk__' section will be omitted.
|
|
||||||
2. The diff uses line prefixes to show changes:
|
|
||||||
'+' → new line code added (will appear only in '__new hunk__')
|
|
||||||
'-' → line code removed (will appear only in '__old hunk__')
|
|
||||||
' ' → unchanged context lines (will appear in both sections)
|
|
||||||
{%- if is_ai_metadata %}
|
{%- if is_ai_metadata %}
|
||||||
3. When available, an AI-generated summary will precede each file's diff, with a high-level overview of the changes. Note that this summary may not be fully accurate or complete.
|
- When available, an AI-generated summary will precede each file's diff, with a high-level overview of the changes. Note that this summary may not be fully accurate or complete.
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
|
||||||
Specific guidelines for generating code suggestions:
|
Specific guidelines for generating code suggestions:
|
||||||
{%- if not focus_only_on_problems %}
|
|
||||||
- Provide up to {{ num_code_suggestions }} distinct and insightful code suggestions.
|
- Provide up to {{ num_code_suggestions }} distinct and insightful code suggestions.
|
||||||
{%- else %}
|
- Focus solely on enhancing new code introduced in the PR, identified by '+' prefixes in '__new hunk__' sections (after the line numbers).
|
||||||
- Provide up to {{ num_code_suggestions }} distinct and insightful code suggestions. Return less suggestions if no pertinent ones are applicable.
|
|
||||||
{%- endif %}
|
|
||||||
- DO NOT suggest implementing changes that are already present in the '+' lines compared to the '-' lines.
|
|
||||||
- Focus your suggestions ONLY on new code introduced in the PR ('+' lines in '__new hunk__' sections).
|
|
||||||
{%- if not focus_only_on_problems %}
|
|
||||||
- Prioritize suggestions that address potential issues, critical problems, and bugs in the PR code. Avoid repeating changes already implemented in the PR. If no pertinent suggestions are applicable, return an empty list.
|
- Prioritize suggestions that address potential issues, critical problems, and bugs in the PR code. Avoid repeating changes already implemented in the PR. If no pertinent suggestions are applicable, return an empty list.
|
||||||
- Don't suggest to add docstring, type hints, or comments, to remove unused imports, or to use more specific exception types.
|
- Don't suggest to add docstring, type hints, or comments, to remove unused imports, or to use more specific exception types.
|
||||||
{%- else %}
|
- When referencing variables or names from the code, enclose them in backticks (`). Example: "ensure that `variable_name` is..."
|
||||||
- Only give suggestions that address critical problems and bugs in the PR code. If no relevant suggestions are applicable, return an empty list.
|
- Be mindful you are viewing a partial PR code diff, not the full codebase. Avoid suggestions that might conflict with unseen code or alerting variables not declared in the visible scope, as the context is incomplete.
|
||||||
- Do not suggest to change packages version, add missing import statement, or declare undefined variable.
|
|
||||||
{%- endif %}
|
|
||||||
- When mentioning code elements (variables, names, or files) in your response, surround them with backticks (`). For example: "verify that `user_id` is..."
|
|
||||||
- Note that you only see changed code segments (diff hunks in a PR), not the entire codebase. Avoid suggestions that might duplicate existing functionality or questioning code elements (like variables declerations or import statements) that may be defined elsewhere in the codebase.
|
|
||||||
|
|
||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
@ -84,14 +67,12 @@ class CodeSuggestion(BaseModel):
|
|||||||
relevant_file: str = Field(description="Full path of the relevant file")
|
relevant_file: str = Field(description="Full path of the relevant file")
|
||||||
language: str = Field(description="Programming language used by the relevant file")
|
language: str = Field(description="Programming language used by the relevant file")
|
||||||
suggestion_content: str = Field(description="An actionable suggestion to enhance, improve or fix the new code introduced in the PR. Don't present here actual code snippets, just the suggestion. Be short and concise")
|
suggestion_content: str = Field(description="An actionable suggestion to enhance, improve or fix the new code introduced in the PR. Don't present here actual code snippets, just the suggestion. Be short and concise")
|
||||||
existing_code: str = Field(description="A short code snippet from a '__new hunk__' section that the suggestion aims to enhance or fix. Include only complete code lines. Use ellipsis (...) for brevity if needed. This snippet should represent the specific PR code targeted for improvement.")
|
existing_code: str = Field(description="A short code snippet from a '__new hunk__' section that the suggestion aims to enhance or fix. Include only complete code lines, without line numbers. Use ellipsis (...) for brevity if needed. This snippet should represent the specific PR code targeted for improvement.")
|
||||||
improved_code: str = Field(description="A refined code snippet that replaces the 'existing_code' snippet after implementing the suggestion.")
|
improved_code: str = Field(description="A refined code snippet that replaces the 'existing_code' snippet after implementing the suggestion.")
|
||||||
one_sentence_summary: str = Field(description="A concise, single-sentence overview (up to 6 words) of the suggested improvement. Focus on the 'what'. Be general, and avoid method or variable names.")
|
one_sentence_summary: str = Field(description="A concise, single-sentence overview of the suggested improvement. Focus on the 'what'. Be general, and avoid method or variable names.")
|
||||||
{%- if not focus_only_on_problems %}
|
relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the beginning of the 'existing code' snippet above")
|
||||||
label: str = Field(description="A single, descriptive label that best characterizes the suggestion type. Possible labels include 'security', 'possible bug', 'possible issue', 'performance', 'enhancement', 'best practice', 'maintainability', 'typo'. Other relevant labels are also acceptable.")
|
relevant_lines_end: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion ends (inclusive). Should be derived from the hunk line numbers, and correspond to the end of the 'existing code' snippet above")
|
||||||
{%- else %}
|
label: str = Field(description="A single, descriptive label that best characterizes the suggestion type. Possible labels include 'security', 'possible bug', 'possible issue', 'performance', 'enhancement', 'best practice', 'maintainability'. Other relevant labels are also acceptable.")
|
||||||
label: str = Field(description="A single, descriptive label that best characterizes the suggestion type. Possible labels include 'security', 'critical bug', 'general'. The 'general' section should be used for suggestions that address a major issue, but are necessarily on a critical level.")
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
|
|
||||||
class PRCodeSuggestions(BaseModel):
|
class PRCodeSuggestions(BaseModel):
|
||||||
@ -114,10 +95,13 @@ code_suggestions:
|
|||||||
...
|
...
|
||||||
one_sentence_summary: |
|
one_sentence_summary: |
|
||||||
...
|
...
|
||||||
|
relevant_lines_start: 12
|
||||||
|
relevant_lines_end: 13
|
||||||
label: |
|
label: |
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
Each YAML output MUST be after a newline, indented, with block scalar indicator ('|').
|
Each YAML output MUST be after a newline, indented, with block scalar indicator ('|').
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -125,40 +109,12 @@ user="""--PR Info--
|
|||||||
|
|
||||||
Title: '{{title}}'
|
Title: '{{title}}'
|
||||||
|
|
||||||
{%- if date %}
|
|
||||||
|
|
||||||
Today's Date: {{date}}
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
The PR Diff:
|
The PR Diff:
|
||||||
======
|
======
|
||||||
{{ diff_no_line_numbers|trim }}
|
{{ diff|trim }}
|
||||||
======
|
======
|
||||||
|
|
||||||
{%- if duplicate_prompt_examples %}
|
|
||||||
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```yaml
|
|
||||||
code_suggestions:
|
|
||||||
- relevant_file: |
|
|
||||||
src/file1.py
|
|
||||||
language: |
|
|
||||||
python
|
|
||||||
suggestion_content: |
|
|
||||||
...
|
|
||||||
existing_code: |
|
|
||||||
...
|
|
||||||
improved_code: |
|
|
||||||
...
|
|
||||||
one_sentence_summary: |
|
|
||||||
...
|
|
||||||
label: |
|
|
||||||
...
|
|
||||||
```
|
|
||||||
(replace '...' with actual content)
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
|
|
||||||
Response (should be a valid YAML, and nothing else):
|
Response (should be a valid YAML, and nothing else):
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -15,8 +15,8 @@ Be particularly vigilant for suggestions that:
|
|||||||
- Contradict or ignore parts of the PR's modifications
|
- Contradict or ignore parts of the PR's modifications
|
||||||
In such cases, assign the suggestion a score of 0.
|
In such cases, assign the suggestion a score of 0.
|
||||||
|
|
||||||
Evaluate each valid suggestion by scoring its potential impact on the PR's correctness, quality and functionality.
|
For valid suggestions, your role is to provide an impartial and precise score assessment that accurately reflects each suggestion's potential impact on the PR's correctness, quality and functionality.
|
||||||
In addition, you should also detect the line numbers in the '__new hunk__' section that correspond to the 'existing_code' snippet.
|
|
||||||
|
|
||||||
Key guidelines for evaluation:
|
Key guidelines for evaluation:
|
||||||
- Thoroughly examine both the suggestion content and the corresponding PR code diff. Be vigilant for potential errors in each suggestion, ensuring they are logically sound, accurate, and directly derived from the PR code diff.
|
- Thoroughly examine both the suggestion content and the corresponding PR code diff. Be vigilant for potential errors in each suggestion, ensuring they are logically sound, accurate, and directly derived from the PR code diff.
|
||||||
@ -49,14 +49,14 @@ The PR code diff will be presented in the following structured format:
|
|||||||
|
|
||||||
@@ ... @@ def func1():
|
@@ ... @@ def func1():
|
||||||
__new hunk__
|
__new hunk__
|
||||||
11 unchanged code line0
|
11 unchanged code line0 in the PR
|
||||||
12 unchanged code line1
|
12 unchanged code line1 in the PR
|
||||||
13 +new code line2 added
|
13 +new code line2 added in the PR
|
||||||
14 unchanged code line3
|
14 unchanged code line3 in the PR
|
||||||
__old hunk__
|
__old hunk__
|
||||||
unchanged code line0
|
unchanged code line0
|
||||||
unchanged code line1
|
unchanged code line1
|
||||||
-old code line2 removed
|
-old code line2 removed in the PR
|
||||||
unchanged code line3
|
unchanged code line3
|
||||||
|
|
||||||
@@ ... @@ def func2():
|
@@ ... @@ def func2():
|
||||||
@ -82,8 +82,6 @@ The output must be a YAML object equivalent to type $PRCodeSuggestionsFeedback,
|
|||||||
class CodeSuggestionFeedback(BaseModel):
|
class CodeSuggestionFeedback(BaseModel):
|
||||||
suggestion_summary: str = Field(description="Repeated from the input")
|
suggestion_summary: str = Field(description="Repeated from the input")
|
||||||
relevant_file: str = Field(description="Repeated from the input")
|
relevant_file: str = Field(description="Repeated from the input")
|
||||||
relevant_lines_start: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion starts (inclusive). Should be derived from the hunk line numbers, and correspond to the beginning of the relevant 'existing code' snippet")
|
|
||||||
relevant_lines_end: int = Field(description="The relevant line number, from a '__new hunk__' section, where the suggestion ends (inclusive). Should be derived from the hunk line numbers, and correspond to the end of the relevant 'existing code' snippet")
|
|
||||||
suggestion_score: int = Field(description="Evaluate the suggestion and assign a score from 0 to 10. Give 0 if the suggestion is wrong. For valid suggestions, score from 1 (lowest impact/importance) to 10 (highest impact/importance).")
|
suggestion_score: int = Field(description="Evaluate the suggestion and assign a score from 0 to 10. Give 0 if the suggestion is wrong. For valid suggestions, score from 1 (lowest impact/importance) to 10 (highest impact/importance).")
|
||||||
why: str = Field(description="Briefly explain the score given in 1-2 sentences, focusing on the suggestion's impact, relevance, and accuracy.")
|
why: str = Field(description="Briefly explain the score given in 1-2 sentences, focusing on the suggestion's impact, relevance, and accuracy.")
|
||||||
|
|
||||||
@ -98,8 +96,6 @@ code_suggestions:
|
|||||||
- suggestion_summary: |
|
- suggestion_summary: |
|
||||||
Use a more descriptive variable name here
|
Use a more descriptive variable name here
|
||||||
relevant_file: "src/file1.py"
|
relevant_file: "src/file1.py"
|
||||||
relevant_lines_start: 13
|
|
||||||
relevant_lines_end: 14
|
|
||||||
suggestion_score: 6
|
suggestion_score: 6
|
||||||
why: |
|
why: |
|
||||||
The variable name 't' is not descriptive enough
|
The variable name 't' is not descriptive enough
|
||||||
@ -122,25 +118,6 @@ Below are {{ num_code_suggestions }} AI-generated code suggestions for enhancing
|
|||||||
======
|
======
|
||||||
|
|
||||||
|
|
||||||
{%- if duplicate_prompt_examples %}
|
|
||||||
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```yaml
|
|
||||||
code_suggestions:
|
|
||||||
- suggestion_summary: |
|
|
||||||
...
|
|
||||||
relevant_file: "..."
|
|
||||||
relevant_lines_start: ...
|
|
||||||
relevant_lines_end: ...
|
|
||||||
suggestion_score: ...
|
|
||||||
why: |
|
|
||||||
...
|
|
||||||
- ...
|
|
||||||
```
|
|
||||||
(replace '...' with actual content)
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
Response (should be a valid YAML, and nothing else):
|
Response (should be a valid YAML, and nothing else):
|
||||||
```yaml
|
```yaml
|
||||||
"""
|
"""
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
[pr_description_prompt]
|
[pr_description_prompt]
|
||||||
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
|
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
|
||||||
Your task is to provide a full description for the PR content - type, description, title and files walkthrough.
|
{%- if enable_custom_labels %}
|
||||||
- Focus on the new PR code (lines starting with '+' in the 'PR Git Diff' section).
|
Your task is to provide a full description for the PR content - files walkthrough, title, type, description and labels.
|
||||||
|
{%- else %}
|
||||||
|
Your task is to provide a full description for the PR content - files walkthrough, title, type, and description.
|
||||||
|
{%- endif %}
|
||||||
|
- Focus on the new PR code (lines starting with '+').
|
||||||
- Keep in mind that the 'Previous title', 'Previous description' and 'Commit messages' sections may be partial, simplistic, non-informative or out of date. Hence, compare them to the PR diff code, and use them only as a reference.
|
- Keep in mind that the 'Previous title', 'Previous description' and 'Commit messages' sections may be partial, simplistic, non-informative or out of date. Hence, compare them to the PR diff code, and use them only as a reference.
|
||||||
- The generated title and description should prioritize the most significant changes.
|
- The generated title and description should prioritize the most significant changes.
|
||||||
- If needed, each YAML output should be in block scalar indicator ('|')
|
- If needed, each YAML output should be in block scalar indicator ('|-')
|
||||||
- When quoting variables, names or file paths from the code, use backticks (`) instead of single quote (').
|
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
|
||||||
|
|
||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
@ -34,20 +38,22 @@ class PRType(str, Enum):
|
|||||||
{%- if enable_semantic_files_types %}
|
{%- if enable_semantic_files_types %}
|
||||||
|
|
||||||
class FileDescription(BaseModel):
|
class FileDescription(BaseModel):
|
||||||
filename: str = Field(description="The full file path of the relevant file")
|
filename: str = Field(description="The full file path of the relevant file.")
|
||||||
{%- if include_file_summary_changes %}
|
language: str = Field(description="The programming language of the relevant file.")
|
||||||
changes_summary: str = Field(description="concise summary of the changes in the relevant file, in bullet points (1-4 bullet points).")
|
changes_summary: str = Field(description="concise summary of the changes in the relevant file, in bullet points (1-4 bullet points).")
|
||||||
{%- endif %}
|
changes_title: str = Field(description="an informative title for the changes in the files, describing its main theme (5-10 words).")
|
||||||
changes_title: str = Field(description="one-line summary (5-10 words) capturing the main theme of changes in the file")
|
|
||||||
label: str = Field(description="a single semantic label that represents a type of code changes that occurred in the File. Possible values (partial list): 'bug fix', 'tests', 'enhancement', 'documentation', 'error handling', 'configuration changes', 'dependencies', 'formatting', 'miscellaneous', ...")
|
label: str = Field(description="a single semantic label that represents a type of code changes that occurred in the File. Possible values (partial list): 'bug fix', 'tests', 'enhancement', 'documentation', 'error handling', 'configuration changes', 'dependencies', 'formatting', 'miscellaneous', ...")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
class PRDescription(BaseModel):
|
class PRDescription(BaseModel):
|
||||||
type: List[PRType] = Field(description="one or more types that describe the PR content. Return the label member value (e.g. 'Bug fix', not 'bug_fix')")
|
type: List[PRType] = Field(description="one or more types that describe the PR content. Return the label member value (e.g. 'Bug fix', not 'bug_fix')")
|
||||||
description: str = Field(description="summarize the PR changes in up to four bullet points, each up to 8 words. For large PRs, add sub-bullets if needed. Order bullets by importance, with each bullet highlighting a key change group.")
|
|
||||||
title: str = Field(description="a concise and descriptive title that captures the PR's main theme")
|
|
||||||
{%- if enable_semantic_files_types %}
|
{%- if enable_semantic_files_types %}
|
||||||
pr_files: List[FileDescription] = Field(max_items=20, description="a list of all the files that were changed in the PR, and summary of their changes. Each file must be analyzed regardless of change size.")
|
pr_files: List[FileDescription] = Field(max_items=15, description="a list of the files in the PR, and summary of their changes")
|
||||||
|
{%- endif %}
|
||||||
|
description: str = Field(description="an informative and concise description of the PR. Use bullet points. Display first the most significant changes.")
|
||||||
|
title: str = Field(description="an informative title for the PR, describing its main theme")
|
||||||
|
{%- if enable_custom_labels %}
|
||||||
|
labels: List[Label] = Field(min_items=0, description="choose the relevant custom labels that describe the PR content, and return their keys. Use the value field of the Label object to better understand the label meaning.")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
=====
|
=====
|
||||||
|
|
||||||
@ -58,22 +64,29 @@ Example output:
|
|||||||
type:
|
type:
|
||||||
- ...
|
- ...
|
||||||
- ...
|
- ...
|
||||||
description: |
|
|
||||||
...
|
|
||||||
title: |
|
|
||||||
...
|
|
||||||
{%- if enable_semantic_files_types %}
|
{%- if enable_semantic_files_types %}
|
||||||
pr_files:
|
pr_files:
|
||||||
- filename: |
|
- filename: |
|
||||||
...
|
...
|
||||||
{%- if include_file_summary_changes %}
|
language: |
|
||||||
|
...
|
||||||
changes_summary: |
|
changes_summary: |
|
||||||
...
|
...
|
||||||
{%- endif %}
|
|
||||||
changes_title: |
|
changes_title: |
|
||||||
...
|
...
|
||||||
label: |
|
label: |
|
||||||
label_key_1
|
...
|
||||||
|
...
|
||||||
|
{%- endif %}
|
||||||
|
description: |
|
||||||
|
...
|
||||||
|
title: |
|
||||||
|
...
|
||||||
|
{%- if enable_custom_labels %}
|
||||||
|
labels:
|
||||||
|
- |
|
||||||
|
...
|
||||||
|
- |
|
||||||
...
|
...
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
```
|
```
|
||||||
@ -123,44 +136,13 @@ Commit messages:
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
|
||||||
The PR Git Diff:
|
The PR Diff:
|
||||||
=====
|
=====
|
||||||
{{ diff|trim }}
|
{{ diff|trim }}
|
||||||
=====
|
=====
|
||||||
|
|
||||||
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions, and ' ' (a space) for unchanged lines.
|
Note that lines in the diff body are prefixed with a symbol that represents the type of change: '-' for deletions, '+' for additions, and ' ' (a space) for unchanged lines.
|
||||||
|
|
||||||
{%- if duplicate_prompt_examples %}
|
|
||||||
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```yaml
|
|
||||||
type:
|
|
||||||
- Bug fix
|
|
||||||
- Refactoring
|
|
||||||
- ...
|
|
||||||
description: |
|
|
||||||
...
|
|
||||||
title: |
|
|
||||||
...
|
|
||||||
{%- if enable_semantic_files_types %}
|
|
||||||
pr_files:
|
|
||||||
- filename: |
|
|
||||||
...
|
|
||||||
{%- if include_file_summary_changes %}
|
|
||||||
changes_summary: |
|
|
||||||
...
|
|
||||||
{%- endif %}
|
|
||||||
changes_title: |
|
|
||||||
...
|
|
||||||
label: |
|
|
||||||
label_key_1
|
|
||||||
...
|
|
||||||
{%- endif %}
|
|
||||||
```
|
|
||||||
(replace '...' with the actual values)
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
|
|
||||||
Response (should be a valid YAML, and nothing else):
|
Response (should be a valid YAML, and nothing else):
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
[pr_review_prompt]
|
[pr_review_prompt]
|
||||||
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
|
system="""You are PR-Reviewer, a language model designed to review a Git Pull Request (PR).
|
||||||
|
{%- if num_code_suggestions > 0 %}
|
||||||
|
Your task is to provide constructive and concise feedback for the PR, and also provide meaningful code suggestions.
|
||||||
|
{%- else %}
|
||||||
Your task is to provide constructive and concise feedback for the PR.
|
Your task is to provide constructive and concise feedback for the PR.
|
||||||
|
{%- endif %}
|
||||||
The review should focus on new code added in the PR code diff (lines starting with '+')
|
The review should focus on new code added in the PR code diff (lines starting with '+')
|
||||||
|
|
||||||
|
|
||||||
@ -16,20 +20,20 @@ The format we will use to present the PR code diff:
|
|||||||
|
|
||||||
@@ ... @@ def func1():
|
@@ ... @@ def func1():
|
||||||
__new hunk__
|
__new hunk__
|
||||||
11 unchanged code line0
|
11 unchanged code line0 in the PR
|
||||||
12 unchanged code line1
|
12 unchanged code line1 in the PR
|
||||||
13 +new code line2 added
|
13 +new code line2 added in the PR
|
||||||
14 unchanged code line3
|
14 unchanged code line3 in the PR
|
||||||
__old hunk__
|
__old hunk__
|
||||||
unchanged code line0
|
unchanged code line0
|
||||||
unchanged code line1
|
unchanged code line1
|
||||||
-old code line2 removed
|
-old code line2 removed in the PR
|
||||||
unchanged code line3
|
unchanged code line3
|
||||||
|
|
||||||
@@ ... @@ def func2():
|
@@ ... @@ def func2():
|
||||||
__new hunk__
|
__new hunk__
|
||||||
unchanged code line4
|
unchanged code line4
|
||||||
+new code line5 removed
|
+new code line5 removed in the PR
|
||||||
unchanged code line6
|
unchanged code line6
|
||||||
|
|
||||||
## File: 'src/file2.py'
|
## File: 'src/file2.py'
|
||||||
@ -43,8 +47,18 @@ __new hunk__
|
|||||||
{%- if is_ai_metadata %}
|
{%- if is_ai_metadata %}
|
||||||
- If available, an AI-generated summary will appear and provide a high-level overview of the file changes. Note that this summary may not be fully accurate or complete.
|
- If available, an AI-generated summary will appear and provide a high-level overview of the file changes. Note that this summary may not be fully accurate or complete.
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
- When quoting variables, names or file paths from the code, use backticks (`) instead of single quote (').
|
- When quoting variables or names from the code, use backticks (`) instead of single quote (').
|
||||||
|
|
||||||
|
{%- if num_code_suggestions > 0 %}
|
||||||
|
|
||||||
|
|
||||||
|
Code suggestions guidelines:
|
||||||
|
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
|
||||||
|
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
|
||||||
|
- 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.
|
||||||
|
- Suggestions should address the new code added in the PR diff (lines starting with '+')
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
@ -66,8 +80,8 @@ class SubPR(BaseModel):
|
|||||||
|
|
||||||
class KeyIssuesComponentLink(BaseModel):
|
class KeyIssuesComponentLink(BaseModel):
|
||||||
relevant_file: str = Field(description="The full file path of the relevant file")
|
relevant_file: str = Field(description="The full file path of the relevant file")
|
||||||
issue_header: str = Field(description="One or two word title for the the issue. For example: 'Possible Bug', etc.")
|
issue_header: str = Field(description="One or two word title for the the issue. For example: 'Possible Bug', 'Performance Issue', 'Code Smell', etc.")
|
||||||
issue_content: str = Field(description="A short and concise summary of what should be further inspected and validated during the PR review process for this issue. Do not reference line numbers in this field.")
|
issue_content: str = Field(description="A short and concise summary of what should be further inspected and validated during the PR review process for this issue. Don't state line numbers here")
|
||||||
start_line: int = Field(description="The start line that corresponds to this issue in the relevant file")
|
start_line: int = Field(description="The start line that corresponds to this issue in the relevant file")
|
||||||
end_line: int = Field(description="The end line that corresponds to this issue in the relevant file")
|
end_line: int = Field(description="The end line that corresponds to this issue in the relevant file")
|
||||||
|
|
||||||
@ -97,16 +111,32 @@ class Review(BaseModel):
|
|||||||
{%- if question_str %}
|
{%- if question_str %}
|
||||||
insights_from_user_answers: str = Field(description="shortly summarize the insights you gained from the user's answers to the questions")
|
insights_from_user_answers: str = Field(description="shortly summarize the insights you gained from the user's answers to the questions")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
key_issues_to_review: List[KeyIssuesComponentLink] = Field("A short and diverse list (0-3 issues) of high-priority bugs, problems or performance concerns introduced in the PR code, which the PR reviewer should further focus on and validate during the review process.")
|
key_issues_to_review: List[KeyIssuesComponentLink] = Field("A diverse list of bugs, issue or major performance concerns introduced in this PR, which the PR reviewer should further investigate")
|
||||||
{%- if require_security_review %}
|
{%- if require_security_review %}
|
||||||
security_concerns: str = Field(description="Does this PR code introduce possible vulnerabilities such as exposure of sensitive information (e.g., API keys, secrets, passwords), or security concerns like SQL injection, XSS, CSRF, and others ? Answer 'No' (without explaining why) if there are no possible issues. If there are security concerns or issues, start your answer with a short header, such as: 'Sensitive information exposure: ...', 'SQL injection: ...' etc. Explain your answer. Be specific and give examples if possible")
|
security_concerns: str = Field(description="Does this PR code introduce possible vulnerabilities such as exposure of sensitive information (e.g., API keys, secrets, passwords), or security concerns like SQL injection, XSS, CSRF, and others ? Answer 'No' (without explaining why) if there are no possible issues. If there are security concerns or issues, start your answer with a short header, such as: 'Sensitive information exposure: ...', 'SQL injection: ...' etc. Explain your answer. Be specific and give examples if possible")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- if require_can_be_split_review %}
|
{%- if require_can_be_split_review %}
|
||||||
can_be_split: List[SubPR] = Field(min_items=0, max_items=3, description="Can this PR, which contains {{ num_pr_files }} changed files in total, be divided into smaller sub-PRs with distinct tasks that can be reviewed and merged independently, regardless of the order ? Make sure that the sub-PRs are indeed independent, with no code dependencies between them, and that each sub-PR represent a meaningful independent task. Output an empty list if the PR code does not need to be split.")
|
can_be_split: List[SubPR] = Field(min_items=0, max_items=3, description="Can this PR, which contains {{ num_pr_files }} changed files in total, be divided into smaller sub-PRs with distinct tasks that can be reviewed and merged independently, regardless of the order ? Make sure that the sub-PRs are indeed independent, with no code dependencies between them, and that each sub-PR represent a meaningful independent task. Output an empty list if the PR code does not need to be split.")
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
{%- if num_code_suggestions > 0 %}
|
||||||
|
|
||||||
|
class CodeSuggestion(BaseModel):
|
||||||
|
relevant_file: str = Field(description="The full file path of the relevant file")
|
||||||
|
language: str = Field(description="The programming language of the relevant file")
|
||||||
|
suggestion: str = Field(description="a concrete suggestion for meaningfully improving the new PR code. Also describe how, specifically, the suggestion can be applied to new PR code. Add tags with importance measure that matches each suggestion ('important' or 'medium'). Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.")
|
||||||
|
relevant_line: str = Field(description="a single code line taken from the relevant file, to which the suggestion applies. The code line should start with a '+'. Make sure to output the line exactly as it appears in the relevant file")
|
||||||
|
{%- endif %}
|
||||||
|
{%- if num_code_suggestions > 0 %}
|
||||||
|
|
||||||
class PRReview(BaseModel):
|
class PRReview(BaseModel):
|
||||||
review: Review
|
review: Review
|
||||||
|
code_feedback: List[CodeSuggestion]
|
||||||
|
{%- else %}
|
||||||
|
|
||||||
|
|
||||||
|
class PRReview(BaseModel):
|
||||||
|
review: Review
|
||||||
|
{%- endif %}
|
||||||
=====
|
=====
|
||||||
|
|
||||||
|
|
||||||
@ -155,6 +185,18 @@ review:
|
|||||||
title: ...
|
title: ...
|
||||||
- ...
|
- ...
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if num_code_suggestions > 0 %}
|
||||||
|
code_feedback:
|
||||||
|
- relevant_file: |
|
||||||
|
directory/xxx.py
|
||||||
|
language: |
|
||||||
|
python
|
||||||
|
suggestion: |
|
||||||
|
xxx [important]
|
||||||
|
relevant_line: |
|
||||||
|
xxx
|
||||||
|
{%- endif %}
|
||||||
```
|
```
|
||||||
|
|
||||||
Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|')
|
Answer should be a valid YAML, and nothing else. Each YAML output MUST be after a newline, with proper indent, and block scalar indicator ('|')
|
||||||
@ -221,59 +263,6 @@ The PR code diff:
|
|||||||
======
|
======
|
||||||
|
|
||||||
|
|
||||||
{%- if duplicate_prompt_examples %}
|
|
||||||
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
```yaml
|
|
||||||
review:
|
|
||||||
{%- if related_tickets %}
|
|
||||||
ticket_compliance_check:
|
|
||||||
- ticket_url: |
|
|
||||||
...
|
|
||||||
ticket_requirements: |
|
|
||||||
...
|
|
||||||
fully_compliant_requirements: |
|
|
||||||
...
|
|
||||||
not_compliant_requirements: |
|
|
||||||
...
|
|
||||||
overall_compliance_level: |
|
|
||||||
...
|
|
||||||
{%- endif %}
|
|
||||||
{%- if require_estimate_effort_to_review %}
|
|
||||||
estimated_effort_to_review_[1-5]: |
|
|
||||||
3
|
|
||||||
{%- endif %}
|
|
||||||
{%- if require_score %}
|
|
||||||
score: 89
|
|
||||||
{%- endif %}
|
|
||||||
relevant_tests: |
|
|
||||||
No
|
|
||||||
key_issues_to_review:
|
|
||||||
- relevant_file: |
|
|
||||||
...
|
|
||||||
issue_header: |
|
|
||||||
...
|
|
||||||
issue_content: |
|
|
||||||
...
|
|
||||||
start_line: ...
|
|
||||||
end_line: ...
|
|
||||||
- ...
|
|
||||||
security_concerns: |
|
|
||||||
No
|
|
||||||
{%- if require_can_be_split_review %}
|
|
||||||
can_be_split:
|
|
||||||
- relevant_files:
|
|
||||||
- ...
|
|
||||||
- ...
|
|
||||||
title: ...
|
|
||||||
- ...
|
|
||||||
{%- endif %}
|
|
||||||
```
|
|
||||||
(replace '...' with the actual values)
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
|
|
||||||
Response (should be a valid YAML, and nothing else):
|
Response (should be a valid YAML, and nothing else):
|
||||||
```yaml
|
```yaml
|
||||||
"""
|
"""
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
[pr_update_changelog_prompt]
|
[pr_update_changelog_prompt]
|
||||||
system="""You are a language model called PR-Changelog-Updater.
|
system="""You are a language model called PR-Changelog-Updater.
|
||||||
Your task is to add a brief summary of this PR's changes to CHANGELOG.md file of the project:
|
Your task is to update the CHANGELOG.md file of the project, to shortly summarize important changes introduced in this PR (the '+' lines).
|
||||||
- Follow the file's existing format and style conventions like dates, section titles, etc.
|
- The output should match the existing CHANGELOG.md format, style and conventions, so it will look like a natural part of the file. For example, if previous changes were summarized in a single line, you should do the same.
|
||||||
- Only add new changes (don't repeat existing entries)
|
- Don't repeat previous changes. Generate only new content, that is not already in the CHANGELOG.md file.
|
||||||
- Be general, and avoid specific details, files, etc. The output should be minimal, no more than 3-4 short lines.
|
- Be general, and avoid specific details, files, etc. The output should be minimal, no more than 3-4 short lines. Ignore non-relevant subsections.
|
||||||
- Write only the new content to be added to CHANGELOG.md, without any introduction or summary. The content should appear as if it's a natural part of the existing file.
|
|
||||||
{%- if pr_link %}
|
|
||||||
- If relevant, convert the changelog main header into a clickable link using the PR URL '{{ pr_link }}'. Format: header [*][pr_link]
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
|
|
||||||
{%- if extra_instructions %}
|
{%- if extra_instructions %}
|
||||||
|
|
||||||
@ -52,19 +47,16 @@ The PR Git Diff:
|
|||||||
{{ diff|trim }}
|
{{ diff|trim }}
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|
||||||
Current date:
|
Current date:
|
||||||
```
|
```
|
||||||
{{today}}
|
{{today}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The current CHANGELOG.md:
|
||||||
The current 'CHANGELOG.md' file
|
|
||||||
======
|
======
|
||||||
{{ changelog_file_str }}
|
{{ changelog_file_str }}
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
```markdown
|
|
||||||
"""
|
"""
|
||||||
|
@ -1,31 +1,25 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
import difflib
|
|
||||||
import re
|
|
||||||
import textwrap
|
import textwrap
|
||||||
import traceback
|
|
||||||
from datetime import datetime
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Dict, List
|
from typing import Dict, List
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.pr_processing import (add_ai_metadata_to_diff_files,
|
from pr_agent.algo.pr_processing import get_pr_diff, get_pr_multi_diffs, retry_with_fallback_models, \
|
||||||
get_pr_diff, get_pr_multi_diffs,
|
add_ai_metadata_to_diff_files
|
||||||
retry_with_fallback_models)
|
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import (ModelType, load_yaml, replace_code_tags,
|
from pr_agent.algo.utils import load_yaml, replace_code_tags, ModelType, show_relevant_configurations
|
||||||
show_relevant_configurations)
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import (AzureDevopsProvider, GithubProvider,
|
from pr_agent.git_providers import get_git_provider, get_git_provider_with_context, GithubProvider, GitLabProvider, \
|
||||||
GitLabProvider, get_git_provider,
|
AzureDevopsProvider
|
||||||
get_git_provider_with_context)
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language, GitProvider
|
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
from pr_agent.servers.help import HelpMessage
|
from pr_agent.servers.help import HelpMessage
|
||||||
from pr_agent.tools.pr_description import insert_br_after_x_chars
|
from pr_agent.tools.pr_description import insert_br_after_x_chars
|
||||||
|
import difflib
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
class PRCodeSuggestions:
|
class PRCodeSuggestions:
|
||||||
@ -50,7 +44,7 @@ class PRCodeSuggestions:
|
|||||||
self.is_extended = self._get_is_extended(args or [])
|
self.is_extended = self._get_is_extended(args or [])
|
||||||
except:
|
except:
|
||||||
self.is_extended = False
|
self.is_extended = False
|
||||||
num_code_suggestions = int(get_settings().pr_code_suggestions.num_code_suggestions_per_chunk)
|
num_code_suggestions = get_settings().pr_code_suggestions.num_code_suggestions_per_chunk
|
||||||
|
|
||||||
|
|
||||||
self.ai_handler = ai_handler()
|
self.ai_handler = ai_handler()
|
||||||
@ -75,15 +69,11 @@ class PRCodeSuggestions:
|
|||||||
"description": self.pr_description,
|
"description": self.pr_description,
|
||||||
"language": self.main_language,
|
"language": self.main_language,
|
||||||
"diff": "", # empty diff for initial calculation
|
"diff": "", # empty diff for initial calculation
|
||||||
"diff_no_line_numbers": "", # empty diff for initial calculation
|
|
||||||
"num_code_suggestions": num_code_suggestions,
|
"num_code_suggestions": num_code_suggestions,
|
||||||
"extra_instructions": get_settings().pr_code_suggestions.extra_instructions,
|
"extra_instructions": get_settings().pr_code_suggestions.extra_instructions,
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages(),
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
"relevant_best_practices": "",
|
"relevant_best_practices": "",
|
||||||
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False),
|
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False),
|
||||||
"focus_only_on_problems": get_settings().get("pr_code_suggestions.focus_only_on_problems", False),
|
|
||||||
"date": datetime.now().strftime('%Y-%m-%d'),
|
|
||||||
'duplicate_prompt_examples': get_settings().config.get('duplicate_prompt_examples', False),
|
|
||||||
}
|
}
|
||||||
self.pr_code_suggestions_prompt_system = get_settings().pr_code_suggestions_prompt.system
|
self.pr_code_suggestions_prompt_system = get_settings().pr_code_suggestions_prompt.system
|
||||||
|
|
||||||
@ -106,8 +96,6 @@ class PRCodeSuggestions:
|
|||||||
relevant_configs = {'pr_code_suggestions': dict(get_settings().pr_code_suggestions),
|
relevant_configs = {'pr_code_suggestions': dict(get_settings().pr_code_suggestions),
|
||||||
'config': dict(get_settings().config)}
|
'config': dict(get_settings().config)}
|
||||||
get_logger().debug("Relevant configs", artifacts=relevant_configs)
|
get_logger().debug("Relevant configs", artifacts=relevant_configs)
|
||||||
|
|
||||||
# publish "Preparing suggestions..." comments
|
|
||||||
if (get_settings().config.publish_output and get_settings().config.publish_output_progress and
|
if (get_settings().config.publish_output and get_settings().config.publish_output_progress and
|
||||||
not get_settings().config.get('is_auto_command', False)):
|
not get_settings().config.get('is_auto_command', False)):
|
||||||
if self.git_provider.is_supported("gfm_markdown"):
|
if self.git_provider.is_supported("gfm_markdown"):
|
||||||
@ -115,26 +103,31 @@ class PRCodeSuggestions:
|
|||||||
else:
|
else:
|
||||||
self.git_provider.publish_comment("Preparing suggestions...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing suggestions...", is_temporary=True)
|
||||||
|
|
||||||
# call the model to get the suggestions, and self-reflect on them
|
|
||||||
if not self.is_extended:
|
if not self.is_extended:
|
||||||
data = await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.REGULAR)
|
data = await retry_with_fallback_models(self._prepare_prediction)
|
||||||
else:
|
else:
|
||||||
data = await retry_with_fallback_models(self._prepare_prediction_extended, model_type=ModelType.REGULAR)
|
data = await retry_with_fallback_models(self._prepare_prediction_extended)
|
||||||
if not data:
|
if not data:
|
||||||
data = {"code_suggestions": []}
|
data = {"code_suggestions": []}
|
||||||
self.data = data
|
|
||||||
|
|
||||||
# Handle the case where the PR has no suggestions
|
if (data is None or 'code_suggestions' not in data or not data['code_suggestions']
|
||||||
if (data is None or 'code_suggestions' not in data or not data['code_suggestions']):
|
and get_settings().config.publish_output):
|
||||||
await self.publish_no_suggestions()
|
get_logger().warning('No code suggestions found for the PR.')
|
||||||
|
pr_body = "## PR Code Suggestions ✨\n\nNo code suggestions found for the PR."
|
||||||
|
get_logger().debug(f"PR output", artifact=pr_body)
|
||||||
|
if self.progress_response:
|
||||||
|
self.git_provider.edit_comment(self.progress_response, body=pr_body)
|
||||||
|
else:
|
||||||
|
self.git_provider.publish_comment(pr_body)
|
||||||
return
|
return
|
||||||
|
|
||||||
# publish the suggestions
|
if (not self.is_extended and get_settings().pr_code_suggestions.rank_suggestions) or \
|
||||||
if get_settings().config.publish_output:
|
(self.is_extended and get_settings().pr_code_suggestions.rank_extended_suggestions):
|
||||||
# If a temporary comment was published, remove it
|
get_logger().info('Ranking Suggestions...')
|
||||||
self.git_provider.remove_initial_comment()
|
data['code_suggestions'] = await self.rank_suggestions(data['code_suggestions'])
|
||||||
|
|
||||||
# Publish table summarized suggestions
|
if get_settings().config.publish_output:
|
||||||
|
self.git_provider.remove_initial_comment()
|
||||||
if ((not get_settings().pr_code_suggestions.commitable_code_suggestions) and
|
if ((not get_settings().pr_code_suggestions.commitable_code_suggestions) and
|
||||||
self.git_provider.is_supported("gfm_markdown")):
|
self.git_provider.is_supported("gfm_markdown")):
|
||||||
|
|
||||||
@ -144,7 +137,10 @@ class PRCodeSuggestions:
|
|||||||
|
|
||||||
# require self-review
|
# require self-review
|
||||||
if get_settings().pr_code_suggestions.demand_code_suggestions_self_review:
|
if get_settings().pr_code_suggestions.demand_code_suggestions_self_review:
|
||||||
pr_body = await self.add_self_review_text(pr_body)
|
text = get_settings().pr_code_suggestions.code_suggestions_self_review_text
|
||||||
|
pr_body += f"\n\n- [ ] {text}"
|
||||||
|
if get_settings().pr_code_suggestions.approve_pr_on_self_review:
|
||||||
|
pr_body += ' <!-- approve pr self-review -->'
|
||||||
|
|
||||||
# add usage guide
|
# add usage guide
|
||||||
if (get_settings().pr_code_suggestions.enable_chat_text and get_settings().config.is_auto_command
|
if (get_settings().pr_code_suggestions.enable_chat_text and get_settings().config.is_auto_command
|
||||||
@ -160,13 +156,13 @@ class PRCodeSuggestions:
|
|||||||
pr_body += show_relevant_configurations(relevant_section='pr_code_suggestions')
|
pr_body += show_relevant_configurations(relevant_section='pr_code_suggestions')
|
||||||
|
|
||||||
# publish the PR comment
|
# publish the PR comment
|
||||||
if get_settings().pr_code_suggestions.persistent_comment: # true by default
|
if get_settings().pr_code_suggestions.persistent_comment:
|
||||||
self.publish_persistent_comment_with_history(self.git_provider,
|
final_update_message = False
|
||||||
pr_body,
|
self.publish_persistent_comment_with_history(pr_body,
|
||||||
initial_header="## PR Code Suggestions ✨",
|
initial_header="## PR Code Suggestions ✨",
|
||||||
update_header=True,
|
update_header=True,
|
||||||
name="suggestions",
|
name="suggestions",
|
||||||
final_update_message=False,
|
final_update_message=final_update_message,
|
||||||
max_previous_comments=get_settings().pr_code_suggestions.max_history_len,
|
max_previous_comments=get_settings().pr_code_suggestions.max_history_len,
|
||||||
progress_response=self.progress_response)
|
progress_response=self.progress_response)
|
||||||
else:
|
else:
|
||||||
@ -177,60 +173,10 @@ class PRCodeSuggestions:
|
|||||||
|
|
||||||
# dual publishing mode
|
# dual publishing mode
|
||||||
if int(get_settings().pr_code_suggestions.dual_publishing_score_threshold) > 0:
|
if int(get_settings().pr_code_suggestions.dual_publishing_score_threshold) > 0:
|
||||||
await self.dual_publishing(data)
|
|
||||||
else:
|
|
||||||
await self.push_inline_code_suggestions(data)
|
|
||||||
if self.progress_response:
|
|
||||||
self.git_provider.remove_comment(self.progress_response)
|
|
||||||
else:
|
|
||||||
get_logger().info('Code suggestions generated for PR, but not published since publish_output is False.')
|
|
||||||
pr_body = self.generate_summarized_suggestions(data)
|
|
||||||
get_settings().data = {"artifact": pr_body}
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}",
|
|
||||||
artifact={"traceback": traceback.format_exc()})
|
|
||||||
if get_settings().config.publish_output:
|
|
||||||
if self.progress_response:
|
|
||||||
self.progress_response.delete()
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self.git_provider.remove_initial_comment()
|
|
||||||
self.git_provider.publish_comment(f"Failed to generate code suggestions for PR")
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().exception(f"Failed to update persistent review, error: {e}")
|
|
||||||
|
|
||||||
async def add_self_review_text(self, pr_body):
|
|
||||||
text = get_settings().pr_code_suggestions.code_suggestions_self_review_text
|
|
||||||
pr_body += f"\n\n- [ ] {text}"
|
|
||||||
approve_pr_on_self_review = get_settings().pr_code_suggestions.approve_pr_on_self_review
|
|
||||||
fold_suggestions_on_self_review = get_settings().pr_code_suggestions.fold_suggestions_on_self_review
|
|
||||||
if approve_pr_on_self_review and not fold_suggestions_on_self_review:
|
|
||||||
pr_body += ' <!-- approve pr self-review -->'
|
|
||||||
elif fold_suggestions_on_self_review and not approve_pr_on_self_review:
|
|
||||||
pr_body += ' <!-- fold suggestions self-review -->'
|
|
||||||
else:
|
|
||||||
pr_body += ' <!-- approve and fold suggestions self-review -->'
|
|
||||||
return pr_body
|
|
||||||
|
|
||||||
async def publish_no_suggestions(self):
|
|
||||||
pr_body = "## PR Code Suggestions ✨\n\nNo code suggestions found for the PR."
|
|
||||||
if get_settings().config.publish_output and get_settings().config.publish_output_no_suggestions:
|
|
||||||
get_logger().warning('No code suggestions found for the PR.')
|
|
||||||
get_logger().debug(f"PR output", artifact=pr_body)
|
|
||||||
if self.progress_response:
|
|
||||||
self.git_provider.edit_comment(self.progress_response, body=pr_body)
|
|
||||||
else:
|
|
||||||
self.git_provider.publish_comment(pr_body)
|
|
||||||
else:
|
|
||||||
get_settings().data = {"artifact": ""}
|
|
||||||
|
|
||||||
async def dual_publishing(self, data):
|
|
||||||
data_above_threshold = {'code_suggestions': []}
|
data_above_threshold = {'code_suggestions': []}
|
||||||
try:
|
try:
|
||||||
for suggestion in data['code_suggestions']:
|
for suggestion in data['code_suggestions']:
|
||||||
if int(suggestion.get('score', 0)) >= int(
|
if int(suggestion.get('score', 0)) >= int(get_settings().pr_code_suggestions.dual_publishing_score_threshold) \
|
||||||
get_settings().pr_code_suggestions.dual_publishing_score_threshold) \
|
|
||||||
and suggestion.get('improved_code'):
|
and suggestion.get('improved_code'):
|
||||||
data_above_threshold['code_suggestions'].append(suggestion)
|
data_above_threshold['code_suggestions'].append(suggestion)
|
||||||
if not data_above_threshold['code_suggestions'][-1]['existing_code']:
|
if not data_above_threshold['code_suggestions'][-1]['existing_code']:
|
||||||
@ -240,66 +186,66 @@ class PRCodeSuggestions:
|
|||||||
if data_above_threshold['code_suggestions']:
|
if data_above_threshold['code_suggestions']:
|
||||||
get_logger().info(
|
get_logger().info(
|
||||||
f"Publishing {len(data_above_threshold['code_suggestions'])} suggestions in dual publishing mode")
|
f"Publishing {len(data_above_threshold['code_suggestions'])} suggestions in dual publishing mode")
|
||||||
await self.push_inline_code_suggestions(data_above_threshold)
|
self.push_inline_code_suggestions(data_above_threshold)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to publish dual publishing suggestions, error: {e}")
|
get_logger().error(f"Failed to publish dual publishing suggestions, error: {e}")
|
||||||
|
else:
|
||||||
|
self.push_inline_code_suggestions(data)
|
||||||
|
if self.progress_response:
|
||||||
|
self.git_provider.remove_comment(self.progress_response)
|
||||||
|
else:
|
||||||
|
get_logger().info('Code suggestions generated for PR, but not published since publish_output is False.')
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().error(f"Failed to generate code suggestions for PR, error: {e}")
|
||||||
|
if get_settings().config.publish_output:
|
||||||
|
if self.progress_response:
|
||||||
|
self.progress_response.delete()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.git_provider.remove_initial_comment()
|
||||||
|
self.git_provider.publish_comment(f"Failed to generate code suggestions for PR")
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
@staticmethod
|
def publish_persistent_comment_with_history(self, pr_comment: str,
|
||||||
def publish_persistent_comment_with_history(git_provider: GitProvider,
|
|
||||||
pr_comment: str,
|
|
||||||
initial_header: str,
|
initial_header: str,
|
||||||
update_header: bool = True,
|
update_header: bool = True,
|
||||||
name='review',
|
name='review',
|
||||||
final_update_message=True,
|
final_update_message=True,
|
||||||
max_previous_comments=4,
|
max_previous_comments=4,
|
||||||
progress_response=None,
|
progress_response=None):
|
||||||
only_fold=False):
|
|
||||||
|
|
||||||
def _extract_link(comment_text: str):
|
if isinstance(self.git_provider, AzureDevopsProvider): # get_latest_commit_url is not supported yet
|
||||||
r = re.compile(r"<!--.*?-->")
|
|
||||||
match = r.search(comment_text)
|
|
||||||
|
|
||||||
up_to_commit_txt = ""
|
|
||||||
if match:
|
|
||||||
up_to_commit_txt = f" up to commit {match.group(0)[4:-3].strip()}"
|
|
||||||
return up_to_commit_txt
|
|
||||||
|
|
||||||
if isinstance(git_provider, AzureDevopsProvider): # get_latest_commit_url is not supported yet
|
|
||||||
if progress_response:
|
if progress_response:
|
||||||
git_provider.edit_comment(progress_response, pr_comment)
|
self.git_provider.edit_comment(progress_response, pr_comment)
|
||||||
new_comment = progress_response
|
|
||||||
else:
|
else:
|
||||||
new_comment = git_provider.publish_comment(pr_comment)
|
self.git_provider.publish_comment(pr_comment)
|
||||||
return new_comment
|
return
|
||||||
|
|
||||||
history_header = f"#### Previous suggestions\n"
|
history_header = f"#### Previous suggestions\n"
|
||||||
last_commit_num = git_provider.get_latest_commit_url().split('/')[-1][:7]
|
last_commit_num = self.git_provider.get_latest_commit_url().split('/')[-1][:7]
|
||||||
if only_fold: # A user clicked on the 'self-review' checkbox
|
|
||||||
text = get_settings().pr_code_suggestions.code_suggestions_self_review_text
|
|
||||||
latest_suggestion_header = f"\n\n- [x] {text}"
|
|
||||||
else:
|
|
||||||
latest_suggestion_header = f"Latest suggestions up to {last_commit_num}"
|
latest_suggestion_header = f"Latest suggestions up to {last_commit_num}"
|
||||||
latest_commit_html_comment = f"<!-- {last_commit_num} -->"
|
latest_commit_html_comment = f"<!-- {last_commit_num} -->"
|
||||||
found_comment = None
|
found_comment = None
|
||||||
|
|
||||||
if max_previous_comments > 0:
|
if max_previous_comments > 0:
|
||||||
try:
|
try:
|
||||||
prev_comments = list(git_provider.get_issue_comments())
|
prev_comments = list(self.git_provider.get_issue_comments())
|
||||||
for comment in prev_comments:
|
for comment in prev_comments:
|
||||||
if comment.body.startswith(initial_header):
|
if comment.body.startswith(initial_header):
|
||||||
prev_suggestions = comment.body
|
prev_suggestions = comment.body
|
||||||
found_comment = comment
|
found_comment = comment
|
||||||
comment_url = git_provider.get_comment_url(comment)
|
comment_url = self.git_provider.get_comment_url(comment)
|
||||||
|
|
||||||
if history_header.strip() not in comment.body:
|
if history_header.strip() not in comment.body:
|
||||||
# no history section
|
# no history section
|
||||||
# extract everything between <table> and </table> in comment.body including <table> and </table>
|
# extract everything between <table> and </table> in comment.body including <table> and </table>
|
||||||
table_index = comment.body.find("<table>")
|
table_index = comment.body.find("<table>")
|
||||||
if table_index == -1:
|
if table_index == -1:
|
||||||
git_provider.edit_comment(comment, pr_comment)
|
self.git_provider.edit_comment(comment, pr_comment)
|
||||||
continue
|
continue
|
||||||
# find http link from comment.body[:table_index]
|
# find http link from comment.body[:table_index]
|
||||||
up_to_commit_txt = _extract_link(comment.body[:table_index])
|
up_to_commit_txt = self.extract_link(comment.body[:table_index])
|
||||||
prev_suggestion_table = comment.body[
|
prev_suggestion_table = comment.body[
|
||||||
table_index:comment.body.rfind("</table>") + len("</table>")]
|
table_index:comment.body.rfind("</table>") + len("</table>")]
|
||||||
|
|
||||||
@ -320,7 +266,7 @@ class PRCodeSuggestions:
|
|||||||
|
|
||||||
# get text after the latest_suggestion_header in comment.body
|
# get text after the latest_suggestion_header in comment.body
|
||||||
table_ind = latest_table.find("<table>")
|
table_ind = latest_table.find("<table>")
|
||||||
up_to_commit_txt = _extract_link(latest_table[:table_ind])
|
up_to_commit_txt = self.extract_link(latest_table[:table_ind])
|
||||||
|
|
||||||
latest_table = latest_table[table_ind:latest_table.rfind("</table>") + len("</table>")]
|
latest_table = latest_table[table_ind:latest_table.rfind("</table>") + len("</table>")]
|
||||||
# enforce max_previous_comments
|
# enforce max_previous_comments
|
||||||
@ -347,12 +293,11 @@ class PRCodeSuggestions:
|
|||||||
|
|
||||||
get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message")
|
get_logger().info(f"Persistent mode - updating comment {comment_url} to latest {name} message")
|
||||||
if progress_response: # publish to 'progress_response' comment, because it refreshes immediately
|
if progress_response: # publish to 'progress_response' comment, because it refreshes immediately
|
||||||
git_provider.edit_comment(progress_response, pr_comment_updated)
|
self.git_provider.edit_comment(progress_response, pr_comment_updated)
|
||||||
git_provider.remove_comment(comment)
|
self.git_provider.remove_comment(comment)
|
||||||
comment = progress_response
|
|
||||||
else:
|
else:
|
||||||
git_provider.edit_comment(comment, pr_comment_updated)
|
self.git_provider.edit_comment(comment, pr_comment_updated)
|
||||||
return comment
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to update persistent review, error: {e}")
|
get_logger().exception(f"Failed to update persistent review, error: {e}")
|
||||||
pass
|
pass
|
||||||
@ -361,12 +306,9 @@ class PRCodeSuggestions:
|
|||||||
body = pr_comment.replace(initial_header, "").strip()
|
body = pr_comment.replace(initial_header, "").strip()
|
||||||
pr_comment = f"{initial_header}\n\n{latest_commit_html_comment}\n\n{body}\n\n"
|
pr_comment = f"{initial_header}\n\n{latest_commit_html_comment}\n\n{body}\n\n"
|
||||||
if progress_response:
|
if progress_response:
|
||||||
git_provider.edit_comment(progress_response, pr_comment)
|
self.git_provider.edit_comment(progress_response, pr_comment)
|
||||||
new_comment = progress_response
|
|
||||||
else:
|
else:
|
||||||
new_comment = git_provider.publish_comment(pr_comment)
|
self.git_provider.publish_comment(pr_comment)
|
||||||
return new_comment
|
|
||||||
|
|
||||||
|
|
||||||
def extract_link(self, s):
|
def extract_link(self, s):
|
||||||
r = re.compile(r"<!--.*?-->")
|
r = re.compile(r"<!--.*?-->")
|
||||||
@ -383,12 +325,10 @@ class PRCodeSuggestions:
|
|||||||
model,
|
model,
|
||||||
add_line_numbers_to_hunks=True,
|
add_line_numbers_to_hunks=True,
|
||||||
disable_extra_lines=False)
|
disable_extra_lines=False)
|
||||||
self.patches_diff_list = [self.patches_diff]
|
|
||||||
self.patches_diff_no_line_number = self.remove_line_numbers([self.patches_diff])[0]
|
|
||||||
|
|
||||||
if self.patches_diff:
|
if self.patches_diff:
|
||||||
get_logger().debug(f"PR diff", artifact=self.patches_diff)
|
get_logger().debug(f"PR diff", artifact=self.patches_diff)
|
||||||
self.prediction = await self._get_prediction(model, self.patches_diff, self.patches_diff_no_line_number)
|
self.prediction = await self._get_prediction(model, self.patches_diff)
|
||||||
else:
|
else:
|
||||||
get_logger().warning(f"Empty PR diff")
|
get_logger().warning(f"Empty PR diff")
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
@ -396,28 +336,37 @@ class PRCodeSuggestions:
|
|||||||
data = self.prediction
|
data = self.prediction
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def _get_prediction(self, model: str, patches_diff: str, patches_diff_no_line_number: str) -> dict:
|
async def _get_prediction(self, model: str, patches_diff: str) -> dict:
|
||||||
variables = copy.deepcopy(self.vars)
|
variables = copy.deepcopy(self.vars)
|
||||||
variables["diff"] = patches_diff # update diff
|
variables["diff"] = patches_diff # update diff
|
||||||
variables["diff_no_line_numbers"] = patches_diff_no_line_number # update diff
|
|
||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
system_prompt = environment.from_string(self.pr_code_suggestions_prompt_system).render(variables)
|
system_prompt = environment.from_string(self.pr_code_suggestions_prompt_system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_code_suggestions_prompt.user).render(variables)
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
|
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
|
||||||
if not get_settings().config.publish_output:
|
|
||||||
get_settings().system_prompt = system_prompt
|
|
||||||
get_settings().user_prompt = user_prompt
|
|
||||||
|
|
||||||
# load suggestions from the AI response
|
# load suggestions from the AI response
|
||||||
data = self._prepare_pr_code_suggestions(response)
|
data = self._prepare_pr_code_suggestions(response)
|
||||||
|
|
||||||
# self-reflect on suggestions (mandatory, since line numbers are generated now here)
|
# self-reflect on suggestions
|
||||||
model_reflection = get_settings().config.model
|
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
||||||
|
model_turbo = get_settings().config.model_turbo # use turbo model for self-reflection, since it is an easier task
|
||||||
response_reflect = await self.self_reflect_on_suggestions(data["code_suggestions"],
|
response_reflect = await self.self_reflect_on_suggestions(data["code_suggestions"],
|
||||||
patches_diff, model=model_reflection)
|
patches_diff, model=model_turbo)
|
||||||
if response_reflect:
|
if response_reflect:
|
||||||
await self.analyze_self_reflection_response(data, response_reflect)
|
response_reflect_yaml = load_yaml(response_reflect)
|
||||||
|
code_suggestions_feedback = response_reflect_yaml["code_suggestions"]
|
||||||
|
if len(code_suggestions_feedback) == len(data["code_suggestions"]):
|
||||||
|
for i, suggestion in enumerate(data["code_suggestions"]):
|
||||||
|
try:
|
||||||
|
suggestion["score"] = code_suggestions_feedback[i]["suggestion_score"]
|
||||||
|
suggestion["score_why"] = code_suggestions_feedback[i]["why"]
|
||||||
|
except Exception as e: #
|
||||||
|
get_logger().error(f"Error processing suggestion score {i}",
|
||||||
|
artifact={"suggestion": suggestion,
|
||||||
|
"code_suggestions_feedback": code_suggestions_feedback[i]})
|
||||||
|
suggestion["score"] = 7
|
||||||
|
suggestion["score_why"] = ""
|
||||||
else:
|
else:
|
||||||
# get_logger().error(f"Could not self-reflect on suggestions. using default score 7")
|
# get_logger().error(f"Could not self-reflect on suggestions. using default score 7")
|
||||||
for i, suggestion in enumerate(data["code_suggestions"]):
|
for i, suggestion in enumerate(data["code_suggestions"]):
|
||||||
@ -426,68 +375,16 @@ class PRCodeSuggestions:
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def analyze_self_reflection_response(self, data, response_reflect):
|
|
||||||
response_reflect_yaml = load_yaml(response_reflect)
|
|
||||||
code_suggestions_feedback = response_reflect_yaml.get("code_suggestions", [])
|
|
||||||
if code_suggestions_feedback and len(code_suggestions_feedback) == len(data["code_suggestions"]):
|
|
||||||
for i, suggestion in enumerate(data["code_suggestions"]):
|
|
||||||
try:
|
|
||||||
suggestion["score"] = code_suggestions_feedback[i]["suggestion_score"]
|
|
||||||
suggestion["score_why"] = code_suggestions_feedback[i]["why"]
|
|
||||||
|
|
||||||
if 'relevant_lines_start' not in suggestion:
|
|
||||||
relevant_lines_start = code_suggestions_feedback[i].get('relevant_lines_start', -1)
|
|
||||||
relevant_lines_end = code_suggestions_feedback[i].get('relevant_lines_end', -1)
|
|
||||||
suggestion['relevant_lines_start'] = relevant_lines_start
|
|
||||||
suggestion['relevant_lines_end'] = relevant_lines_end
|
|
||||||
if relevant_lines_start < 0 or relevant_lines_end < 0:
|
|
||||||
suggestion["score"] = 0
|
|
||||||
|
|
||||||
try:
|
|
||||||
if get_settings().config.publish_output:
|
|
||||||
if not suggestion["score"]:
|
|
||||||
score = -1
|
|
||||||
else:
|
|
||||||
score = int(suggestion["score"])
|
|
||||||
label = suggestion["label"].lower().strip()
|
|
||||||
label = label.replace('<br>', ' ')
|
|
||||||
suggestion_statistics_dict = {'score': score,
|
|
||||||
'label': label}
|
|
||||||
get_logger().info(f"PR-Agent suggestions statistics",
|
|
||||||
statistics=suggestion_statistics_dict, analytics=True)
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Failed to log suggestion statistics, error: {e}")
|
|
||||||
pass
|
|
||||||
|
|
||||||
except Exception as e: #
|
|
||||||
get_logger().error(f"Error processing suggestion score {i}",
|
|
||||||
artifact={"suggestion": suggestion,
|
|
||||||
"code_suggestions_feedback": code_suggestions_feedback[i]})
|
|
||||||
suggestion["score"] = 7
|
|
||||||
suggestion["score_why"] = ""
|
|
||||||
|
|
||||||
# if the before and after code is the same, clear one of them
|
|
||||||
try:
|
|
||||||
if suggestion['existing_code'] == suggestion['improved_code']:
|
|
||||||
get_logger().debug(
|
|
||||||
f"edited improved suggestion {i + 1}, because equal to existing code: {suggestion['existing_code']}")
|
|
||||||
if get_settings().pr_code_suggestions.commitable_code_suggestions:
|
|
||||||
suggestion['improved_code'] = "" # we need 'existing_code' to locate the code in the PR
|
|
||||||
else:
|
|
||||||
suggestion['existing_code'] = ""
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Error processing suggestion {i + 1}, error: {e}")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _truncate_if_needed(suggestion):
|
def _truncate_if_needed(suggestion):
|
||||||
max_code_suggestion_length = get_settings().get("PR_CODE_SUGGESTIONS.MAX_CODE_SUGGESTION_LENGTH", 0)
|
max_code_suggestion_length = get_settings().get("PR_CODE_SUGGESTIONS.MAX_CODE_SUGGESTION_LENGTH", 0)
|
||||||
suggestion_truncation_message = get_settings().get("PR_CODE_SUGGESTIONS.SUGGESTION_TRUNCATION_MESSAGE", "")
|
suggestion_truncation_message = get_settings().get("PR_CODE_SUGGESTIONS.SUGGESTION_TRUNCATION_MESSAGE", "")
|
||||||
if max_code_suggestion_length > 0:
|
if max_code_suggestion_length > 0:
|
||||||
if len(suggestion['improved_code']) > max_code_suggestion_length:
|
if len(suggestion['improved_code']) > max_code_suggestion_length:
|
||||||
get_logger().info(f"Truncated suggestion from {len(suggestion['improved_code'])} "
|
|
||||||
f"characters to {max_code_suggestion_length} characters")
|
|
||||||
suggestion['improved_code'] = suggestion['improved_code'][:max_code_suggestion_length]
|
suggestion['improved_code'] = suggestion['improved_code'][:max_code_suggestion_length]
|
||||||
suggestion['improved_code'] += f"\n{suggestion_truncation_message}"
|
suggestion['improved_code'] += f"\n{suggestion_truncation_message}"
|
||||||
|
get_logger().info(f"Truncated suggestion from {len(suggestion['improved_code'])} "
|
||||||
|
f"characters to {max_code_suggestion_length} characters")
|
||||||
return suggestion
|
return suggestion
|
||||||
|
|
||||||
def _prepare_pr_code_suggestions(self, predictions: str) -> Dict:
|
def _prepare_pr_code_suggestions(self, predictions: str) -> Dict:
|
||||||
@ -502,7 +399,8 @@ class PRCodeSuggestions:
|
|||||||
one_sentence_summary_list = []
|
one_sentence_summary_list = []
|
||||||
for i, suggestion in enumerate(data['code_suggestions']):
|
for i, suggestion in enumerate(data['code_suggestions']):
|
||||||
try:
|
try:
|
||||||
needed_keys = ['one_sentence_summary', 'label', 'relevant_file']
|
needed_keys = ['one_sentence_summary', 'label', 'relevant_file', 'relevant_lines_start',
|
||||||
|
'relevant_lines_end']
|
||||||
is_valid_keys = True
|
is_valid_keys = True
|
||||||
for key in needed_keys:
|
for key in needed_keys:
|
||||||
if key not in suggestion:
|
if key not in suggestion:
|
||||||
@ -513,11 +411,6 @@ class PRCodeSuggestions:
|
|||||||
if not is_valid_keys:
|
if not is_valid_keys:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if get_settings().get("pr_code_suggestions.focus_only_on_problems", False):
|
|
||||||
CRITICAL_LABEL = 'critical'
|
|
||||||
if CRITICAL_LABEL in suggestion['label'].lower(): # we want the published labels to be less declarative
|
|
||||||
suggestion['label'] = 'possible issue'
|
|
||||||
|
|
||||||
if suggestion['one_sentence_summary'] in one_sentence_summary_list:
|
if suggestion['one_sentence_summary'] in one_sentence_summary_list:
|
||||||
get_logger().debug(f"Skipping suggestion {i + 1}, because it is a duplicate: {suggestion}")
|
get_logger().debug(f"Skipping suggestion {i + 1}, because it is a duplicate: {suggestion}")
|
||||||
continue
|
continue
|
||||||
@ -529,6 +422,13 @@ class PRCodeSuggestions:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if ('existing_code' in suggestion) and ('improved_code' in suggestion):
|
if ('existing_code' in suggestion) and ('improved_code' in suggestion):
|
||||||
|
if suggestion['existing_code'] == suggestion['improved_code']:
|
||||||
|
get_logger().debug(
|
||||||
|
f"edited improved suggestion {i + 1}, because equal to existing code: {suggestion['existing_code']}")
|
||||||
|
if get_settings().pr_code_suggestions.commitable_code_suggestions:
|
||||||
|
suggestion['improved_code'] = "" # we need 'existing_code' to locate the code in the PR
|
||||||
|
else:
|
||||||
|
suggestion['existing_code'] = ""
|
||||||
suggestion = self._truncate_if_needed(suggestion)
|
suggestion = self._truncate_if_needed(suggestion)
|
||||||
one_sentence_summary_list.append(suggestion['one_sentence_summary'])
|
one_sentence_summary_list.append(suggestion['one_sentence_summary'])
|
||||||
suggestion_list.append(suggestion)
|
suggestion_list.append(suggestion)
|
||||||
@ -541,7 +441,7 @@ class PRCodeSuggestions:
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def push_inline_code_suggestions(self, data):
|
def push_inline_code_suggestions(self, data):
|
||||||
code_suggestions = []
|
code_suggestions = []
|
||||||
|
|
||||||
if not data['code_suggestions']:
|
if not data['code_suggestions']:
|
||||||
@ -631,35 +531,9 @@ class PRCodeSuggestions:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove_line_numbers(self, patches_diff_list: List[str]) -> List[str]:
|
|
||||||
# create a copy of the patches_diff_list, without line numbers for '__new hunk__' sections
|
|
||||||
try:
|
|
||||||
self.patches_diff_list_no_line_numbers = []
|
|
||||||
for patches_diff in self.patches_diff_list:
|
|
||||||
patches_diff_lines = patches_diff.splitlines()
|
|
||||||
for i, line in enumerate(patches_diff_lines):
|
|
||||||
if line.strip():
|
|
||||||
if line.isnumeric():
|
|
||||||
patches_diff_lines[i] = ''
|
|
||||||
elif line[0].isdigit():
|
|
||||||
# find the first letter in the line that starts with a valid letter
|
|
||||||
for j, char in enumerate(line):
|
|
||||||
if not char.isdigit():
|
|
||||||
patches_diff_lines[i] = line[j + 1:]
|
|
||||||
break
|
|
||||||
self.patches_diff_list_no_line_numbers.append('\n'.join(patches_diff_lines))
|
|
||||||
return self.patches_diff_list_no_line_numbers
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Error removing line numbers from patches_diff_list, error: {e}")
|
|
||||||
return patches_diff_list
|
|
||||||
|
|
||||||
async def _prepare_prediction_extended(self, model: str) -> dict:
|
async def _prepare_prediction_extended(self, model: str) -> dict:
|
||||||
self.patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model,
|
self.patches_diff_list = get_pr_multi_diffs(self.git_provider, self.token_handler, model,
|
||||||
max_calls=get_settings().pr_code_suggestions.max_number_of_calls)
|
max_calls=get_settings().pr_code_suggestions.max_number_of_calls)
|
||||||
|
|
||||||
# create a copy of the patches_diff_list, without line numbers for '__new hunk__' sections
|
|
||||||
self.patches_diff_list_no_line_numbers = self.remove_line_numbers(self.patches_diff_list)
|
|
||||||
|
|
||||||
if self.patches_diff_list:
|
if self.patches_diff_list:
|
||||||
get_logger().info(f"Number of PR chunk calls: {len(self.patches_diff_list)}")
|
get_logger().info(f"Number of PR chunk calls: {len(self.patches_diff_list)}")
|
||||||
get_logger().debug(f"PR diff:", artifact=self.patches_diff_list)
|
get_logger().debug(f"PR diff:", artifact=self.patches_diff_list)
|
||||||
@ -667,14 +541,12 @@ class PRCodeSuggestions:
|
|||||||
# parallelize calls to AI:
|
# parallelize calls to AI:
|
||||||
if get_settings().pr_code_suggestions.parallel_calls:
|
if get_settings().pr_code_suggestions.parallel_calls:
|
||||||
prediction_list = await asyncio.gather(
|
prediction_list = await asyncio.gather(
|
||||||
*[self._get_prediction(model, patches_diff, patches_diff_no_line_numbers) for
|
*[self._get_prediction(model, patches_diff) for patches_diff in self.patches_diff_list])
|
||||||
patches_diff, patches_diff_no_line_numbers in
|
|
||||||
zip(self.patches_diff_list, self.patches_diff_list_no_line_numbers)])
|
|
||||||
self.prediction_list = prediction_list
|
self.prediction_list = prediction_list
|
||||||
else:
|
else:
|
||||||
prediction_list = []
|
prediction_list = []
|
||||||
for patches_diff, patches_diff_no_line_numbers in zip(self.patches_diff_list, self.patches_diff_list_no_line_numbers):
|
for i, patches_diff in enumerate(self.patches_diff_list):
|
||||||
prediction = await self._get_prediction(model, patches_diff, patches_diff_no_line_numbers)
|
prediction = await self._get_prediction(model, patches_diff)
|
||||||
prediction_list.append(prediction)
|
prediction_list.append(prediction)
|
||||||
|
|
||||||
data = {"code_suggestions": []}
|
data = {"code_suggestions": []}
|
||||||
@ -683,6 +555,7 @@ class PRCodeSuggestions:
|
|||||||
score_threshold = max(1, int(get_settings().pr_code_suggestions.suggestions_score_threshold))
|
score_threshold = max(1, int(get_settings().pr_code_suggestions.suggestions_score_threshold))
|
||||||
for i, prediction in enumerate(predictions["code_suggestions"]):
|
for i, prediction in enumerate(predictions["code_suggestions"]):
|
||||||
try:
|
try:
|
||||||
|
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
||||||
score = int(prediction.get("score", 1))
|
score = int(prediction.get("score", 1))
|
||||||
if score >= score_threshold:
|
if score >= score_threshold:
|
||||||
data["code_suggestions"].append(prediction)
|
data["code_suggestions"].append(prediction)
|
||||||
@ -690,15 +563,72 @@ class PRCodeSuggestions:
|
|||||||
get_logger().info(
|
get_logger().info(
|
||||||
f"Removing suggestions {i} from call {j}, because score is {score}, and score_threshold is {score_threshold}",
|
f"Removing suggestions {i} from call {j}, because score is {score}, and score_threshold is {score_threshold}",
|
||||||
artifact=prediction)
|
artifact=prediction)
|
||||||
|
else:
|
||||||
|
data["code_suggestions"].append(prediction)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Error getting PR diff for suggestion {i} in call {j}, error: {e}",
|
get_logger().error(f"Error getting PR diff for suggestion {i} in call {j}, error: {e}")
|
||||||
artifact={"prediction": prediction})
|
|
||||||
self.data = data
|
self.data = data
|
||||||
else:
|
else:
|
||||||
get_logger().warning(f"Empty PR diff list")
|
get_logger().warning(f"Empty PR diff list")
|
||||||
self.data = data = None
|
self.data = data = None
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
async def rank_suggestions(self, data: List) -> List:
|
||||||
|
"""
|
||||||
|
Call a model to rank (sort) code suggestions based on their importance order.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (List): A list of code suggestions to be ranked.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List: The ranked list of code suggestions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
suggestion_list = []
|
||||||
|
if not data:
|
||||||
|
return suggestion_list
|
||||||
|
for suggestion in data:
|
||||||
|
suggestion_list.append(suggestion)
|
||||||
|
data_sorted = [[]] * len(suggestion_list)
|
||||||
|
|
||||||
|
if len(suggestion_list) == 1:
|
||||||
|
return suggestion_list
|
||||||
|
|
||||||
|
try:
|
||||||
|
suggestion_str = ""
|
||||||
|
for i, suggestion in enumerate(suggestion_list):
|
||||||
|
suggestion_str += f"suggestion {i + 1}: " + str(suggestion) + '\n\n'
|
||||||
|
|
||||||
|
variables = {'suggestion_list': suggestion_list, 'suggestion_str': suggestion_str}
|
||||||
|
model = get_settings().config.model
|
||||||
|
environment = Environment(undefined=StrictUndefined)
|
||||||
|
system_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.system).render(
|
||||||
|
variables)
|
||||||
|
user_prompt = environment.from_string(get_settings().pr_sort_code_suggestions_prompt.user).render(variables)
|
||||||
|
response, finish_reason = await self.ai_handler.chat_completion(model=model, system=system_prompt,
|
||||||
|
user=user_prompt)
|
||||||
|
|
||||||
|
sort_order = load_yaml(response)
|
||||||
|
for s in sort_order['Sort Order']:
|
||||||
|
suggestion_number = s['suggestion number']
|
||||||
|
importance_order = s['importance order']
|
||||||
|
data_sorted[importance_order - 1] = suggestion_list[suggestion_number - 1]
|
||||||
|
|
||||||
|
if get_settings().pr_code_suggestions.final_clip_factor != 1:
|
||||||
|
max_len = max(
|
||||||
|
len(data_sorted),
|
||||||
|
get_settings().pr_code_suggestions.num_code_suggestions_per_chunk,
|
||||||
|
)
|
||||||
|
new_len = int(0.5 + max_len * get_settings().pr_code_suggestions.final_clip_factor)
|
||||||
|
if new_len < len(data_sorted):
|
||||||
|
data_sorted = data_sorted[:new_len]
|
||||||
|
except Exception as e:
|
||||||
|
if get_settings().config.verbosity_level >= 1:
|
||||||
|
get_logger().info(f"Could not sort suggestions, error: {e}")
|
||||||
|
data_sorted = suggestion_list
|
||||||
|
|
||||||
|
return data_sorted
|
||||||
|
|
||||||
def generate_summarized_suggestions(self, data: Dict) -> str:
|
def generate_summarized_suggestions(self, data: Dict) -> str:
|
||||||
try:
|
try:
|
||||||
pr_body = "## PR Code Suggestions ✨\n\n"
|
pr_body = "## PR Code Suggestions ✨\n\n"
|
||||||
@ -720,7 +650,10 @@ class PRCodeSuggestions:
|
|||||||
header = f"Suggestion"
|
header = f"Suggestion"
|
||||||
delta = 66
|
delta = 66
|
||||||
header += " " * delta
|
header += " " * delta
|
||||||
|
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
||||||
pr_body += f"""<thead><tr><td>Category</td><td align=left>{header}</td><td align=center>Score</td></tr>"""
|
pr_body += f"""<thead><tr><td>Category</td><td align=left>{header}</td><td align=center>Score</td></tr>"""
|
||||||
|
else:
|
||||||
|
pr_body += f"""<thead><tr><td>Category</td><td align=left>{header}</td></tr>"""
|
||||||
pr_body += """<tbody>"""
|
pr_body += """<tbody>"""
|
||||||
suggestions_labels = dict()
|
suggestions_labels = dict()
|
||||||
# add all suggestions related to each label
|
# add all suggestions related to each label
|
||||||
@ -731,6 +664,7 @@ class PRCodeSuggestions:
|
|||||||
suggestions_labels[label].append(suggestion)
|
suggestions_labels[label].append(suggestion)
|
||||||
|
|
||||||
# sort suggestions_labels by the suggestion with the highest score
|
# sort suggestions_labels by the suggestion with the highest score
|
||||||
|
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
||||||
suggestions_labels = dict(
|
suggestions_labels = dict(
|
||||||
sorted(suggestions_labels.items(), key=lambda x: max([s['score'] for s in x[1]]), reverse=True))
|
sorted(suggestions_labels.items(), key=lambda x: max([s['score'] for s in x[1]]), reverse=True))
|
||||||
# sort the suggestions inside each label group by score
|
# sort the suggestions inside each label group by score
|
||||||
@ -794,6 +728,7 @@ class PRCodeSuggestions:
|
|||||||
|
|
||||||
{example_code.rstrip()}
|
{example_code.rstrip()}
|
||||||
"""
|
"""
|
||||||
|
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
||||||
pr_body += f"<details><summary>Suggestion importance[1-10]: {suggestion['score']}</summary>\n\n"
|
pr_body += f"<details><summary>Suggestion importance[1-10]: {suggestion['score']}</summary>\n\n"
|
||||||
pr_body += f"Why: {suggestion['score_why']}\n\n"
|
pr_body += f"Why: {suggestion['score_why']}\n\n"
|
||||||
pr_body += f"</details>"
|
pr_body += f"</details>"
|
||||||
@ -801,6 +736,7 @@ class PRCodeSuggestions:
|
|||||||
pr_body += f"</details>"
|
pr_body += f"</details>"
|
||||||
|
|
||||||
# # add another column for 'score'
|
# # add another column for 'score'
|
||||||
|
if get_settings().pr_code_suggestions.self_reflect_on_suggestions:
|
||||||
pr_body += f"</td><td align=center>{suggestion['score']}\n\n"
|
pr_body += f"</td><td align=center>{suggestion['score']}\n\n"
|
||||||
|
|
||||||
pr_body += f"</td></tr>"
|
pr_body += f"</td></tr>"
|
||||||
@ -814,12 +750,7 @@ class PRCodeSuggestions:
|
|||||||
get_logger().info(f"Failed to publish summarized code suggestions, error: {e}")
|
get_logger().info(f"Failed to publish summarized code suggestions, error: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def self_reflect_on_suggestions(self,
|
async def self_reflect_on_suggestions(self, suggestion_list: List, patches_diff: str, model: str) -> str:
|
||||||
suggestion_list: List,
|
|
||||||
patches_diff: str,
|
|
||||||
model: str,
|
|
||||||
prev_suggestions_str: str = "",
|
|
||||||
dedicated_prompt: str = "") -> str:
|
|
||||||
if not suggestion_list:
|
if not suggestion_list:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@ -832,22 +763,13 @@ class PRCodeSuggestions:
|
|||||||
'suggestion_str': suggestion_str,
|
'suggestion_str': suggestion_str,
|
||||||
"diff": patches_diff,
|
"diff": patches_diff,
|
||||||
'num_code_suggestions': len(suggestion_list),
|
'num_code_suggestions': len(suggestion_list),
|
||||||
'prev_suggestions_str': prev_suggestions_str,
|
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False)}
|
||||||
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False),
|
|
||||||
'duplicate_prompt_examples': get_settings().config.get('duplicate_prompt_examples', False)}
|
|
||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
|
|
||||||
if dedicated_prompt:
|
|
||||||
system_prompt_reflect = environment.from_string(
|
system_prompt_reflect = environment.from_string(
|
||||||
get_settings().get(dedicated_prompt).system).render(variables)
|
get_settings().pr_code_suggestions_reflect_prompt.system).render(
|
||||||
user_prompt_reflect = environment.from_string(
|
variables)
|
||||||
get_settings().get(dedicated_prompt).user).render(variables)
|
|
||||||
else:
|
|
||||||
system_prompt_reflect = environment.from_string(
|
|
||||||
get_settings().pr_code_suggestions_reflect_prompt.system).render(variables)
|
|
||||||
user_prompt_reflect = environment.from_string(
|
user_prompt_reflect = environment.from_string(
|
||||||
get_settings().pr_code_suggestions_reflect_prompt.user).render(variables)
|
get_settings().pr_code_suggestions_reflect_prompt.user).render(variables)
|
||||||
|
|
||||||
with get_logger().contextualize(command="self_reflect_on_suggestions"):
|
with get_logger().contextualize(command="self_reflect_on_suggestions"):
|
||||||
response_reflect, finish_reason_reflect = await self.ai_handler.chat_completion(model=model,
|
response_reflect, finish_reason_reflect = await self.ai_handler.chat_completion(model=model,
|
||||||
system=system_prompt_reflect,
|
system=system_prompt_reflect,
|
||||||
@ -856,3 +778,4 @@ class PRCodeSuggestions:
|
|||||||
get_logger().info(f"Could not reflect on suggestions, error: {e}")
|
get_logger().info(f"Could not reflect on suggestions, error: {e}")
|
||||||
return ""
|
return ""
|
||||||
return response_reflect
|
return response_reflect
|
||||||
|
|
||||||
|
@ -38,15 +38,12 @@ class PRConfig:
|
|||||||
if (header.lower().startswith("pr_") or header.lower().startswith("config")) and header.lower() in configuration_headers
|
if (header.lower().startswith("pr_") or header.lower().startswith("config")) and header.lower() in configuration_headers
|
||||||
}
|
}
|
||||||
|
|
||||||
skip_keys = ['ai_disclaimer', 'ai_disclaimer_title', 'ANALYTICS_FOLDER', 'secret_provider', "skip_keys", "app_id", "redirect",
|
skip_keys = ['ai_disclaimer', 'ai_disclaimer_title', 'ANALYTICS_FOLDER', 'secret_provider', "skip_keys",
|
||||||
'trial_prefix_message', 'no_eligible_message', 'identity_provider', 'ALLOWED_REPOS',
|
'trial_prefix_message', 'no_eligible_message', 'identity_provider', 'ALLOWED_REPOS',
|
||||||
'APP_NAME', 'PERSONAL_ACCESS_TOKEN', 'shared_secret', 'key', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'user_token',
|
'APP_NAME']
|
||||||
'private_key', 'private_key_id', 'client_id', 'client_secret', 'token', 'bearer_token']
|
|
||||||
extra_skip_keys = get_settings().config.get('config.skip_keys', [])
|
extra_skip_keys = get_settings().config.get('config.skip_keys', [])
|
||||||
if extra_skip_keys:
|
if extra_skip_keys:
|
||||||
skip_keys.extend(extra_skip_keys)
|
skip_keys.extend(extra_skip_keys)
|
||||||
skip_keys_lower = [key.lower() for key in skip_keys]
|
|
||||||
|
|
||||||
|
|
||||||
markdown_text = "<details> <summary><strong>🛠️ PR-Agent Configurations:</strong></summary> \n\n"
|
markdown_text = "<details> <summary><strong>🛠️ PR-Agent Configurations:</strong></summary> \n\n"
|
||||||
markdown_text += f"\n\n```yaml\n\n"
|
markdown_text += f"\n\n```yaml\n\n"
|
||||||
@ -55,7 +52,7 @@ class PRConfig:
|
|||||||
markdown_text += "\n\n"
|
markdown_text += "\n\n"
|
||||||
markdown_text += f"==================== {header} ===================="
|
markdown_text += f"==================== {header} ===================="
|
||||||
for key, value in configs.items():
|
for key, value in configs.items():
|
||||||
if key.lower() in skip_keys_lower:
|
if key in skip_keys:
|
||||||
continue
|
continue
|
||||||
markdown_text += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}"
|
markdown_text += f"\n{header.lower()}.{key.lower()} = {repr(value) if isinstance(value, str) else value}"
|
||||||
markdown_text += " "
|
markdown_text += " "
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
import traceback
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
@ -10,24 +9,19 @@ from jinja2 import Environment, StrictUndefined
|
|||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.pr_processing import (OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD,
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models, get_pr_diff_multiple_patchs, \
|
||||||
get_pr_diff,
|
OUTPUT_BUFFER_TOKENS_HARD_THRESHOLD
|
||||||
get_pr_diff_multiple_patchs,
|
|
||||||
retry_with_fallback_models)
|
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import (ModelType, PRDescriptionHeader, clip_tokens,
|
from pr_agent.algo.utils import set_custom_labels
|
||||||
get_max_tokens, get_user_labels, load_yaml,
|
from pr_agent.algo.utils import load_yaml, get_user_labels, ModelType, show_relevant_configurations, get_max_tokens, \
|
||||||
set_custom_labels,
|
clip_tokens
|
||||||
show_relevant_configurations)
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import (GithubProvider, get_git_provider,
|
from pr_agent.git_providers import get_git_provider, GithubProvider, get_git_provider_with_context
|
||||||
get_git_provider_with_context)
|
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
from pr_agent.servers.help import HelpMessage
|
from pr_agent.servers.help import HelpMessage
|
||||||
from pr_agent.tools.ticket_pr_compliance_check import (
|
from pr_agent.tools.ticket_pr_compliance_check import extract_ticket_links_from_pr_description, extract_tickets, \
|
||||||
extract_and_cache_pr_tickets, extract_ticket_links_from_pr_description,
|
extract_and_cache_pr_tickets
|
||||||
extract_tickets)
|
|
||||||
|
|
||||||
|
|
||||||
class PRDescription:
|
class PRDescription:
|
||||||
@ -58,7 +52,6 @@ class PRDescription:
|
|||||||
self.ai_handler.main_pr_language = self.main_pr_language
|
self.ai_handler.main_pr_language = self.main_pr_language
|
||||||
|
|
||||||
# Initialize the variables dictionary
|
# Initialize the variables dictionary
|
||||||
self.COLLAPSIBLE_FILE_LIST_THRESHOLD = get_settings().pr_description.get("collapsible_file_list_threshold", 8)
|
|
||||||
self.vars = {
|
self.vars = {
|
||||||
"title": self.git_provider.pr.title,
|
"title": self.git_provider.pr.title,
|
||||||
"branch": self.git_provider.get_pr_branch(),
|
"branch": self.git_provider.get_pr_branch(),
|
||||||
@ -71,8 +64,6 @@ class PRDescription:
|
|||||||
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function
|
"custom_labels_class": "", # will be filled if necessary in 'set_custom_labels' function
|
||||||
"enable_semantic_files_types": get_settings().pr_description.enable_semantic_files_types,
|
"enable_semantic_files_types": get_settings().pr_description.enable_semantic_files_types,
|
||||||
"related_tickets": "",
|
"related_tickets": "",
|
||||||
"include_file_summary_changes": len(self.git_provider.get_diff_files()) <= self.COLLAPSIBLE_FILE_LIST_THRESHOLD,
|
|
||||||
'duplicate_prompt_examples': get_settings().config.get('duplicate_prompt_examples', False),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.user_description = self.git_provider.get_user_description()
|
self.user_description = self.git_provider.get_user_description()
|
||||||
@ -89,6 +80,7 @@ class PRDescription:
|
|||||||
self.patches_diff = None
|
self.patches_diff = None
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
self.file_label_dict = None
|
self.file_label_dict = None
|
||||||
|
self.COLLAPSIBLE_FILE_LIST_THRESHOLD = 8
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
try:
|
try:
|
||||||
@ -102,7 +94,7 @@ class PRDescription:
|
|||||||
# ticket extraction if exists
|
# ticket extraction if exists
|
||||||
await extract_and_cache_pr_tickets(self.git_provider, self.vars)
|
await extract_and_cache_pr_tickets(self.git_provider, self.vars)
|
||||||
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction, ModelType.WEAK)
|
await retry_with_fallback_models(self._prepare_prediction, ModelType.TURBO)
|
||||||
|
|
||||||
if self.prediction:
|
if self.prediction:
|
||||||
self._prepare_data()
|
self._prepare_data()
|
||||||
@ -117,8 +109,6 @@ class PRDescription:
|
|||||||
pr_labels, pr_file_changes = [], []
|
pr_labels, pr_file_changes = [], []
|
||||||
if get_settings().pr_description.publish_labels:
|
if get_settings().pr_description.publish_labels:
|
||||||
pr_labels = self._prepare_labels()
|
pr_labels = self._prepare_labels()
|
||||||
else:
|
|
||||||
get_logger().debug(f"Publishing labels disabled")
|
|
||||||
|
|
||||||
if get_settings().pr_description.use_description_markers:
|
if get_settings().pr_description.use_description_markers:
|
||||||
pr_title, pr_body, changes_walkthrough, pr_file_changes = self._prepare_pr_answer_with_markers()
|
pr_title, pr_body, changes_walkthrough, pr_file_changes = self._prepare_pr_answer_with_markers()
|
||||||
@ -142,7 +132,6 @@ class PRDescription:
|
|||||||
pr_body += show_relevant_configurations(relevant_section='pr_description')
|
pr_body += show_relevant_configurations(relevant_section='pr_description')
|
||||||
|
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
|
|
||||||
# publish labels
|
# publish labels
|
||||||
if get_settings().pr_description.publish_labels and pr_labels and self.git_provider.is_supported("get_labels"):
|
if get_settings().pr_description.publish_labels and pr_labels and self.git_provider.is_supported("get_labels"):
|
||||||
original_labels = self.git_provider.get_pr_labels(update=True)
|
original_labels = self.git_provider.get_pr_labels(update=True)
|
||||||
@ -170,49 +159,43 @@ class PRDescription:
|
|||||||
self.git_provider.publish_description(pr_title, pr_body)
|
self.git_provider.publish_description(pr_title, pr_body)
|
||||||
|
|
||||||
# publish final update message
|
# publish final update message
|
||||||
if (get_settings().pr_description.final_update_message and not get_settings().config.get('is_auto_command', False)):
|
if (get_settings().pr_description.final_update_message):
|
||||||
latest_commit_url = self.git_provider.get_latest_commit_url()
|
latest_commit_url = self.git_provider.get_latest_commit_url()
|
||||||
if latest_commit_url:
|
if latest_commit_url:
|
||||||
pr_url = self.git_provider.get_pr_url()
|
pr_url = self.git_provider.get_pr_url()
|
||||||
update_comment = f"**[PR Description]({pr_url})** updated to latest commit ({latest_commit_url})"
|
update_comment = f"**[PR Description]({pr_url})** updated to latest commit ({latest_commit_url})"
|
||||||
self.git_provider.publish_comment(update_comment)
|
self.git_provider.publish_comment(update_comment)
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
else:
|
|
||||||
get_logger().info('PR description, but not published since publish_output is False.')
|
|
||||||
get_settings().data = {"artifact": pr_body}
|
|
||||||
return
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Error generating PR description {self.pr_id}: {e}",
|
get_logger().error(f"Error generating PR description {self.pr_id}: {e}")
|
||||||
artifact={"traceback": traceback.format_exc()})
|
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
async def _prepare_prediction(self, model: str) -> None:
|
async def _prepare_prediction(self, model: str) -> None:
|
||||||
if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description:
|
if get_settings().pr_description.use_description_markers and 'pr_agent:' not in self.user_description:
|
||||||
get_logger().info("Markers were enabled, but user description does not contain markers. skipping AI prediction")
|
get_logger().info(
|
||||||
|
"Markers were enabled, but user description does not contain markers. skipping AI prediction")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
large_pr_handling = get_settings().pr_description.enable_large_pr_handling and "pr_description_only_files_prompts" in get_settings()
|
large_pr_handling = get_settings().pr_description.enable_large_pr_handling and "pr_description_only_files_prompts" in get_settings()
|
||||||
output = get_pr_diff(self.git_provider, self.token_handler, model, large_pr_handling=large_pr_handling, return_remaining_files=True)
|
output = get_pr_diff(self.git_provider, self.token_handler, model, large_pr_handling=large_pr_handling,
|
||||||
|
return_remaining_files=True)
|
||||||
if isinstance(output, tuple):
|
if isinstance(output, tuple):
|
||||||
patches_diff, remaining_files_list = output
|
patches_diff, remaining_files_list = output
|
||||||
else:
|
else:
|
||||||
patches_diff = output
|
patches_diff = output
|
||||||
remaining_files_list = []
|
remaining_files_list = []
|
||||||
|
|
||||||
if not large_pr_handling or patches_diff:
|
if not large_pr_handling or patches_diff:
|
||||||
self.patches_diff = patches_diff
|
self.patches_diff = patches_diff
|
||||||
if patches_diff:
|
if patches_diff:
|
||||||
# generate the prediction
|
|
||||||
get_logger().debug(f"PR diff", artifact=self.patches_diff)
|
get_logger().debug(f"PR diff", artifact=self.patches_diff)
|
||||||
self.prediction = await self._get_prediction(model, patches_diff, prompt="pr_description_prompt")
|
self.prediction = await self._get_prediction(model, patches_diff, prompt="pr_description_prompt")
|
||||||
|
if (remaining_files_list and 'pr_files' in self.prediction and 'label:' in self.prediction and
|
||||||
# extend the prediction with additional files not shown
|
get_settings().pr_description.mention_extra_files):
|
||||||
if get_settings().pr_description.enable_semantic_files_types:
|
get_logger().debug(f"Extending additional files, {len(remaining_files_list)} files")
|
||||||
self.prediction = await self.extend_uncovered_files(self.prediction)
|
self.prediction = await self.extend_additional_files(remaining_files_list)
|
||||||
else:
|
else:
|
||||||
get_logger().error(f"Error getting PR diff {self.pr_id}",
|
get_logger().error(f"Error getting PR diff {self.pr_id}")
|
||||||
artifact={"traceback": traceback.format_exc()})
|
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
else:
|
else:
|
||||||
# get the diff in multiple patches, with the token handler only for the files prompt
|
# get the diff in multiple patches, with the token handler only for the files prompt
|
||||||
@ -297,81 +280,43 @@ class PRDescription:
|
|||||||
prompt="pr_description_only_description_prompts")
|
prompt="pr_description_only_description_prompts")
|
||||||
prediction_headers = prediction_headers.strip().removeprefix('```yaml').strip('`').strip()
|
prediction_headers = prediction_headers.strip().removeprefix('```yaml').strip('`').strip()
|
||||||
|
|
||||||
# extend the tables with the files not shown
|
# manually add extra files to final prediction
|
||||||
files_walkthrough_extended = await self.extend_uncovered_files(files_walkthrough)
|
MAX_EXTRA_FILES_TO_OUTPUT = 100
|
||||||
|
if get_settings().pr_description.mention_extra_files:
|
||||||
|
for i, file in enumerate(remaining_files_list):
|
||||||
|
extra_file_yaml = f"""\
|
||||||
|
- filename: |
|
||||||
|
{file}
|
||||||
|
changes_summary: |
|
||||||
|
...
|
||||||
|
changes_title: |
|
||||||
|
...
|
||||||
|
label: |
|
||||||
|
additional files (token-limit)
|
||||||
|
"""
|
||||||
|
files_walkthrough = files_walkthrough.strip() + "\n" + extra_file_yaml.strip()
|
||||||
|
if i >= MAX_EXTRA_FILES_TO_OUTPUT:
|
||||||
|
files_walkthrough += f"""\
|
||||||
|
extra_file_yaml =
|
||||||
|
- filename: |
|
||||||
|
Additional {len(remaining_files_list) - MAX_EXTRA_FILES_TO_OUTPUT} files not shown
|
||||||
|
changes_summary: |
|
||||||
|
...
|
||||||
|
changes_title: |
|
||||||
|
...
|
||||||
|
label: |
|
||||||
|
additional files (token-limit)
|
||||||
|
"""
|
||||||
|
break
|
||||||
|
|
||||||
# final processing
|
# final processing
|
||||||
self.prediction = prediction_headers + "\n" + "pr_files:\n" + files_walkthrough_extended
|
self.prediction = prediction_headers + "\n" + "pr_files:\n" + files_walkthrough
|
||||||
if not load_yaml(self.prediction, keys_fix_yaml=self.keys_fix):
|
if not load_yaml(self.prediction, keys_fix_yaml=self.keys_fix):
|
||||||
get_logger().error(f"Error getting valid YAML in large PR handling for describe {self.pr_id}")
|
get_logger().error(f"Error getting valid YAML in large PR handling for describe {self.pr_id}")
|
||||||
if load_yaml(prediction_headers, keys_fix_yaml=self.keys_fix):
|
if load_yaml(prediction_headers, keys_fix_yaml=self.keys_fix):
|
||||||
get_logger().debug(f"Using only headers for describe {self.pr_id}")
|
get_logger().debug(f"Using only headers for describe {self.pr_id}")
|
||||||
self.prediction = prediction_headers
|
self.prediction = prediction_headers
|
||||||
|
|
||||||
async def extend_uncovered_files(self, original_prediction: str) -> str:
|
|
||||||
try:
|
|
||||||
prediction = original_prediction
|
|
||||||
|
|
||||||
# get the original prediction filenames
|
|
||||||
original_prediction_loaded = load_yaml(original_prediction, keys_fix_yaml=self.keys_fix)
|
|
||||||
if isinstance(original_prediction_loaded, list):
|
|
||||||
original_prediction_dict = {"pr_files": original_prediction_loaded}
|
|
||||||
else:
|
|
||||||
original_prediction_dict = original_prediction_loaded
|
|
||||||
filenames_predicted = [file['filename'].strip() for file in original_prediction_dict.get('pr_files', [])]
|
|
||||||
|
|
||||||
# extend the prediction with additional files not included in the original prediction
|
|
||||||
pr_files = self.git_provider.get_diff_files()
|
|
||||||
prediction_extra = "pr_files:"
|
|
||||||
MAX_EXTRA_FILES_TO_OUTPUT = 100
|
|
||||||
counter_extra_files = 0
|
|
||||||
for file in pr_files:
|
|
||||||
if file.filename in filenames_predicted:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# add up to MAX_EXTRA_FILES_TO_OUTPUT files
|
|
||||||
counter_extra_files += 1
|
|
||||||
if counter_extra_files > MAX_EXTRA_FILES_TO_OUTPUT:
|
|
||||||
extra_file_yaml = f"""\
|
|
||||||
- filename: |
|
|
||||||
Additional files not shown
|
|
||||||
changes_title: |
|
|
||||||
...
|
|
||||||
label: |
|
|
||||||
additional files
|
|
||||||
"""
|
|
||||||
prediction_extra = prediction_extra + "\n" + extra_file_yaml.strip()
|
|
||||||
get_logger().debug(f"Too many remaining files, clipping to {MAX_EXTRA_FILES_TO_OUTPUT}")
|
|
||||||
break
|
|
||||||
|
|
||||||
extra_file_yaml = f"""\
|
|
||||||
- filename: |
|
|
||||||
{file.filename}
|
|
||||||
changes_title: |
|
|
||||||
...
|
|
||||||
label: |
|
|
||||||
additional files
|
|
||||||
"""
|
|
||||||
prediction_extra = prediction_extra + "\n" + extra_file_yaml.strip()
|
|
||||||
|
|
||||||
# merge the two dictionaries
|
|
||||||
if counter_extra_files > 0:
|
|
||||||
get_logger().info(f"Adding {counter_extra_files} unprocessed extra files to table prediction")
|
|
||||||
prediction_extra_dict = load_yaml(prediction_extra, keys_fix_yaml=self.keys_fix)
|
|
||||||
if isinstance(original_prediction_dict, dict) and isinstance(prediction_extra_dict, dict):
|
|
||||||
original_prediction_dict["pr_files"].extend(prediction_extra_dict["pr_files"])
|
|
||||||
new_yaml = yaml.dump(original_prediction_dict)
|
|
||||||
if load_yaml(new_yaml, keys_fix_yaml=self.keys_fix):
|
|
||||||
prediction = new_yaml
|
|
||||||
if isinstance(original_prediction, list):
|
|
||||||
prediction = yaml.dump(original_prediction_dict["pr_files"])
|
|
||||||
|
|
||||||
return prediction
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Error extending uncovered files {self.pr_id}: {e}")
|
|
||||||
return original_prediction
|
|
||||||
|
|
||||||
|
|
||||||
async def extend_additional_files(self, remaining_files_list) -> str:
|
async def extend_additional_files(self, remaining_files_list) -> str:
|
||||||
prediction = self.prediction
|
prediction = self.prediction
|
||||||
try:
|
try:
|
||||||
@ -443,44 +388,35 @@ class PRDescription:
|
|||||||
self.data['pr_files'] = self.data.pop('pr_files')
|
self.data['pr_files'] = self.data.pop('pr_files')
|
||||||
|
|
||||||
def _prepare_labels(self) -> List[str]:
|
def _prepare_labels(self) -> List[str]:
|
||||||
pr_labels = []
|
pr_types = []
|
||||||
|
|
||||||
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
|
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
|
||||||
if 'labels' in self.data and self.data['labels']:
|
if 'labels' in self.data:
|
||||||
if type(self.data['labels']) == list:
|
if type(self.data['labels']) == list:
|
||||||
pr_labels = self.data['labels']
|
pr_types = self.data['labels']
|
||||||
elif type(self.data['labels']) == str:
|
elif type(self.data['labels']) == str:
|
||||||
pr_labels = self.data['labels'].split(',')
|
pr_types = self.data['labels'].split(',')
|
||||||
elif 'type' in self.data and self.data['type'] and get_settings().pr_description.publish_labels:
|
elif 'type' in self.data:
|
||||||
if type(self.data['type']) == list:
|
if type(self.data['type']) == list:
|
||||||
pr_labels = self.data['type']
|
pr_types = self.data['type']
|
||||||
elif type(self.data['type']) == str:
|
elif type(self.data['type']) == str:
|
||||||
pr_labels = self.data['type'].split(',')
|
pr_types = self.data['type'].split(',')
|
||||||
pr_labels = [label.strip() for label in pr_labels]
|
pr_types = [label.strip() for label in pr_types]
|
||||||
|
|
||||||
# convert lowercase labels to original case
|
# convert lowercase labels to original case
|
||||||
try:
|
try:
|
||||||
if "labels_minimal_to_labels_dict" in self.variables:
|
if "labels_minimal_to_labels_dict" in self.variables:
|
||||||
d: dict = self.variables["labels_minimal_to_labels_dict"]
|
d: dict = self.variables["labels_minimal_to_labels_dict"]
|
||||||
for i, label_i in enumerate(pr_labels):
|
for i, label_i in enumerate(pr_types):
|
||||||
if label_i in d:
|
if label_i in d:
|
||||||
pr_labels[i] = d[label_i]
|
pr_types[i] = d[label_i]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Error converting labels to original case {self.pr_id}: {e}")
|
get_logger().error(f"Error converting labels to original case {self.pr_id}: {e}")
|
||||||
return pr_labels
|
return pr_types
|
||||||
|
|
||||||
def _prepare_pr_answer_with_markers(self) -> Tuple[str, str, str, List[dict]]:
|
def _prepare_pr_answer_with_markers(self) -> Tuple[str, str, str, List[dict]]:
|
||||||
get_logger().info(f"Using description marker replacements {self.pr_id}")
|
get_logger().info(f"Using description marker replacements {self.pr_id}")
|
||||||
|
|
||||||
# Remove the 'PR Title' key from the dictionary
|
|
||||||
ai_title = self.data.pop('title', self.vars["title"])
|
|
||||||
if (not get_settings().pr_description.generate_ai_title):
|
|
||||||
# Assign the original PR title to the 'title' variable
|
|
||||||
title = self.vars["title"]
|
title = self.vars["title"]
|
||||||
else:
|
|
||||||
# Assign the value of the 'PR Title' key to 'title' variable
|
|
||||||
title = ai_title
|
|
||||||
|
|
||||||
body = self.user_description
|
body = self.user_description
|
||||||
if get_settings().pr_description.include_generated_by_header:
|
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"
|
ai_header = f"### 🤖 Generated by PR Agent at {self.git_provider.last_commit_id.sha}\n\n"
|
||||||
@ -489,10 +425,6 @@ class PRDescription:
|
|||||||
|
|
||||||
ai_type = self.data.get('type')
|
ai_type = self.data.get('type')
|
||||||
if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body):
|
if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body):
|
||||||
if isinstance(ai_type, list):
|
|
||||||
pr_types = [f"{ai_header}{t}" for t in ai_type]
|
|
||||||
pr_type = ','.join(pr_types)
|
|
||||||
else:
|
|
||||||
pr_type = f"{ai_header}{ai_type}"
|
pr_type = f"{ai_header}{ai_type}"
|
||||||
body = body.replace('pr_agent:type', pr_type)
|
body = body.replace('pr_agent:type', pr_type)
|
||||||
|
|
||||||
@ -569,12 +501,7 @@ class PRDescription:
|
|||||||
pr_body += "</details>\n"
|
pr_body += "</details>\n"
|
||||||
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types:
|
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types:
|
||||||
changes_walkthrough, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
|
changes_walkthrough, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
|
||||||
changes_walkthrough = f"{PRDescriptionHeader.CHANGES_WALKTHROUGH.value}\n{changes_walkthrough}"
|
changes_walkthrough = f"### **Changes walkthrough** 📝\n{changes_walkthrough}"
|
||||||
elif key.lower().strip() == 'description':
|
|
||||||
if isinstance(value, list):
|
|
||||||
value = ', '.join(v.rstrip() for v in value)
|
|
||||||
value = value.replace('\n-', '\n\n-').strip() # makes the bullet points more readable by adding double space
|
|
||||||
pr_body += f"{value}\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 isinstance(value, list):
|
if isinstance(value, list):
|
||||||
@ -592,18 +519,14 @@ class PRDescription:
|
|||||||
return file_label_dict
|
return file_label_dict
|
||||||
for file in self.data['pr_files']:
|
for file in self.data['pr_files']:
|
||||||
try:
|
try:
|
||||||
required_fields = ['changes_title', 'filename', 'label']
|
required_fields = ['changes_summary', 'changes_title', 'filename', 'label']
|
||||||
if not all(field in file for field in required_fields):
|
if not all(field in file for field in required_fields):
|
||||||
# can happen for example if a YAML generation was interrupted in the middle (no more tokens)
|
# can happen for example if a YAML generation was interrupted in the middle (no more tokens)
|
||||||
get_logger().warning(f"Missing required fields in file label dict {self.pr_id}, skipping file",
|
get_logger().warning(f"Missing required fields in file label dict {self.pr_id}, skipping file",
|
||||||
artifact={"file": file})
|
artifact={"file": file})
|
||||||
continue
|
continue
|
||||||
if not file.get('changes_title'):
|
|
||||||
get_logger().warning(f"Empty changes title or summary in file label dict {self.pr_id}, skipping file",
|
|
||||||
artifact={"file": file})
|
|
||||||
continue
|
|
||||||
filename = file['filename'].replace("'", "`").replace('"', '`')
|
filename = file['filename'].replace("'", "`").replace('"', '`')
|
||||||
changes_summary = file.get('changes_summary', "").strip()
|
changes_summary = file['changes_summary']
|
||||||
changes_title = file['changes_title'].strip()
|
changes_title = file['changes_title'].strip()
|
||||||
label = file.get('label').strip().lower()
|
label = file.get('label').strip().lower()
|
||||||
if label not in file_label_dict:
|
if label not in file_label_dict:
|
||||||
@ -646,14 +569,12 @@ class PRDescription:
|
|||||||
for filename, file_changes_title, file_change_description in list_tuples:
|
for filename, file_changes_title, file_change_description in list_tuples:
|
||||||
filename = filename.replace("'", "`").rstrip()
|
filename = filename.replace("'", "`").rstrip()
|
||||||
filename_publish = filename.split("/")[-1]
|
filename_publish = filename.split("/")[-1]
|
||||||
if file_changes_title and file_changes_title.strip() != "...":
|
|
||||||
file_changes_title_code = f"<code>{file_changes_title}</code>"
|
file_changes_title_code = f"<code>{file_changes_title}</code>"
|
||||||
file_changes_title_code_br = insert_br_after_x_chars(file_changes_title_code, x=(delta - 5)).strip()
|
file_changes_title_code_br = insert_br_after_x_chars(file_changes_title_code, x=(delta - 5)).strip()
|
||||||
if len(file_changes_title_code_br) < (delta - 5):
|
if len(file_changes_title_code_br) < (delta - 5):
|
||||||
file_changes_title_code_br += " " * ((delta - 5) - len(file_changes_title_code_br))
|
file_changes_title_code_br += " " * ((delta - 5) - len(file_changes_title_code_br))
|
||||||
filename_publish = f"<strong>{filename_publish}</strong><dd>{file_changes_title_code_br}</dd>"
|
filename_publish = f"<strong>{filename_publish}</strong><dd>{file_changes_title_code_br}</dd>"
|
||||||
else:
|
|
||||||
filename_publish = f"<strong>{filename_publish}</strong>"
|
|
||||||
diff_plus_minus = ""
|
diff_plus_minus = ""
|
||||||
delta_nbsp = ""
|
delta_nbsp = ""
|
||||||
diff_files = self.git_provider.get_diff_files()
|
diff_files = self.git_provider.get_diff_files()
|
||||||
@ -662,8 +583,6 @@ class PRDescription:
|
|||||||
num_plus_lines = f.num_plus_lines
|
num_plus_lines = f.num_plus_lines
|
||||||
num_minus_lines = f.num_minus_lines
|
num_minus_lines = f.num_minus_lines
|
||||||
diff_plus_minus += f"+{num_plus_lines}/-{num_minus_lines}"
|
diff_plus_minus += f"+{num_plus_lines}/-{num_minus_lines}"
|
||||||
if len(diff_plus_minus) > 12 or diff_plus_minus == "+0/-0":
|
|
||||||
diff_plus_minus = "[link]"
|
|
||||||
delta_nbsp = " " * max(0, (8 - len(diff_plus_minus)))
|
delta_nbsp = " " * max(0, (8 - len(diff_plus_minus)))
|
||||||
break
|
break
|
||||||
|
|
||||||
@ -672,39 +591,8 @@ class PRDescription:
|
|||||||
if hasattr(self.git_provider, 'get_line_link'):
|
if hasattr(self.git_provider, 'get_line_link'):
|
||||||
filename = filename.strip()
|
filename = filename.strip()
|
||||||
link = self.git_provider.get_line_link(filename, relevant_line_start=-1)
|
link = self.git_provider.get_line_link(filename, relevant_line_start=-1)
|
||||||
if (not link or not diff_plus_minus) and ('additional files' not in filename.lower()):
|
|
||||||
get_logger().warning(f"Error getting line link for '{filename}'")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Add file data to the PR body
|
|
||||||
file_change_description_br = insert_br_after_x_chars(file_change_description, x=(delta - 5))
|
file_change_description_br = insert_br_after_x_chars(file_change_description, x=(delta - 5))
|
||||||
pr_body = self.add_file_data(delta_nbsp, diff_plus_minus, file_change_description_br, filename,
|
|
||||||
filename_publish, link, pr_body)
|
|
||||||
|
|
||||||
# Close the collapsible file list
|
|
||||||
if use_collapsible_file_list:
|
|
||||||
pr_body += """</table></details></td></tr>"""
|
|
||||||
else:
|
|
||||||
pr_body += """</table></td></tr>"""
|
|
||||||
pr_body += """</tr></tbody></table>"""
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {str(e)}")
|
|
||||||
pass
|
|
||||||
return pr_body, pr_comments
|
|
||||||
|
|
||||||
def add_file_data(self, delta_nbsp, diff_plus_minus, file_change_description_br, filename, filename_publish, link,
|
|
||||||
pr_body) -> str:
|
|
||||||
|
|
||||||
if not file_change_description_br:
|
|
||||||
pr_body += f"""
|
|
||||||
<tr>
|
|
||||||
<td>{filename_publish}</td>
|
|
||||||
<td><a href="{link}">{diff_plus_minus}</a>{delta_nbsp}</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
"""
|
|
||||||
else:
|
|
||||||
pr_body += f"""
|
pr_body += f"""
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@ -725,7 +613,17 @@ class PRDescription:
|
|||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
"""
|
"""
|
||||||
return pr_body
|
if use_collapsible_file_list:
|
||||||
|
pr_body += """</table></details></td></tr>"""
|
||||||
|
else:
|
||||||
|
pr_body += """</table></td></tr>"""
|
||||||
|
pr_body += """</tr></tbody></table>"""
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {e}")
|
||||||
|
pass
|
||||||
|
return pr_body, pr_comments
|
||||||
|
|
||||||
|
|
||||||
def count_chars_without_html(string):
|
def count_chars_without_html(string):
|
||||||
if '<' not in string:
|
if '<' not in string:
|
||||||
@ -734,14 +632,11 @@ def count_chars_without_html(string):
|
|||||||
return len(no_html_string)
|
return len(no_html_string)
|
||||||
|
|
||||||
|
|
||||||
def insert_br_after_x_chars(text: str, x=70):
|
def insert_br_after_x_chars(text, x=70):
|
||||||
"""
|
"""
|
||||||
Insert <br> into a string after a word that increases its length above x characters.
|
Insert <br> into a string after a word that increases its length above x characters.
|
||||||
Use proper HTML tags for code and new lines.
|
Use proper HTML tags for code and new lines.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not text:
|
|
||||||
return ""
|
|
||||||
if count_chars_without_html(text) < x:
|
if count_chars_without_html(text) < x:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
|||||||
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import get_user_labels, load_yaml, set_custom_labels
|
from pr_agent.algo.utils import load_yaml, set_custom_labels, get_user_labels
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
|
@ -9,10 +9,10 @@ from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
|||||||
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.pr_processing import retry_with_fallback_models
|
from pr_agent.algo.pr_processing import retry_with_fallback_models
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import ModelType, clip_tokens, load_yaml
|
from pr_agent.algo.utils import ModelType, load_yaml, clip_tokens
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import (BitbucketServerProvider, GithubProvider,
|
from pr_agent.git_providers import GithubProvider, BitbucketServerProvider, \
|
||||||
get_git_provider_with_context)
|
get_git_provider_with_context
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class PRHelpMessage:
|
|||||||
self.vars['snippets'] = docs_prompt.strip()
|
self.vars['snippets'] = docs_prompt.strip()
|
||||||
|
|
||||||
# run the AI model
|
# run the AI model
|
||||||
response = await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.WEAK)
|
response = await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.REGULAR)
|
||||||
response_yaml = load_yaml(response)
|
response_yaml = load_yaml(response)
|
||||||
response_str = response_yaml.get('response')
|
response_str = response_yaml.get('response')
|
||||||
relevant_sections = response_yaml.get('relevant_sections')
|
relevant_sections = response_yaml.get('relevant_sections')
|
||||||
@ -232,7 +232,7 @@ class PRHelpMessage:
|
|||||||
for i in range(len(tool_names)):
|
for i in range(len(tool_names)):
|
||||||
pr_comment += f"\n<tr><td align='left'>\n\n<strong>{tool_names[i]}</strong></td>\n<td>{descriptions[i]}</td>\n<td>\n\n{checkbox_list[i]}\n</td></tr>"
|
pr_comment += f"\n<tr><td align='left'>\n\n<strong>{tool_names[i]}</strong></td>\n<td>{descriptions[i]}</td>\n<td>\n\n{checkbox_list[i]}\n</td></tr>"
|
||||||
pr_comment += "</table>\n\n"
|
pr_comment += "</table>\n\n"
|
||||||
pr_comment += f"""\n\n(1) Note that each tool can be [triggered automatically](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#github-app-automatic-tools-when-a-new-pr-is-opened) when a new PR is opened, or called manually by [commenting on a PR](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#online-usage)."""
|
pr_comment += f"""\n\n(1) Note that each tool be [triggered automatically](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#github-app-automatic-tools-when-a-new-pr-is-opened) when a new PR is opened, or called manually by [commenting on a PR](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#online-usage)."""
|
||||||
pr_comment += f"""\n\n(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the `/ask` tool, you need to comment on a PR: `/ask "<question content>"`. See the relevant documentation for each tool for more details."""
|
pr_comment += f"""\n\n(2) Tools marked with [*] require additional parameters to be passed. For example, to invoke the `/ask` tool, you need to comment on a PR: `/ask "<question content>"`. See the relevant documentation for each tool for more details."""
|
||||||
elif isinstance(self.git_provider, BitbucketServerProvider):
|
elif isinstance(self.git_provider, BitbucketServerProvider):
|
||||||
# only support basic commands in BBDC
|
# only support basic commands in BBDC
|
||||||
@ -242,7 +242,7 @@ class PRHelpMessage:
|
|||||||
for i in range(len(tool_names)):
|
for i in range(len(tool_names)):
|
||||||
pr_comment += f"\n<tr><td align='left'>\n\n<strong>{tool_names[i]}</strong></td><td>{commands[i]}</td><td>{descriptions[i]}</td></tr>"
|
pr_comment += f"\n<tr><td align='left'>\n\n<strong>{tool_names[i]}</strong></td><td>{commands[i]}</td><td>{descriptions[i]}</td></tr>"
|
||||||
pr_comment += "</table>\n\n"
|
pr_comment += "</table>\n\n"
|
||||||
pr_comment += f"""\n\nNote that each tool can be [invoked automatically](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/) when a new PR is opened, or called manually by [commenting on a PR](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#online-usage)."""
|
pr_comment += f"""\n\nNote that each tool be [invoked automatically](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/) when a new PR is opened, or called manually by [commenting on a PR](https://pr-agent-docs.codium.ai/usage-guide/automations_and_usage/#online-usage)."""
|
||||||
|
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
self.git_provider.publish_comment(pr_comment)
|
self.git_provider.publish_comment(pr_comment)
|
||||||
|
79
pr_agent/tools/pr_information_from_user.py
Normal file
79
pr_agent/tools/pr_information_from_user.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import copy
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from jinja2 import Environment, StrictUndefined
|
||||||
|
|
||||||
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
||||||
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.git_providers import get_git_provider
|
||||||
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
class PRInformationFromUser:
|
||||||
|
def __init__(self, pr_url: str, args: list = None,
|
||||||
|
ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
|
||||||
|
self.git_provider = get_git_provider()(pr_url)
|
||||||
|
self.main_pr_language = get_main_pr_language(
|
||||||
|
self.git_provider.get_languages(), self.git_provider.get_files()
|
||||||
|
)
|
||||||
|
self.ai_handler = ai_handler()
|
||||||
|
self.ai_handler.main_pr_language = self.main_pr_language
|
||||||
|
|
||||||
|
self.vars = {
|
||||||
|
"title": self.git_provider.pr.title,
|
||||||
|
"branch": self.git_provider.get_pr_branch(),
|
||||||
|
"description": self.git_provider.get_pr_description(),
|
||||||
|
"language": self.main_pr_language,
|
||||||
|
"diff": "", # empty diff for initial calculation
|
||||||
|
"commit_messages_str": self.git_provider.get_commit_messages(),
|
||||||
|
}
|
||||||
|
self.token_handler = TokenHandler(self.git_provider.pr,
|
||||||
|
self.vars,
|
||||||
|
get_settings().pr_information_from_user_prompt.system,
|
||||||
|
get_settings().pr_information_from_user_prompt.user)
|
||||||
|
self.patches_diff = None
|
||||||
|
self.prediction = None
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
get_logger().info('Generating question to the user...')
|
||||||
|
if get_settings().config.publish_output:
|
||||||
|
self.git_provider.publish_comment("Preparing questions...", is_temporary=True)
|
||||||
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
|
get_logger().info('Preparing questions...')
|
||||||
|
pr_comment = self._prepare_pr_answer()
|
||||||
|
if get_settings().config.publish_output:
|
||||||
|
get_logger().info('Pushing questions...')
|
||||||
|
self.git_provider.publish_comment(pr_comment)
|
||||||
|
self.git_provider.remove_initial_comment()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
async def _prepare_prediction(self, model):
|
||||||
|
get_logger().info('Getting PR diff...')
|
||||||
|
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler, model)
|
||||||
|
get_logger().info('Getting AI prediction...')
|
||||||
|
self.prediction = await self._get_prediction(model)
|
||||||
|
|
||||||
|
async def _get_prediction(self, model: str):
|
||||||
|
variables = copy.deepcopy(self.vars)
|
||||||
|
variables["diff"] = self.patches_diff # update diff
|
||||||
|
environment = Environment(undefined=StrictUndefined)
|
||||||
|
system_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.system).render(variables)
|
||||||
|
user_prompt = environment.from_string(get_settings().pr_information_from_user_prompt.user).render(variables)
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().info(f"\nSystem prompt:\n{system_prompt}")
|
||||||
|
get_logger().info(f"\nUser prompt:\n{user_prompt}")
|
||||||
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
|
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _prepare_pr_answer(self) -> str:
|
||||||
|
model_output = self.prediction.strip()
|
||||||
|
if get_settings().config.verbosity_level >= 2:
|
||||||
|
get_logger().info(f"answer_str:\n{model_output}")
|
||||||
|
answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\
|
||||||
|
"\n>/answer\n>1) ...\n>2) ...\n>...\n"
|
||||||
|
return answer_str
|
@ -6,8 +6,8 @@ from jinja2 import Environment, StrictUndefined
|
|||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.git_patch_processing import (
|
from pr_agent.algo.git_patch_processing import convert_to_hunks_with_lines_numbers, \
|
||||||
convert_to_hunks_with_lines_numbers, extract_hunk_lines_from_patch)
|
extract_hunk_lines_from_patch
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import ModelType
|
from pr_agent.algo.utils import ModelType
|
||||||
@ -79,17 +79,13 @@ class PR_LineQuestions:
|
|||||||
line_end=line_end,
|
line_end=line_end,
|
||||||
side=side)
|
side=side)
|
||||||
if self.patch_with_lines:
|
if self.patch_with_lines:
|
||||||
model_answer = await retry_with_fallback_models(self._get_prediction, model_type=ModelType.WEAK)
|
response = await retry_with_fallback_models(self._get_prediction, model_type=ModelType.TURBO)
|
||||||
# sanitize the answer so that no line will start with "/"
|
|
||||||
model_answer_sanitized = model_answer.strip().replace("\n/", "\n /")
|
|
||||||
if model_answer_sanitized.startswith("/"):
|
|
||||||
model_answer_sanitized = " " + model_answer_sanitized
|
|
||||||
|
|
||||||
get_logger().info('Preparing answer...')
|
get_logger().info('Preparing answer...')
|
||||||
if comment_id:
|
if comment_id:
|
||||||
self.git_provider.reply_to_comment_from_comment_id(comment_id, model_answer_sanitized)
|
self.git_provider.reply_to_comment_from_comment_id(comment_id, response)
|
||||||
else:
|
else:
|
||||||
self.git_provider.publish_comment(model_answer_sanitized)
|
self.git_provider.publish_comment(response)
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class PRQuestions:
|
|||||||
if img_path:
|
if img_path:
|
||||||
get_logger().debug(f"Image path identified", artifact=img_path)
|
get_logger().debug(f"Image path identified", artifact=img_path)
|
||||||
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.WEAK)
|
await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.TURBO)
|
||||||
|
|
||||||
pr_comment = self._prepare_pr_answer()
|
pr_comment = self._prepare_pr_answer()
|
||||||
get_logger().debug(f"PR output", artifact=pr_comment)
|
get_logger().debug(f"PR output", artifact=pr_comment)
|
||||||
@ -117,16 +117,6 @@ class PRQuestions:
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
def _prepare_pr_answer(self) -> str:
|
def _prepare_pr_answer(self) -> str:
|
||||||
model_answer = self.prediction.strip()
|
|
||||||
# sanitize the answer so that no line will start with "/"
|
|
||||||
model_answer_sanitized = model_answer.replace("\n/", "\n /")
|
|
||||||
if model_answer_sanitized.startswith("/"):
|
|
||||||
model_answer_sanitized = " " + model_answer_sanitized
|
|
||||||
if model_answer_sanitized != model_answer:
|
|
||||||
get_logger().debug(f"Sanitized model answer",
|
|
||||||
artifact={"model_answer": model_answer, "sanitized_answer": model_answer_sanitized})
|
|
||||||
|
|
||||||
|
|
||||||
answer_str = f"### **Ask**❓\n{self.question_str}\n\n"
|
answer_str = f"### **Ask**❓\n{self.question_str}\n\n"
|
||||||
answer_str += f"### **Answer:**\n{model_answer_sanitized}\n\n"
|
answer_str += f"### **Answer:**\n{self.prediction.strip()}\n\n"
|
||||||
return answer_str
|
return answer_str
|
||||||
|
@ -4,27 +4,19 @@ import traceback
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.pr_processing import (add_ai_metadata_to_diff_files,
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models, add_ai_metadata_to_diff_files
|
||||||
get_pr_diff,
|
|
||||||
retry_with_fallback_models)
|
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import (ModelType, PRReviewHeader,
|
from pr_agent.algo.utils import github_action_output, load_yaml, ModelType, \
|
||||||
convert_to_markdown_v2, github_action_output,
|
show_relevant_configurations, convert_to_markdown_v2, PRReviewHeader
|
||||||
load_yaml, show_relevant_configurations)
|
|
||||||
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, get_git_provider_with_context
|
||||||
get_git_provider_with_context)
|
from pr_agent.git_providers.git_provider import IncrementalPR, get_main_pr_language
|
||||||
from pr_agent.git_providers.git_provider import (IncrementalPR,
|
|
||||||
get_main_pr_language)
|
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
from pr_agent.servers.help import HelpMessage
|
from pr_agent.servers.help import HelpMessage
|
||||||
from pr_agent.tools.ticket_pr_compliance_check import (
|
from pr_agent.tools.ticket_pr_compliance_check import extract_tickets, extract_and_cache_pr_tickets
|
||||||
extract_and_cache_pr_tickets, extract_tickets)
|
|
||||||
|
|
||||||
|
|
||||||
class PRReviewer:
|
class PRReviewer:
|
||||||
@ -86,6 +78,7 @@ class PRReviewer:
|
|||||||
"require_estimate_effort_to_review": get_settings().pr_reviewer.require_estimate_effort_to_review,
|
"require_estimate_effort_to_review": get_settings().pr_reviewer.require_estimate_effort_to_review,
|
||||||
'require_can_be_split_review': get_settings().pr_reviewer.require_can_be_split_review,
|
'require_can_be_split_review': get_settings().pr_reviewer.require_can_be_split_review,
|
||||||
'require_security_review': get_settings().pr_reviewer.require_security_review,
|
'require_security_review': get_settings().pr_reviewer.require_security_review,
|
||||||
|
'num_code_suggestions': get_settings().pr_reviewer.num_code_suggestions,
|
||||||
'question_str': question_str,
|
'question_str': question_str,
|
||||||
'answer_str': answer_str,
|
'answer_str': answer_str,
|
||||||
"extra_instructions": get_settings().pr_reviewer.extra_instructions,
|
"extra_instructions": get_settings().pr_reviewer.extra_instructions,
|
||||||
@ -94,7 +87,6 @@ class PRReviewer:
|
|||||||
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
"enable_custom_labels": get_settings().config.enable_custom_labels,
|
||||||
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False),
|
"is_ai_metadata": get_settings().get("config.enable_ai_metadata", False),
|
||||||
"related_tickets": get_settings().get('related_tickets', []),
|
"related_tickets": get_settings().get('related_tickets', []),
|
||||||
'duplicate_prompt_examples': get_settings().config.get('duplicate_prompt_examples', False),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.token_handler = TokenHandler(
|
self.token_handler = TokenHandler(
|
||||||
@ -148,7 +140,7 @@ class PRReviewer:
|
|||||||
if get_settings().config.publish_output and not get_settings().config.get('is_auto_command', False):
|
if get_settings().config.publish_output and not get_settings().config.get('is_auto_command', False):
|
||||||
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing review...", is_temporary=True)
|
||||||
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.REGULAR)
|
await retry_with_fallback_models(self._prepare_prediction)
|
||||||
if not self.prediction:
|
if not self.prediction:
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
return None
|
return None
|
||||||
@ -168,10 +160,8 @@ class PRReviewer:
|
|||||||
self.git_provider.publish_comment(pr_review)
|
self.git_provider.publish_comment(pr_review)
|
||||||
|
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
else:
|
if get_settings().pr_reviewer.inline_code_comments:
|
||||||
get_logger().info("Review output is not published")
|
self._publish_inline_code_comments()
|
||||||
get_settings().data = {"artifact": pr_review}
|
|
||||||
return
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to review PR: {e}")
|
get_logger().error(f"Failed to review PR: {e}")
|
||||||
|
|
||||||
@ -233,6 +223,33 @@ class PRReviewer:
|
|||||||
key_issues_to_review = data['review'].pop('key_issues_to_review')
|
key_issues_to_review = data['review'].pop('key_issues_to_review')
|
||||||
data['review']['key_issues_to_review'] = key_issues_to_review
|
data['review']['key_issues_to_review'] = key_issues_to_review
|
||||||
|
|
||||||
|
if 'code_feedback' in data:
|
||||||
|
code_feedback = data['code_feedback']
|
||||||
|
|
||||||
|
# Filter out code suggestions that can be submitted as inline comments
|
||||||
|
if get_settings().pr_reviewer.inline_code_comments:
|
||||||
|
del data['code_feedback']
|
||||||
|
else:
|
||||||
|
for suggestion in code_feedback:
|
||||||
|
if ('relevant_file' in suggestion) and (not suggestion['relevant_file'].startswith('``')):
|
||||||
|
suggestion['relevant_file'] = f"``{suggestion['relevant_file']}``"
|
||||||
|
|
||||||
|
if 'relevant_line' not in suggestion:
|
||||||
|
suggestion['relevant_line'] = ''
|
||||||
|
|
||||||
|
relevant_line_str = suggestion['relevant_line'].split('\n')[0]
|
||||||
|
|
||||||
|
# removing '+'
|
||||||
|
suggestion['relevant_line'] = relevant_line_str.lstrip('+').strip()
|
||||||
|
|
||||||
|
# try to add line numbers link to code suggestions
|
||||||
|
if hasattr(self.git_provider, 'generate_link_to_relevant_line_number'):
|
||||||
|
link = self.git_provider.generate_link_to_relevant_line_number(suggestion)
|
||||||
|
if link:
|
||||||
|
suggestion['relevant_line'] = f"[{suggestion['relevant_line']}]({link})"
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
incremental_review_markdown_text = None
|
incremental_review_markdown_text = None
|
||||||
# Add incremental review section
|
# Add incremental review section
|
||||||
if self.incremental.is_incremental:
|
if self.incremental.is_incremental:
|
||||||
@ -241,9 +258,7 @@ class PRReviewer:
|
|||||||
incremental_review_markdown_text = f"Starting from commit {last_commit_url}"
|
incremental_review_markdown_text = f"Starting from commit {last_commit_url}"
|
||||||
|
|
||||||
markdown_text = convert_to_markdown_v2(data, self.git_provider.is_supported("gfm_markdown"),
|
markdown_text = convert_to_markdown_v2(data, self.git_provider.is_supported("gfm_markdown"),
|
||||||
incremental_review_markdown_text,
|
incremental_review_markdown_text, git_provider=self.git_provider)
|
||||||
git_provider=self.git_provider,
|
|
||||||
files=self.git_provider.get_diff_files())
|
|
||||||
|
|
||||||
# Add help text if gfm_markdown is supported
|
# Add help text if gfm_markdown is supported
|
||||||
if self.git_provider.is_supported("gfm_markdown") and get_settings().pr_reviewer.enable_help_text:
|
if self.git_provider.is_supported("gfm_markdown") and get_settings().pr_reviewer.enable_help_text:
|
||||||
@ -263,6 +278,38 @@ class PRReviewer:
|
|||||||
|
|
||||||
return markdown_text
|
return markdown_text
|
||||||
|
|
||||||
|
def _publish_inline_code_comments(self) -> None:
|
||||||
|
"""
|
||||||
|
Publishes inline comments on a pull request with code suggestions generated by the AI model.
|
||||||
|
"""
|
||||||
|
if get_settings().pr_reviewer.num_code_suggestions == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
first_key = 'review'
|
||||||
|
last_key = 'security_concerns'
|
||||||
|
data = load_yaml(self.prediction.strip(),
|
||||||
|
keys_fix_yaml=["ticket_compliance_check", "estimated_effort_to_review_[1-5]:", "security_concerns:", "key_issues_to_review:",
|
||||||
|
"relevant_file:", "relevant_line:", "suggestion:"],
|
||||||
|
first_key=first_key, last_key=last_key)
|
||||||
|
comments: List[str] = []
|
||||||
|
for suggestion in data.get('code_feedback', []):
|
||||||
|
relevant_file = suggestion.get('relevant_file', '').strip()
|
||||||
|
relevant_line_in_file = suggestion.get('relevant_line', '').strip()
|
||||||
|
content = suggestion.get('suggestion', '')
|
||||||
|
if not relevant_file or not relevant_line_in_file or not content:
|
||||||
|
get_logger().info("Skipping inline comment with missing file/line/content")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.git_provider.is_supported("create_inline_comment"):
|
||||||
|
comment = self.git_provider.create_inline_comment(content, relevant_file, relevant_line_in_file)
|
||||||
|
if comment:
|
||||||
|
comments.append(comment)
|
||||||
|
else:
|
||||||
|
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file, suggestion)
|
||||||
|
|
||||||
|
if comments:
|
||||||
|
self.git_provider.publish_inline_comments(comments)
|
||||||
|
|
||||||
def _get_user_answers(self) -> Tuple[str, str]:
|
def _get_user_answers(self) -> Tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Retrieves the question and answer strings from the discussion messages related to a pull request.
|
Retrieves the question and answer strings from the discussion messages related to a pull request.
|
||||||
|
@ -34,9 +34,9 @@ class PRSimilarIssue:
|
|||||||
|
|
||||||
if get_settings().pr_similar_issue.vectordb == "pinecone":
|
if get_settings().pr_similar_issue.vectordb == "pinecone":
|
||||||
try:
|
try:
|
||||||
import pandas as pd
|
|
||||||
import pinecone
|
import pinecone
|
||||||
from pinecone_datasets import Dataset, DatasetMetadata
|
from pinecone_datasets import Dataset, DatasetMetadata
|
||||||
|
import pandas as pd
|
||||||
except:
|
except:
|
||||||
raise Exception("Please install 'pinecone' and 'pinecone_datasets' to use pinecone as vectordb")
|
raise Exception("Please install 'pinecone' and 'pinecone_datasets' to use pinecone as vectordb")
|
||||||
# assuming pinecone api key and environment are set in secrets file
|
# assuming pinecone api key and environment are set in secrets file
|
||||||
|
@ -3,16 +3,14 @@ from datetime import date
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from jinja2 import Environment, StrictUndefined
|
from jinja2 import Environment, StrictUndefined
|
||||||
|
|
||||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||||
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
from pr_agent.algo.ai_handlers.litellm_ai_handler import LiteLLMAIHandler
|
||||||
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
from pr_agent.algo.pr_processing import get_pr_diff, retry_with_fallback_models
|
||||||
from pr_agent.algo.token_handler import TokenHandler
|
from pr_agent.algo.token_handler import TokenHandler
|
||||||
from pr_agent.algo.utils import ModelType, show_relevant_configurations
|
from pr_agent.algo.utils import ModelType, show_relevant_configurations
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import GithubProvider, get_git_provider
|
from pr_agent.git_providers import get_git_provider, GithubProvider
|
||||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
@ -41,7 +39,6 @@ class PRUpdateChangelog:
|
|||||||
"description": self.git_provider.get_pr_description(),
|
"description": self.git_provider.get_pr_description(),
|
||||||
"language": self.main_language,
|
"language": self.main_language,
|
||||||
"diff": "", # empty diff for initial calculation
|
"diff": "", # empty diff for initial calculation
|
||||||
"pr_link": "",
|
|
||||||
"changelog_file_str": self.changelog_file_str,
|
"changelog_file_str": self.changelog_file_str,
|
||||||
"today": date.today(),
|
"today": date.today(),
|
||||||
"extra_instructions": get_settings().pr_update_changelog.extra_instructions,
|
"extra_instructions": get_settings().pr_update_changelog.extra_instructions,
|
||||||
@ -74,7 +71,7 @@ class PRUpdateChangelog:
|
|||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
self.git_provider.publish_comment("Preparing changelog updates...", is_temporary=True)
|
self.git_provider.publish_comment("Preparing changelog updates...", is_temporary=True)
|
||||||
|
|
||||||
await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.WEAK)
|
await retry_with_fallback_models(self._prepare_prediction, model_type=ModelType.TURBO)
|
||||||
|
|
||||||
new_file_content, answer = self._prepare_changelog_update()
|
new_file_content, answer = self._prepare_changelog_update()
|
||||||
|
|
||||||
@ -103,23 +100,12 @@ class PRUpdateChangelog:
|
|||||||
async def _get_prediction(self, model: str):
|
async def _get_prediction(self, model: str):
|
||||||
variables = copy.deepcopy(self.vars)
|
variables = copy.deepcopy(self.vars)
|
||||||
variables["diff"] = self.patches_diff # update diff
|
variables["diff"] = self.patches_diff # update diff
|
||||||
if get_settings().pr_update_changelog.add_pr_link:
|
|
||||||
variables["pr_link"] = self.git_provider.get_pr_url()
|
|
||||||
environment = Environment(undefined=StrictUndefined)
|
environment = Environment(undefined=StrictUndefined)
|
||||||
system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables)
|
system_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.system).render(variables)
|
||||||
user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables)
|
user_prompt = environment.from_string(get_settings().pr_update_changelog_prompt.user).render(variables)
|
||||||
response, finish_reason = await self.ai_handler.chat_completion(
|
response, finish_reason = await self.ai_handler.chat_completion(
|
||||||
model=model, system=system_prompt, user=user_prompt, temperature=get_settings().config.temperature)
|
model=model, system=system_prompt, user=user_prompt, temperature=get_settings().config.temperature)
|
||||||
|
|
||||||
# post-process the response
|
|
||||||
response = response.strip()
|
|
||||||
if not response:
|
|
||||||
return ""
|
|
||||||
if response.startswith("```"):
|
|
||||||
response_lines = response.splitlines()
|
|
||||||
response_lines = response_lines[1:]
|
|
||||||
response = "\n".join(response_lines)
|
|
||||||
response = response.strip("`")
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _prepare_changelog_update(self) -> Tuple[str, str]:
|
def _prepare_changelog_update(self) -> Tuple[str, str]:
|
||||||
|
@ -108,7 +108,7 @@ async def extract_tickets(git_provider):
|
|||||||
|
|
||||||
|
|
||||||
async def extract_and_cache_pr_tickets(git_provider, vars):
|
async def extract_and_cache_pr_tickets(git_provider, vars):
|
||||||
if not get_settings().get('pr_reviewer.require_ticket_analysis_review', False):
|
if get_settings().get('config.require_ticket_analysis_review', False):
|
||||||
return
|
return
|
||||||
related_tickets = get_settings().get('related_tickets', [])
|
related_tickets = get_settings().get('related_tickets', [])
|
||||||
if not related_tickets:
|
if not related_tickets:
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pr-agent"
|
name = "pr-agent"
|
||||||
version = "0.2.5"
|
version = "0.2.4"
|
||||||
|
|
||||||
authors = [{name= "CodiumAI", email = "tal.r@codium.ai"}]
|
authors = [{name= "CodiumAI", email = "tal.r@codium.ai"}]
|
||||||
|
|
||||||
@ -40,17 +40,17 @@ license-files = ["LICENSE"]
|
|||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
include = [
|
include = ["pr_agent*"] # include pr_agent and any sub-packages it finds under it.
|
||||||
"pr_agent*",
|
|
||||||
] # include pr_agent and any sub-packages it finds under it.
|
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
pr-agent = "pr_agent.cli:run"
|
pr-agent = "pr_agent.cli:run"
|
||||||
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
lint.select = [
|
select = [
|
||||||
"E", # Pyflakes
|
"E", # Pyflakes
|
||||||
"F", # Pyflakes
|
"F", # Pyflakes
|
||||||
"B", # flake8-bugbear
|
"B", # flake8-bugbear
|
||||||
@ -59,24 +59,22 @@ lint.select = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# First commit - only fixing isort
|
# First commit - only fixing isort
|
||||||
lint.fixable = [
|
fixable = [
|
||||||
"I001", # isort basic checks
|
"I001", # isort basic checks
|
||||||
]
|
]
|
||||||
|
|
||||||
lint.unfixable = [
|
unfixable = [
|
||||||
"B", # Avoid trying to fix flake8-bugbear (`B`) violations.
|
"B", # Avoid trying to fix flake8-bugbear (`B`) violations.
|
||||||
]
|
]
|
||||||
|
|
||||||
lint.exclude = ["api/code_completions"]
|
exclude = [
|
||||||
|
"api/code_completions",
|
||||||
|
]
|
||||||
|
|
||||||
lint.ignore = ["E999", "B008"]
|
ignore = [
|
||||||
|
"E999", "B008"
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.per-file-ignores]
|
||||||
"__init__.py" = [
|
"__init__.py" = ["E402"] # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
|
||||||
"E402",
|
# TODO: should decide if maybe not to ignore these.
|
||||||
] # Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
|
|
||||||
|
|
||||||
[tool.bandit]
|
|
||||||
exclude_dirs = ["tests"]
|
|
||||||
skips = ["B101"]
|
|
||||||
tests = []
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
pytest==7.4.0
|
pytest==7.4.0
|
||||||
poetry
|
poetry
|
||||||
twine
|
twine
|
||||||
pre-commit>=4,<5
|
|
||||||
|
@ -1,28 +1,26 @@
|
|||||||
aiohttp==3.9.5
|
aiohttp==3.9.5
|
||||||
anthropic[vertex]==0.39.0
|
anthropic[vertex]==0.37.1
|
||||||
atlassian-python-api==3.41.4
|
atlassian-python-api==3.41.4
|
||||||
azure-devops==7.1.0b3
|
azure-devops==7.1.0b3
|
||||||
azure-identity==1.15.0
|
azure-identity==1.15.0
|
||||||
boto3==1.33.6
|
boto3==1.33.6
|
||||||
certifi==2024.8.30
|
|
||||||
dynaconf==3.2.4
|
dynaconf==3.2.4
|
||||||
fastapi==0.111.0
|
fastapi==0.111.0
|
||||||
GitPython==3.1.41
|
GitPython==3.1.41
|
||||||
google-cloud-aiplatform==1.38.0
|
google-cloud-aiplatform==1.38.0
|
||||||
google-generativeai==0.8.3
|
|
||||||
google-cloud-storage==2.10.0
|
google-cloud-storage==2.10.0
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
litellm==1.52.12
|
litellm==1.50.2
|
||||||
loguru==0.7.2
|
loguru==0.7.2
|
||||||
msrest==0.7.1
|
msrest==0.7.1
|
||||||
openai==1.55.3
|
openai==1.52.1
|
||||||
pytest==7.4.0
|
pytest==7.4.0
|
||||||
PyGithub==1.59.*
|
PyGithub==1.59.*
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
python-gitlab==3.15.0
|
python-gitlab==3.15.0
|
||||||
retry==0.9.2
|
retry==0.9.2
|
||||||
starlette-context==0.3.6
|
starlette-context==0.3.6
|
||||||
tiktoken==0.8.0
|
tiktoken==0.7.0
|
||||||
ujson==5.8.0
|
ujson==5.8.0
|
||||||
uvicorn==0.22.0
|
uvicorn==0.22.0
|
||||||
tenacity==8.2.3
|
tenacity==8.2.3
|
||||||
|
1
setup.py
1
setup.py
@ -3,3 +3,4 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
print("aaa")
|
||||||
|
@ -32,3 +32,4 @@ def main():
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -5,16 +5,16 @@ import time
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
import requests
|
|
||||||
from atlassian.bitbucket import Cloud
|
from atlassian.bitbucket import Cloud
|
||||||
|
|
||||||
|
import requests
|
||||||
from requests.auth import HTTPBasicAuth
|
from requests.auth import HTTPBasicAuth
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.log import get_logger, setup_logger
|
from pr_agent.log import setup_logger, get_logger
|
||||||
from tests.e2e_tests.e2e_utils import (FILE_PATH,
|
from tests.e2e_tests.e2e_utils import NEW_FILE_CONTENT, FILE_PATH, PR_HEADER_START_WITH, REVIEW_START_WITH, \
|
||||||
IMPROVE_START_WITH_REGEX_PATTERN,
|
IMPROVE_START_WITH_REGEX_PATTERN, NUM_MINUTES
|
||||||
NEW_FILE_CONTENT, NUM_MINUTES,
|
|
||||||
PR_HEADER_START_WITH, REVIEW_START_WITH)
|
|
||||||
|
|
||||||
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
||||||
setup_logger(log_level)
|
setup_logger(log_level)
|
||||||
|
@ -5,11 +5,9 @@ from datetime import datetime
|
|||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.log import get_logger, setup_logger
|
from pr_agent.log import setup_logger, get_logger
|
||||||
from tests.e2e_tests.e2e_utils import (FILE_PATH,
|
from tests.e2e_tests.e2e_utils import NEW_FILE_CONTENT, FILE_PATH, PR_HEADER_START_WITH, REVIEW_START_WITH, \
|
||||||
IMPROVE_START_WITH_REGEX_PATTERN,
|
IMPROVE_START_WITH_REGEX_PATTERN, NUM_MINUTES
|
||||||
NEW_FILE_CONTENT, NUM_MINUTES,
|
|
||||||
PR_HEADER_START_WITH, REVIEW_START_WITH)
|
|
||||||
|
|
||||||
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
log_level = os.environ.get("LOG_LEVEL", "INFO")
|
||||||
setup_logger(log_level)
|
setup_logger(log_level)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user