mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-21 04:50:39 +08:00
Compare commits
72 Commits
dbf96ff749
...
main
Author | SHA1 | Date | |
---|---|---|---|
1663eaad4a | |||
0ee115e19c | |||
aaba9b6b3c | |||
93aaa59b2d | |||
f1c068bc44 | |||
fffbee5b34 | |||
5e555d09c7 | |||
7f95e39361 | |||
730fa66594 | |||
f42dc28a55 | |||
7251e6df96 | |||
b01a2b5f4a | |||
65e71cb2ee | |||
9773afe155 | |||
0a8a263809 | |||
597f553dd5 | |||
4b6fcfe60e | |||
7cc4206b70 | |||
8906a81a2e | |||
6179eeca58 | |||
e8c73e7baa | |||
754d47f187 | |||
bec70dc96a | |||
fd32c83c29 | |||
7efeeb1de8 | |||
d7d4b7de89 | |||
2a37225574 | |||
e87fdd0ab5 | |||
c0d7fd8c36 | |||
5933280417 | |||
8e0c5c8784 | |||
0e9cf274ef | |||
3aae48f09c | |||
c4dd07b3b8 | |||
8c7680d85d | |||
11fb6ccc7e | |||
3aaa727e05 | |||
6108f96bff | |||
5a00897cbe | |||
e12b27879c | |||
fac2141df3 | |||
1dbfd27d8e | |||
eaeee97535 | |||
71bbc52a99 | |||
4a8e9b79e8 | |||
efdb0f5744 | |||
28750c70e0 | |||
583ed10dca | |||
07d71f2d25 | |||
447a384aee | |||
d9eb0367cf | |||
85484899c3 | |||
00b5815785 | |||
9becad2eaf | |||
74df3f8bd5 | |||
4ab97d8969 | |||
6057812a20 | |||
598e2c731b | |||
0742d8052f | |||
1713cded21 | |||
e7268dd314 | |||
50c2578cfd | |||
5a56d11e16 | |||
31e25a5965 | |||
85e1e2d4ee | |||
2d8bee0d6d | |||
e0d7083768 | |||
5e82d0a316 | |||
e2d71acb9d | |||
8127d52ab3 | |||
6a55bbcd23 | |||
12af211c13 |
28
README.md
28
README.md
@ -119,19 +119,20 @@ Here are some advantages of PR-Agent:
|
||||
|
||||
PR-Agent and Qodo Merge offer comprehensive pull request functionalities integrated with various git providers:
|
||||
|
||||
| | | GitHub | GitLab | Bitbucket | Azure DevOps | Gitea |
|
||||
|---------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|:-----:|
|
||||
| [TOOLS](https://qodo-merge-docs.qodo.ai/tools/) | [Describe](https://qodo-merge-docs.qodo.ai/tools/describe/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Review](https://qodo-merge-docs.qodo.ai/tools/review/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Improve](https://qodo-merge-docs.qodo.ai/tools/improve/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Ask](https://qodo-merge-docs.qodo.ai/tools/ask/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | ⮑ [Ask on code lines](https://qodo-merge-docs.qodo.ai/tools/ask/#ask-lines) | ✅ | ✅ | | | |
|
||||
| | [Help Docs](https://qodo-merge-docs.qodo.ai/tools/help_docs/?h=auto#auto-approval) | ✅ | ✅ | ✅ | | |
|
||||
| | [Update CHANGELOG](https://qodo-merge-docs.qodo.ai/tools/update_changelog/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Add Documentation](https://qodo-merge-docs.qodo.ai/tools/documentation/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Analyze](https://qodo-merge-docs.qodo.ai/tools/analyze/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Auto-Approve](https://qodo-merge-docs.qodo.ai/tools/improve/?h=auto#auto-approval) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [CI Feedback](https://qodo-merge-docs.qodo.ai/tools/ci_feedback/) 💎 | ✅ | | | | |
|
||||
| | | GitHub | GitLab | Bitbucket | Azure DevOps | Gitea |
|
||||
|---------------------------------------------------------|----------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|:-----:|
|
||||
| [TOOLS](https://qodo-merge-docs.qodo.ai/tools/) | [Describe](https://qodo-merge-docs.qodo.ai/tools/describe/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Review](https://qodo-merge-docs.qodo.ai/tools/review/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Improve](https://qodo-merge-docs.qodo.ai/tools/improve/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Ask](https://qodo-merge-docs.qodo.ai/tools/ask/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | ⮑ [Ask on code lines](https://qodo-merge-docs.qodo.ai/tools/ask/#ask-lines) | ✅ | ✅ | | | |
|
||||
| | [Help Docs](https://qodo-merge-docs.qodo.ai/tools/help_docs/?h=auto#auto-approval) | ✅ | ✅ | ✅ | | |
|
||||
| | [Update CHANGELOG](https://qodo-merge-docs.qodo.ai/tools/update_changelog/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Add Documentation](https://qodo-merge-docs.qodo.ai/tools/documentation/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Analyze](https://qodo-merge-docs.qodo.ai/tools/analyze/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Auto-Approve](https://qodo-merge-docs.qodo.ai/tools/improve/?h=auto#auto-approval) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [CI Feedback](https://qodo-merge-docs.qodo.ai/tools/ci_feedback/) 💎 | ✅ | | | | |
|
||||
| | [Compliance](https://qodo-merge-docs.qodo.ai/tools/compliance/) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [Custom Prompt](https://qodo-merge-docs.qodo.ai/tools/custom_prompt/) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [Generate Custom Labels](https://qodo-merge-docs.qodo.ai/tools/custom_labels/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Generate Tests](https://qodo-merge-docs.qodo.ai/tools/test/) 💎 | ✅ | ✅ | | | |
|
||||
@ -141,6 +142,7 @@ PR-Agent and Qodo Merge offer comprehensive pull request functionalities integra
|
||||
| | [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) 💎 | ✅ | | | | |
|
||||
| | [PR to Ticket](https://qodo-merge-docs.qodo.ai/tools/pr_to_ticket/) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [Suggestion Tracking](https://qodo-merge-docs.qodo.ai/tools/improve/#suggestion-tracking) 💎 | ✅ | ✅ | | | |
|
||||
| | | | | | | |
|
||||
| [USAGE](https://qodo-merge-docs.qodo.ai/usage-guide/) | [CLI](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#local-repo-cli) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
|
@ -104,7 +104,7 @@ Installation steps:
|
||||
2. Click on the Connect **Jira Cloud** button to connect the Jira Cloud app
|
||||
|
||||
3. Click the `accept` button.<br>
|
||||
{width=384}
|
||||
{width=384}
|
||||
|
||||
4. After installing the app, you will be redirected to the Qodo Merge registration page. and you will see a success message.<br>
|
||||
{width=384}
|
||||
|
@ -28,49 +28,51 @@ PR-Agent and Qodo Merge offer comprehensive pull request functionalities integra
|
||||
|
||||
| | | GitHub | GitLab | Bitbucket | Azure DevOps | Gitea |
|
||||
| ----- |---------------------------------------------------------------------------------------------------------------------|:------:|:------:|:---------:|:------------:|:-----:|
|
||||
| [TOOLS](https://qodo-merge-docs.qodo.ai/tools/) | [Describe](https://qodo-merge-docs.qodo.ai/tools/describe/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Review](https://qodo-merge-docs.qodo.ai/tools/review/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Improve](https://qodo-merge-docs.qodo.ai/tools/improve/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Ask](https://qodo-merge-docs.qodo.ai/tools/ask/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| [TOOLS](https://qodo-merge-docs.qodo.ai/tools/) | [Describe](https://qodo-merge-docs.qodo.ai/tools/describe/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Review](https://qodo-merge-docs.qodo.ai/tools/review/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Improve](https://qodo-merge-docs.qodo.ai/tools/improve/) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [Ask](https://qodo-merge-docs.qodo.ai/tools/ask/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | ⮑ [Ask on code lines](https://qodo-merge-docs.qodo.ai/tools/ask/#ask-lines) | ✅ | ✅ | | | |
|
||||
| | [Help Docs](https://qodo-merge-docs.qodo.ai/tools/help_docs/?h=auto#auto-approval) | ✅ | ✅ | ✅ | | |
|
||||
| | [Update CHANGELOG](https://qodo-merge-docs.qodo.ai/tools/update_changelog/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Update CHANGELOG](https://qodo-merge-docs.qodo.ai/tools/update_changelog/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Add Documentation](https://qodo-merge-docs.qodo.ai/tools/documentation/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Analyze](https://qodo-merge-docs.qodo.ai/tools/analyze/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Auto-Approve](https://qodo-merge-docs.qodo.ai/tools/improve/?h=auto#auto-approval) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [CI Feedback](https://qodo-merge-docs.qodo.ai/tools/ci_feedback/) 💎 | ✅ | | | | |
|
||||
| | [Compliance](https://qodo-merge-docs.qodo.ai/tools/compliance/) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [Custom Prompt](https://qodo-merge-docs.qodo.ai/tools/custom_prompt/) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [Generate Custom Labels](https://qodo-merge-docs.qodo.ai/tools/custom_labels/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Generate Tests](https://qodo-merge-docs.qodo.ai/tools/test/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Implement](https://qodo-merge-docs.qodo.ai/tools/implement/) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [PR Chat](https://qodo-merge-docs.qodo.ai/chrome-extension/features/#pr-chat) 💎 | ✅ | | | | |
|
||||
| | [PR to Ticket](https://qodo-merge-docs.qodo.ai/tools/pr_to_ticket/) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [Scan Repo Discussions](https://qodo-merge-docs.qodo.ai/tools/scan_repo_discussions/) 💎 | ✅ | | | | |
|
||||
| | [Similar Code](https://qodo-merge-docs.qodo.ai/tools/similar_code/) 💎 | ✅ | | | | |
|
||||
| | [Suggestion Tracking](https://qodo-merge-docs.qodo.ai/tools/improve/#suggestion-tracking) 💎 | ✅ | ✅ | | | |
|
||||
| | [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) 💎 | ✅ | ✅ | | | |
|
||||
| | | | | | | |
|
||||
| [USAGE](https://qodo-merge-docs.qodo.ai/usage-guide/) | [CLI](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#local-repo-cli) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [App / webhook](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-app) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| [USAGE](https://qodo-merge-docs.qodo.ai/usage-guide/) | [CLI](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#local-repo-cli) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [App / webhook](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-app) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| | [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) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Actions](https://qodo-merge-docs.qodo.ai/installation/github/#run-as-a-github-action) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | | | | | | |
|
||||
| [CORE](https://qodo-merge-docs.qodo.ai/core-abilities/) | [Adaptive and token-aware file patch fitting](https://qodo-merge-docs.qodo.ai/core-abilities/compression_strategy/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| [CORE](https://qodo-merge-docs.qodo.ai/core-abilities/) | [Adaptive and token-aware file patch fitting](https://qodo-merge-docs.qodo.ai/core-abilities/compression_strategy/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Auto Best Practices 💎](https://qodo-merge-docs.qodo.ai/core-abilities/auto_best_practices/) | ✅ | | | | |
|
||||
| | [Chat on code suggestions](https://qodo-merge-docs.qodo.ai/core-abilities/chat_on_code_suggestions/) | ✅ | ✅ | | | |
|
||||
| | [Code Validation 💎](https://qodo-merge-docs.qodo.ai/core-abilities/code_validation/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Dynamic context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic_context/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Code Validation 💎](https://qodo-merge-docs.qodo.ai/core-abilities/code_validation/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Dynamic context](https://qodo-merge-docs.qodo.ai/core-abilities/dynamic_context/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Fetching ticket context](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/) | ✅ | ✅ | ✅ | | |
|
||||
| | [Global and wiki configurations](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/) 💎 | ✅ | ✅ | ✅ | | |
|
||||
| | [Impact Evaluation](https://qodo-merge-docs.qodo.ai/core-abilities/impact_evaluation/) 💎 | ✅ | ✅ | | | |
|
||||
| | [Incremental Update 💎](https://qodo-merge-docs.qodo.ai/core-abilities/incremental_update/) | ✅ | | | | |
|
||||
| | [Interactivity](https://qodo-merge-docs.qodo.ai/core-abilities/interactivity/) | ✅ | ✅ | | | |
|
||||
| | [Local and global metadata](https://qodo-merge-docs.qodo.ai/core-abilities/metadata/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Multiple models support](https://qodo-merge-docs.qodo.ai/usage-guide/changing_a_model/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [PR compression](https://qodo-merge-docs.qodo.ai/core-abilities/compression_strategy/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Local and global metadata](https://qodo-merge-docs.qodo.ai/core-abilities/metadata/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [Multiple models support](https://qodo-merge-docs.qodo.ai/usage-guide/changing_a_model/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [PR compression](https://qodo-merge-docs.qodo.ai/core-abilities/compression_strategy/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [PR interactive actions](https://www.qodo.ai/images/pr_agent/pr-actions.mp4) 💎 | ✅ | ✅ | | | |
|
||||
| | [RAG context enrichment](https://qodo-merge-docs.qodo.ai/core-abilities/rag_context_enrichment/) | ✅ | | ✅ | | |
|
||||
| | [Self reflection](https://qodo-merge-docs.qodo.ai/core-abilities/self_reflection/) | ✅ | ✅ | ✅ | ✅ | |
|
||||
| | [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/) 💎 | ✅ | ✅ | | | |
|
||||
!!! note "💎 means Qodo Merge only"
|
||||
All along the documentation, 💎 marks a feature available only in [Qodo Merge](https://www.codium.ai/pricing/){:target="_blank"}, and not in the open-source version.
|
||||
|
@ -51,6 +51,430 @@ When you open your next PR, you should see a comment from `github-actions` bot w
|
||||
|
||||
See detailed usage instructions in the [USAGE GUIDE](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-action)
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
This section provides detailed, step-by-step examples for configuring PR-Agent with different models and advanced options in GitHub Actions.
|
||||
|
||||
### Quick Start Examples
|
||||
|
||||
#### Basic Setup (OpenAI Default)
|
||||
|
||||
Copy this minimal workflow to get started with the default OpenAI models:
|
||||
|
||||
```yaml
|
||||
name: PR Agent
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
#### Gemini Setup
|
||||
|
||||
Ready-to-use workflow for Gemini models:
|
||||
|
||||
```yaml
|
||||
name: PR Agent (Gemini)
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
config.model: "gemini/gemini-1.5-flash"
|
||||
config.fallback_models: '["gemini/gemini-1.5-flash"]'
|
||||
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
#### Claude Setup
|
||||
|
||||
Ready-to-use workflow for Claude models:
|
||||
|
||||
```yaml
|
||||
name: PR Agent (Claude)
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
config.model: "anthropic/claude-3-opus-20240229"
|
||||
config.fallback_models: '["anthropic/claude-3-haiku-20240307"]'
|
||||
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
### Basic Configuration with Tool Controls
|
||||
|
||||
Start with this enhanced workflow that includes tool configuration:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
name: Run pr agent on every pull request, respond to user comments
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Enable/disable automatic tools
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
# Configure which PR events trigger the action
|
||||
github_action_config.pr_actions: '["opened", "reopened", "ready_for_review", "review_requested"]'
|
||||
```
|
||||
|
||||
### Switching Models
|
||||
|
||||
#### Using Gemini (Google AI Studio)
|
||||
|
||||
To use Gemini models instead of the default OpenAI models:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Set the model to Gemini
|
||||
config.model: "gemini/gemini-1.5-flash"
|
||||
config.fallback_models: '["gemini/gemini-1.5-flash"]'
|
||||
# Add your Gemini API key
|
||||
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
**Required Secrets:**
|
||||
- Add `GEMINI_API_KEY` to your repository secrets (get it from [Google AI Studio](https://aistudio.google.com/))
|
||||
|
||||
**Note:** When using non-OpenAI models like Gemini, you don't need to set `OPENAI_KEY` - only the model-specific API key is required.
|
||||
|
||||
#### Using Claude (Anthropic)
|
||||
|
||||
To use Claude models:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Set the model to Claude
|
||||
config.model: "anthropic/claude-3-opus-20240229"
|
||||
config.fallback_models: '["anthropic/claude-3-haiku-20240307"]'
|
||||
# Add your Anthropic API key
|
||||
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
**Required Secrets:**
|
||||
- Add `ANTHROPIC_KEY` to your repository secrets (get it from [Anthropic Console](https://console.anthropic.com/))
|
||||
|
||||
**Note:** When using non-OpenAI models like Claude, you don't need to set `OPENAI_KEY` - only the model-specific API key is required.
|
||||
|
||||
#### Using Azure OpenAI
|
||||
|
||||
To use Azure OpenAI services:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Azure OpenAI configuration
|
||||
OPENAI.API_TYPE: "azure"
|
||||
OPENAI.API_VERSION: "2023-05-15"
|
||||
OPENAI.API_BASE: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
|
||||
OPENAI.DEPLOYMENT_ID: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
|
||||
# Set the model to match your Azure deployment
|
||||
config.model: "gpt-4o"
|
||||
config.fallback_models: '["gpt-4o"]'
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
**Required Secrets:**
|
||||
- `AZURE_OPENAI_KEY`: Your Azure OpenAI API key
|
||||
- `AZURE_OPENAI_ENDPOINT`: Your Azure OpenAI endpoint URL
|
||||
- `AZURE_OPENAI_DEPLOYMENT`: Your deployment name
|
||||
|
||||
#### Using Local Models (Ollama)
|
||||
|
||||
To use local models via Ollama:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Set the model to a local Ollama model
|
||||
config.model: "ollama/qwen2.5-coder:32b"
|
||||
config.fallback_models: '["ollama/qwen2.5-coder:32b"]'
|
||||
config.custom_model_max_tokens: "128000"
|
||||
# Ollama configuration
|
||||
OLLAMA.API_BASE: "http://localhost:11434"
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
**Note:** For local models, you'll need to use a self-hosted runner with Ollama installed, as GitHub Actions hosted runners cannot access localhost services.
|
||||
|
||||
### Advanced Configuration Options
|
||||
|
||||
#### Custom Review Instructions
|
||||
|
||||
Add specific instructions for the review process:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Custom review instructions
|
||||
pr_reviewer.extra_instructions: "Focus on security vulnerabilities and performance issues. Check for proper error handling."
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
#### Language-Specific Configuration
|
||||
|
||||
Configure for specific programming languages:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Language-specific settings
|
||||
pr_reviewer.extra_instructions: "Focus on Python best practices, type hints, and docstrings."
|
||||
pr_code_suggestions.num_code_suggestions: "8"
|
||||
pr_code_suggestions.suggestions_score_threshold: "7"
|
||||
# Tool configuration
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
#### Selective Tool Execution
|
||||
|
||||
Run only specific tools automatically:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Only run review and describe, skip improve
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "false"
|
||||
# Only trigger on PR open and reopen
|
||||
github_action_config.pr_actions: '["opened", "reopened"]'
|
||||
```
|
||||
|
||||
### Using Configuration Files
|
||||
|
||||
Instead of setting all options via environment variables, you can use a `.pr_agent.toml` file in your repository root:
|
||||
|
||||
1. Create a `.pr_agent.toml` file in your repository root:
|
||||
|
||||
```toml
|
||||
[config]
|
||||
model = "gemini/gemini-1.5-flash"
|
||||
fallback_models = ["anthropic/claude-3-opus-20240229"]
|
||||
|
||||
[pr_reviewer]
|
||||
extra_instructions = "Focus on security issues and code quality."
|
||||
|
||||
[pr_code_suggestions]
|
||||
num_code_suggestions = 6
|
||||
suggestions_score_threshold = 7
|
||||
```
|
||||
|
||||
2. Use a simpler workflow file:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
name: Run pr agent on every pull request, respond to user comments
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GOOGLE_AI_STUDIO.GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
ANTHROPIC.KEY: ${{ secrets.ANTHROPIC_KEY }}
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
### Troubleshooting Common Issues
|
||||
|
||||
#### Model Not Found Errors
|
||||
|
||||
If you get model not found errors:
|
||||
|
||||
1. **Check model name format**: Ensure you're using the correct model identifier format (e.g., `gemini/gemini-1.5-flash`, not just `gemini-1.5-flash`)
|
||||
|
||||
2. **Verify API keys**: Make sure your API keys are correctly set as repository secrets
|
||||
|
||||
3. **Check model availability**: Some models may not be available in all regions or may require specific access
|
||||
|
||||
#### Environment Variable Format
|
||||
|
||||
Remember these key points about environment variables:
|
||||
|
||||
- Use dots (`.`) or double underscores (`__`) to separate sections and keys
|
||||
- Boolean values should be strings: `"true"` or `"false"`
|
||||
- Arrays should be JSON strings: `'["item1", "item2"]'`
|
||||
- Model names are case-sensitive
|
||||
|
||||
#### Rate Limiting
|
||||
|
||||
If you encounter rate limiting:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Add fallback models for better reliability
|
||||
config.fallback_models: '["gpt-4o", "gpt-3.5-turbo"]'
|
||||
# Increase timeout for slower models
|
||||
config.ai_timeout: "300"
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
#### Common Error Messages and Solutions
|
||||
|
||||
**Error: "Model not found"**
|
||||
- **Solution**: Check the model name format and ensure it matches the exact identifier. See the [Changing a model in PR-Agent](../usage-guide/changing_a_model.md) guide for supported models and their correct identifiers.
|
||||
|
||||
**Error: "API key not found"**
|
||||
- **Solution**: Verify that your API key is correctly set as a repository secret and the environment variable name matches exactly
|
||||
- **Note**: For non-OpenAI models (Gemini, Claude, etc.), you only need the model-specific API key, not `OPENAI_KEY`
|
||||
|
||||
**Error: "Rate limit exceeded"**
|
||||
- **Solution**: Add fallback models or increase the `config.ai_timeout` value
|
||||
|
||||
**Error: "Permission denied"**
|
||||
- **Solution**: Ensure your workflow has the correct permissions set:
|
||||
```yaml
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
```
|
||||
|
||||
**Error: "Invalid JSON format"**
|
||||
- **Solution**: Check that arrays are properly formatted as JSON strings:
|
||||
```yaml
|
||||
# Correct
|
||||
config.fallback_models: '["model1", "model2"]'
|
||||
# Incorrect (interpreted as a YAML list, not a string)
|
||||
config.fallback_models: ["model1", "model2"]
|
||||
```
|
||||
|
||||
#### Debugging Tips
|
||||
|
||||
1. **Enable verbose logging**: Add `config.verbosity_level: "2"` to see detailed logs
|
||||
2. **Check GitHub Actions logs**: Look at the step output for specific error messages
|
||||
3. **Test with minimal configuration**: Start with just the basic setup and add options one by one
|
||||
4. **Verify secrets**: Double-check that all required secrets are set in your repository settings
|
||||
|
||||
#### Performance Optimization
|
||||
|
||||
For better performance with large repositories:
|
||||
|
||||
```yaml
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Optimize for large PRs
|
||||
config.large_patch_policy: "clip"
|
||||
config.max_model_tokens: "32000"
|
||||
config.patch_extra_lines_before: "3"
|
||||
config.patch_extra_lines_after: "1"
|
||||
github_action_config.auto_review: "true"
|
||||
github_action_config.auto_describe: "true"
|
||||
github_action_config.auto_improve: "true"
|
||||
```
|
||||
|
||||
### Reference
|
||||
|
||||
For more detailed configuration options, see:
|
||||
- [Changing a model in PR-Agent](../usage-guide/changing_a_model.md)
|
||||
- [Configuration options](../usage-guide/configuration_options.md)
|
||||
- [Automations and usage](../usage-guide/automations_and_usage.md#github-action)
|
||||
|
||||
### Using a specific release
|
||||
|
||||
!!! tip ""
|
||||
@ -296,4 +720,4 @@ After you set up AWS CodeCommit using the instructions above, here is an example
|
||||
PYTHONPATH="/PATH/TO/PROJECTS/pr-agent" python pr_agent/cli.py \
|
||||
--pr_url https://us-east-1.console.aws.amazon.com/codesuite/codecommit/repositories/MY_REPO_NAME/pull-requests/321 \
|
||||
review
|
||||
```
|
||||
```
|
@ -58,6 +58,12 @@ A list of the models used for generating the baseline suggestions, and example r
|
||||
<td style="text-align:left;">1024</td>
|
||||
<td style="text-align:center;"><b>44.3</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:left;">Grok-4</td>
|
||||
<td style="text-align:left;">2025-07-09</td>
|
||||
<td style="text-align:left;">unknown</td>
|
||||
<td style="text-align:center;"><b>41.7</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align:left;">Claude-4-sonnet</td>
|
||||
<td style="text-align:left;">2025-05-14</td>
|
||||
@ -262,6 +268,23 @@ weaknesses:
|
||||
- **Frequent incorrect or no-op fixes:** It sometimes supplies identical “before/after” code, flags non-issues, or suggests changes that would break compilation or logic, reducing reviewer trust.
|
||||
- **Shaky guideline consistency:** Although generally compliant, it still occasionally violates rules (touches unchanged lines, offers stylistic advice, adds imports) and duplicates suggestions, indicating unstable internal checks.
|
||||
|
||||
### Grok-4
|
||||
|
||||
final score: **32.8**
|
||||
|
||||
strengths:
|
||||
|
||||
- **Focused and concise fixes:** When the model does detect a problem it usually proposes a minimal, well-scoped patch that compiles and directly addresses the defect without unnecessary noise.
|
||||
- **Good critical-bug instinct:** It often prioritises show-stoppers (compile failures, crashes, security issues) over cosmetic matters and occasionally spots subtle issues that all other reviewers miss.
|
||||
- **Clear explanations & snippets:** Explanations are short, readable and paired with ready-to-paste code, making the advice easy to apply.
|
||||
|
||||
weaknesses:
|
||||
|
||||
- **High miss rate:** In a large fraction of examples the model returned an empty list or covered only one minor issue while overlooking more serious newly-introduced bugs.
|
||||
- **Inconsistent accuracy:** A noticeable subset of answers contain wrong or even harmful fixes (e.g., removing valid flags, creating compile errors, re-introducing bugs).
|
||||
- **Limited breadth:** Even when it finds a real defect it rarely reports additional related problems that peers catch, leading to partial reviews.
|
||||
- **Occasional guideline slips:** A few replies modify unchanged lines, suggest new imports, or duplicate suggestions, showing imperfect compliance with instructions.
|
||||
|
||||
## Appendix - Example Results
|
||||
|
||||
Some examples of benchmarked PRs and their results:
|
||||
|
304
docs/docs/tools/compliance.md
Normal file
304
docs/docs/tools/compliance.md
Normal file
@ -0,0 +1,304 @@
|
||||
`Platforms supported: GitHub, GitLab, Bitbucket`
|
||||
|
||||
## Overview
|
||||
|
||||
The `compliance` tool performs comprehensive compliance checks on PR code changes, validating them against security standards, ticket requirements, and custom organizational compliance checklists, thereby helping teams, enterprises, and agents maintain consistent code quality and security practices while ensuring that development work aligns with business requirements.
|
||||
|
||||
=== "Fully Compliant"
|
||||
{width=256}
|
||||
|
||||
=== "Partially Compliant"
|
||||
{width=256}
|
||||
|
||||
___
|
||||
|
||||
[//]: # (???+ note "The following features are available only for Qodo Merge 💎 users:")
|
||||
|
||||
[//]: # ( - Custom compliance checklists and hierarchical compliance checklists)
|
||||
|
||||
[//]: # ( - Ticket compliance validation with Jira/Linear integration)
|
||||
|
||||
[//]: # ( - Auto-approval based on compliance status)
|
||||
|
||||
[//]: # ( - Compliance labels and automated enforcement)
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Manual Triggering
|
||||
|
||||
Invoke the tool manually by commenting `/compliance` on any PR. The compliance results are presented in a comprehensive table:
|
||||
|
||||
To edit [configurations](#configuration-options) related to the `compliance` tool, use the following template:
|
||||
|
||||
```toml
|
||||
/compliance --pr_compliance.some_config1=... --pr_compliance.some_config2=...
|
||||
```
|
||||
|
||||
For example, you can enable ticket compliance labels by running:
|
||||
|
||||
```toml
|
||||
/compliance --pr_compliance.enable_ticket_labels=true
|
||||
```
|
||||
|
||||
### Automatic Triggering
|
||||
|
||||
|
||||
The tool can be triggered automatically every time a new PR is [opened](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#github-app-automatic-tools-when-a-new-pr-is-opened), or in a [push](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/?h=push#github-app-automatic-tools-for-push-actions-commits-to-an-open-pr) event to an existing PR.
|
||||
|
||||
To run the `compliance` tool automatically when a PR is opened, define the following in the [configuration file](https://qodo-merge-docs.qodo.ai/usage-guide/configuration_options/):
|
||||
|
||||
```toml
|
||||
[github_app] # for example
|
||||
pr_commands = [
|
||||
"/compliance",
|
||||
...
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
## Compliance Categories
|
||||
|
||||
The compliance tool evaluates three main categories:
|
||||
|
||||
|
||||
### 1. Security Compliance
|
||||
|
||||
Scans for security vulnerabilities and potential exploits in the PR code changes:
|
||||
|
||||
- **Verified Security Concerns** 🔴: Clear security vulnerabilities that require immediate attention
|
||||
- **Possible Security Risks** ⚪: Potential security issues that need human verification
|
||||
- **No Security Concerns** 🟢: No security vulnerabilities detected
|
||||
|
||||
Examples of security issues:
|
||||
|
||||
- Exposure of sensitive information (API keys, passwords, secrets)
|
||||
- SQL injection vulnerabilities
|
||||
- Cross-site scripting (XSS) risks
|
||||
- Cross-site request forgery (CSRF) vulnerabilities
|
||||
- Insecure data handling patterns
|
||||
|
||||
|
||||
### 2. Ticket Compliance
|
||||
|
||||
???+ tip "How to set up ticket compliance"
|
||||
Follow the guide on how to set up [ticket compliance](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/) with Qodo Merge.
|
||||
|
||||
???+ tip "Auto-create ticket"
|
||||
Follow this [guide](https://qodo-merge-docs.qodo.ai/tools/pr_to_ticket/) to learn how to enable triggering `create tickets` based on PR content.
|
||||
|
||||
{width=256}
|
||||
|
||||
|
||||
Validates that PR changes fulfill the requirements specified in linked tickets:
|
||||
|
||||
- **Fully Compliant** 🟢: All ticket requirements are satisfied
|
||||
- **Partially Compliant** 🟡: Some requirements are met, others need attention
|
||||
- **Not Compliant** 🔴: Clear violations of ticket requirements
|
||||
- **Requires Verification** ⚪: Requirements that need human review
|
||||
|
||||
|
||||
### 3. Custom Compliance
|
||||
|
||||
Validates against an organization-specific compliance checklist:
|
||||
|
||||
- **Fully Compliant** 🟢: All custom compliance are satisfied
|
||||
- **Not Compliant** 🔴: Violations of custom compliance
|
||||
- **Requires Verification** ⚪: Compliance that need human assessment
|
||||
|
||||
## Custom Compliance
|
||||
|
||||
### Setting Up Custom Compliance
|
||||
|
||||
Each compliance is defined in a YAML file as follows:
|
||||
- `title`: Used to provide a clear name for the compliance
|
||||
- `compliance_label`: Used to automatically generate labels for non-compliance issues
|
||||
- `objective`, `success_criteria`, and `failure_criteria`: These fields are used to clearly define what constitutes compliance
|
||||
|
||||
???+ tip "Example of a compliance checklist"
|
||||
|
||||
```yaml
|
||||
# pr_compliance_checklist.yaml
|
||||
pr_compliances:
|
||||
- title: "Error Handling"
|
||||
compliance_label: true
|
||||
objective: "All external API calls must have proper error handling"
|
||||
success_criteria: "Try-catch blocks around external calls with appropriate logging"
|
||||
failure_criteria: "External API calls without error handling or logging"
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
???+ tip "Writing effective compliance checklists"
|
||||
- Avoid overly complex or subjective compliances that are hard to verify
|
||||
- Keep compliances focused on security, business requirements, and critical standards
|
||||
- Use clear, actionable language that developers can understand
|
||||
- Focus on meaningful compliance requirements, not style preferences
|
||||
|
||||
|
||||
### Global Hierarchical Compliance
|
||||
|
||||
Qodo Merge supports hierarchical compliance checklists using a dedicated global configuration repository.
|
||||
|
||||
#### Setting up global hierarchical compliance
|
||||
|
||||
1\. Create a new repository named `pr-agent-settings` in your organization or workspace.
|
||||
|
||||
2\. Build the folder hierarchy in your `pr-agent-settings` repository:
|
||||
|
||||
```bash
|
||||
pr-agent-settings/
|
||||
├── metadata.yaml # Maps repos/folders to compliance paths
|
||||
└── compliance_standards/ # Root for all compliance definitions
|
||||
├── global/ # Global compliance, inherited widely
|
||||
│ └── pr_compliance_checklist.yaml
|
||||
├── groups/ # For groups of repositories
|
||||
│ ├── frontend_repos/
|
||||
│ │ └── pr_compliance_checklist.yaml
|
||||
│ └── backend_repos/
|
||||
│ └── pr_compliance_checklist.yaml
|
||||
├── qodo-merge/ # For standalone repositories
|
||||
│ └── pr_compliance_checklist.yaml
|
||||
└── qodo-monorepo/ # For monorepo-specific compliance
|
||||
├── pr_compliance_checklist.yaml # Root-level monorepo compliance
|
||||
├── qodo-github/ # Subproject compliance
|
||||
│ └── pr_compliance_checklist.yaml
|
||||
└── qodo-gitlab/ # Another subproject
|
||||
└── pr_compliance_checklist.yaml
|
||||
```
|
||||
|
||||
3\. Define the metadata file `metadata.yaml` in the root of `pr-agent-settings`:
|
||||
|
||||
```yaml
|
||||
# Standalone repos
|
||||
qodo-merge:
|
||||
pr_compliance_checklist_paths:
|
||||
- "qodo-merge"
|
||||
|
||||
# Group-associated repos
|
||||
repo_b:
|
||||
pr_compliance_checklist_paths:
|
||||
- "groups/backend_repos"
|
||||
|
||||
# Multi-group repos
|
||||
repo_c:
|
||||
pr_compliance_checklist_paths:
|
||||
- "groups/frontend_repos"
|
||||
- "groups/backend_repos"
|
||||
|
||||
# Monorepo with subprojects
|
||||
qodo-monorepo:
|
||||
pr_compliance_checklist_paths:
|
||||
- "qodo-monorepo"
|
||||
monorepo_subprojects:
|
||||
frontend:
|
||||
pr_compliance_checklist_paths:
|
||||
- "qodo-monorepo/qodo-github"
|
||||
backend:
|
||||
pr_compliance_checklist_paths:
|
||||
- "qodo-monorepo/qodo-gitlab"
|
||||
```
|
||||
|
||||
4\. Set the following configuration:
|
||||
|
||||
```toml
|
||||
[pr_compliance]
|
||||
enable_global_pr_compliance = true
|
||||
```
|
||||
|
||||
???- info "Compliance priority and fallback behavior"
|
||||
|
||||
1\. **Primary**: Global hierarchical compliance checklists from the `pr-agent-settings` repository:
|
||||
|
||||
1.1 If the repository is mapped in `metadata.yaml`, it uses the specified paths
|
||||
|
||||
1.2 For monorepos, it automatically collects compliance checklists matching PR file paths
|
||||
|
||||
1.3 If no mapping exists, it falls back to the global compliance checklists
|
||||
|
||||
2\. **Fallback**: Local repository wiki `pr_compliance_checklist.yaml` file:
|
||||
|
||||
2.1 Used when global compliance checklists are not found or configured
|
||||
|
||||
2.2 Acts as a safety net for repositories not yet configured in the global system
|
||||
|
||||
2.3 Local wiki compliance checklists are completely ignored when compliance checklists are successfully loaded
|
||||
|
||||
|
||||
## Configuration Options
|
||||
|
||||
???+ example "General options"
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>extra_instructions</b></td>
|
||||
<td>Optional extra instructions for the tool. For example: "Ensure that all error-handling paths in the code contain appropriate logging statements". Default is empty string.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>persistent_comment</b></td>
|
||||
<td>If set to true, the compliance comment will be persistent, meaning that every new compliance request will edit the previous one. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_user_defined_compliance_labels</b></td>
|
||||
<td>If set to true, the tool will add the label `Failed compliance check` for custom compliance violations. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_estimate_effort_to_review</b></td>
|
||||
<td>If set to true, the tool will estimate the effort required to review the PR (1-5 scale) as a label. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_todo_scan</b></td>
|
||||
<td>If set to true, the tool will scan for TODO comments in the PR code. Default is false.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_update_pr_compliance_checkbox</b></td>
|
||||
<td>If set to true, the tool will add an update checkbox to refresh compliance status following push events. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_help_text</b></td>
|
||||
<td>If set to true, the tool will display help text in the comment. Default is false.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
???+ example "Security compliance options"
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>enable_security_compliance</b></td>
|
||||
<td>If set to true, the tool will check for security vulnerabilities. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_compliance_labels_security</b></td>
|
||||
<td>If set to true, the tool will add a `Possible security concern` label to the PR when security-related concerns are detected. Default is true.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
???+ example "Ticket compliance options"
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>enable_ticket_labels</b></td>
|
||||
<td>If set to true, the tool will add ticket compliance labels to the PR. Default is false.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>enable_no_ticket_labels</b></td>
|
||||
<td>If set to true, the tool will add a label when no ticket is found. Default is false.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>check_pr_additional_content</b></td>
|
||||
<td>If set to true, the tool will check if the PR contains content not related to the ticket. Default is false.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Usage Tips
|
||||
|
||||
### Blocking PRs Based on Compliance
|
||||
|
||||
!!! tip ""
|
||||
You can configure CI/CD Actions to prevent merging PRs with specific compliance labels:
|
||||
|
||||
- `Possible security concern` - Block PRs with potential security issues
|
||||
- `Failed compliance check` - Block PRs that violate custom compliance checklists
|
||||
|
||||
Implement a dedicated [GitHub Action](https://medium.com/sequra-tech/quick-tip-block-pull-request-merge-using-labels-6cc326936221) to enforce these checklists.
|
||||
|
@ -124,6 +124,10 @@ This option is enabled by default via the `pr_description.enable_pr_diagram` par
|
||||
<td><b>enable_semantic_files_types</b></td>
|
||||
<td>If set to true, "Changes walkthrough" section will be generated. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>file_table_collapsible_open_by_default</b></td>
|
||||
<td>If set to true, the file list in the "Changes walkthrough" section will be open by default. If set to false, it will be closed by default. Default is false.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>collapsible_file_list</b></td>
|
||||
<td>If set to true, the file list in the "Changes walkthrough" section will be collapsible. If set to "adaptive", the file list will be collapsible only if there are more than 8 files. Default is "adaptive".</td>
|
||||
@ -140,6 +144,10 @@ This option is enabled by default via the `pr_description.enable_pr_diagram` par
|
||||
<td><b>enable_pr_diagram</b></td>
|
||||
<td>If set to true, the tool will generate a horizontal Mermaid flowchart summarizing the main pull request changes. This field remains empty if not applicable. Default is true.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>auto_create_ticket</b></td>
|
||||
<td>If set to true, this will <a href="https://qodo-merge-docs.qodo.ai/tools/pr_to_ticket/">automatically create a ticket</a> in the ticketing system when a PR is opened. Default is false.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Inline file summary 💎
|
||||
|
@ -598,6 +598,10 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 600 line
|
||||
<td><b>num_code_suggestions_per_chunk</b></td>
|
||||
<td>Number of code suggestions provided by the 'improve' tool, per chunk. Default is 3.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>num_best_practice_suggestions 💎</b></td>
|
||||
<td>Number of code suggestions provided by the 'improve' tool for best practices. Default is 1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>max_number_of_calls</b></td>
|
||||
<td>Maximum number of chunks. Default is 3.</td>
|
||||
|
@ -14,11 +14,13 @@ 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 |
|
||||
| **💎 [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 |
|
||||
| **💎 [CI Feedback (`/checks ci_job`](./ci_feedback.md))** | Automatically generates feedback and analysis for a failed CI job |
|
||||
| **💎 [Compliance (`/compliance`](./compliance.md))** | Comprehensive compliance checks for security, ticket requirements, and custom organizational rules |
|
||||
| **💎 [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 Custom Labels (`/generate_labels`](./custom_labels.md))** | Generates custom labels for the PR, based on specific guidelines defined by the user |
|
||||
| **💎 [Generate Tests (`/test`](./test.md))** | Automatically generates unit tests for a selected component, based on the PR code changes |
|
||||
| **💎 [Implement (`/implement`](./implement.md))** | Generates implementation code from review suggestions |
|
||||
| **💎 [Improve Component (`/improve_component component_name`](./improve_component.md))** | Generates code suggestions for a specific code component that changed in the PR |
|
||||
| **💎 [PR to Ticket (`/create_ticket`](./pr_to_ticket.md))** | Generates ticket in the ticket tracking systems (Jira, Linear, or Git provider issues) based on PR content |
|
||||
| **💎 [Scan Repo Discussions (`/scan_repo_discussions`](./scan_repo_discussions.md))** | Generates `best_practices.md` file based on previous discussions in the repository |
|
||||
| **💎 [Similar Code (`/similar_code`](./similar_code.md))** | Retrieves the most similar code components from inside the organization's codebase, or from open-source code. |
|
||||
|
||||
|
87
docs/docs/tools/pr_to_ticket.md
Normal file
87
docs/docs/tools/pr_to_ticket.md
Normal file
@ -0,0 +1,87 @@
|
||||
`Platforms supported: GitHub, GitLab, Bitbucket`
|
||||
|
||||
## Overview
|
||||
The `create_ticket` tool automatically generates tickets in ticket tracking systems (`Jira`, `Linear`, or `GitHub Issues`) based on PR content.
|
||||
|
||||
It analyzes the PR's data (code changes, commit messages, and description) to create well-structured tickets that capture the essence of the development work, helping teams maintain traceability between code changes and project management systems.
|
||||
|
||||
When a ticket is created, it appears in the PR description under an `Auto-created Ticket` section, complete with a link to the generated ticket.
|
||||
|
||||
{width=512}
|
||||
|
||||
!!! info "Pre-requisites"
|
||||
- To use this tool you need to integrate your ticketing system with Qodo-merge, follow the [Ticket Compliance Documentation](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/).
|
||||
- For Jira Cloud users, please re-integrate your connection through the [qodo merge integration page](https://app.qodo.ai/qodo-merge/integrations) to enable the `update` permission required for ticket creation
|
||||
- You need to configure the project key in ticket corresponding to the repository where the PR is created. This is done by adding the `default_project_key`.
|
||||
|
||||
```toml
|
||||
[pr_to_ticket]
|
||||
default_project_key = "PROJECT_KEY" # e.g., "SCRUM"
|
||||
```
|
||||
|
||||
## Usage
|
||||
there are 3 ways to use the `create_ticket` tool:
|
||||
|
||||
1. [**Automatic Ticket Creation**](#automatic-ticket-creation)
|
||||
2. [**Interactive Triggering via Compliance Tool**](#interactive-triggering-via-compliance-tool)
|
||||
3. [**Manual Ticket Creation**](#manual-ticket-creation)
|
||||
|
||||
### Automatic Ticket Creation
|
||||
The tool can be configured to automatically create tickets when a PR is opened or updated and the PR does not already have a ticket associated with it.
|
||||
This ensures that every code change is documented in the ticketing system without manual intervention.
|
||||
|
||||
To configure automatic ticket creation, add the following to `.pr_agent.toml`:
|
||||
|
||||
```toml
|
||||
[pr_description]
|
||||
auto_create_ticket = true
|
||||
```
|
||||
|
||||
### Interactive Triggering via Compliance Tool
|
||||
`Supported only in Github and Gitlab`
|
||||
|
||||
The tool can be triggered interactively through a checkbox in the compliance tool. This allows users to create tickets as part of their PR Compliance Review workflow.
|
||||
|
||||
{width=512}
|
||||
|
||||
- After clicking the checkbox, the tool will create a ticket and will add/update the `PR Description` with a section called `Auto-created Ticket` with the link to the created ticket.
|
||||
- Then you can click `update` in the `Ticket compliance` section in the `Compliance` tool
|
||||
|
||||
{width=512}
|
||||
|
||||
### Manual Ticket Creation
|
||||
Users can manually trigger the ticket creation process from the PR interface.
|
||||
|
||||
To trigger ticket creation manually, the user can call this tool from the PR comment:
|
||||
|
||||
```
|
||||
/create_ticket
|
||||
```
|
||||
|
||||
After triggering, the tool will create a ticket and will add/update the `PR Description` with a section called `Auto-created Ticket` with the link to the created ticket.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
## Configuration Options
|
||||
|
||||
???+ example "Configuration"
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><b>default_project_key</b></td>
|
||||
<td>The default project key for your ticketing system (e.g., `SCRUM`). This is required unless `fallback_to_git_provider_issues` is set to `true`.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>default_base_url</b></td>
|
||||
<td>If your organization have integrated to multiple ticketing systems, you can set the default base URL for the ticketing system. This will be used to create tickets in the default system. Example: `https://YOUR-ORG.atlassian.net`.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>fallback_to_git_provider_issues</b></td>
|
||||
<td>If set to `true`, the tool will create issues in the Git provider's issue tracker (GitHub) if the `default_project_key` is not configured in the repository configuration. Default is `false`.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## Helping Your Organization Meet SOC-2 Requirements
|
||||
The `create_ticket` tool helps your organization satisfy SOC-2 compliance. By automatically creating tickets from PRs and establishing bidirectional links between them, it ensures every code change is traceable to its corresponding business requirement or task.
|
@ -246,7 +246,7 @@ To supplement the automatic bot detection, you can manually specify users to ign
|
||||
ignore_pr_authors = ["my-special-bot-user", ...]
|
||||
```
|
||||
|
||||
Where the `ignore_pr_authors` is a list of usernames that you want to ignore.
|
||||
Where the `ignore_pr_authors` is a regex list of usernames that you want to ignore.
|
||||
|
||||
!!! note
|
||||
There is one specific case where bots will receive an automatic response - when they generated a PR with a _failed test_. In that case, the [`ci_feedback`](https://qodo-merge-docs.qodo.ai/tools/ci_feedback/) tool will be invoked.
|
||||
|
@ -30,7 +30,7 @@ verbosity_level=2
|
||||
This is useful for debugging or experimenting with different tools.
|
||||
|
||||
3. **git provider**: The [git_provider](https://github.com/Codium-ai/pr-agent/blob/main/pr_agent/settings/configuration.toml#L5) field in a configuration file determines the GIT provider that will be used by Qodo Merge. Currently, the following providers are supported:
|
||||
`github` **(default)**, `gitlab`, `bitbucket`, `azure`, `codecommit`, `local`,`gitea`, and `gerrit`.
|
||||
`github` **(default)**, `gitlab`, `bitbucket`, `azure`, `codecommit`, `local`, and `gitea`.
|
||||
|
||||
### CLI Health Check
|
||||
|
||||
@ -202,6 +202,25 @@ publish_labels = false
|
||||
|
||||
to prevent Qodo Merge from publishing labels when running the `describe` tool.
|
||||
|
||||
#### Quick Reference: Model Configuration in GitHub Actions
|
||||
|
||||
For detailed step-by-step examples of configuring different models (Gemini, Claude, Azure OpenAI, etc.) in GitHub Actions, see the [Configuration Examples](../installation/github.md#configuration-examples) section in the installation guide.
|
||||
|
||||
**Common Model Configuration Patterns:**
|
||||
|
||||
- **OpenAI**: Set `config.model: "gpt-4o"` and `OPENAI_KEY`
|
||||
- **Gemini**: Set `config.model: "gemini/gemini-1.5-flash"` and `GOOGLE_AI_STUDIO.GEMINI_API_KEY` (no `OPENAI_KEY` needed)
|
||||
- **Claude**: Set `config.model: "anthropic/claude-3-opus-20240229"` and `ANTHROPIC.KEY` (no `OPENAI_KEY` needed)
|
||||
- **Azure OpenAI**: Set `OPENAI.API_TYPE: "azure"`, `OPENAI.API_BASE`, and `OPENAI.DEPLOYMENT_ID`
|
||||
- **Local Models**: Set `config.model: "ollama/model-name"` and `OLLAMA.API_BASE`
|
||||
|
||||
**Environment Variable Format:**
|
||||
- Use dots (`.`) to separate sections and keys: `config.model`, `pr_reviewer.extra_instructions`
|
||||
- Boolean values as strings: `"true"` or `"false"`
|
||||
- Arrays as JSON strings: `'["item1", "item2"]'`
|
||||
|
||||
For complete model configuration details, see [Changing a model in PR-Agent](changing_a_model.md).
|
||||
|
||||
### 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:
|
||||
|
@ -32,6 +32,16 @@ OPENAI__API_BASE=https://api.openai.com/v1
|
||||
OPENAI__KEY=sk-...
|
||||
```
|
||||
|
||||
### OpenAI Flex Processing
|
||||
|
||||
To reduce costs for non-urgent/background tasks, enable Flex Processing:
|
||||
|
||||
```toml
|
||||
[litellm]
|
||||
extra_body='{"processing_mode": "flex"}'
|
||||
```
|
||||
|
||||
See [OpenAI Flex Processing docs](https://platform.openai.com/docs/guides/flex-processing) for details.
|
||||
|
||||
### Azure
|
||||
|
||||
|
@ -34,11 +34,13 @@ nav:
|
||||
- 💎 Add Documentation: 'tools/documentation.md'
|
||||
- 💎 Analyze: 'tools/analyze.md'
|
||||
- 💎 CI Feedback: 'tools/ci_feedback.md'
|
||||
- 💎 Compliance: 'tools/compliance.md'
|
||||
- 💎 Custom Prompt: 'tools/custom_prompt.md'
|
||||
- 💎 Generate Labels: 'tools/custom_labels.md'
|
||||
- 💎 Generate Tests: 'tools/test.md'
|
||||
- 💎 Implement: 'tools/implement.md'
|
||||
- 💎 Improve Components: 'tools/improve_component.md'
|
||||
- 💎 PR to Ticket: 'tools/pr_to_ticket.md'
|
||||
- 💎 Scan Repo Discussions: 'tools/scan_repo_discussions.md'
|
||||
- 💎 Similar Code: 'tools/similar_code.md'
|
||||
- Core Abilities:
|
||||
|
@ -3,5 +3,5 @@
|
||||
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=
|
||||
'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 -->
|
||||
|
@ -45,6 +45,7 @@ MAX_TOKENS = {
|
||||
'command-nightly': 4096,
|
||||
'deepseek/deepseek-chat': 128000, # 128K, but may be limited by config.max_model_tokens
|
||||
'deepseek/deepseek-reasoner': 64000, # 64K, but may be limited by config.max_model_tokens
|
||||
'openai/qwq-plus': 131072, # 131K context length, but may be limited by config.max_model_tokens
|
||||
'replicate/llama-2-70b-chat:2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1': 4096,
|
||||
'meta-llama/Llama-2-7b-chat-hf': 4096,
|
||||
'vertex_ai/codechat-bison': 6144,
|
||||
@ -193,3 +194,8 @@ CLAUDE_EXTENDED_THINKING_MODELS = [
|
||||
"anthropic/claude-3-7-sonnet-20250219",
|
||||
"claude-3-7-sonnet-20250219"
|
||||
]
|
||||
|
||||
# Models that require streaming mode
|
||||
STREAMING_REQUIRED_MODELS = [
|
||||
"openai/qwq-plus"
|
||||
]
|
||||
|
@ -5,14 +5,16 @@ import requests
|
||||
from litellm import acompletion
|
||||
from tenacity import retry, retry_if_exception_type, retry_if_not_exception_type, stop_after_attempt
|
||||
|
||||
from pr_agent.algo import CLAUDE_EXTENDED_THINKING_MODELS, NO_SUPPORT_TEMPERATURE_MODELS, SUPPORT_REASONING_EFFORT_MODELS, USER_MESSAGE_ONLY_MODELS
|
||||
from pr_agent.algo import CLAUDE_EXTENDED_THINKING_MODELS, NO_SUPPORT_TEMPERATURE_MODELS, SUPPORT_REASONING_EFFORT_MODELS, USER_MESSAGE_ONLY_MODELS, STREAMING_REQUIRED_MODELS
|
||||
from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
|
||||
from pr_agent.algo.ai_handlers.litellm_helpers import _handle_streaming_response, MockResponse, _get_azure_ad_token, \
|
||||
_process_litellm_extra_body
|
||||
from pr_agent.algo.utils import ReasoningEffort, get_version
|
||||
from pr_agent.config_loader import get_settings
|
||||
from pr_agent.log import get_logger
|
||||
import json
|
||||
|
||||
OPENAI_RETRIES = 5
|
||||
MODEL_RETRIES = 2
|
||||
|
||||
|
||||
class LiteLLMAIHandler(BaseAiHandler):
|
||||
@ -110,7 +112,7 @@ class LiteLLMAIHandler(BaseAiHandler):
|
||||
if get_settings().get("AZURE_AD.CLIENT_ID", None):
|
||||
self.azure = True
|
||||
# Generate access token using Azure AD credentials from settings
|
||||
access_token = self._get_azure_ad_token()
|
||||
access_token = _get_azure_ad_token()
|
||||
litellm.api_key = access_token
|
||||
openai.api_key = access_token
|
||||
|
||||
@ -143,25 +145,8 @@ class LiteLLMAIHandler(BaseAiHandler):
|
||||
# Models that support extended thinking
|
||||
self.claude_extended_thinking_models = CLAUDE_EXTENDED_THINKING_MODELS
|
||||
|
||||
def _get_azure_ad_token(self):
|
||||
"""
|
||||
Generates an access token using Azure AD credentials from settings.
|
||||
Returns:
|
||||
str: The access token
|
||||
"""
|
||||
from azure.identity import ClientSecretCredential
|
||||
try:
|
||||
credential = ClientSecretCredential(
|
||||
tenant_id=get_settings().azure_ad.tenant_id,
|
||||
client_id=get_settings().azure_ad.client_id,
|
||||
client_secret=get_settings().azure_ad.client_secret
|
||||
)
|
||||
# Get token for Azure OpenAI service
|
||||
token = credential.get_token("https://cognitiveservices.azure.com/.default")
|
||||
return token.token
|
||||
except Exception as e:
|
||||
get_logger().error(f"Failed to get Azure AD token: {e}")
|
||||
raise
|
||||
# Models that require streaming
|
||||
self.streaming_required_models = STREAMING_REQUIRED_MODELS
|
||||
|
||||
def prepare_logs(self, response, system, user, resp, finish_reason):
|
||||
response_log = response.dict().copy()
|
||||
@ -275,7 +260,7 @@ class LiteLLMAIHandler(BaseAiHandler):
|
||||
|
||||
@retry(
|
||||
retry=retry_if_exception_type(openai.APIError) & retry_if_not_exception_type(openai.RateLimitError),
|
||||
stop=stop_after_attempt(OPENAI_RETRIES),
|
||||
stop=stop_after_attempt(MODEL_RETRIES),
|
||||
)
|
||||
async def chat_completion(self, model: str, system: str, user: str, temperature: float = 0.2, img_path: str = None):
|
||||
try:
|
||||
@ -364,13 +349,18 @@ class LiteLLMAIHandler(BaseAiHandler):
|
||||
raise ValueError(f"LITELLM.EXTRA_HEADERS contains invalid JSON: {str(e)}")
|
||||
kwargs["extra_headers"] = litellm_extra_headers
|
||||
|
||||
# Support for custom OpenAI body fields (e.g., Flex Processing)
|
||||
kwargs = _process_litellm_extra_body(kwargs)
|
||||
|
||||
get_logger().debug("Prompts", artifact={"system": system, "user": user})
|
||||
|
||||
if get_settings().config.verbosity_level >= 2:
|
||||
get_logger().info(f"\nSystem prompt:\n{system}")
|
||||
get_logger().info(f"\nUser prompt:\n{user}")
|
||||
|
||||
response = await acompletion(**kwargs)
|
||||
# Get completion with automatic streaming detection
|
||||
resp, finish_reason, response_obj = await self._get_completion(**kwargs)
|
||||
|
||||
except openai.RateLimitError as e:
|
||||
get_logger().error(f"Rate limit error during LLM inference: {e}")
|
||||
raise
|
||||
@ -380,19 +370,36 @@ class LiteLLMAIHandler(BaseAiHandler):
|
||||
except Exception as e:
|
||||
get_logger().warning(f"Unknown error during LLM inference: {e}")
|
||||
raise openai.APIError from e
|
||||
if response is None or len(response["choices"]) == 0:
|
||||
raise openai.APIError
|
||||
else:
|
||||
resp = response["choices"][0]['message']['content']
|
||||
finish_reason = response["choices"][0]["finish_reason"]
|
||||
get_logger().debug(f"\nAI response:\n{resp}")
|
||||
|
||||
# log the full response for debugging
|
||||
response_log = self.prepare_logs(response, system, user, resp, finish_reason)
|
||||
get_logger().debug("Full_response", artifact=response_log)
|
||||
get_logger().debug(f"\nAI response:\n{resp}")
|
||||
|
||||
# for CLI debugging
|
||||
if get_settings().config.verbosity_level >= 2:
|
||||
get_logger().info(f"\nAI response:\n{resp}")
|
||||
# log the full response for debugging
|
||||
response_log = self.prepare_logs(response_obj, system, user, resp, finish_reason)
|
||||
get_logger().debug("Full_response", artifact=response_log)
|
||||
|
||||
# for CLI debugging
|
||||
if get_settings().config.verbosity_level >= 2:
|
||||
get_logger().info(f"\nAI response:\n{resp}")
|
||||
|
||||
return resp, finish_reason
|
||||
|
||||
async def _get_completion(self, **kwargs):
|
||||
"""
|
||||
Wrapper that automatically handles streaming for required models.
|
||||
"""
|
||||
model = kwargs["model"]
|
||||
if model in self.streaming_required_models:
|
||||
kwargs["stream"] = True
|
||||
get_logger().info(f"Using streaming mode for model {model}")
|
||||
response = await acompletion(**kwargs)
|
||||
resp, finish_reason = await _handle_streaming_response(response)
|
||||
# Create MockResponse for streaming since we don't have the full response object
|
||||
mock_response = MockResponse(resp, finish_reason)
|
||||
return resp, finish_reason, mock_response
|
||||
else:
|
||||
response = await acompletion(**kwargs)
|
||||
if response is None or len(response["choices"]) == 0:
|
||||
raise openai.APIError
|
||||
return (response["choices"][0]['message']['content'],
|
||||
response["choices"][0]["finish_reason"],
|
||||
response)
|
||||
|
112
pr_agent/algo/ai_handlers/litellm_helpers.py
Normal file
112
pr_agent/algo/ai_handlers/litellm_helpers.py
Normal file
@ -0,0 +1,112 @@
|
||||
import json
|
||||
|
||||
import openai
|
||||
|
||||
from pr_agent.config_loader import get_settings
|
||||
from pr_agent.log import get_logger
|
||||
|
||||
|
||||
async def _handle_streaming_response(response):
|
||||
"""
|
||||
Handle streaming response from acompletion and collect the full response.
|
||||
|
||||
Args:
|
||||
response: The streaming response object from acompletion
|
||||
|
||||
Returns:
|
||||
tuple: (full_response_content, finish_reason)
|
||||
"""
|
||||
full_response = ""
|
||||
finish_reason = None
|
||||
|
||||
try:
|
||||
async for chunk in response:
|
||||
if chunk.choices and len(chunk.choices) > 0:
|
||||
choice = chunk.choices[0]
|
||||
delta = choice.delta
|
||||
content = getattr(delta, 'content', None)
|
||||
if content:
|
||||
full_response += content
|
||||
if choice.finish_reason:
|
||||
finish_reason = choice.finish_reason
|
||||
except Exception as e:
|
||||
get_logger().error(f"Error handling streaming response: {e}")
|
||||
raise
|
||||
|
||||
if not full_response and finish_reason is None:
|
||||
get_logger().warning("Streaming response resulted in empty content with no finish reason")
|
||||
raise openai.APIError("Empty streaming response received without proper completion")
|
||||
elif not full_response and finish_reason:
|
||||
get_logger().debug(f"Streaming response resulted in empty content but completed with finish_reason: {finish_reason}")
|
||||
raise openai.APIError(f"Streaming response completed with finish_reason '{finish_reason}' but no content received")
|
||||
return full_response, finish_reason
|
||||
|
||||
|
||||
class MockResponse:
|
||||
"""Mock response object for streaming models to enable consistent logging."""
|
||||
|
||||
def __init__(self, resp, finish_reason):
|
||||
self._data = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {"content": resp},
|
||||
"finish_reason": finish_reason
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def dict(self):
|
||||
return self._data
|
||||
|
||||
|
||||
def _get_azure_ad_token():
|
||||
"""
|
||||
Generates an access token using Azure AD credentials from settings.
|
||||
Returns:
|
||||
str: The access token
|
||||
"""
|
||||
from azure.identity import ClientSecretCredential
|
||||
try:
|
||||
credential = ClientSecretCredential(
|
||||
tenant_id=get_settings().azure_ad.tenant_id,
|
||||
client_id=get_settings().azure_ad.client_id,
|
||||
client_secret=get_settings().azure_ad.client_secret
|
||||
)
|
||||
# Get token for Azure OpenAI service
|
||||
token = credential.get_token("https://cognitiveservices.azure.com/.default")
|
||||
return token.token
|
||||
except Exception as e:
|
||||
get_logger().error(f"Failed to get Azure AD token: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def _process_litellm_extra_body(kwargs: dict) -> dict:
|
||||
"""
|
||||
Process LITELLM.EXTRA_BODY configuration and update kwargs accordingly.
|
||||
|
||||
Args:
|
||||
kwargs: The current kwargs dictionary to update
|
||||
|
||||
Returns:
|
||||
Updated kwargs dictionary
|
||||
|
||||
Raises:
|
||||
ValueError: If extra_body contains invalid JSON, unsupported keys, or colliding keys
|
||||
"""
|
||||
allowed_extra_body_keys = {"processing_mode", "service_tier"}
|
||||
extra_body = getattr(getattr(get_settings(), "litellm", None), "extra_body", None)
|
||||
if extra_body:
|
||||
try:
|
||||
litellm_extra_body = json.loads(extra_body)
|
||||
if not isinstance(litellm_extra_body, dict):
|
||||
raise ValueError("LITELLM.EXTRA_BODY must be a JSON object")
|
||||
unsupported_keys = set(litellm_extra_body.keys()) - allowed_extra_body_keys
|
||||
if unsupported_keys:
|
||||
raise ValueError(f"LITELLM.EXTRA_BODY contains unsupported keys: {', '.join(unsupported_keys)}. Allowed keys: {', '.join(allowed_extra_body_keys)}")
|
||||
colliding_keys = kwargs.keys() & litellm_extra_body.keys()
|
||||
if colliding_keys:
|
||||
raise ValueError(f"LITELLM.EXTRA_BODY cannot override existing parameters: {', '.join(colliding_keys)}")
|
||||
kwargs.update(litellm_extra_body)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"LITELLM.EXTRA_BODY contains invalid JSON: {str(e)}")
|
||||
return kwargs
|
@ -70,7 +70,8 @@ class ReasoningEffort(str, Enum):
|
||||
|
||||
|
||||
class PRDescriptionHeader(str, Enum):
|
||||
CHANGES_WALKTHROUGH = "### **Changes walkthrough** 📝"
|
||||
DIAGRAM_WALKTHROUGH = "Diagram Walkthrough"
|
||||
FILE_WALKTHROUGH = "File Walkthrough"
|
||||
|
||||
|
||||
def get_setting(key: str) -> Any:
|
||||
@ -1284,14 +1285,35 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
||||
if not description_full:
|
||||
return "", []
|
||||
|
||||
description_split = description_full.split(PRDescriptionHeader.CHANGES_WALKTHROUGH.value)
|
||||
base_description_str = description_split[0]
|
||||
changes_walkthrough_str = ""
|
||||
files = []
|
||||
if len(description_split) > 1:
|
||||
changes_walkthrough_str = description_split[1]
|
||||
# description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value)
|
||||
if PRDescriptionHeader.FILE_WALKTHROUGH.value in description_full:
|
||||
try:
|
||||
# FILE_WALKTHROUGH are presented in a collapsible section in the description
|
||||
regex_pattern = r'<details.*?>\s*<summary>\s*<h3>\s*' + re.escape(PRDescriptionHeader.FILE_WALKTHROUGH.value) + r'\s*</h3>\s*</summary>'
|
||||
description_split = re.split(regex_pattern, description_full, maxsplit=1, flags=re.DOTALL)
|
||||
|
||||
# If the regex pattern is not found, fallback to the previous method
|
||||
if len(description_split) == 1:
|
||||
get_logger().debug("Could not find regex pattern for file walkthrough, falling back to simple split")
|
||||
description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value, 1)
|
||||
except Exception as e:
|
||||
get_logger().warning(f"Failed to split description using regex, falling back to simple split: {e}")
|
||||
description_split = description_full.split(PRDescriptionHeader.FILE_WALKTHROUGH.value, 1)
|
||||
|
||||
if len(description_split) < 2:
|
||||
get_logger().error("Failed to split description into base and changes walkthrough", artifact={'description': description_full})
|
||||
return description_full.strip(), []
|
||||
|
||||
base_description_str = description_split[0].strip()
|
||||
changes_walkthrough_str = ""
|
||||
files = []
|
||||
if len(description_split) > 1:
|
||||
changes_walkthrough_str = description_split[1]
|
||||
else:
|
||||
get_logger().debug("No changes walkthrough found")
|
||||
else:
|
||||
get_logger().debug("No changes walkthrough found")
|
||||
base_description_str = description_full.strip()
|
||||
return base_description_str, []
|
||||
|
||||
try:
|
||||
if changes_walkthrough_str:
|
||||
@ -1314,18 +1336,20 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
||||
try:
|
||||
if isinstance(file_data, tuple):
|
||||
file_data = file_data[0]
|
||||
pattern = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*<li>(.*?)</details>'
|
||||
pattern = r'<details>\s*<summary><strong>(.*?)</strong>\s*<dd><code>(.*?)</code>.*?</summary>\s*<hr>\s*(.*?)\s*(?:<li>|•)(.*?)</details>'
|
||||
res = re.search(pattern, file_data, re.DOTALL)
|
||||
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>'
|
||||
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 hyphen ('- ')
|
||||
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:
|
||||
short_filename = res.group(1).strip()
|
||||
short_summary = res.group(2).strip()
|
||||
long_filename = res.group(3).strip()
|
||||
if long_filename.endswith('<ul>'):
|
||||
long_filename = long_filename[:-4].strip()
|
||||
long_summary = res.group(4).strip()
|
||||
long_summary = long_summary.replace('<br> *', '\n*').replace('<br>','').replace('\n','<br>')
|
||||
long_summary = h.handle(long_summary).strip()
|
||||
@ -1344,7 +1368,7 @@ def process_description(description_full: str) -> Tuple[str, List]:
|
||||
if '<code>...</code>' in file_data:
|
||||
pass # PR with many files. some did not get analyzed
|
||||
else:
|
||||
get_logger().error(f"Failed to parse description", artifact={'description': file_data})
|
||||
get_logger().warning(f"Failed to parse description", artifact={'description': file_data})
|
||||
except Exception as e:
|
||||
get_logger().exception(f"Failed to process description: {e}", artifact={'description': file_data})
|
||||
|
||||
|
@ -380,7 +380,7 @@ class AzureDevopsProvider(GitProvider):
|
||||
pr_body = pr_body[:ind]
|
||||
|
||||
if len(pr_body) > MAX_PR_DESCRIPTION_AZURE_LENGTH:
|
||||
changes_walkthrough_text = PRDescriptionHeader.CHANGES_WALKTHROUGH.value
|
||||
changes_walkthrough_text = PRDescriptionHeader.FILE_WALKTHROUGH.value
|
||||
ind = pr_body.find(changes_walkthrough_text)
|
||||
if ind != -1:
|
||||
pr_body = pr_body[:ind]
|
||||
|
@ -139,7 +139,7 @@ def should_process_pr_logic(data) -> bool:
|
||||
# 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:
|
||||
if any(re.search(regex, sender) for regex in ignore_pr_users):
|
||||
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' setting")
|
||||
return False
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
import uvicorn
|
||||
@ -40,6 +41,88 @@ def handle_request(
|
||||
|
||||
background_tasks.add_task(inner)
|
||||
|
||||
def should_process_pr_logic(data) -> bool:
|
||||
try:
|
||||
pr_data = data.get("pullRequest", {})
|
||||
title = pr_data.get("title", "")
|
||||
|
||||
from_ref = pr_data.get("fromRef", {})
|
||||
source_branch = from_ref.get("displayId", "") if from_ref else ""
|
||||
|
||||
to_ref = pr_data.get("toRef", {})
|
||||
target_branch = to_ref.get("displayId", "") if to_ref else ""
|
||||
|
||||
author = pr_data.get("author", {})
|
||||
user = author.get("user", {}) if author else {}
|
||||
sender = user.get("name", "") if user else ""
|
||||
|
||||
repository = to_ref.get("repository", {}) if to_ref else {}
|
||||
project = repository.get("project", {}) if repository else {}
|
||||
project_key = project.get("key", "") if project else ""
|
||||
repo_slug = repository.get("slug", "") if repository else ""
|
||||
|
||||
repo_full_name = f"{project_key}/{repo_slug}" if project_key and repo_slug else ""
|
||||
pr_id = pr_data.get("id", None)
|
||||
|
||||
# To ignore PRs from specific repositories
|
||||
ignore_repos = get_settings().get("CONFIG.IGNORE_REPOSITORIES", [])
|
||||
if repo_full_name and ignore_repos:
|
||||
if any(re.search(regex, repo_full_name) for regex in ignore_repos):
|
||||
get_logger().info(f"Ignoring PR from repository '{repo_full_name}' due to 'config.ignore_repositories' setting")
|
||||
return False
|
||||
|
||||
# To ignore PRs from specific users
|
||||
ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", [])
|
||||
if ignore_pr_users and sender:
|
||||
if any(re.search(regex, sender) for regex in ignore_pr_users):
|
||||
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' setting")
|
||||
return False
|
||||
|
||||
# To ignore PRs with specific titles
|
||||
if title:
|
||||
ignore_pr_title_re = get_settings().get("CONFIG.IGNORE_PR_TITLE", [])
|
||||
if not isinstance(ignore_pr_title_re, list):
|
||||
ignore_pr_title_re = [ignore_pr_title_re]
|
||||
if ignore_pr_title_re and any(re.search(regex, title) for regex in ignore_pr_title_re):
|
||||
get_logger().info(f"Ignoring PR with title '{title}' due to config.ignore_pr_title setting")
|
||||
return False
|
||||
|
||||
ignore_pr_source_branches = get_settings().get("CONFIG.IGNORE_PR_SOURCE_BRANCHES", [])
|
||||
ignore_pr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", [])
|
||||
if (ignore_pr_source_branches or ignore_pr_target_branches):
|
||||
if any(re.search(regex, source_branch) for regex in ignore_pr_source_branches):
|
||||
get_logger().info(
|
||||
f"Ignoring PR with source branch '{source_branch}' due to config.ignore_pr_source_branches settings")
|
||||
return False
|
||||
if any(re.search(regex, target_branch) for regex in ignore_pr_target_branches):
|
||||
get_logger().info(
|
||||
f"Ignoring PR with target branch '{target_branch}' due to config.ignore_pr_target_branches settings")
|
||||
return False
|
||||
|
||||
# Allow_only_specific_folders
|
||||
allowed_folders = get_settings().config.get("allow_only_specific_folders", [])
|
||||
if allowed_folders and pr_id and project_key and repo_slug:
|
||||
from pr_agent.git_providers.bitbucket_server_provider import BitbucketServerProvider
|
||||
bitbucket_server_url = get_settings().get("BITBUCKET_SERVER.URL", "")
|
||||
pr_url = f"{bitbucket_server_url}/projects/{project_key}/repos/{repo_slug}/pull-requests/{pr_id}"
|
||||
provider = BitbucketServerProvider(pr_url=pr_url)
|
||||
changed_files = provider.get_files()
|
||||
if changed_files:
|
||||
# Check if ALL files are outside allowed folders
|
||||
all_files_outside = True
|
||||
for file_path in changed_files:
|
||||
if any(file_path.startswith(folder) for folder in allowed_folders):
|
||||
all_files_outside = False
|
||||
break
|
||||
|
||||
if all_files_outside:
|
||||
get_logger().info(f"Ignoring PR because all files {changed_files} are outside allowed folders {allowed_folders}")
|
||||
return False
|
||||
except Exception as e:
|
||||
get_logger().error(f"Failed 'should_process_pr_logic': {e}")
|
||||
return True # On exception - we continue. Otherwise, we could just end up with filtering all PRs
|
||||
return True
|
||||
|
||||
@router.post("/")
|
||||
async def redirect_to_webhook():
|
||||
return RedirectResponse(url="/webhook")
|
||||
@ -73,6 +156,11 @@ async def handle_webhook(background_tasks: BackgroundTasks, request: Request):
|
||||
|
||||
if data["eventKey"] == "pr:opened":
|
||||
apply_repo_settings(pr_url)
|
||||
if not should_process_pr_logic(data):
|
||||
get_logger().info(f"PR ignored due to config settings", **log_context)
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "PR ignored by config"})
|
||||
)
|
||||
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
|
||||
|
@ -270,7 +270,7 @@ def should_process_pr_logic(body) -> bool:
|
||||
# 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:
|
||||
if any(re.search(regex, sender) for regex in ignore_pr_users):
|
||||
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' setting")
|
||||
return False
|
||||
|
||||
|
@ -125,7 +125,7 @@ def should_process_pr_logic(data) -> bool:
|
||||
# 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:
|
||||
if any(re.search(regex, sender) for regex in ignore_pr_users):
|
||||
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' settings")
|
||||
return False
|
||||
|
||||
|
@ -16,6 +16,10 @@ key = "" # Acquire through https://platform.openai.com
|
||||
#deployment_id = "" # The deployment name you chose when you deployed the engine
|
||||
#fallback_deployments = [] # For each fallback model specified in configuration.toml in the [config] section, specify the appropriate deployment_id
|
||||
|
||||
# OpenAI Flex Processing (optional, for cost savings)
|
||||
# [litellm]
|
||||
# extra_body='{"processing_mode": "flex"}'
|
||||
|
||||
[pinecone]
|
||||
api_key = "..."
|
||||
environment = "gcp-starter"
|
||||
|
@ -106,7 +106,7 @@ extra_instructions = ""
|
||||
enable_pr_type=true
|
||||
final_update_message = true
|
||||
enable_help_text=false
|
||||
enable_help_comment=true
|
||||
enable_help_comment=false
|
||||
enable_pr_diagram=true # adds a section with a diagram of the PR changes
|
||||
# describe as comment
|
||||
publish_description_as_comment=false
|
||||
@ -152,7 +152,8 @@ new_score_mechanism_th_high=9
|
||||
new_score_mechanism_th_medium=7
|
||||
# params for '/improve --extended' mode
|
||||
auto_extended_mode=true
|
||||
num_code_suggestions_per_chunk=4
|
||||
num_code_suggestions_per_chunk=3
|
||||
num_best_practice_suggestions=1 # 💎
|
||||
max_number_of_calls = 3
|
||||
parallel_calls = true
|
||||
|
||||
|
@ -48,8 +48,8 @@ class PRDescription(BaseModel):
|
||||
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_pr_diagram %}
|
||||
changes_diagram: str = Field(description="a horizontal diagram that represents the main PR changes, in the format of a valid mermaid LR flowchart. The diagram should be concise and easy to read. Leave empty if no diagram is relevant. To create robust Mermaid diagrams, follow this two-step process: (1) Declare the nodes: nodeID[\"node description\"]. (2) Then define the links: nodeID1 -- \"link text\" --> nodeID2. Node description must always be surrounded with quotation marks.")
|
||||
{%- endif %}
|
||||
changes_diagram: str = Field(description='a horizontal diagram that represents the main PR changes, in the format of a valid mermaid LR flowchart. The diagram should be concise and easy to read. Leave empty if no diagram is relevant. To create robust Mermaid diagrams, follow this two-step process: (1) Declare the nodes: nodeID["node description"]. (2) Then define the links: nodeID1 -- "link text" --> nodeID2. Node description must always be surrounded with double quotation marks')
|
||||
'{%- endif %}
|
||||
{%- 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.")
|
||||
{%- endif %}
|
||||
@ -67,11 +67,11 @@ description: |
|
||||
title: |
|
||||
...
|
||||
{%- if enable_pr_diagram %}
|
||||
changes_diagram: |
|
||||
```mermaid
|
||||
flowchart LR
|
||||
...
|
||||
```
|
||||
changes_diagram: |
|
||||
```mermaid
|
||||
flowchart LR
|
||||
...
|
||||
```
|
||||
{%- endif %}
|
||||
{%- if enable_semantic_files_types %}
|
||||
pr_files:
|
||||
@ -155,11 +155,11 @@ description: |
|
||||
title: |
|
||||
...
|
||||
{%- if enable_pr_diagram %}
|
||||
changes_diagram: |
|
||||
```mermaid
|
||||
flowchart LR
|
||||
...
|
||||
```
|
||||
changes_diagram: |
|
||||
```mermaid
|
||||
flowchart LR
|
||||
...
|
||||
```
|
||||
{%- endif %}
|
||||
{%- if enable_semantic_files_types %}
|
||||
pr_files:
|
||||
|
@ -128,7 +128,7 @@ class PRDescription:
|
||||
pr_title, pr_body, changes_walkthrough, pr_file_changes = self._prepare_pr_answer()
|
||||
if not self.git_provider.is_supported(
|
||||
"publish_file_comments") or not get_settings().pr_description.inline_file_summary:
|
||||
pr_body += "\n\n" + changes_walkthrough
|
||||
pr_body += "\n\n" + changes_walkthrough + "___\n\n"
|
||||
get_logger().debug("PR output", artifact={"title": pr_title, "body": pr_body})
|
||||
|
||||
# Add help text if gfm_markdown is supported
|
||||
@ -169,7 +169,7 @@ class PRDescription:
|
||||
|
||||
# publish description
|
||||
if get_settings().pr_description.publish_description_as_comment:
|
||||
full_markdown_description = f"## Title\n\n{pr_title}\n\n___\n{pr_body}"
|
||||
full_markdown_description = f"## Title\n\n{pr_title.strip()}\n\n___\n{pr_body}"
|
||||
if get_settings().pr_description.publish_description_as_comment_persistent:
|
||||
self.git_provider.publish_persistent_comment(full_markdown_description,
|
||||
initial_header="## Title",
|
||||
@ -179,7 +179,7 @@ class PRDescription:
|
||||
else:
|
||||
self.git_provider.publish_comment(full_markdown_description)
|
||||
else:
|
||||
self.git_provider.publish_description(pr_title, pr_body)
|
||||
self.git_provider.publish_description(pr_title.strip(), pr_body)
|
||||
|
||||
# publish final update message
|
||||
if (get_settings().pr_description.final_update_message and not get_settings().config.get('is_auto_command', False)):
|
||||
@ -331,7 +331,8 @@ class PRDescription:
|
||||
else:
|
||||
original_prediction_dict = original_prediction_loaded
|
||||
if original_prediction_dict:
|
||||
filenames_predicted = [file.get('filename', '').strip() for file in original_prediction_dict.get('pr_files', [])]
|
||||
files = original_prediction_dict.get('pr_files', [])
|
||||
filenames_predicted = [file.get('filename', '').strip() for file in files if isinstance(file, dict)]
|
||||
else:
|
||||
filenames_predicted = []
|
||||
|
||||
@ -555,15 +556,11 @@ class PRDescription:
|
||||
"""
|
||||
|
||||
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
|
||||
markdown_text = ""
|
||||
# Don't display 'PR Labels'
|
||||
if 'labels' in self.data and self.git_provider.is_supported("get_labels"):
|
||||
self.data.pop('labels')
|
||||
if not get_settings().pr_description.enable_pr_type:
|
||||
self.data.pop('type')
|
||||
for key, value in self.data.items():
|
||||
markdown_text += f"## **{key}**\n\n"
|
||||
markdown_text += f"{value}\n\n"
|
||||
|
||||
# Remove the 'PR Title' key from the dictionary
|
||||
ai_title = self.data.pop('title', self.vars["title"])
|
||||
@ -579,6 +576,10 @@ class PRDescription:
|
||||
pr_body, changes_walkthrough = "", ""
|
||||
pr_file_changes = []
|
||||
for idx, (key, value) in enumerate(self.data.items()):
|
||||
if key == 'changes_diagram':
|
||||
pr_body += f"### {PRDescriptionHeader.DIAGRAM_WALKTHROUGH.value}\n\n"
|
||||
pr_body += f"{value}\n\n"
|
||||
continue
|
||||
if key == 'pr_files':
|
||||
value = self.file_label_dict
|
||||
else:
|
||||
@ -597,9 +598,15 @@ class PRDescription:
|
||||
pr_body += f'- `{filename}`: {description}\n'
|
||||
if self.git_provider.is_supported("gfm_markdown"):
|
||||
pr_body += "</details>\n"
|
||||
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 = f"{PRDescriptionHeader.CHANGES_WALKTHROUGH.value}\n{changes_walkthrough}"
|
||||
elif 'pr_files' in key.lower() and get_settings().pr_description.enable_semantic_files_types: # 'File Walkthrough' section
|
||||
changes_walkthrough_table, pr_file_changes = self.process_pr_files_prediction(changes_walkthrough, value)
|
||||
if get_settings().pr_description.get('file_table_collapsible_open_by_default', False):
|
||||
initial_status = " open"
|
||||
else:
|
||||
initial_status = ""
|
||||
changes_walkthrough = f"<details{initial_status}> <summary><h3> {PRDescriptionHeader.FILE_WALKTHROUGH.value}</h3></summary>\n\n"
|
||||
changes_walkthrough += f"{changes_walkthrough_table}\n\n"
|
||||
changes_walkthrough += "</details>\n\n"
|
||||
elif key.lower().strip() == 'description':
|
||||
if isinstance(value, list):
|
||||
value = ', '.join(v.rstrip() for v in value)
|
||||
@ -633,14 +640,19 @@ class PRDescription:
|
||||
artifact={"file": file})
|
||||
continue
|
||||
filename = file['filename'].replace("'", "`").replace('"', '`')
|
||||
changes_summary = file.get('changes_summary', "").strip()
|
||||
changes_summary = file.get('changes_summary', "")
|
||||
if not changes_summary:
|
||||
get_logger().warning(f"Empty changes summary in file label dict, skipping file",
|
||||
artifact={"file": file})
|
||||
continue
|
||||
changes_summary = changes_summary.strip()
|
||||
changes_title = file['changes_title'].strip()
|
||||
label = file.get('label').strip().lower()
|
||||
if label not in file_label_dict:
|
||||
file_label_dict[label] = []
|
||||
file_label_dict[label].append((filename, changes_title, changes_summary))
|
||||
except Exception as e:
|
||||
get_logger().error(f"Error preparing file label dict {self.pr_id}: {e}")
|
||||
get_logger().exception(f"Error preparing file label dict {self.pr_id}")
|
||||
pass
|
||||
return file_label_dict
|
||||
|
||||
@ -720,7 +732,7 @@ class PRDescription:
|
||||
pr_body += """</tr></tbody></table>"""
|
||||
|
||||
except Exception as e:
|
||||
get_logger().error(f"Error processing PR files to markdown {self.pr_id}: {str(e)}")
|
||||
get_logger().error(f"Error processing pr files to markdown {self.pr_id}: {str(e)}")
|
||||
pass
|
||||
return pr_body, pr_comments
|
||||
|
||||
@ -776,14 +788,21 @@ def insert_br_after_x_chars(text: str, x=70):
|
||||
if count_chars_without_html(text) < x:
|
||||
return text
|
||||
|
||||
is_list = text.lstrip().startswith(("- ", "* "))
|
||||
|
||||
# replace odd instances of ` with <code> and even instances of ` with </code>
|
||||
text = replace_code_tags(text)
|
||||
|
||||
# convert list items to <li>
|
||||
if text.startswith("- ") or text.startswith("* "):
|
||||
text = "<li>" + text[2:]
|
||||
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
|
||||
text = text.replace("\n* ", '<br><li> ').replace("\n * ", '<br><li> ')
|
||||
# convert list items to <li> only if the text is identified as a list
|
||||
if is_list:
|
||||
# To handle lists that start with indentation
|
||||
leading_whitespace = text[:len(text) - len(text.lstrip())]
|
||||
body = text.lstrip()
|
||||
body = "<li>" + body[2:]
|
||||
text = leading_whitespace + body
|
||||
|
||||
text = text.replace("\n- ", '<br><li> ').replace("\n - ", '<br><li> ')
|
||||
text = text.replace("\n* ", '<br><li> ').replace("\n * ", '<br><li> ')
|
||||
|
||||
# convert new lines to <br>
|
||||
text = text.replace("\n", '<br>')
|
||||
@ -823,7 +842,13 @@ def insert_br_after_x_chars(text: str, x=70):
|
||||
is_inside_code = True
|
||||
if "</code>" in word:
|
||||
is_inside_code = False
|
||||
return ''.join(new_text).strip()
|
||||
|
||||
processed_text = ''.join(new_text).strip()
|
||||
|
||||
if is_list:
|
||||
processed_text = f"<ul>{processed_text}</ul>"
|
||||
|
||||
return processed_text
|
||||
|
||||
|
||||
def replace_code_tags(text):
|
||||
|
@ -51,7 +51,7 @@ class TestConvertToMarkdown:
|
||||
input_data = {'review': {
|
||||
'estimated_effort_to_review_[1-5]': '1, because the changes are minimal and straightforward, focusing on a single functionality addition.\n',
|
||||
'relevant_tests': 'No\n', 'possible_issues': 'No\n', 'security_concerns': 'No\n'}}
|
||||
|
||||
|
||||
expected_output = textwrap.dedent(f"""\
|
||||
{PRReviewHeader.REGULAR.value} 🔍
|
||||
|
||||
@ -67,12 +67,12 @@ class TestConvertToMarkdown:
|
||||
""")
|
||||
|
||||
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
|
||||
|
||||
|
||||
def test_simple_dictionary_input_without_gfm_supported(self):
|
||||
input_data = {'review': {
|
||||
'estimated_effort_to_review_[1-5]': '1, because the changes are minimal and straightforward, focusing on a single functionality addition.\n',
|
||||
'relevant_tests': 'No\n', 'possible_issues': 'No\n', 'security_concerns': 'No\n'}}
|
||||
|
||||
|
||||
expected_output = textwrap.dedent("""\
|
||||
## PR Reviewer Guide 🔍
|
||||
|
||||
@ -89,74 +89,74 @@ class TestConvertToMarkdown:
|
||||
""")
|
||||
|
||||
assert convert_to_markdown_v2(input_data, gfm_supported=False).strip() == expected_output.strip()
|
||||
|
||||
|
||||
def test_key_issues_to_review(self):
|
||||
input_data = {'review': {
|
||||
'key_issues_to_review': [
|
||||
{
|
||||
'relevant_file' : 'src/utils.py',
|
||||
'issue_header' : 'Code Smell',
|
||||
'issue_content' : 'The function is too long and complex.',
|
||||
'relevant_file': 'src/utils.py',
|
||||
'issue_header': 'Code Smell',
|
||||
'issue_content': 'The function is too long and complex.',
|
||||
'start_line': 30,
|
||||
'end_line': 50,
|
||||
}
|
||||
]
|
||||
}}
|
||||
mock_git_provider = Mock()
|
||||
reference_link = 'https://github.com/qodo/pr-agent/pull/1/files#diff-hashvalue-R174'
|
||||
reference_link = 'https://github.com/qodo/pr-agent/pull/1/files#diff-hashvalue-R174'
|
||||
mock_git_provider.get_line_link.return_value = reference_link
|
||||
|
||||
expected_output = textwrap.dedent(f"""\
|
||||
## PR Reviewer Guide 🔍
|
||||
|
||||
|
||||
Here are some key observations to aid the review process:
|
||||
|
||||
|
||||
<table>
|
||||
<tr><td>⚡ <strong>Recommended focus areas for review</strong><br><br>
|
||||
|
||||
|
||||
<a href='{reference_link}'><strong>Code Smell</strong></a><br>The function is too long and complex.
|
||||
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
""")
|
||||
|
||||
|
||||
assert convert_to_markdown_v2(input_data, git_provider=mock_git_provider).strip() == expected_output.strip()
|
||||
mock_git_provider.get_line_link.assert_called_with('src/utils.py', 30, 50)
|
||||
|
||||
|
||||
def test_ticket_compliance(self):
|
||||
input_data = {'review': {
|
||||
'ticket_compliance_check': [
|
||||
{
|
||||
'ticket_url': 'https://example.com/ticket/123',
|
||||
'ticket_requirements': '- Requirement 1\n- Requirement 2\n',
|
||||
'fully_compliant_requirements': '- Requirement 1\n- Requirement 2\n',
|
||||
'ticket_requirements': '- Requirement 1\n- Requirement 2\n',
|
||||
'fully_compliant_requirements': '- Requirement 1\n- Requirement 2\n',
|
||||
'not_compliant_requirements': '',
|
||||
'requires_further_human_verification': '',
|
||||
}
|
||||
]
|
||||
}}
|
||||
|
||||
|
||||
expected_output = textwrap.dedent("""\
|
||||
## PR Reviewer Guide 🔍
|
||||
|
||||
|
||||
Here are some key observations to aid the review process:
|
||||
|
||||
|
||||
<table>
|
||||
<tr><td>
|
||||
|
||||
|
||||
**🎫 Ticket compliance analysis ✅**
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**[123](https://example.com/ticket/123) - Fully compliant**
|
||||
|
||||
|
||||
Compliant requirements:
|
||||
|
||||
|
||||
- Requirement 1
|
||||
- Requirement 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
""")
|
||||
@ -179,43 +179,43 @@ class TestConvertToMarkdown:
|
||||
],
|
||||
'title': 'Bug Fix',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
expected_output = textwrap.dedent("""\
|
||||
## PR Reviewer Guide 🔍
|
||||
|
||||
|
||||
Here are some key observations to aid the review process:
|
||||
|
||||
<table>
|
||||
<tr><td>🔀 <strong>Multiple PR themes</strong><br><br>
|
||||
|
||||
|
||||
<details><summary>
|
||||
Sub-PR theme: <b>Refactoring</b></summary>
|
||||
|
||||
|
||||
___
|
||||
|
||||
|
||||
Relevant files:
|
||||
|
||||
|
||||
- src/file1.py
|
||||
- src/file2.py
|
||||
___
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary>
|
||||
Sub-PR theme: <b>Bug Fix</b></summary>
|
||||
|
||||
|
||||
___
|
||||
|
||||
|
||||
Relevant files:
|
||||
|
||||
|
||||
- src/file3.py
|
||||
___
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
</td></tr>
|
||||
</table>
|
||||
""")
|
||||
@ -228,7 +228,6 @@ class TestConvertToMarkdown:
|
||||
|
||||
expected_output = ''
|
||||
|
||||
|
||||
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
|
||||
|
||||
def test_dictionary_with_empty_dictionaries(self):
|
||||
@ -236,16 +235,16 @@ class TestConvertToMarkdown:
|
||||
|
||||
expected_output = ''
|
||||
|
||||
|
||||
assert convert_to_markdown_v2(input_data).strip() == expected_output.strip()
|
||||
|
||||
|
||||
class TestBR:
|
||||
def test_br1(self):
|
||||
file_change_description = '- Imported `FilePatchInfo` and `EDIT_TYPE` from `pr_agent.algo.types` instead of `pr_agent.git_providers.git_provider`.'
|
||||
file_change_description_br = insert_br_after_x_chars(file_change_description)
|
||||
expected_output = ('<li>Imported <code>FilePatchInfo</code> and <code>EDIT_TYPE</code> from '
|
||||
expected_output = ('<ul><li>Imported <code>FilePatchInfo</code> and <code>EDIT_TYPE</code> from '
|
||||
'<code>pr_agent.algo.types</code> instead <br>of '
|
||||
'<code>pr_agent.git_providers.git_provider</code>.')
|
||||
'<code>pr_agent.git_providers.git_provider</code>.</ul>')
|
||||
assert file_change_description_br == expected_output
|
||||
# print("-----")
|
||||
# print(file_change_description_br)
|
||||
@ -255,9 +254,9 @@ class TestBR:
|
||||
'- Created a - new -class `ColorPaletteResourcesCollection ColorPaletteResourcesCollection '
|
||||
'ColorPaletteResourcesCollection ColorPaletteResourcesCollection`')
|
||||
file_change_description_br = insert_br_after_x_chars(file_change_description)
|
||||
expected_output = ('<li>Created a - new -class <code>ColorPaletteResourcesCollection </code><br><code>'
|
||||
expected_output = ('<ul><li>Created a - new -class <code>ColorPaletteResourcesCollection </code><br><code>'
|
||||
'ColorPaletteResourcesCollection ColorPaletteResourcesCollection '
|
||||
'</code><br><code>ColorPaletteResourcesCollection</code>')
|
||||
'</code><br><code>ColorPaletteResourcesCollection</code></ul>')
|
||||
assert file_change_description_br == expected_output
|
||||
# print("-----")
|
||||
# print(file_change_description_br)
|
||||
|
Reference in New Issue
Block a user