mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-21 04:50:39 +08:00
Compare commits
36 Commits
v0.30
...
dd4fe4dcb4
Author | SHA1 | Date | |
---|---|---|---|
dd4fe4dcb4 | |||
1c174f263f | |||
d860e17b3b | |||
f83970bc6b | |||
9c87056263 | |||
3251f19a19 | |||
299a2c89d1 | |||
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 |
@ -65,6 +65,11 @@ Zero-setup hosted solution with advanced features and priority support
|
||||
|
||||
## News and Updates
|
||||
|
||||
## Jun 21, 2025
|
||||
|
||||
v0.30 was [released](https://github.com/qodo-ai/pr-agent/releases)
|
||||
|
||||
|
||||
## Jun 3, 2025
|
||||
|
||||
Qodo Merge now offers a simplified free tier 💎.
|
||||
|
@ -202,7 +202,23 @@ h1 {
|
||||
|
||||
<script>
|
||||
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 spinner = document.getElementById('spinner');
|
||||
const searchContainer = document.querySelector('.search-container');
|
||||
@ -214,8 +230,6 @@ window.addEventListener('load', function() {
|
||||
searchContainer.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
try {
|
||||
const results = JSON.parse(responseText);
|
||||
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
@ -223,7 +237,7 @@ window.addEventListener('load', function() {
|
||||
sanitize: false
|
||||
});
|
||||
|
||||
const htmlContent = marked.parse(results.message);
|
||||
const htmlContent = marked.parse(msg);
|
||||
|
||||
resultsContainer.className = 'markdown-content';
|
||||
resultsContainer.innerHTML = htmlContent;
|
||||
@ -242,7 +256,7 @@ window.addEventListener('load', function() {
|
||||
}, 100);
|
||||
} catch (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)
|
||||
};
|
||||
|
||||
// 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 response = await fetch(API_ENDPOINT, options);
|
||||
const responseText = await response.text();
|
||||
const msg = extractText(responseText);
|
||||
|
||||
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(responseText);
|
||||
|
||||
displayResults(msg);
|
||||
} catch (error) {
|
||||
spinner.style.display = 'none';
|
||||
resultsContainer.innerHTML = `
|
||||
<div class="error-message">
|
||||
An error occurred while searching. Please try again later.
|
||||
</div>
|
||||
`;
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'error-message';
|
||||
errorDiv.textContent = `${error}`;
|
||||
resultsContainer.value = "";
|
||||
resultsContainer.appendChild(errorDiv);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"}.
|
||||
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:
|
||||
|
||||
|
@ -19,7 +19,7 @@ This approach provides not just a quantitative score but also a detailed analysi
|
||||
|
||||
[//]: # ()
|
||||
|
||||
## Results
|
||||
## PR Benchmark Results
|
||||
|
||||
<table>
|
||||
<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:center;"><b>39.0</b></td>
|
||||
</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>
|
||||
<td style="text-align:left;">Gemini-2.5-flash</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.
|
||||
- **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.
|
||||
-
|
||||
|
||||
### GPT-4.1
|
||||
|
||||
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”).
|
||||
- **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
|
||||
|
||||
|
@ -7,6 +7,7 @@ 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.
|
||||
|
||||
=== "Recent Updates"
|
||||
- **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.
|
||||
- **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.
|
||||
- **Linear tickets support**: Qodo Merge now supports Linear tickets. ([Learn more](https://qodo-merge-docs.qodo.ai/core-abilities/fetching_ticket_context/#linear-integration))
|
||||
@ -17,7 +18,6 @@ It also outlines our development roadmap for the upcoming three months. Please n
|
||||
|
||||
|
||||
=== "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.
|
||||
- **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.
|
||||
|
@ -59,17 +59,23 @@ Everything below this marker is treated as previously auto-generated content and
|
||||
### 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.
|
||||
|
||||
This diagram represents interactions between components/functions based on the diff content.
|
||||
This diagram represents interactions between components/functions based on the PR content.
|
||||
|
||||
### How to enable
|
||||
[//]: # (### How to enable\disable)
|
||||
|
||||
In your configuration:
|
||||
[//]: # ()
|
||||
[//]: # (In your configuration:)
|
||||
|
||||
```
|
||||
toml
|
||||
[pr_description]
|
||||
enable_pr_diagram = true
|
||||
```
|
||||
[//]: # ()
|
||||
[//]: # (```)
|
||||
|
||||
[//]: # (toml)
|
||||
|
||||
[//]: # ([pr_description])
|
||||
|
||||
[//]: # (enable_pr_diagram = true)
|
||||
|
||||
[//]: # (```)
|
||||
|
||||
## Configuration options
|
||||
|
||||
@ -126,7 +132,7 @@ enable_pr_diagram = true
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</table>
|
||||
|
||||
|
@ -232,6 +232,14 @@ AWS_SECRET_ACCESS_KEY="..."
|
||||
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.
|
||||
|
||||
### DeepSeek
|
||||
|
@ -51,7 +51,7 @@ class PRAgent:
|
||||
def __init__(self, ai_handler: partial[BaseAiHandler,] = LiteLLMAIHandler):
|
||||
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
|
||||
apply_repo_settings(pr_url)
|
||||
|
||||
@ -117,3 +117,10 @@ class PRAgent:
|
||||
else:
|
||||
return False
|
||||
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-05-06': 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-2.0-flash': 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': 1048576,
|
||||
'vertex_ai/gemma2': 8200,
|
||||
'gemini/gemini-1.5-pro': 1048576,
|
||||
'gemini/gemini-1.5-flash': 1048576,
|
||||
'gemini/gemini-2.0-flash': 1048576,
|
||||
'gemini/gemini-2.5-flash-preview-04-17': 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-05-06': 1048576,
|
||||
'gemini/gemini-2.5-pro-preview-06-05': 1048576,
|
||||
'gemini/gemini-2.5-pro': 1048576,
|
||||
'codechat-bison': 6144,
|
||||
'codechat-bison-32k': 32000,
|
||||
'anthropic.claude-instant-v1': 100000,
|
||||
@ -109,6 +113,8 @@ MAX_TOKENS = {
|
||||
'claude-3-5-sonnet': 100000,
|
||||
'groq/meta-llama/llama-4-scout-17b-16e-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-70b-8192': 8192,
|
||||
'groq/llama-3.1-8b-instant': 8192,
|
||||
|
@ -22,6 +22,7 @@ try:
|
||||
from azure.devops.connection import Connection
|
||||
# noinspection PyUnresolvedReferences
|
||||
from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext, CommentPosition)
|
||||
from azure.devops.released.work_item_tracking import WorkItemTrackingClient
|
||||
# noinspection PyUnresolvedReferences
|
||||
from azure.identity import DefaultAzureCredential
|
||||
from msrest.authentication import BasicAuthentication
|
||||
@ -39,7 +40,7 @@ class AzureDevopsProvider(GitProvider):
|
||||
"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.workspace_slug = None
|
||||
self.repo_slug = None
|
||||
@ -566,7 +567,7 @@ class AzureDevopsProvider(GitProvider):
|
||||
return workspace_slug, repo_slug, pr_number
|
||||
|
||||
@staticmethod
|
||||
def _get_azure_devops_client() -> GitClient:
|
||||
def _get_azure_devops_client() -> Tuple[GitClient, WorkItemTrackingClient]:
|
||||
org = get_settings().azure_devops.get("org", 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}")
|
||||
raise
|
||||
|
||||
credentials = BasicAuthentication("", auth_token)
|
||||
|
||||
credentials = BasicAuthentication("", auth_token)
|
||||
azure_devops_connection = Connection(base_url=org, creds=credentials)
|
||||
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):
|
||||
if self.repo is None:
|
||||
@ -635,4 +635,50 @@ class AzureDevopsProvider(GitProvider):
|
||||
last = commits[0]
|
||||
url = self.azure_devops_client.normalized_url + "/" + self.workspace_slug + "/_git/" + self.repo_slug + "/commit/" + last.commit_id
|
||||
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"
|
||||
}
|
||||
}
|
@ -106,7 +106,7 @@ enable_pr_type=true
|
||||
final_update_message = true
|
||||
enable_help_text=false
|
||||
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
|
||||
publish_description_as_comment=false
|
||||
publish_description_as_comment_persistent=true
|
||||
|
@ -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.")
|
||||
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 ")
|
||||
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 %}
|
||||
{%- 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.")
|
||||
|
@ -196,6 +196,13 @@ Ticket Description:
|
||||
{{ ticket.body }}
|
||||
#####
|
||||
{%- endif %}
|
||||
|
||||
{%- if ticket.requirements %}
|
||||
Ticket Requirements:
|
||||
#####
|
||||
{{ ticket.requirements }}
|
||||
#####
|
||||
{%- endif %}
|
||||
=====
|
||||
{% endfor %}
|
||||
{%- endif %}
|
||||
|
@ -59,6 +59,7 @@ class PRDescription:
|
||||
|
||||
# Initialize the variables dictionary
|
||||
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 = {
|
||||
"title": self.git_provider.pr.title,
|
||||
"branch": self.git_provider.get_pr_branch(),
|
||||
@ -73,7 +74,7 @@ class PRDescription:
|
||||
"related_tickets": "",
|
||||
"include_file_summary_changes": len(self.git_provider.get_diff_files()) <= self.COLLAPSIBLE_FILE_LIST_THRESHOLD,
|
||||
"duplicate_prompt_examples": get_settings().config.get("duplicate_prompt_examples", False),
|
||||
"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()
|
||||
|
@ -3,6 +3,7 @@ import traceback
|
||||
|
||||
from pr_agent.config_loader import get_settings
|
||||
from pr_agent.git_providers import GithubProvider
|
||||
from pr_agent.git_providers import AzureDevopsProvider
|
||||
from pr_agent.log import get_logger
|
||||
|
||||
# Compile the regex pattern once, outside the function
|
||||
@ -131,6 +132,32 @@ async def extract_tickets(git_provider):
|
||||
|
||||
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:
|
||||
get_logger().error(f"Error extracting tickets error= {e}",
|
||||
artifact={"traceback": traceback.format_exc()})
|
||||
|
@ -1,10 +1,10 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pr-agent"
|
||||
version = "0.2.7"
|
||||
version = "0.3.0"
|
||||
|
||||
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"
|
||||
requires-python = ">=3.12"
|
||||
keywords = ["AI", "Agents", "Pull Request", "Automation", "Code Review"]
|
||||
license = "Apache-2.0"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
classifiers = [
|
||||
"Intended Audience :: Developers",
|
||||
@ -34,7 +34,6 @@ dependencies = { file = ["requirements.txt"] }
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = true
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
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}"
|
||||
)
|
Reference in New Issue
Block a user