mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-02 11:50:37 +08:00
Compare commits
61 Commits
Author | SHA1 | Date | |
---|---|---|---|
6dc38e5bca | |||
f7efa2c7c7 | |||
e1a112d26e | |||
1b46d64d71 | |||
38eda2f7b6 | |||
53b9c8ec97 | |||
7e8e95b748 | |||
7f51661e64 | |||
70023d2c4f | |||
c5d34f5ad5 | |||
8d3e51c205 | |||
b213753420 | |||
2eb8019325 | |||
9115cb7d31 | |||
62adad8f12 | |||
56f7ae0b46 | |||
446c1fb49a | |||
7d50625bd6 | |||
bd9ddc8b86 | |||
dd4fe4dcb4 | |||
1c174f263f | |||
d860e17b3b | |||
f83970bc6b | |||
87a245bf9c | |||
2d1afc634e | |||
e4f477dae0 | |||
8e210f8ea0 | |||
9c87056263 | |||
3251f19a19 | |||
299a2c89d1 | |||
c7241ca093 | |||
1a00e61239 | |||
f166e7f497 | |||
8dc08e4596 | |||
ead2c9273f | |||
5062543325 | |||
35e865bfb6 | |||
abb576c84f | |||
2d61ff7b88 | |||
e75b863f3b | |||
849cb2ea5a | |||
ab80677e3a | |||
bd7017d630 | |||
6e2bc01294 | |||
22c16f586b | |||
a42e3331d8 | |||
e14834c84e | |||
915a1c563b | |||
bc99cf83dd | |||
d00cbd4da7 | |||
721ff18a63 | |||
1a003fe4d3 | |||
68f78e1a30 | |||
7759d1d3fc | |||
738f9856a4 | |||
fbce8cd2f5 | |||
ea63c8e63a | |||
d8fea6afc4 | |||
ff16e1cd26 | |||
9b5ae1a322 | |||
8b8464163d |
19
README.md
19
README.md
@ -57,24 +57,33 @@ Add automated PR reviews to your repository with a simple workflow file using [G
|
|||||||
### CLI Usage
|
### CLI Usage
|
||||||
Run PR-Agent locally on your repository via command line: [Local CLI setup guide](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#local-repo-cli)
|
Run PR-Agent locally on your repository via command line: [Local CLI setup guide](https://qodo-merge-docs.qodo.ai/usage-guide/automations_and_usage/#local-repo-cli)
|
||||||
|
|
||||||
|
### Qodo Merge as post-commit in your local IDE
|
||||||
|
See [here](https://github.com/qodo-ai/agents/tree/main/agents/qodo-merge-post-commit)
|
||||||
|
|
||||||
### Discover Qodo Merge 💎
|
### Discover Qodo Merge 💎
|
||||||
Zero-setup hosted solution with advanced features and priority support
|
Zero-setup hosted solution with advanced features and priority support
|
||||||
- [Intro and Installation guide](https://qodo-merge-docs.qodo.ai/installation/qodo_merge/)
|
- [Intro and Installation guide](https://qodo-merge-docs.qodo.ai/installation/qodo_merge/)
|
||||||
- [Plans & Pricing](https://www.qodo.ai/pricing/)
|
- [Plans & Pricing](https://www.qodo.ai/pricing/)
|
||||||
|
|
||||||
|
### Qodo Merge as a Post-commit in Your Local IDE
|
||||||
|
You can receive automatic feedback from Qodo Merge on your local IDE after each [commit](https://github.com/qodo-ai/agents/tree/main/agents/qodo-merge-post-commit)
|
||||||
|
|
||||||
|
|
||||||
## News and Updates
|
## News and Updates
|
||||||
|
|
||||||
|
## Jul 1, 2025
|
||||||
|
You can now receive automatic feedback from Qodo Merge in your local IDE after each commit. Read more about it [here](https://github.com/qodo-ai/agents/tree/main/agents/qodo-merge-post-commit).
|
||||||
|
|
||||||
|
## Jun 21, 2025
|
||||||
|
|
||||||
|
v0.30 was [released](https://github.com/qodo-ai/pr-agent/releases)
|
||||||
|
|
||||||
|
|
||||||
## Jun 3, 2025
|
## Jun 3, 2025
|
||||||
|
|
||||||
Qodo Merge now offers a simplified free tier 💎.
|
Qodo Merge now offers a simplified free tier 💎.
|
||||||
Organizations can use Qodo Merge at no cost, with a [monthly limit](https://qodo-merge-docs.qodo.ai/installation/qodo_merge/#cloud-users) of 75 PR reviews per organization.
|
Organizations can use Qodo Merge at no cost, with a [monthly limit](https://qodo-merge-docs.qodo.ai/installation/qodo_merge/#cloud-users) of 75 PR reviews per organization.
|
||||||
|
|
||||||
## May 17, 2025
|
|
||||||
|
|
||||||
- v0.29 was [released](https://github.com/qodo-ai/pr-agent/releases)
|
|
||||||
- `Qodo Merge Pull Request Benchmark` was [released](https://qodo-merge-docs.qodo.ai/pr_benchmark/). This benchmark evaluates and compares the performance of LLMs in analyzing pull request code.
|
|
||||||
- `Recent Updates and Future Roadmap` page was added to the [Qodo Merge Docs](https://qodo-merge-docs.qodo.ai/recent_updates/)
|
|
||||||
|
|
||||||
## Apr 30, 2025
|
## Apr 30, 2025
|
||||||
|
|
||||||
|
@ -202,7 +202,23 @@ h1 {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('load', function() {
|
window.addEventListener('load', function() {
|
||||||
function displayResults(responseText) {
|
function extractText(responseText) {
|
||||||
|
try {
|
||||||
|
console.log('responseText: ', responseText);
|
||||||
|
const results = JSON.parse(responseText);
|
||||||
|
const msg = results.message;
|
||||||
|
|
||||||
|
if (!msg || msg.trim() === '') {
|
||||||
|
return "No results found";
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing results:', error);
|
||||||
|
throw new Error("Failed parsing response message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayResults(msg) {
|
||||||
const resultsContainer = document.getElementById('results');
|
const resultsContainer = document.getElementById('results');
|
||||||
const spinner = document.getElementById('spinner');
|
const spinner = document.getElementById('spinner');
|
||||||
const searchContainer = document.querySelector('.search-container');
|
const searchContainer = document.querySelector('.search-container');
|
||||||
@ -214,8 +230,6 @@ window.addEventListener('load', function() {
|
|||||||
searchContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
searchContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = JSON.parse(responseText);
|
|
||||||
|
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
breaks: true,
|
breaks: true,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
@ -223,7 +237,7 @@ window.addEventListener('load', function() {
|
|||||||
sanitize: false
|
sanitize: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const htmlContent = marked.parse(results.message);
|
const htmlContent = marked.parse(msg);
|
||||||
|
|
||||||
resultsContainer.className = 'markdown-content';
|
resultsContainer.className = 'markdown-content';
|
||||||
resultsContainer.innerHTML = htmlContent;
|
resultsContainer.innerHTML = htmlContent;
|
||||||
@ -242,7 +256,7 @@ window.addEventListener('load', function() {
|
|||||||
}, 100);
|
}, 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error parsing results:', error);
|
console.error('Error parsing results:', error);
|
||||||
resultsContainer.innerHTML = '<div class="error-message">Error processing results</div>';
|
resultsContainer.innerHTML = '<div class="error-message">Cannot process results</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,24 +289,25 @@ window.addEventListener('load', function() {
|
|||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
};
|
};
|
||||||
|
|
||||||
// const API_ENDPOINT = 'http://0.0.0.0:3000/api/v1/docs_help';
|
//const API_ENDPOINT = 'http://0.0.0.0:3000/api/v1/docs_help';
|
||||||
const API_ENDPOINT = 'https://help.merge.qodo.ai/api/v1/docs_help';
|
const API_ENDPOINT = 'https://help.merge.qodo.ai/api/v1/docs_help';
|
||||||
|
|
||||||
const response = await fetch(API_ENDPOINT, options);
|
const response = await fetch(API_ENDPOINT, options);
|
||||||
|
const responseText = await response.text();
|
||||||
|
const msg = extractText(responseText);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`An error (${response.status}) occurred during search: "${msg}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseText = await response.text();
|
displayResults(msg);
|
||||||
displayResults(responseText);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
spinner.style.display = 'none';
|
spinner.style.display = 'none';
|
||||||
resultsContainer.innerHTML = `
|
const errorDiv = document.createElement('div');
|
||||||
<div class="error-message">
|
errorDiv.className = 'error-message';
|
||||||
An error occurred while searching. Please try again later.
|
errorDiv.textContent = `${error}`;
|
||||||
</div>
|
resultsContainer.value = "";
|
||||||
`;
|
resultsContainer.appendChild(errorDiv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Incremental Update 💎
|
# Incremental Update 💎
|
||||||
|
|
||||||
`Supported Git Platforms: GitHub`
|
`Supported Git Platforms: GitHub, GitLab (Both cloud & server. For server: Version 17 and above)`
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
The Incremental Update feature helps users focus on feedback for their newest changes, making large PRs more manageable.
|
The Incremental Update feature helps users focus on feedback for their newest changes, making large PRs more manageable.
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
[Qodo Merge](https://www.codium.ai/pricing/){:target="_blank"} is a hosted version of the open-source [PR-Agent](https://github.com/Codium-ai/pr-agent){:target="_blank"}.
|
[Qodo Merge](https://www.codium.ai/pricing/){:target="_blank"} is a hosted version of the open-source [PR-Agent](https://github.com/Codium-ai/pr-agent){:target="_blank"}.
|
||||||
It is designed for companies and teams that require additional features and capabilities.
|
It is designed for companies and teams that require additional features and capabilities.
|
||||||
|
|
||||||
Free users receive a monthly quota of 75 PR reviews per git organization, while unlimited usage requires a paid subscription. See [details](https://qodo-merge-docs.qodo.ai/installation/qodo_merge/#cloud-users).
|
Free users receive a quota of 75 monthly PR feedbacks per git organization. Unlimited usage requires a paid subscription. See [details](https://qodo-merge-docs.qodo.ai/installation/qodo_merge/#cloud-users).
|
||||||
|
|
||||||
|
|
||||||
Qodo Merge provides the following benefits:
|
Qodo Merge provides the following benefits:
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ This approach provides not just a quantitative score but also a detailed analysi
|
|||||||
|
|
||||||
[//]: # ()
|
[//]: # ()
|
||||||
|
|
||||||
## Results
|
## PR Benchmark Results
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@ -67,6 +67,12 @@ This approach provides not just a quantitative score but also a detailed analysi
|
|||||||
<td style="text-align:left;"></td>
|
<td style="text-align:left;"></td>
|
||||||
<td style="text-align:center;"><b>39.0</b></td>
|
<td style="text-align:center;"><b>39.0</b></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="text-align:left;">Codex-mini</td>
|
||||||
|
<td style="text-align:left;">2025-06-20</td>
|
||||||
|
<td style="text-align:left;"><a href="https://platform.openai.com/docs/models/codex-mini-latest">unknown</a></td>
|
||||||
|
<td style="text-align:center;"><b>37.2</b></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="text-align:left;">Gemini-2.5-flash</td>
|
<td style="text-align:left;">Gemini-2.5-flash</td>
|
||||||
<td style="text-align:left;">2025-04-17</td>
|
<td style="text-align:left;">2025-04-17</td>
|
||||||
@ -196,7 +202,7 @@ weaknesses:
|
|||||||
- **Very low recall / shallow coverage:** In a large majority of cases it gives 0-1 suggestions and misses other evident, critical bugs highlighted by peer models, leading to inferior rankings.
|
- **Very low recall / shallow coverage:** In a large majority of cases it gives 0-1 suggestions and misses other evident, critical bugs highlighted by peer models, leading to inferior rankings.
|
||||||
- **Occasional incorrect or harmful fixes:** A noticeable subset of answers propose changes that break functionality or misunderstand the code (e.g. bad constant, wrong header logic, speculative rollbacks).
|
- **Occasional incorrect or harmful fixes:** A noticeable subset of answers propose changes that break functionality or misunderstand the code (e.g. bad constant, wrong header logic, speculative rollbacks).
|
||||||
- **Non-actionable placeholders:** Some “improved_code” sections contain comments or “…” rather than real patches, reducing practical value.
|
- **Non-actionable placeholders:** Some “improved_code” sections contain comments or “…” rather than real patches, reducing practical value.
|
||||||
-
|
|
||||||
### GPT-4.1
|
### GPT-4.1
|
||||||
|
|
||||||
Final score: **26.5**
|
Final score: **26.5**
|
||||||
@ -214,6 +220,22 @@ weaknesses:
|
|||||||
- **Occasional technical inaccuracies:** A noticeable subset of suggestions are wrong (mis-ordered assertions, harmful Bash `set` change, false dangling-reference claims) or carry metadata errors (mis-labeling files as “python”).
|
- **Occasional technical inaccuracies:** A noticeable subset of suggestions are wrong (mis-ordered assertions, harmful Bash `set` change, false dangling-reference claims) or carry metadata errors (mis-labeling files as “python”).
|
||||||
- **Repetitive / derivative fixes:** Many outputs duplicate earlier simplistic ideas (e.g., single null-check) without new insight, showing limited reasoning breadth.
|
- **Repetitive / derivative fixes:** Many outputs duplicate earlier simplistic ideas (e.g., single null-check) without new insight, showing limited reasoning breadth.
|
||||||
|
|
||||||
|
### OpenAI codex-mini
|
||||||
|
|
||||||
|
final score: **37.2**
|
||||||
|
|
||||||
|
strengths:
|
||||||
|
|
||||||
|
- **Can spot high-impact defects:** When it “locks on”, codex-mini often identifies the main runtime or security regression (e.g., race-conditions, logic inversions, blocking I/O, resource leaks) and proposes a minimal, direct patch that compiles and respects neighbouring style.
|
||||||
|
- **Produces concise, scoped fixes:** Valid answers usually stay within the allowed 3-suggestion limit, reference only the added lines, and contain clear before/after snippets that reviewers can apply verbatim.
|
||||||
|
- **Occasional broad coverage:** In a minority of cases the model catches multiple independent issues (logic + tests + docs) and outperforms every baseline answer, showing good contextual understanding of heterogeneous diffs.
|
||||||
|
|
||||||
|
weaknesses:
|
||||||
|
|
||||||
|
- **Output instability / format errors:** A very large share of responses are unusable—plain refusals, shell commands, or malformed/empty YAML—indicating brittle adherence to the required schema and tanking overall usefulness.
|
||||||
|
- **Critical-miss rate:** Even when the format is correct the model frequently overlooks the single most serious bug the diff introduces, instead focusing on stylistic nits or speculative refactors.
|
||||||
|
- **Introduces new problems:** Several suggestions add unsupported APIs, undeclared variables, wrong types, or break compilation, hurting trust in the recommendations.
|
||||||
|
- **Rule violations:** It often edits lines outside the diff, exceeds the 3-suggestion cap, or labels cosmetic tweaks as “critical”, showing inconsistent guideline compliance.
|
||||||
|
|
||||||
## Appendix - models used for generating the benchmark baseline
|
## Appendix - models used for generating the benchmark baseline
|
||||||
|
|
||||||
|
@ -1,23 +1,21 @@
|
|||||||
# Recent Updates and Future Roadmap
|
# Recent Updates and Future Roadmap
|
||||||
|
|
||||||
`Page last updated: 2025-06-01`
|
`Page last updated: 2025-07-01`
|
||||||
|
|
||||||
This page summarizes recent enhancements to Qodo Merge (last three months).
|
This page summarizes recent enhancements to Qodo Merge (last three months).
|
||||||
|
|
||||||
It also outlines our development roadmap for the upcoming three months. Please note that the roadmap is subject to change, and features may be adjusted, added, or reprioritized.
|
It also outlines our development roadmap for the upcoming three months. Please note that the roadmap is subject to change, and features may be adjusted, added, or reprioritized.
|
||||||
|
|
||||||
=== "Recent Updates"
|
=== "Recent Updates"
|
||||||
|
- **Receiving Qodo Merge feedback locally**: You can receive automatic feedback from Qodo Merge on your local IDE after each commit. ([Learn more](https://github.com/qodo-ai/agents/tree/main/agents/qodo-merge-post-commit)).
|
||||||
|
- **Mermaid Diagrams**: Qodo Merge now generates by default Mermaid diagrams for PRs, providing a visual representation of code changes. ([Learn more](https://qodo-merge-docs.qodo.ai/tools/describe/#sequence-diagram-support))
|
||||||
|
- **Best Practices Hierarchy**: Introducing support for structured best practices, such as for folders in monorepos or a unified best practice file for a group of repositories. ([Learn more](https://qodo-merge-docs.qodo.ai/tools/improve/#global-hierarchical-best-practices))
|
||||||
- **Simplified Free Tier**: Qodo Merge now offers a simplified free tier with a monthly limit of 75 PR reviews per organization, replacing the previous two-week trial. ([Learn more](https://qodo-merge-docs.qodo.ai/installation/qodo_merge/#cloud-users))
|
- **Simplified Free Tier**: Qodo Merge now offers a simplified free tier with a monthly limit of 75 PR reviews per organization, replacing the previous two-week trial. ([Learn more](https://qodo-merge-docs.qodo.ai/installation/qodo_merge/#cloud-users))
|
||||||
- **CLI Endpoint**: A new Qodo Merge endpoint that accepts a lists of before/after code changes, executes Qodo Merge commands, and return the results. Currently available for enterprise customers. Contact [Qodo](https://www.qodo.ai/contact/) for more information.
|
- **CLI Endpoint**: A new Qodo Merge endpoint that accepts a lists of before/after code changes, executes Qodo Merge commands, and return the results. Currently available for enterprise customers. Contact [Qodo](https://www.qodo.ai/contact/) for more information.
|
||||||
- **Linear tickets support**: Qodo Merge now supports Linear tickets. ([Learn more](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/#linear-integration))
|
- **Linear tickets support**: Qodo Merge now supports Linear tickets. ([Learn more](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/#linear-integration))
|
||||||
- **Smart Update**: Upon PR updates, Qodo Merge will offer tailored code suggestions, addressing both the entire PR and the specific incremental changes since the last feedback ([Learn more](https://qodo-merge-docs.qodo.ai/core-abilities/incremental_update//))
|
- **Smart Update**: Upon PR updates, Qodo Merge will offer tailored code suggestions, addressing both the entire PR and the specific incremental changes since the last feedback ([Learn more](https://qodo-merge-docs.qodo.ai/core-abilities/incremental_update//))
|
||||||
- **Qodo Merge Pull Request Benchmark** - evaluating the performance of LLMs in analyzing pull request code ([Learn more](https://qodo-merge-docs.qodo.ai/pr_benchmark/))
|
|
||||||
- **Chat on Suggestions**: Users can now chat with code suggestions ([Learn more](https://qodo-merge-docs.qodo.ai/tools/improve/#chat-on-code-suggestions))
|
|
||||||
- **Scan Repo Discussions Tool**: A new tool that analyzes past code discussions to generate a `best_practices.md` file, distilling key insights and recommendations. ([Learn more](https://qodo-merge-docs.qodo.ai/tools/scan_repo_discussions/))
|
|
||||||
|
|
||||||
|
|
||||||
=== "Future Roadmap"
|
=== "Future Roadmap"
|
||||||
- **Best Practices Hierarchy**: Introducing support for structured best practices, such as for folders in monorepos or a unified best practice file for a group of repositories.
|
|
||||||
- **Enhanced `review` tool**: Enhancing the `review` tool validate compliance across multiple categories including security, tickets, and custom best practices.
|
- **Enhanced `review` tool**: Enhancing the `review` tool validate compliance across multiple categories including security, tickets, and custom best practices.
|
||||||
- **Smarter context retrieval**: Leverage AST and LSP analysis to gather relevant context from across the entire repository.
|
- **Smarter context retrieval**: Leverage AST and LSP analysis to gather relevant context from across the entire repository.
|
||||||
- **Enhanced portal experience**: Improved user experience in the Qodo Merge portal with new options and capabilities.
|
- **Enhanced portal experience**: Improved user experience in the Qodo Merge portal with new options and capabilities.
|
||||||
|
@ -56,20 +56,27 @@ Everything below this marker is treated as previously auto-generated content and
|
|||||||
|
|
||||||
{width=512}
|
{width=512}
|
||||||
|
|
||||||
### Sequence Diagram Support
|
## Sequence Diagram Support
|
||||||
When the `enable_pr_diagram` option is enabled in your configuration, the `/describe` tool will include a `Mermaid` sequence diagram in the PR description.
|
The `/describe` tool includes a Mermaid sequence diagram showing component/function interactions.
|
||||||
|
|
||||||
This diagram represents interactions between components/functions based on the diff content.
|
This option is enabled by default via the `pr_description.enable_pr_diagram` param.
|
||||||
|
|
||||||
### How to enable
|
|
||||||
|
|
||||||
In your configuration:
|
[//]: # (### How to enable\disable)
|
||||||
|
|
||||||
```
|
[//]: # ()
|
||||||
toml
|
[//]: # (In your configuration:)
|
||||||
[pr_description]
|
|
||||||
enable_pr_diagram = true
|
[//]: # ()
|
||||||
```
|
[//]: # (```)
|
||||||
|
|
||||||
|
[//]: # (toml)
|
||||||
|
|
||||||
|
[//]: # ([pr_description])
|
||||||
|
|
||||||
|
[//]: # (enable_pr_diagram = true)
|
||||||
|
|
||||||
|
[//]: # (```)
|
||||||
|
|
||||||
## Configuration options
|
## Configuration options
|
||||||
|
|
||||||
@ -126,7 +133,7 @@ enable_pr_diagram = true
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>enable_pr_diagram</b></td>
|
<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 false.</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>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -437,9 +437,26 @@ dual_publishing_score_threshold = x
|
|||||||
|
|
||||||
Where x represents the minimum score threshold (>=) for suggestions to be presented as committable PR comments in addition to the table. Default is -1 (disabled).
|
Where x represents the minimum score threshold (>=) for suggestions to be presented as committable PR comments in addition to the table. Default is -1 (disabled).
|
||||||
|
|
||||||
|
### Controlling suggestions depth
|
||||||
|
|
||||||
|
> `💎 feature`
|
||||||
|
|
||||||
|
You can control the depth and comprehensiveness of the code suggestions by using the `pr_code_suggestions.suggestions_depth` parameter.
|
||||||
|
|
||||||
|
Available options:
|
||||||
|
|
||||||
|
- `selective` - Shows only suggestions above a score threshold of 6
|
||||||
|
- `regular` - Default mode with balanced suggestion coverage
|
||||||
|
- `exhaustive` - Provides maximum suggestion comprehensiveness
|
||||||
|
|
||||||
|
(Alternatively, use numeric values: 1, 2, or 3 respectively)
|
||||||
|
|
||||||
|
We recommend starting with `regular` mode, then exploring `exhaustive` mode, which can provide more comprehensive suggestions and enhanced bug detection.
|
||||||
|
|
||||||
|
|
||||||
### Self-review
|
### Self-review
|
||||||
|
|
||||||
> `💎 feature` Platforms supported: GitHub, GitLab
|
> `💎 feature. Platforms supported: GitHub, GitLab`
|
||||||
|
|
||||||
If you set in a configuration file:
|
If you set in a configuration file:
|
||||||
|
|
||||||
@ -521,6 +538,10 @@ Note: Chunking is primarily relevant for large PRs. For most PRs (up to 600 line
|
|||||||
<td><b>enable_chat_in_code_suggestions</b></td>
|
<td><b>enable_chat_in_code_suggestions</b></td>
|
||||||
<td>If set to true, QM bot will interact with comments made on code changes it has proposed. Default is true.</td>
|
<td>If set to true, QM bot will interact with comments made on code changes it has proposed. Default is true.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b>suggestions_depth 💎</b></td>
|
||||||
|
<td> Controls the depth of the suggestions. Can be set to 'selective', 'regular', or 'exhaustive'. Default is 'regular'.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><b>dual_publishing_score_threshold</b></td>
|
<td><b>dual_publishing_score_threshold</b></td>
|
||||||
<td>Minimum score threshold for suggestions to be presented as committable PR comments in addition to the table. Default is -1 (disabled).</td>
|
<td>Minimum score threshold for suggestions to be presented as committable PR comments in addition to the table. Default is -1 (disabled).</td>
|
||||||
|
@ -250,3 +250,15 @@ Where the `ignore_pr_authors` is a list of usernames that you want to ignore.
|
|||||||
|
|
||||||
!!! note
|
!!! 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.
|
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.
|
||||||
|
|
||||||
|
### Ignoring Generated Files by Language/Framework
|
||||||
|
|
||||||
|
To automatically exclude files generated by specific languages or frameworks, you can add the following to your `configuration.toml` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
[config]
|
||||||
|
ignore_language_framework = ['protobuf', ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
You can view the list of auto-generated file patterns in [`generated_code_ignore.toml`](https://github.com/qodo-ai/pr-agent/blob/main/pr_agent/settings/generated_code_ignore.toml).
|
||||||
|
Files matching these glob patterns will be automatically excluded from PR Agent analysis.
|
@ -232,6 +232,14 @@ AWS_SECRET_ACCESS_KEY="..."
|
|||||||
AWS_REGION_NAME="..."
|
AWS_REGION_NAME="..."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also use the new Meta Llama 4 models available on Amazon Bedrock:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[config] # in configuration.toml
|
||||||
|
model="bedrock/us.meta.llama4-scout-17b-instruct-v1:0"
|
||||||
|
fallback_models=["bedrock/us.meta.llama4-maverick-17b-instruct-v1:0"]
|
||||||
|
```
|
||||||
|
|
||||||
See [litellm](https://docs.litellm.ai/docs/providers/bedrock#usage) documentation for more information about the environment variables required for Amazon Bedrock.
|
See [litellm](https://docs.litellm.ai/docs/providers/bedrock#usage) documentation for more information about the environment variables required for Amazon Bedrock.
|
||||||
|
|
||||||
### DeepSeek
|
### DeepSeek
|
||||||
|
@ -51,7 +51,7 @@ class PRAgent:
|
|||||||
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
|
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
|
||||||
self.ai_handler = ai_handler # will be initialized in run_action
|
self.ai_handler = ai_handler # will be initialized in run_action
|
||||||
|
|
||||||
async def handle_request(self, pr_url, request, notify=None) -> bool:
|
async def _handle_request(self, pr_url, request, notify=None) -> bool:
|
||||||
# First, apply repo specific settings if exists
|
# First, apply repo specific settings if exists
|
||||||
apply_repo_settings(pr_url)
|
apply_repo_settings(pr_url)
|
||||||
|
|
||||||
@ -117,3 +117,10 @@ class PRAgent:
|
|||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def handle_request(self, pr_url, request, notify=None) -> bool:
|
||||||
|
try:
|
||||||
|
return await self._handle_request(pr_url, request, notify)
|
||||||
|
except:
|
||||||
|
get_logger().exception("Failed to process the command.")
|
||||||
|
return False
|
||||||
|
@ -62,19 +62,23 @@ MAX_TOKENS = {
|
|||||||
'vertex_ai/gemini-2.5-pro-preview-03-25': 1048576,
|
'vertex_ai/gemini-2.5-pro-preview-03-25': 1048576,
|
||||||
'vertex_ai/gemini-2.5-pro-preview-05-06': 1048576,
|
'vertex_ai/gemini-2.5-pro-preview-05-06': 1048576,
|
||||||
'vertex_ai/gemini-2.5-pro-preview-06-05': 1048576,
|
'vertex_ai/gemini-2.5-pro-preview-06-05': 1048576,
|
||||||
|
'vertex_ai/gemini-2.5-pro': 1048576,
|
||||||
'vertex_ai/gemini-1.5-flash': 1048576,
|
'vertex_ai/gemini-1.5-flash': 1048576,
|
||||||
'vertex_ai/gemini-2.0-flash': 1048576,
|
'vertex_ai/gemini-2.0-flash': 1048576,
|
||||||
'vertex_ai/gemini-2.5-flash-preview-04-17': 1048576,
|
'vertex_ai/gemini-2.5-flash-preview-04-17': 1048576,
|
||||||
'vertex_ai/gemini-2.5-flash-preview-05-20': 1048576,
|
'vertex_ai/gemini-2.5-flash-preview-05-20': 1048576,
|
||||||
|
'vertex_ai/gemini-2.5-flash': 1048576,
|
||||||
'vertex_ai/gemma2': 8200,
|
'vertex_ai/gemma2': 8200,
|
||||||
'gemini/gemini-1.5-pro': 1048576,
|
'gemini/gemini-1.5-pro': 1048576,
|
||||||
'gemini/gemini-1.5-flash': 1048576,
|
'gemini/gemini-1.5-flash': 1048576,
|
||||||
'gemini/gemini-2.0-flash': 1048576,
|
'gemini/gemini-2.0-flash': 1048576,
|
||||||
'gemini/gemini-2.5-flash-preview-04-17': 1048576,
|
'gemini/gemini-2.5-flash-preview-04-17': 1048576,
|
||||||
'gemini/gemini-2.5-flash-preview-05-20': 1048576,
|
'gemini/gemini-2.5-flash-preview-05-20': 1048576,
|
||||||
|
'gemini/gemini-2.5-flash': 1048576,
|
||||||
'gemini/gemini-2.5-pro-preview-03-25': 1048576,
|
'gemini/gemini-2.5-pro-preview-03-25': 1048576,
|
||||||
'gemini/gemini-2.5-pro-preview-05-06': 1048576,
|
'gemini/gemini-2.5-pro-preview-05-06': 1048576,
|
||||||
'gemini/gemini-2.5-pro-preview-06-05': 1048576,
|
'gemini/gemini-2.5-pro-preview-06-05': 1048576,
|
||||||
|
'gemini/gemini-2.5-pro': 1048576,
|
||||||
'codechat-bison': 6144,
|
'codechat-bison': 6144,
|
||||||
'codechat-bison-32k': 32000,
|
'codechat-bison-32k': 32000,
|
||||||
'anthropic.claude-instant-v1': 100000,
|
'anthropic.claude-instant-v1': 100000,
|
||||||
@ -109,6 +113,8 @@ MAX_TOKENS = {
|
|||||||
'claude-3-5-sonnet': 100000,
|
'claude-3-5-sonnet': 100000,
|
||||||
'groq/meta-llama/llama-4-scout-17b-16e-instruct': 131072,
|
'groq/meta-llama/llama-4-scout-17b-16e-instruct': 131072,
|
||||||
'groq/meta-llama/llama-4-maverick-17b-128e-instruct': 131072,
|
'groq/meta-llama/llama-4-maverick-17b-128e-instruct': 131072,
|
||||||
|
'bedrock/us.meta.llama4-scout-17b-instruct-v1:0': 128000,
|
||||||
|
'bedrock/us.meta.llama4-maverick-17b-instruct-v1:0': 128000,
|
||||||
'groq/llama3-8b-8192': 8192,
|
'groq/llama3-8b-8192': 8192,
|
||||||
'groq/llama3-70b-8192': 8192,
|
'groq/llama3-70b-8192': 8192,
|
||||||
'groq/llama-3.1-8b-instant': 8192,
|
'groq/llama-3.1-8b-instant': 8192,
|
||||||
|
@ -2,6 +2,7 @@ import fnmatch
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
|
|
||||||
def filter_ignored(files, platform = 'github'):
|
def filter_ignored(files, platform = 'github'):
|
||||||
@ -17,7 +18,17 @@ def filter_ignored(files, platform = 'github'):
|
|||||||
glob_setting = get_settings().ignore.glob
|
glob_setting = get_settings().ignore.glob
|
||||||
if isinstance(glob_setting, str): # --ignore.glob=[.*utils.py], --ignore.glob=.*utils.py
|
if isinstance(glob_setting, str): # --ignore.glob=[.*utils.py], --ignore.glob=.*utils.py
|
||||||
glob_setting = glob_setting.strip('[]').split(",")
|
glob_setting = glob_setting.strip('[]').split(",")
|
||||||
patterns += [fnmatch.translate(glob) for glob in glob_setting]
|
patterns += translate_globs_to_regexes(glob_setting)
|
||||||
|
|
||||||
|
code_generators = get_settings().config.get('ignore_language_framework', [])
|
||||||
|
if isinstance(code_generators, str):
|
||||||
|
get_logger().warning("'ignore_language_framework' should be a list. Skipping language framework filtering.")
|
||||||
|
code_generators = []
|
||||||
|
for cg in code_generators:
|
||||||
|
glob_patterns = get_settings().generated_code.get(cg, [])
|
||||||
|
if isinstance(glob_patterns, str):
|
||||||
|
glob_patterns = [glob_patterns]
|
||||||
|
patterns += translate_globs_to_regexes(glob_patterns)
|
||||||
|
|
||||||
# compile all valid patterns
|
# compile all valid patterns
|
||||||
compiled_patterns = []
|
compiled_patterns = []
|
||||||
@ -66,3 +77,11 @@ def filter_ignored(files, platform = 'github'):
|
|||||||
print(f"Could not filter file list: {e}")
|
print(f"Could not filter file list: {e}")
|
||||||
|
|
||||||
return files
|
return files
|
||||||
|
|
||||||
|
def translate_globs_to_regexes(globs: list):
|
||||||
|
regexes = []
|
||||||
|
for pattern in globs:
|
||||||
|
regexes.append(fnmatch.translate(pattern))
|
||||||
|
if pattern.startswith("**/"): # cover root-level files
|
||||||
|
regexes.append(fnmatch.translate(pattern[3:]))
|
||||||
|
return regexes
|
||||||
|
@ -14,6 +14,7 @@ global_settings = Dynaconf(
|
|||||||
settings_files=[join(current_dir, f) for f in [
|
settings_files=[join(current_dir, f) for f in [
|
||||||
"settings/configuration.toml",
|
"settings/configuration.toml",
|
||||||
"settings/ignore.toml",
|
"settings/ignore.toml",
|
||||||
|
"settings/generated_code_ignore.toml",
|
||||||
"settings/language_extensions.toml",
|
"settings/language_extensions.toml",
|
||||||
"settings/pr_reviewer_prompts.toml",
|
"settings/pr_reviewer_prompts.toml",
|
||||||
"settings/pr_questions_prompts.toml",
|
"settings/pr_questions_prompts.toml",
|
||||||
|
@ -22,6 +22,7 @@ try:
|
|||||||
from azure.devops.connection import Connection
|
from azure.devops.connection import Connection
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext, CommentPosition)
|
from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext, CommentPosition)
|
||||||
|
from azure.devops.released.work_item_tracking import WorkItemTrackingClient
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from azure.identity import DefaultAzureCredential
|
from azure.identity import DefaultAzureCredential
|
||||||
from msrest.authentication import BasicAuthentication
|
from msrest.authentication import BasicAuthentication
|
||||||
@ -39,7 +40,7 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
"Azure DevOps provider is not available. Please install the required dependencies."
|
"Azure DevOps provider is not available. Please install the required dependencies."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.azure_devops_client = self._get_azure_devops_client()
|
self.azure_devops_client, self.azure_devops_board_client = self._get_azure_devops_client()
|
||||||
self.diff_files = None
|
self.diff_files = None
|
||||||
self.workspace_slug = None
|
self.workspace_slug = None
|
||||||
self.repo_slug = None
|
self.repo_slug = None
|
||||||
@ -566,7 +567,7 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
return workspace_slug, repo_slug, pr_number
|
return workspace_slug, repo_slug, pr_number
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_azure_devops_client() -> GitClient:
|
def _get_azure_devops_client() -> Tuple[GitClient, WorkItemTrackingClient]:
|
||||||
org = get_settings().azure_devops.get("org", None)
|
org = get_settings().azure_devops.get("org", None)
|
||||||
pat = get_settings().azure_devops.get("pat", None)
|
pat = get_settings().azure_devops.get("pat", None)
|
||||||
|
|
||||||
@ -588,13 +589,12 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
get_logger().error(f"No PAT found in settings, and Azure Default Authentication failed, error: {e}")
|
get_logger().error(f"No PAT found in settings, and Azure Default Authentication failed, error: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
credentials = BasicAuthentication("", auth_token)
|
|
||||||
|
|
||||||
credentials = BasicAuthentication("", auth_token)
|
credentials = BasicAuthentication("", auth_token)
|
||||||
azure_devops_connection = Connection(base_url=org, creds=credentials)
|
azure_devops_connection = Connection(base_url=org, creds=credentials)
|
||||||
azure_devops_client = azure_devops_connection.clients.get_git_client()
|
azure_devops_client = azure_devops_connection.clients.get_git_client()
|
||||||
|
azure_devops_board_client = azure_devops_connection.clients.get_work_item_tracking_client()
|
||||||
|
|
||||||
return azure_devops_client
|
return azure_devops_client, azure_devops_board_client
|
||||||
|
|
||||||
def _get_repo(self):
|
def _get_repo(self):
|
||||||
if self.repo is None:
|
if self.repo is None:
|
||||||
@ -635,4 +635,50 @@ class AzureDevopsProvider(GitProvider):
|
|||||||
last = commits[0]
|
last = commits[0]
|
||||||
url = self.azure_devops_client.normalized_url + "/" + self.workspace_slug + "/_git/" + self.repo_slug + "/commit/" + last.commit_id
|
url = self.azure_devops_client.normalized_url + "/" + self.workspace_slug + "/_git/" + self.repo_slug + "/commit/" + last.commit_id
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
def get_linked_work_items(self) -> list:
|
||||||
|
"""
|
||||||
|
Get linked work items from the PR.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
work_items = self.azure_devops_client.get_pull_request_work_item_refs(
|
||||||
|
project=self.workspace_slug,
|
||||||
|
repository_id=self.repo_slug,
|
||||||
|
pull_request_id=self.pr_num,
|
||||||
|
)
|
||||||
|
ids = [work_item.id for work_item in work_items]
|
||||||
|
if not work_items:
|
||||||
|
return []
|
||||||
|
items = self.get_work_items(ids)
|
||||||
|
return items
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().exception(f"Failed to get linked work items, error: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_work_items(self, work_item_ids: list) -> list:
|
||||||
|
"""
|
||||||
|
Get work items by their IDs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
raw_work_items = self.azure_devops_board_client.get_work_items(
|
||||||
|
project=self.workspace_slug,
|
||||||
|
ids=work_item_ids,
|
||||||
|
)
|
||||||
|
work_items = []
|
||||||
|
for item in raw_work_items:
|
||||||
|
work_items.append(
|
||||||
|
{
|
||||||
|
"id": item.id,
|
||||||
|
"title": item.fields.get("System.Title", ""),
|
||||||
|
"url": item.url,
|
||||||
|
"body": item.fields.get("System.Description", ""),
|
||||||
|
"acceptance_criteria": item.fields.get(
|
||||||
|
"Microsoft.VSTS.Common.AcceptanceCriteria", ""
|
||||||
|
),
|
||||||
|
"tags": item.fields.get("System.Tags", "").split("; ") if item.fields.get("System.Tags") else [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return work_items
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().exception(f"Failed to get work items, error: {e}")
|
||||||
|
return []
|
||||||
|
38
pr_agent/servers/atlassian-connect-qodo-merge.json
Normal file
38
pr_agent/servers/atlassian-connect-qodo-merge.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "Qodo Merge",
|
||||||
|
"description": "Qodo Merge",
|
||||||
|
"key": "app_key",
|
||||||
|
"vendor": {
|
||||||
|
"name": "Qodo",
|
||||||
|
"url": "https://qodo.ai"
|
||||||
|
},
|
||||||
|
"authentication": {
|
||||||
|
"type": "jwt"
|
||||||
|
},
|
||||||
|
"baseUrl": "base_url",
|
||||||
|
"lifecycle": {
|
||||||
|
"installed": "/installed",
|
||||||
|
"uninstalled": "/uninstalled"
|
||||||
|
},
|
||||||
|
"scopes": [
|
||||||
|
"account",
|
||||||
|
"repository:write",
|
||||||
|
"pullrequest:write",
|
||||||
|
"wiki"
|
||||||
|
],
|
||||||
|
"contexts": [
|
||||||
|
"account"
|
||||||
|
],
|
||||||
|
"modules": {
|
||||||
|
"webhooks": [
|
||||||
|
{
|
||||||
|
"event": "*",
|
||||||
|
"url": "/webhook"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"links": {
|
||||||
|
"privacy": "https://qodo.ai/privacy-policy",
|
||||||
|
"terms": "https://qodo.ai/terms"
|
||||||
|
}
|
||||||
|
}
|
@ -56,6 +56,7 @@ ignore_pr_source_branches = [] # a list of regular expressions of source branche
|
|||||||
ignore_pr_labels = [] # labels to ignore from PR agent when an PR is created
|
ignore_pr_labels = [] # labels to ignore from PR agent when an PR is created
|
||||||
ignore_pr_authors = [] # authors to ignore from PR agent when an PR is created
|
ignore_pr_authors = [] # authors to ignore from PR agent when an PR is created
|
||||||
ignore_repositories = [] # a list of regular expressions of repository full names (e.g. "org/repo") to ignore from PR agent processing
|
ignore_repositories = [] # a list of regular expressions of repository full names (e.g. "org/repo") to ignore from PR agent processing
|
||||||
|
ignore_language_framework = [] # a list of code-generation languages or frameworks (e.g. 'protobuf', 'go_gen') whose auto-generated source files will be excluded from analysis
|
||||||
#
|
#
|
||||||
is_auto_command = false # will be auto-set to true if the command is triggered by an automation
|
is_auto_command = false # will be auto-set to true if the command is triggered by an automation
|
||||||
enable_ai_metadata = false # will enable adding ai metadata
|
enable_ai_metadata = false # will enable adding ai metadata
|
||||||
@ -106,7 +107,7 @@ enable_pr_type=true
|
|||||||
final_update_message = true
|
final_update_message = true
|
||||||
enable_help_text=false
|
enable_help_text=false
|
||||||
enable_help_comment=true
|
enable_help_comment=true
|
||||||
enable_pr_diagram=false # adds a section with a diagram of the PR changes
|
enable_pr_diagram=true # adds a section with a diagram of the PR changes
|
||||||
# describe as comment
|
# describe as comment
|
||||||
publish_description_as_comment=false
|
publish_description_as_comment=false
|
||||||
publish_description_as_comment_persistent=true
|
publish_description_as_comment_persistent=true
|
||||||
|
42
pr_agent/settings/generated_code_ignore.toml
Normal file
42
pr_agent/settings/generated_code_ignore.toml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
[generated_code]
|
||||||
|
|
||||||
|
# Protocol Buffers
|
||||||
|
protobuf = [
|
||||||
|
"**/*.pb.go",
|
||||||
|
"**/*.pb.cc",
|
||||||
|
"**/*_pb2.py",
|
||||||
|
"**/*.pb.swift",
|
||||||
|
"**/*.pb.rb",
|
||||||
|
"**/*.pb.php",
|
||||||
|
"**/*.pb.h"
|
||||||
|
]
|
||||||
|
|
||||||
|
# OpenAPI / Swagger stubs
|
||||||
|
openapi = [
|
||||||
|
"**/__generated__/**",
|
||||||
|
"**/openapi_client/**",
|
||||||
|
"**/openapi_server/**"
|
||||||
|
]
|
||||||
|
swagger = [
|
||||||
|
"**/swagger.json",
|
||||||
|
"**/swagger.yaml"
|
||||||
|
]
|
||||||
|
|
||||||
|
# GraphQL codegen
|
||||||
|
graphql = [
|
||||||
|
"**/*.graphql.ts",
|
||||||
|
"**/*.generated.ts",
|
||||||
|
"**/*.graphql.js"
|
||||||
|
]
|
||||||
|
|
||||||
|
# RPC / gRPC Generators
|
||||||
|
grpc_python = ["**/*_grpc.py"]
|
||||||
|
grpc_java = ["**/*Grpc.java"]
|
||||||
|
grpc_csharp = ["**/*Grpc.cs"]
|
||||||
|
grpc_typescript = ["**/*_grpc.ts", "**/*_grpc.js"]
|
||||||
|
|
||||||
|
# Go code generators
|
||||||
|
go_gen = [
|
||||||
|
"**/*_gen.go",
|
||||||
|
"**/*generated.go"
|
||||||
|
]
|
@ -48,7 +48,7 @@ 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.")
|
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")
|
title: str = Field(description="a concise and descriptive title that captures the PR's main theme")
|
||||||
{%- if enable_pr_diagram %}
|
{%- 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 ")
|
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 %}
|
{%- endif %}
|
||||||
{%- if enable_semantic_files_types %}
|
{%- if enable_semantic_files_types %}
|
||||||
pr_files: List[FileDescription] = Field(max_items=20, description="a list of all the files that were changed in the PR, and summary of their changes. Each file must be analyzed regardless of change size.")
|
pr_files: List[FileDescription] = Field(max_items=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.")
|
||||||
|
@ -196,6 +196,13 @@ Ticket Description:
|
|||||||
{{ ticket.body }}
|
{{ ticket.body }}
|
||||||
#####
|
#####
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
{%- if ticket.requirements %}
|
||||||
|
Ticket Requirements:
|
||||||
|
#####
|
||||||
|
{{ ticket.requirements }}
|
||||||
|
#####
|
||||||
|
{%- endif %}
|
||||||
=====
|
=====
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
@ -614,11 +614,13 @@ class PRCodeSuggestions:
|
|||||||
break
|
break
|
||||||
if original_initial_line:
|
if original_initial_line:
|
||||||
suggested_initial_line = new_code_snippet.splitlines()[0]
|
suggested_initial_line = new_code_snippet.splitlines()[0]
|
||||||
original_initial_spaces = len(original_initial_line) - len(original_initial_line.lstrip())
|
original_initial_spaces = len(original_initial_line) - len(original_initial_line.lstrip()) # lstrip works both for spaces and tabs
|
||||||
suggested_initial_spaces = len(suggested_initial_line) - len(suggested_initial_line.lstrip())
|
suggested_initial_spaces = len(suggested_initial_line) - len(suggested_initial_line.lstrip())
|
||||||
delta_spaces = original_initial_spaces - suggested_initial_spaces
|
delta_spaces = original_initial_spaces - suggested_initial_spaces
|
||||||
if delta_spaces > 0:
|
if delta_spaces > 0:
|
||||||
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * " ").rstrip('\n')
|
# Detect indentation character from original line
|
||||||
|
indent_char = '\t' if original_initial_line.startswith('\t') else ' '
|
||||||
|
new_code_snippet = textwrap.indent(new_code_snippet, delta_spaces * indent_char).rstrip('\n')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Error when dedenting code snippet for file {relevant_file}, error: {e}")
|
get_logger().error(f"Error when dedenting code snippet for file {relevant_file}, error: {e}")
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ class PRDescription:
|
|||||||
|
|
||||||
# Initialize the variables dictionary
|
# Initialize the variables dictionary
|
||||||
self.COLLAPSIBLE_FILE_LIST_THRESHOLD = get_settings().pr_description.get("collapsible_file_list_threshold", 8)
|
self.COLLAPSIBLE_FILE_LIST_THRESHOLD = get_settings().pr_description.get("collapsible_file_list_threshold", 8)
|
||||||
|
enable_pr_diagram = get_settings().pr_description.get("enable_pr_diagram", False) and self.git_provider.is_supported("gfm_markdown") # github and gitlab support gfm_markdown
|
||||||
self.vars = {
|
self.vars = {
|
||||||
"title": self.git_provider.pr.title,
|
"title": self.git_provider.pr.title,
|
||||||
"branch": self.git_provider.get_pr_branch(),
|
"branch": self.git_provider.get_pr_branch(),
|
||||||
@ -73,7 +74,7 @@ class PRDescription:
|
|||||||
"related_tickets": "",
|
"related_tickets": "",
|
||||||
"include_file_summary_changes": len(self.git_provider.get_diff_files()) <= self.COLLAPSIBLE_FILE_LIST_THRESHOLD,
|
"include_file_summary_changes": len(self.git_provider.get_diff_files()) <= self.COLLAPSIBLE_FILE_LIST_THRESHOLD,
|
||||||
"duplicate_prompt_examples": get_settings().config.get("duplicate_prompt_examples", False),
|
"duplicate_prompt_examples": get_settings().config.get("duplicate_prompt_examples", False),
|
||||||
"enable_pr_diagram": get_settings().pr_description.get("enable_pr_diagram", False),
|
"enable_pr_diagram": enable_pr_diagram,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.user_description = self.git_provider.get_user_description()
|
self.user_description = self.git_provider.get_user_description()
|
||||||
|
@ -3,6 +3,7 @@ import traceback
|
|||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers import GithubProvider
|
from pr_agent.git_providers import GithubProvider
|
||||||
|
from pr_agent.git_providers import AzureDevopsProvider
|
||||||
from pr_agent.log import get_logger
|
from pr_agent.log import get_logger
|
||||||
|
|
||||||
# Compile the regex pattern once, outside the function
|
# Compile the regex pattern once, outside the function
|
||||||
@ -131,6 +132,32 @@ async def extract_tickets(git_provider):
|
|||||||
|
|
||||||
return tickets_content
|
return tickets_content
|
||||||
|
|
||||||
|
elif isinstance(git_provider, AzureDevopsProvider):
|
||||||
|
tickets_info = git_provider.get_linked_work_items()
|
||||||
|
tickets_content = []
|
||||||
|
for ticket in tickets_info:
|
||||||
|
try:
|
||||||
|
ticket_body_str = ticket.get("body", "")
|
||||||
|
if len(ticket_body_str) > MAX_TICKET_CHARACTERS:
|
||||||
|
ticket_body_str = ticket_body_str[:MAX_TICKET_CHARACTERS] + "..."
|
||||||
|
|
||||||
|
tickets_content.append(
|
||||||
|
{
|
||||||
|
"ticket_id": ticket.get("id"),
|
||||||
|
"ticket_url": ticket.get("url"),
|
||||||
|
"title": ticket.get("title"),
|
||||||
|
"body": ticket_body_str,
|
||||||
|
"requirements": ticket.get("acceptance_criteria", ""),
|
||||||
|
"labels": ", ".join(ticket.get("labels", [])),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().error(
|
||||||
|
f"Error processing Azure DevOps ticket: {e}",
|
||||||
|
artifact={"traceback": traceback.format_exc()},
|
||||||
|
)
|
||||||
|
return tickets_content
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Error extracting tickets error= {e}",
|
get_logger().error(f"Error extracting tickets error= {e}",
|
||||||
artifact={"traceback": traceback.format_exc()})
|
artifact={"traceback": traceback.format_exc()})
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0"]
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pr-agent"
|
name = "pr-agent"
|
||||||
version = "0.2.7"
|
version = "0.3.0"
|
||||||
|
|
||||||
authors = [{ name = "QodoAI", email = "tal.r@qodo.ai" }]
|
authors = [{ name = "QodoAI", email = "tal.r@qodo.ai" }]
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ description = "QodoAI PR-Agent aims to help efficiently review and handle pull r
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
keywords = ["AI", "Agents", "Pull Request", "Automation", "Code Review"]
|
keywords = ["AI", "Agents", "Pull Request", "Automation", "Code Review"]
|
||||||
license = "Apache-2.0"
|
license = { file = "LICENSE" }
|
||||||
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
@ -34,7 +34,6 @@ dependencies = { file = ["requirements.txt"] }
|
|||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
include-package-data = true
|
include-package-data = true
|
||||||
license-files = ["LICENSE"]
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
|
92
tests/unittest/test_add_docs_trigger.py
Normal file
92
tests/unittest/test_add_docs_trigger.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import pytest
|
||||||
|
from pr_agent.servers.github_app import handle_new_pr_opened
|
||||||
|
from pr_agent.tools.pr_add_docs import PRAddDocs
|
||||||
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
|
from pr_agent.config_loader import get_settings
|
||||||
|
from pr_agent.identity_providers.identity_provider import Eligibility
|
||||||
|
from pr_agent.identity_providers import get_identity_provider
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"action,draft,state,should_run",
|
||||||
|
[
|
||||||
|
("opened", False, "open", True),
|
||||||
|
("edited", False, "open", False),
|
||||||
|
("opened", True, "open", False),
|
||||||
|
("opened", False, "closed", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_add_docs_trigger(monkeypatch, action, draft, state, should_run):
|
||||||
|
# Mock settings to enable the "/add_docs" auto-command on PR opened
|
||||||
|
settings = get_settings()
|
||||||
|
settings.github_app.pr_commands = ["/add_docs"]
|
||||||
|
settings.github_app.handle_pr_actions = ["opened"]
|
||||||
|
|
||||||
|
# Define a FakeGitProvider for both apply_repo_settings and PRAddDocs
|
||||||
|
class FakeGitProvider:
|
||||||
|
def __init__(self, pr_url, *args, **kwargs):
|
||||||
|
self.pr = type("pr", (), {"title": "Test PR"})()
|
||||||
|
self.get_pr_branch = lambda: "test-branch"
|
||||||
|
self.get_pr_description = lambda: "desc"
|
||||||
|
self.get_languages = lambda: ["Python"]
|
||||||
|
self.get_files = lambda: []
|
||||||
|
self.get_commit_messages = lambda: "msg"
|
||||||
|
self.publish_comment = lambda *args, **kwargs: None
|
||||||
|
self.remove_initial_comment = lambda: None
|
||||||
|
self.publish_code_suggestions = lambda suggestions: True
|
||||||
|
self.diff_files = []
|
||||||
|
self.get_repo_settings = lambda: {}
|
||||||
|
|
||||||
|
# Patch Git provider lookups
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"pr_agent.git_providers.utils.get_git_provider_with_context",
|
||||||
|
lambda pr_url: FakeGitProvider(pr_url),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"pr_agent.tools.pr_add_docs.get_git_provider",
|
||||||
|
lambda: FakeGitProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure identity provider always eligible
|
||||||
|
monkeypatch.setattr(
|
||||||
|
get_identity_provider().__class__,
|
||||||
|
"verify_eligibility",
|
||||||
|
lambda *args, **kwargs: Eligibility.ELIGIBLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Spy on PRAddDocs.run()
|
||||||
|
ran = {"flag": False}
|
||||||
|
|
||||||
|
async def fake_run(self):
|
||||||
|
ran["flag"] = True
|
||||||
|
|
||||||
|
monkeypatch.setattr(PRAddDocs, "run", fake_run)
|
||||||
|
|
||||||
|
# Build minimal PR payload
|
||||||
|
body = {
|
||||||
|
"action": action,
|
||||||
|
"pull_request": {
|
||||||
|
"url": "https://example.com/fake/pr",
|
||||||
|
"state": state,
|
||||||
|
"draft": draft,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
log_context = {}
|
||||||
|
|
||||||
|
# Invoke the PR-open handler
|
||||||
|
agent = PRAgent()
|
||||||
|
await handle_new_pr_opened(
|
||||||
|
body=body,
|
||||||
|
event="pull_request",
|
||||||
|
sender="tester",
|
||||||
|
sender_id="123",
|
||||||
|
action=action,
|
||||||
|
log_context=log_context,
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ran["flag"] is should_run, (
|
||||||
|
f"Expected run() to be {'called' if should_run else 'skipped'}"
|
||||||
|
f" for action={action!r}, draft={draft}, state={state!r}"
|
||||||
|
)
|
@ -80,3 +80,53 @@ class TestIgnoreFilter:
|
|||||||
|
|
||||||
filtered_files = filter_ignored(files)
|
filtered_files = filter_ignored(files)
|
||||||
assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}."
|
assert filtered_files == expected, f"Expected {[file.filename for file in expected]}, but got {[file.filename for file in filtered_files]}."
|
||||||
|
|
||||||
|
def test_language_framework_ignores(self, monkeypatch):
|
||||||
|
"""
|
||||||
|
Test files are ignored based on language/framework mapping (e.g., protobuf).
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr(global_settings.config, 'ignore_language_framework', ['protobuf', 'go_gen'])
|
||||||
|
|
||||||
|
files = [
|
||||||
|
type('', (object,), {'filename': 'main.go'})(),
|
||||||
|
type('', (object,), {'filename': 'dir1/service.pb.go'})(),
|
||||||
|
type('', (object,), {'filename': 'dir1/dir/data_pb2.py'})(),
|
||||||
|
type('', (object,), {'filename': 'file.py'})(),
|
||||||
|
type('', (object,), {'filename': 'dir2/file_gen.go'})(),
|
||||||
|
type('', (object,), {'filename': 'file.generated.go'})()
|
||||||
|
]
|
||||||
|
expected = [
|
||||||
|
files[0],
|
||||||
|
files[3]
|
||||||
|
]
|
||||||
|
|
||||||
|
filtered = filter_ignored(files)
|
||||||
|
assert filtered == expected, (
|
||||||
|
f"Expected {[f.filename for f in expected]}, "
|
||||||
|
f"but got {[f.filename for f in filtered]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_skip_invalid_ignore_language_framework(self, monkeypatch):
|
||||||
|
"""
|
||||||
|
Test skipping of generated code filtering when ignore_language_framework is not a list
|
||||||
|
"""
|
||||||
|
monkeypatch.setattr(global_settings.config, 'ignore_language_framework', 'protobuf')
|
||||||
|
|
||||||
|
files = [
|
||||||
|
type('', (object,), {'filename': 'main.go'})(),
|
||||||
|
type('', (object,), {'filename': 'file.py'})(),
|
||||||
|
type('', (object,), {'filename': 'dir1/service.pb.go'})(),
|
||||||
|
type('', (object,), {'filename': 'file_pb2.py'})()
|
||||||
|
]
|
||||||
|
expected = [
|
||||||
|
files[0],
|
||||||
|
files[1],
|
||||||
|
files[2],
|
||||||
|
files[3]
|
||||||
|
]
|
||||||
|
|
||||||
|
filtered = filter_ignored(files)
|
||||||
|
assert filtered == expected, (
|
||||||
|
f"Expected {[f.filename for f in expected]}, "
|
||||||
|
f"but got {[f.filename for f in filtered]}"
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user