mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-08 23:00:43 +08:00
Compare commits
24 Commits
ok/fix_bit
...
zmeir-exte
Author | SHA1 | Date | |
---|---|---|---|
e6bea76eee | |||
414f2b6767 | |||
6541575a0e | |||
02570ea797 | |||
65bb70a1dd | |||
b6cabda586 | |||
abbce60f18 | |||
5daaaf2c1d | |||
e8f207691e | |||
b0dce4ceae | |||
fc494296d7 | |||
67b4069540 | |||
e6defcc846 | |||
096fcbbc17 | |||
eb7add1c77 | |||
1b6fb3ea53 | |||
c57b70f1d4 | |||
a2c3db463a | |||
193da1c356 | |||
5bc26880b3 | |||
21a1cc970e | |||
954727ad67 | |||
1314898cbf | |||
3673924fe9 |
@ -27,18 +27,14 @@ Under the section 'pr_description', the [configuration file](./../pr_agent/setti
|
|||||||
|
|
||||||
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
- `extra_instructions`: Optional extra instructions to the tool. For example: "focus on the changes in the file X. Ignore change in ...".
|
||||||
|
|
||||||
#### Markers template
|
### Markers template
|
||||||
|
|
||||||
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
|
markers enable to easily integrate user's content and auto-generated content, with a template-like mechanism.
|
||||||
|
|
||||||
- `use_description_markers`: if set to true, the tool will use markers template. It replaces every marker of the form `pr_agent:marker_name` with the relevant content. Default is false.
|
|
||||||
|
|
||||||
For example, if the PR original description was:
|
For example, if the PR original description was:
|
||||||
```
|
```
|
||||||
User content...
|
User content...
|
||||||
|
|
||||||
## PR Type:
|
|
||||||
pr_agent:pr_type
|
|
||||||
|
|
||||||
## PR Description:
|
## PR Description:
|
||||||
pr_agent:summary
|
pr_agent:summary
|
||||||
@ -46,6 +42,21 @@ pr_agent:summary
|
|||||||
## PR Walkthrough:
|
## PR Walkthrough:
|
||||||
pr_agent:walkthrough
|
pr_agent:walkthrough
|
||||||
```
|
```
|
||||||
The marker `pr_agent:pr_type` will be replaced with the PR type, `pr_agent:summary` will be replaced with the PR summary, and `pr_agent:walkthrough` will be replaced with the PR walkthrough.
|
The marker `pr_agent:summary` will be replaced with the PR summary, and `pr_agent:walkthrough` will be replaced with the PR walkthrough.
|
||||||
|
|
||||||
|
##### Example:
|
||||||
|
```
|
||||||
|
env:
|
||||||
|
pr_description.use_description_markers: 'true'
|
||||||
|
```
|
||||||
|
|
||||||
|
<kbd><img src=./../pics/describe_markers_before.png width="768"></kbd>
|
||||||
|
|
||||||
|
==>
|
||||||
|
|
||||||
|
<kbd><img src=./../pics/describe_markers_after.png width="768"></kbd>
|
||||||
|
|
||||||
|
##### Configuration params:
|
||||||
|
|
||||||
|
- `use_description_markers`: if set to true, the tool will use markers template. It replaces every marker of the form `pr_agent:marker_name` with the relevant content. Default is false.
|
||||||
- `include_generated_by_header`: if set to true, the tool will add a dedicated header: 'Generated by PR Agent at ...' to any automatic content. Default is true.
|
- `include_generated_by_header`: if set to true, the tool will add a dedicated header: 'Generated by PR Agent at ...' to any automatic content. Default is true.
|
||||||
|
@ -32,3 +32,14 @@ Under the section 'pr_code_suggestions', the [configuration file](./../pr_agent/
|
|||||||
- `rank_extended_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is true.
|
- `rank_extended_suggestions`: if set to true, the tool will rank the suggestions, based on importance. Default is true.
|
||||||
- `max_number_of_calls`: maximum number of chunks. Default is 5.
|
- `max_number_of_calls`: maximum number of chunks. Default is 5.
|
||||||
- `final_clip_factor`: factor to remove suggestions with low confidence. Default is 0.9.
|
- `final_clip_factor`: factor to remove suggestions with low confidence. Default is 0.9.
|
||||||
|
|
||||||
|
|
||||||
|
#### A note on code suggestions quality
|
||||||
|
|
||||||
|
- With current level of AI for code (GPT-4), mistakes can happen. Not all the suggestions will be perfect, and a user should not accept all of them automatically.
|
||||||
|
|
||||||
|
- Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_code_suggestions_prompts.toml#L34). Instead, they aim to give deep feedback and raise questions, ideas and thoughts to the user, who can then use his judgment, experience, and understanding of the code base.
|
||||||
|
|
||||||
|
- Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project.
|
||||||
|
|
||||||
|
- Best quality will be obtained by using 'improve --extended' mode.
|
@ -44,3 +44,14 @@ The tool will first ask the author questions about the PR, and will guide the re
|
|||||||
<kbd><img src=./../pics/reflection_questions.png width="768"></kbd>
|
<kbd><img src=./../pics/reflection_questions.png width="768"></kbd>
|
||||||
<kbd><img src=./../pics/reflection_answers.png width="768"></kbd>
|
<kbd><img src=./../pics/reflection_answers.png width="768"></kbd>
|
||||||
<kbd><img src=./../pics/reflection_insights.png width="768"></kbd>
|
<kbd><img src=./../pics/reflection_insights.png width="768"></kbd>
|
||||||
|
|
||||||
|
|
||||||
|
#### A note on code suggestions quality
|
||||||
|
|
||||||
|
- With current level of AI for code (GPT-4), mistakes can happen. Not all the suggestions will be perfect, and a user should not accept all of them automatically.
|
||||||
|
|
||||||
|
- Suggestions are not meant to be [simplistic](./../pr_agent/settings/pr_reviewer_prompts.toml#L29). Instead, they aim to give deep feedback and raise questions, ideas and thoughts to the user, who can then use his judgment, experience, and understanding of the code base.
|
||||||
|
|
||||||
|
- Recommended to use the 'extra_instructions' field to guide the model to suggestions that are more relevant to the specific needs of the project.
|
||||||
|
|
||||||
|
- Unlike the 'review' feature, which does a lot of things, the ['improve --extended'](./IMPROVE.md) feature is dedicated only to suggestions, and usually gives better results.
|
BIN
pics/describe_markers_after.png
Normal file
BIN
pics/describe_markers_after.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 224 KiB |
BIN
pics/describe_markers_before.png
Normal file
BIN
pics/describe_markers_before.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
@ -142,10 +142,15 @@ class BitbucketProvider(GitProvider):
|
|||||||
def remove_initial_comment(self):
|
def remove_initial_comment(self):
|
||||||
try:
|
try:
|
||||||
for comment in self.temp_comments:
|
for comment in self.temp_comments:
|
||||||
self.pr.delete(f"comments/{comment}")
|
self.remove_comment(comment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to remove temp comments, error: {e}")
|
get_logger().exception(f"Failed to remove temp comments, error: {e}")
|
||||||
|
|
||||||
|
def remove_comment(self, comment):
|
||||||
|
try:
|
||||||
|
self.pr.delete(f"comments/{comment}")
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().exception(f"Failed to remove comment, error: {e}")
|
||||||
|
|
||||||
# funtion to create_inline_comment
|
# funtion to create_inline_comment
|
||||||
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
|
@ -221,6 +221,9 @@ class CodeCommitProvider(GitProvider):
|
|||||||
def remove_initial_comment(self):
|
def remove_initial_comment(self):
|
||||||
return "" # not implemented yet
|
return "" # not implemented yet
|
||||||
|
|
||||||
|
def remove_comment(self, comment):
|
||||||
|
return "" # not implemented yet
|
||||||
|
|
||||||
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html
|
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/codecommit/client/post_comment_for_compared_commit.html
|
||||||
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
|
raise NotImplementedError("CodeCommit provider does not support publishing inline comments yet")
|
||||||
|
@ -396,5 +396,8 @@ class GerritProvider(GitProvider):
|
|||||||
# shutil.rmtree(self.repo_path)
|
# shutil.rmtree(self.repo_path)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def remove_comment(self, comment):
|
||||||
|
pass
|
||||||
|
|
||||||
def get_pr_branch(self):
|
def get_pr_branch(self):
|
||||||
return self.repo.head
|
return self.repo.head
|
||||||
|
@ -71,6 +71,10 @@ class GitProvider(ABC):
|
|||||||
def remove_initial_comment(self):
|
def remove_initial_comment(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def remove_comment(self, comment):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_languages(self):
|
def get_languages(self):
|
||||||
pass
|
pass
|
||||||
|
@ -50,7 +50,7 @@ class GithubProvider(GitProvider):
|
|||||||
def get_incremental_commits(self):
|
def get_incremental_commits(self):
|
||||||
self.commits = list(self.pr.get_commits())
|
self.commits = list(self.pr.get_commits())
|
||||||
|
|
||||||
self.get_previous_review()
|
self.previous_review = self.get_previous_review(full=True, incremental=True)
|
||||||
if self.previous_review:
|
if self.previous_review:
|
||||||
self.incremental.commits_range = self.get_commit_range()
|
self.incremental.commits_range = self.get_commit_range()
|
||||||
# Get all files changed during the commit range
|
# Get all files changed during the commit range
|
||||||
@ -63,7 +63,7 @@ class GithubProvider(GitProvider):
|
|||||||
|
|
||||||
def get_commit_range(self):
|
def get_commit_range(self):
|
||||||
last_review_time = self.previous_review.created_at
|
last_review_time = self.previous_review.created_at
|
||||||
first_new_commit_index = 0
|
first_new_commit_index = None
|
||||||
for index in range(len(self.commits) - 1, -1, -1):
|
for index in range(len(self.commits) - 1, -1, -1):
|
||||||
if self.commits[index].commit.author.date > last_review_time:
|
if self.commits[index].commit.author.date > last_review_time:
|
||||||
self.incremental.first_new_commit_sha = self.commits[index].sha
|
self.incremental.first_new_commit_sha = self.commits[index].sha
|
||||||
@ -71,15 +71,21 @@ class GithubProvider(GitProvider):
|
|||||||
else:
|
else:
|
||||||
self.incremental.last_seen_commit_sha = self.commits[index].sha
|
self.incremental.last_seen_commit_sha = self.commits[index].sha
|
||||||
break
|
break
|
||||||
return self.commits[first_new_commit_index:]
|
return self.commits[first_new_commit_index:] if first_new_commit_index is not None else []
|
||||||
|
|
||||||
def get_previous_review(self):
|
def get_previous_review(self, *, full: bool, incremental: bool):
|
||||||
self.previous_review = None
|
if not (full or incremental):
|
||||||
self.comments = list(self.pr.get_issue_comments())
|
raise ValueError("At least one of full or incremental must be True")
|
||||||
|
if not getattr(self, "comments", None):
|
||||||
|
self.comments = list(self.pr.get_issue_comments())
|
||||||
|
prefixes = []
|
||||||
|
if full:
|
||||||
|
prefixes.append("## PR Analysis")
|
||||||
|
if incremental:
|
||||||
|
prefixes.append("## Incremental PR Review")
|
||||||
for index in range(len(self.comments) - 1, -1, -1):
|
for index in range(len(self.comments) - 1, -1, -1):
|
||||||
if self.comments[index].body.startswith("## PR Analysis") or self.comments[index].body.startswith("## Incremental PR Review"):
|
if any(self.comments[index].body.startswith(prefix) for prefix in prefixes):
|
||||||
self.previous_review = self.comments[index]
|
return self.comments[index]
|
||||||
break
|
|
||||||
|
|
||||||
def get_files(self):
|
def get_files(self):
|
||||||
if self.incremental.is_incremental and self.file_set:
|
if self.incremental.is_incremental and self.file_set:
|
||||||
@ -218,10 +224,16 @@ class GithubProvider(GitProvider):
|
|||||||
try:
|
try:
|
||||||
for comment in getattr(self.pr, 'comments_list', []):
|
for comment in getattr(self.pr, 'comments_list', []):
|
||||||
if comment.is_temporary:
|
if comment.is_temporary:
|
||||||
comment.delete()
|
self.remove_comment(comment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to remove initial comment, error: {e}")
|
get_logger().exception(f"Failed to remove initial comment, error: {e}")
|
||||||
|
|
||||||
|
def remove_comment(self, comment):
|
||||||
|
try:
|
||||||
|
comment.delete()
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().exception(f"Failed to remove comment, error: {e}")
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.pr.title
|
return self.pr.title
|
||||||
|
|
||||||
|
@ -287,10 +287,16 @@ class GitLabProvider(GitProvider):
|
|||||||
def remove_initial_comment(self):
|
def remove_initial_comment(self):
|
||||||
try:
|
try:
|
||||||
for comment in self.temp_comments:
|
for comment in self.temp_comments:
|
||||||
comment.delete()
|
self.remove_comment(comment)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().exception(f"Failed to remove temp comments, error: {e}")
|
get_logger().exception(f"Failed to remove temp comments, error: {e}")
|
||||||
|
|
||||||
|
def remove_comment(self, comment):
|
||||||
|
try:
|
||||||
|
comment.delete()
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().exception(f"Failed to remove comment, error: {e}")
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.mr.title
|
return self.mr.title
|
||||||
|
|
||||||
|
@ -140,6 +140,9 @@ class LocalGitProvider(GitProvider):
|
|||||||
def remove_initial_comment(self):
|
def remove_initial_comment(self):
|
||||||
pass # Not applicable to the local git provider, but required by the interface
|
pass # Not applicable to the local git provider, but required by the interface
|
||||||
|
|
||||||
|
def remove_comment(self, comment):
|
||||||
|
pass # Not applicable to the local git provider, but required by the interface
|
||||||
|
|
||||||
def get_languages(self):
|
def get_languages(self):
|
||||||
"""
|
"""
|
||||||
Calculate percentage of languages in repository. Used for hunk prioritisation.
|
Calculate percentage of languages in repository. Used for hunk prioritisation.
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import ujson
|
import ujson
|
||||||
|
|
||||||
from google.cloud import storage
|
from google.cloud import storage
|
||||||
|
|
||||||
from pr_agent.config_loader import get_settings
|
from pr_agent.config_loader import get_settings
|
||||||
from pr_agent.git_providers.gitlab_provider import logger
|
from pr_agent.log import get_logger
|
||||||
from pr_agent.secret_providers.secret_provider import SecretProvider
|
from pr_agent.secret_providers.secret_provider import SecretProvider
|
||||||
|
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
|
|||||||
self.bucket_name = get_settings().google_cloud_storage.bucket_name
|
self.bucket_name = get_settings().google_cloud_storage.bucket_name
|
||||||
self.bucket = self.client.bucket(self.bucket_name)
|
self.bucket = self.client.bucket(self.bucket_name)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to initialize Google Cloud Storage Secret Provider: {e}")
|
get_logger().error(f"Failed to initialize Google Cloud Storage Secret Provider: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def get_secret(self, secret_name: str) -> str:
|
def get_secret(self, secret_name: str) -> str:
|
||||||
@ -23,7 +22,7 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
|
|||||||
blob = self.bucket.blob(secret_name)
|
blob = self.bucket.blob(secret_name)
|
||||||
return blob.download_as_string()
|
return blob.download_as_string()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get secret {secret_name} from Google Cloud Storage: {e}")
|
get_logger().error(f"Failed to get secret {secret_name} from Google Cloud Storage: {e}")
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def store_secret(self, secret_name: str, secret_value: str):
|
def store_secret(self, secret_name: str, secret_value: str):
|
||||||
@ -31,5 +30,5 @@ class GoogleCloudStorageSecretProvider(SecretProvider):
|
|||||||
blob = self.bucket.blob(secret_name)
|
blob = self.bucket.blob(secret_name)
|
||||||
blob.upload_from_string(secret_value)
|
blob.upload_from_string(secret_value)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to store secret {secret_name} in Google Cloud Storage: {e}")
|
get_logger().error(f"Failed to store secret {secret_name} in Google Cloud Storage: {e}")
|
||||||
raise e
|
raise e
|
||||||
|
@ -65,15 +65,17 @@ async def handle_manifest(request: Request, response: Response):
|
|||||||
|
|
||||||
@router.post("/webhook")
|
@router.post("/webhook")
|
||||||
async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Request):
|
async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Request):
|
||||||
print(request.headers)
|
log_context = {"server_type": "bitbucket_app"}
|
||||||
|
get_logger().debug(request.headers)
|
||||||
jwt_header = request.headers.get("authorization", None)
|
jwt_header = request.headers.get("authorization", None)
|
||||||
if jwt_header:
|
if jwt_header:
|
||||||
input_jwt = jwt_header.split(" ")[1]
|
input_jwt = jwt_header.split(" ")[1]
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
print(data)
|
get_logger().debug(data)
|
||||||
async def inner():
|
async def inner():
|
||||||
try:
|
try:
|
||||||
owner = data["data"]["repository"]["owner"]["username"]
|
owner = data["data"]["repository"]["owner"]["username"]
|
||||||
|
log_context["sender"] = owner
|
||||||
secrets = json.loads(secret_provider.get_secret(owner))
|
secrets = json.loads(secret_provider.get_secret(owner))
|
||||||
shared_secret = secrets["shared_secret"]
|
shared_secret = secrets["shared_secret"]
|
||||||
client_key = secrets["client_key"]
|
client_key = secrets["client_key"]
|
||||||
@ -85,11 +87,17 @@ async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Req
|
|||||||
agent = PRAgent()
|
agent = PRAgent()
|
||||||
if event == "pullrequest:created":
|
if event == "pullrequest:created":
|
||||||
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
|
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
|
||||||
await agent.handle_request(pr_url, "review")
|
log_context["api_url"] = pr_url
|
||||||
|
log_context["event"] = "pull_request"
|
||||||
|
with get_logger().contextualize(**log_context):
|
||||||
|
await agent.handle_request(pr_url, "review")
|
||||||
elif event == "pullrequest:comment_created":
|
elif event == "pullrequest:comment_created":
|
||||||
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
|
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
|
||||||
|
log_context["api_url"] = pr_url
|
||||||
|
log_context["event"] = "comment"
|
||||||
comment_body = data["data"]["comment"]["content"]["raw"]
|
comment_body = data["data"]["comment"]["content"]["raw"]
|
||||||
await agent.handle_request(pr_url, comment_body)
|
with get_logger().contextualize(**log_context):
|
||||||
|
await agent.handle_request(pr_url, comment_body)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
get_logger().error(f"Failed to handle webhook: {e}")
|
get_logger().error(f"Failed to handle webhook: {e}")
|
||||||
background_tasks.add_task(inner)
|
background_tasks.add_task(inner)
|
||||||
@ -102,9 +110,10 @@ async def handle_github_webhooks(request: Request, response: Response):
|
|||||||
@router.post("/installed")
|
@router.post("/installed")
|
||||||
async def handle_installed_webhooks(request: Request, response: Response):
|
async def handle_installed_webhooks(request: Request, response: Response):
|
||||||
try:
|
try:
|
||||||
print(request.headers)
|
get_logger().info("handle_installed_webhooks")
|
||||||
|
get_logger().info(request.headers)
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
print(data)
|
get_logger().info(data)
|
||||||
shared_secret = data["sharedSecret"]
|
shared_secret = data["sharedSecret"]
|
||||||
client_key = data["clientKey"]
|
client_key = data["clientKey"]
|
||||||
username = data["principal"]["username"]
|
username = data["principal"]["username"]
|
||||||
@ -119,8 +128,10 @@ async def handle_installed_webhooks(request: Request, response: Response):
|
|||||||
|
|
||||||
@router.post("/uninstalled")
|
@router.post("/uninstalled")
|
||||||
async def handle_uninstalled_webhooks(request: Request, response: Response):
|
async def handle_uninstalled_webhooks(request: Request, response: Response):
|
||||||
|
get_logger().info("handle_uninstalled_webhooks")
|
||||||
|
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
print(data)
|
get_logger().info(data)
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import time
|
import asyncio.locks
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
|
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
|
||||||
@ -14,8 +14,9 @@ from pr_agent.algo.utils import update_settings_from_args
|
|||||||
from pr_agent.config_loader import get_settings, global_settings
|
from pr_agent.config_loader import get_settings, global_settings
|
||||||
from pr_agent.git_providers import get_git_provider
|
from pr_agent.git_providers import get_git_provider
|
||||||
from pr_agent.git_providers.utils import apply_repo_settings
|
from pr_agent.git_providers.utils import apply_repo_settings
|
||||||
|
from pr_agent.git_providers.git_provider import IncrementalPR
|
||||||
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
||||||
from pr_agent.servers.utils import verify_signature
|
from pr_agent.servers.utils import verify_signature, DefaultDictWithTimeout
|
||||||
|
|
||||||
setup_logger(fmt=LoggingFormat.JSON)
|
setup_logger(fmt=LoggingFormat.JSON)
|
||||||
|
|
||||||
@ -47,6 +48,7 @@ async def handle_marketplace_webhooks(request: Request, response: Response):
|
|||||||
body = await get_body(request)
|
body = await get_body(request)
|
||||||
get_logger().info(f'Request body:\n{body}')
|
get_logger().info(f'Request body:\n{body}')
|
||||||
|
|
||||||
|
|
||||||
async def get_body(request):
|
async def get_body(request):
|
||||||
try:
|
try:
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
@ -61,7 +63,9 @@ async def get_body(request):
|
|||||||
return body
|
return body
|
||||||
|
|
||||||
|
|
||||||
_duplicate_requests_cache = {}
|
_duplicate_requests_cache = DefaultDictWithTimeout(ttl=get_settings().github_app.duplicate_requests_cache_ttl)
|
||||||
|
_duplicate_push_triggers = DefaultDictWithTimeout(ttl=get_settings().github_app.push_trigger_pending_tasks_ttl)
|
||||||
|
_pending_task_duplicate_push_conditions = DefaultDictWithTimeout(asyncio.locks.Condition, ttl=get_settings().github_app.push_trigger_pending_tasks_ttl)
|
||||||
|
|
||||||
|
|
||||||
async def handle_request(body: Dict[str, Any], event: str):
|
async def handle_request(body: Dict[str, Any], event: str):
|
||||||
@ -109,40 +113,110 @@ async def handle_request(body: Dict[str, Any], event: str):
|
|||||||
# handle pull_request event:
|
# handle pull_request event:
|
||||||
# automatically review opened/reopened/ready_for_review PRs as long as they're not in draft,
|
# automatically review opened/reopened/ready_for_review PRs as long as they're not in draft,
|
||||||
# as well as direct review requests from the bot
|
# as well as direct review requests from the bot
|
||||||
elif event == 'pull_request':
|
elif event == 'pull_request' and action != 'synchronize':
|
||||||
pull_request = body.get("pull_request")
|
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
|
||||||
if not pull_request:
|
if not (pull_request and api_url):
|
||||||
return {}
|
|
||||||
api_url = pull_request.get("url")
|
|
||||||
if not api_url:
|
|
||||||
return {}
|
|
||||||
log_context["api_url"] = api_url
|
|
||||||
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
|
|
||||||
return {}
|
return {}
|
||||||
if action in get_settings().github_app.handle_pr_actions:
|
if action in get_settings().github_app.handle_pr_actions:
|
||||||
if action == "review_requested":
|
if action == "review_requested":
|
||||||
if body.get("requested_reviewer", {}).get("login", "") != bot_user:
|
if body.get("requested_reviewer", {}).get("login", "") != bot_user:
|
||||||
return {}
|
return {}
|
||||||
if pull_request.get("created_at") == pull_request.get("updated_at"):
|
get_logger().info(f"Performing review for {api_url=} because of {event=} and {action=}")
|
||||||
# avoid double reviews when opening a PR for the first time
|
await _perform_commands(get_settings().github_app.pr_commands, agent, body, api_url, log_context)
|
||||||
return {}
|
|
||||||
get_logger().info(f"Performing review because of event={event} and action={action}")
|
# handle pull_request event with synchronize action - "push trigger" for new commits
|
||||||
apply_repo_settings(api_url)
|
elif event == 'pull_request' and action == 'synchronize' and get_settings().github_app.handle_push_trigger:
|
||||||
for command in get_settings().github_app.pr_commands:
|
pull_request, api_url = _check_pull_request_event(action, body, log_context, bot_user)
|
||||||
split_command = command.split(" ")
|
if not (pull_request and api_url):
|
||||||
command = split_command[0]
|
return {}
|
||||||
args = split_command[1:]
|
|
||||||
other_args = update_settings_from_args(args)
|
# TODO: do we still want to get the list of commits to filter bot/merge commits?
|
||||||
new_command = ' '.join([command] + other_args)
|
before_sha = body.get("before")
|
||||||
get_logger().info(body)
|
after_sha = body.get("after")
|
||||||
get_logger().info(f"Performing command: {new_command}")
|
merge_commit_sha = pull_request.get("merge_commit_sha")
|
||||||
with get_logger().contextualize(**log_context):
|
if before_sha == after_sha:
|
||||||
await agent.handle_request(api_url, new_command)
|
return {}
|
||||||
|
if get_settings().github_app.push_trigger_ignore_merge_commits and after_sha == merge_commit_sha:
|
||||||
|
return {}
|
||||||
|
if get_settings().github_app.push_trigger_ignore_bot_commits and body.get("sender", {}).get("login", "") == bot_user:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Prevent triggering multiple times for subsequent push triggers when one is enough:
|
||||||
|
# The first push will trigger the processing, and if there's a second push in the meanwhile it will wait.
|
||||||
|
# Any more events will be discarded, because they will all trigger the exact same processing on the PR.
|
||||||
|
# We let the second event wait instead of discarding it because while the first event was being processed,
|
||||||
|
# more commits may have been pushed that led to the subsequent events,
|
||||||
|
# so we keep just one waiting as a delegate to trigger the processing for the new commits when done waiting.
|
||||||
|
current_active_tasks = _duplicate_push_triggers.setdefault(api_url, 0)
|
||||||
|
max_active_tasks = 2 if get_settings().github_app.push_trigger_pending_tasks_backlog else 1
|
||||||
|
if current_active_tasks < max_active_tasks:
|
||||||
|
# first task can enter, and second tasks too if backlog is enabled
|
||||||
|
get_logger().info(
|
||||||
|
f"Continue processing push trigger for {api_url=} because there are {current_active_tasks} active tasks"
|
||||||
|
)
|
||||||
|
_duplicate_push_triggers[api_url] += 1
|
||||||
|
else:
|
||||||
|
get_logger().info(
|
||||||
|
f"Skipping push trigger for {api_url=} because another event already triggered the same processing"
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
async with _pending_task_duplicate_push_conditions[api_url]:
|
||||||
|
if current_active_tasks == 1:
|
||||||
|
# second task waits
|
||||||
|
get_logger().info(
|
||||||
|
f"Waiting to process push trigger for {api_url=} because the first task is still in progress"
|
||||||
|
)
|
||||||
|
await _pending_task_duplicate_push_conditions[api_url].wait()
|
||||||
|
get_logger().info(f"Finished waiting to process push trigger for {api_url=} - continue with flow")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if get_settings().github_app.push_trigger_wait_for_initial_review and not get_git_provider()(api_url, incremental=IncrementalPR(True)).previous_review:
|
||||||
|
get_logger().info(f"Skipping incremental review because there was no initial review for {api_url=} yet")
|
||||||
|
return {}
|
||||||
|
get_logger().info(f"Performing incremental review for {api_url=} because of {event=} and {action=}")
|
||||||
|
await _perform_commands(get_settings().github_app.push_commands, agent, body, api_url, log_context)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# release the waiting task block
|
||||||
|
async with _pending_task_duplicate_push_conditions[api_url]:
|
||||||
|
_pending_task_duplicate_push_conditions[api_url].notify(1)
|
||||||
|
_duplicate_push_triggers[api_url] -= 1
|
||||||
|
|
||||||
get_logger().info("event or action does not require handling")
|
get_logger().info("event or action does not require handling")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_pull_request_event(action: str, body: dict, log_context: dict, bot_user: str) -> Tuple[Dict[str, Any], str]:
|
||||||
|
invalid_result = {}, ""
|
||||||
|
pull_request = body.get("pull_request")
|
||||||
|
if not pull_request:
|
||||||
|
return invalid_result
|
||||||
|
api_url = pull_request.get("url")
|
||||||
|
if not api_url:
|
||||||
|
return invalid_result
|
||||||
|
log_context["api_url"] = api_url
|
||||||
|
if pull_request.get("draft", True) or pull_request.get("state") != "open" or pull_request.get("user", {}).get("login", "") == bot_user:
|
||||||
|
return invalid_result
|
||||||
|
if action in ("review_requested", "synchronize") and pull_request.get("created_at") == pull_request.get("updated_at"):
|
||||||
|
# avoid double reviews when opening a PR for the first time
|
||||||
|
return invalid_result
|
||||||
|
return pull_request, api_url
|
||||||
|
|
||||||
|
|
||||||
|
async def _perform_commands(commands: List[str], agent: PRAgent, body: dict, api_url: str, log_context: dict):
|
||||||
|
apply_repo_settings(api_url)
|
||||||
|
for command in commands:
|
||||||
|
split_command = command.split(" ")
|
||||||
|
command = split_command[0]
|
||||||
|
args = split_command[1:]
|
||||||
|
other_args = update_settings_from_args(args)
|
||||||
|
new_command = ' '.join([command] + other_args)
|
||||||
|
get_logger().info(body)
|
||||||
|
get_logger().info(f"Performing command: {new_command}")
|
||||||
|
with get_logger().contextualize(**log_context):
|
||||||
|
await agent.handle_request(api_url, new_command)
|
||||||
|
|
||||||
|
|
||||||
def _is_duplicate_request(body: Dict[str, Any]) -> bool:
|
def _is_duplicate_request(body: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
In some deployments its possible to get duplicate requests if the handling is long,
|
In some deployments its possible to get duplicate requests if the handling is long,
|
||||||
@ -150,13 +224,8 @@ def _is_duplicate_request(body: Dict[str, Any]) -> bool:
|
|||||||
"""
|
"""
|
||||||
request_hash = hash(str(body))
|
request_hash = hash(str(body))
|
||||||
get_logger().info(f"request_hash: {request_hash}")
|
get_logger().info(f"request_hash: {request_hash}")
|
||||||
request_time = time.monotonic()
|
is_duplicate = _duplicate_requests_cache.get(request_hash, False)
|
||||||
ttl = get_settings().github_app.duplicate_requests_cache_ttl # in seconds
|
_duplicate_requests_cache[request_hash] = True
|
||||||
to_delete = [key for key, key_time in _duplicate_requests_cache.items() if request_time - key_time > ttl]
|
|
||||||
for key in to_delete:
|
|
||||||
del _duplicate_requests_cache[key]
|
|
||||||
is_duplicate = request_hash in _duplicate_requests_cache
|
|
||||||
_duplicate_requests_cache[request_hash] = request_time
|
|
||||||
if is_duplicate:
|
if is_duplicate:
|
||||||
get_logger().info(f"Ignoring duplicate request {request_hash}")
|
get_logger().info(f"Ignoring duplicate request {request_hash}")
|
||||||
return is_duplicate
|
return is_duplicate
|
||||||
|
@ -12,23 +12,33 @@ from starlette_context.middleware import RawContextMiddleware
|
|||||||
|
|
||||||
from pr_agent.agent.pr_agent import PRAgent
|
from pr_agent.agent.pr_agent import PRAgent
|
||||||
from pr_agent.config_loader import get_settings, global_settings
|
from pr_agent.config_loader import get_settings, global_settings
|
||||||
from pr_agent.log import get_logger, setup_logger
|
from pr_agent.log import LoggingFormat, get_logger, setup_logger
|
||||||
from pr_agent.secret_providers import get_secret_provider
|
from pr_agent.secret_providers import get_secret_provider
|
||||||
|
|
||||||
setup_logger()
|
setup_logger(fmt=LoggingFormat.JSON)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
|
secret_provider = get_secret_provider() if get_settings().get("CONFIG.SECRET_PROVIDER") else None
|
||||||
|
|
||||||
|
|
||||||
|
def handle_request(background_tasks: BackgroundTasks, url: str, body: str, log_context: dict):
|
||||||
|
log_context["action"] = body
|
||||||
|
log_context["event"] = "pull_request" if body == "/review" else "comment"
|
||||||
|
log_context["api_url"] = url
|
||||||
|
with get_logger().contextualize(**log_context):
|
||||||
|
background_tasks.add_task(PRAgent().handle_request, url, body)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/webhook")
|
@router.post("/webhook")
|
||||||
async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
||||||
|
log_context = {"server_type": "gitlab_app"}
|
||||||
if request.headers.get("X-Gitlab-Token") and secret_provider:
|
if request.headers.get("X-Gitlab-Token") and secret_provider:
|
||||||
request_token = request.headers.get("X-Gitlab-Token")
|
request_token = request.headers.get("X-Gitlab-Token")
|
||||||
secret = secret_provider.get_secret(request_token)
|
secret = secret_provider.get_secret(request_token)
|
||||||
try:
|
try:
|
||||||
secret_dict = json.loads(secret)
|
secret_dict = json.loads(secret)
|
||||||
gitlab_token = secret_dict["gitlab_token"]
|
gitlab_token = secret_dict["gitlab_token"]
|
||||||
|
log_context["sender"] = secret_dict["id"]
|
||||||
context["settings"] = copy.deepcopy(global_settings)
|
context["settings"] = copy.deepcopy(global_settings)
|
||||||
context["settings"].gitlab.personal_access_token = gitlab_token
|
context["settings"].gitlab.personal_access_token = gitlab_token
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -48,13 +58,13 @@ async def gitlab_webhook(background_tasks: BackgroundTasks, request: Request):
|
|||||||
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
if data.get('object_kind') == 'merge_request' and data['object_attributes'].get('action') in ['open', 'reopen']:
|
||||||
get_logger().info(f"A merge request has been opened: {data['object_attributes'].get('title')}")
|
get_logger().info(f"A merge request has been opened: {data['object_attributes'].get('title')}")
|
||||||
url = data['object_attributes'].get('url')
|
url = data['object_attributes'].get('url')
|
||||||
background_tasks.add_task(PRAgent().handle_request, url, "/review")
|
handle_request(background_tasks, url, "/review")
|
||||||
elif data.get('object_kind') == 'note' and data['event_type'] == 'note':
|
elif data.get('object_kind') == 'note' and data['event_type'] == 'note':
|
||||||
if 'merge_request' in data:
|
if 'merge_request' in data:
|
||||||
mr = data['merge_request']
|
mr = data['merge_request']
|
||||||
url = mr.get('url')
|
url = mr.get('url')
|
||||||
body = data.get('object_attributes', {}).get('note')
|
body = data.get('object_attributes', {}).get('note')
|
||||||
background_tasks.add_task(PRAgent().handle_request, url, body)
|
handle_request(background_tasks, url, body)
|
||||||
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
|
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder({"message": "success"}))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Callable, Any
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
@ -25,3 +28,59 @@ def verify_signature(payload_body, secret_token, signature_header):
|
|||||||
class RateLimitExceeded(Exception):
|
class RateLimitExceeded(Exception):
|
||||||
"""Raised when the git provider API rate limit has been exceeded."""
|
"""Raised when the git provider API rate limit has been exceeded."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultDictWithTimeout(defaultdict):
|
||||||
|
"""A defaultdict with a time-to-live (TTL)."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
default_factory: Callable[[], Any] = None,
|
||||||
|
ttl: int = None,
|
||||||
|
refresh_interval: int = 60,
|
||||||
|
update_key_time_on_get: bool = True,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
default_factory: The default factory to use for keys that are not in the dictionary.
|
||||||
|
ttl: The time-to-live (TTL) in seconds.
|
||||||
|
refresh_interval: How often to refresh the dict and delete items older than the TTL.
|
||||||
|
update_key_time_on_get: Whether to update the access time of a key also on get (or only when set).
|
||||||
|
"""
|
||||||
|
super().__init__(default_factory, *args, **kwargs)
|
||||||
|
self.__key_times = dict()
|
||||||
|
self.__ttl = ttl
|
||||||
|
self.__refresh_interval = refresh_interval
|
||||||
|
self.__update_key_time_on_get = update_key_time_on_get
|
||||||
|
self.__last_refresh = self.__time() - self.__refresh_interval
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __time():
|
||||||
|
return time.monotonic()
|
||||||
|
|
||||||
|
def __refresh(self):
|
||||||
|
if self.__ttl is None:
|
||||||
|
return
|
||||||
|
request_time = self.__time()
|
||||||
|
if request_time - self.__last_refresh > self.__refresh_interval:
|
||||||
|
return
|
||||||
|
to_delete = [key for key, key_time in self.__key_times.items() if request_time - key_time > self.__ttl]
|
||||||
|
for key in to_delete:
|
||||||
|
del self[key]
|
||||||
|
self.__last_refresh = request_time
|
||||||
|
|
||||||
|
def __getitem__(self, __key):
|
||||||
|
if self.__update_key_time_on_get:
|
||||||
|
self.__key_times[__key] = self.__time()
|
||||||
|
self.__refresh()
|
||||||
|
return super().__getitem__(__key)
|
||||||
|
|
||||||
|
def __setitem__(self, __key, __value):
|
||||||
|
self.__key_times[__key] = self.__time()
|
||||||
|
return super().__setitem__(__key, __value)
|
||||||
|
|
||||||
|
def __delitem__(self, __key):
|
||||||
|
del self.__key_times[__key]
|
||||||
|
return super().__delitem__(__key)
|
||||||
|
@ -24,6 +24,7 @@ num_code_suggestions=4
|
|||||||
inline_code_comments = false
|
inline_code_comments = false
|
||||||
ask_and_reflect=false
|
ask_and_reflect=false
|
||||||
automatic_review=true
|
automatic_review=true
|
||||||
|
remove_previous_review_comment=false
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
|
|
||||||
[pr_description] # /describe #
|
[pr_description] # /describe #
|
||||||
@ -31,6 +32,7 @@ publish_labels=true
|
|||||||
publish_description_as_comment=false
|
publish_description_as_comment=false
|
||||||
add_original_user_description=false
|
add_original_user_description=false
|
||||||
keep_original_user_title=false
|
keep_original_user_title=false
|
||||||
|
use_bullet_points=true
|
||||||
extra_instructions = ""
|
extra_instructions = ""
|
||||||
# markers
|
# markers
|
||||||
use_description_markers=false
|
use_description_markers=false
|
||||||
@ -82,6 +84,27 @@ pr_commands = [
|
|||||||
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
||||||
"/auto_review",
|
"/auto_review",
|
||||||
]
|
]
|
||||||
|
# settings for "pull_request" event with "synchronize" action - used to detect and handle push triggers for new commits
|
||||||
|
handle_push_trigger = false
|
||||||
|
push_trigger_ignore_bot_commits = true
|
||||||
|
push_trigger_ignore_merge_commits = true
|
||||||
|
push_trigger_wait_for_initial_review = true
|
||||||
|
push_trigger_pending_tasks_backlog = true
|
||||||
|
push_trigger_pending_tasks_ttl = 300
|
||||||
|
push_commands = [
|
||||||
|
"/describe --pr_description.add_original_user_description=true --pr_description.keep_original_user_title=true",
|
||||||
|
"""/auto_review -i \
|
||||||
|
--pr_reviewer.require_focused_review=false \
|
||||||
|
--pr_reviewer.require_score_review=false \
|
||||||
|
--pr_reviewer.require_tests_review=false \
|
||||||
|
--pr_reviewer.require_security_review=false \
|
||||||
|
--pr_reviewer.require_estimate_effort_to_review=false \
|
||||||
|
--pr_reviewer.num_code_suggestions=0 \
|
||||||
|
--pr_reviewer.inline_code_comments=false \
|
||||||
|
--pr_reviewer.remove_previous_review_comment=true \
|
||||||
|
--pr_reviewer.extra_instructions='' \
|
||||||
|
"""
|
||||||
|
]
|
||||||
|
|
||||||
[gitlab]
|
[gitlab]
|
||||||
# URL to the gitlab service
|
# URL to the gitlab service
|
||||||
|
@ -433,3 +433,6 @@ reStructuredText = [".rst", ".rest", ".rest.txt", ".rst.txt", ]
|
|||||||
wisp = [".wisp", ]
|
wisp = [".wisp", ]
|
||||||
xBase = [".prg", ".prw", ]
|
xBase = [".prg", ".prw", ]
|
||||||
|
|
||||||
|
[docs_blacklist_extensions]
|
||||||
|
# Disable docs for these extensions of text files and scripts that are not programming languages of function, classes and methods
|
||||||
|
docs_blacklist = ['sql', 'txt', 'yaml', 'json', 'xml', 'md', 'rst', 'rest', 'rest.txt', 'rst.txt', 'mdpolicy', 'mdown', 'markdown', 'mdwn', 'mkd', 'mkdn', 'mkdown', 'sh']
|
@ -31,7 +31,8 @@ PR Type:
|
|||||||
- Other
|
- Other
|
||||||
PR Description:
|
PR Description:
|
||||||
type: string
|
type: string
|
||||||
description: an informative and concise description of the PR
|
description: an informative and concise description of the PR.
|
||||||
|
{%- if use_bullet_points %} Use bullet points. {% endif %}
|
||||||
PR Main Files Walkthrough:
|
PR Main Files Walkthrough:
|
||||||
type: array
|
type: array
|
||||||
maxItems: 10
|
maxItems: 10
|
||||||
|
@ -25,7 +25,7 @@ code line that already existed in the file....
|
|||||||
The review should focus on new code added in the PR (lines starting with '+'), and not on code that already existed in the file (lines starting with '-', or without prefix).
|
The review should focus on new code added in the PR (lines starting with '+'), and not on code that already existed in the file (lines starting with '-', or without prefix).
|
||||||
|
|
||||||
{%- if num_code_suggestions > 0 %}
|
{%- if num_code_suggestions > 0 %}
|
||||||
- Provide up to {{ num_code_suggestions }} code suggestions.
|
- Provide up to {{ num_code_suggestions }} code suggestions. Try to provide diverse and insightful suggestions.
|
||||||
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
|
- Focus on important suggestions like fixing code problems, issues and bugs. As a second priority, provide suggestions for meaningful code improvements, like performance, vulnerability, modularity, and best practices.
|
||||||
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
|
- Avoid making suggestions that have already been implemented in the PR code. For example, if you want to add logs, or change a variable to const, or anything else, make sure it isn't already in the PR code.
|
||||||
- Don't suggest to add docstring, type hints, or comments.
|
- Don't suggest to add docstring, type hints, or comments.
|
||||||
@ -99,10 +99,10 @@ PR Feedback:
|
|||||||
General suggestions:
|
General suggestions:
|
||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
General suggestions and feedback for the contributors and maintainers of
|
General suggestions and feedback for the contributors and maintainers of this PR.
|
||||||
this PR. May include important suggestions for the overall structure,
|
May include important suggestions for the overall structure,
|
||||||
primary purpose, best practices, critical bugs, and other aspects of the
|
primary purpose, best practices, critical bugs, and other aspects of the PR.
|
||||||
PR. Don't address PR title and description, or lack of tests. Explain your suggestions.
|
Don't address PR title and description, or lack of tests. Explain your suggestions.
|
||||||
{%- if num_code_suggestions > 0 %}
|
{%- if num_code_suggestions > 0 %}
|
||||||
Code feedback:
|
Code feedback:
|
||||||
type: array
|
type: array
|
||||||
@ -115,11 +115,10 @@ PR Feedback:
|
|||||||
suggestion:
|
suggestion:
|
||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
a concrete suggestion for meaningfully improving the new PR code. Also
|
a concrete suggestion for meaningfully improving the new PR code.
|
||||||
describe how, specifically, the suggestion can be applied to new PR
|
Also describe how, specifically, the suggestion can be applied to new PR code.
|
||||||
code. Add tags with importance measure that matches each suggestion
|
Add tags with importance measure that matches each suggestion ('important' or 'medium').
|
||||||
('important' or 'medium'). Do not make suggestions for updating or
|
Do not make suggestions for updating or adding docstrings, renaming PR title and description, or linter like.
|
||||||
adding docstrings, renaming PR title and description, or linter like.
|
|
||||||
relevant line:
|
relevant line:
|
||||||
type: string
|
type: string
|
||||||
description: |-
|
description: |-
|
||||||
|
@ -65,6 +65,11 @@ class PRAddDocs:
|
|||||||
|
|
||||||
async def _prepare_prediction(self, model: str):
|
async def _prepare_prediction(self, model: str):
|
||||||
get_logger().info('Getting PR diff...')
|
get_logger().info('Getting PR diff...')
|
||||||
|
|
||||||
|
# Disable adding docs to scripts and other non-relevant text files
|
||||||
|
from pr_agent.algo.language_handler import bad_extensions
|
||||||
|
bad_extensions += get_settings().docs_blacklist_extensions.docs_blacklist
|
||||||
|
|
||||||
self.patches_diff = get_pr_diff(self.git_provider,
|
self.patches_diff = get_pr_diff(self.git_provider,
|
||||||
self.token_handler,
|
self.token_handler,
|
||||||
model,
|
model,
|
||||||
|
@ -40,6 +40,7 @@ class PRDescription:
|
|||||||
"description": self.git_provider.get_pr_description(full=False),
|
"description": self.git_provider.get_pr_description(full=False),
|
||||||
"language": self.main_pr_language,
|
"language": self.main_pr_language,
|
||||||
"diff": "", # empty diff for initial calculation
|
"diff": "", # empty diff for initial calculation
|
||||||
|
"use_bullet_points": get_settings().pr_description.use_bullet_points,
|
||||||
"extra_instructions": get_settings().pr_description.extra_instructions,
|
"extra_instructions": get_settings().pr_description.extra_instructions,
|
||||||
"commit_messages_str": self.git_provider.get_commit_messages()
|
"commit_messages_str": self.git_provider.get_commit_messages()
|
||||||
}
|
}
|
||||||
@ -185,6 +186,11 @@ class PRDescription:
|
|||||||
else:
|
else:
|
||||||
ai_header = ""
|
ai_header = ""
|
||||||
|
|
||||||
|
ai_type = self.data.get('PR Type')
|
||||||
|
if ai_type and not re.search(r'<!--\s*pr_agent:type\s*-->', body):
|
||||||
|
pr_type = f"{ai_header}{ai_type}"
|
||||||
|
body = body.replace('pr_agent:type', pr_type)
|
||||||
|
|
||||||
ai_summary = self.data.get('PR Description')
|
ai_summary = self.data.get('PR Description')
|
||||||
if ai_summary and not re.search(r'<!--\s*pr_agent:summary\s*-->', body):
|
if ai_summary and not re.search(r'<!--\s*pr_agent:summary\s*-->', body):
|
||||||
summary = f"{ai_header}{ai_summary}"
|
summary = f"{ai_header}{ai_summary}"
|
||||||
|
@ -98,6 +98,9 @@ class PRReviewer:
|
|||||||
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
if self.is_auto and not get_settings().pr_reviewer.automatic_review:
|
||||||
get_logger().info(f'Automatic review is disabled {self.pr_url}')
|
get_logger().info(f'Automatic review is disabled {self.pr_url}')
|
||||||
return None
|
return None
|
||||||
|
if self.is_auto and self.incremental.is_incremental and not self.incremental.first_new_commit_sha:
|
||||||
|
get_logger().info(f"Incremental review is enabled for {self.pr_url} but there are no new commits")
|
||||||
|
return None
|
||||||
|
|
||||||
get_logger().info(f'Reviewing PR: {self.pr_url} ...')
|
get_logger().info(f'Reviewing PR: {self.pr_url} ...')
|
||||||
|
|
||||||
@ -111,9 +114,10 @@ class PRReviewer:
|
|||||||
|
|
||||||
if get_settings().config.publish_output:
|
if get_settings().config.publish_output:
|
||||||
get_logger().info('Pushing PR review...')
|
get_logger().info('Pushing PR review...')
|
||||||
|
previous_review_comment = self._get_previous_review_comment()
|
||||||
self.git_provider.publish_comment(pr_comment)
|
self.git_provider.publish_comment(pr_comment)
|
||||||
self.git_provider.remove_initial_comment()
|
self.git_provider.remove_initial_comment()
|
||||||
|
self._remove_previous_review_comment(previous_review_comment)
|
||||||
if get_settings().pr_reviewer.inline_code_comments:
|
if get_settings().pr_reviewer.inline_code_comments:
|
||||||
get_logger().info('Pushing inline code comments...')
|
get_logger().info('Pushing inline code comments...')
|
||||||
self._publish_inline_code_comments()
|
self._publish_inline_code_comments()
|
||||||
@ -228,9 +232,13 @@ class PRReviewer:
|
|||||||
if self.incremental.is_incremental:
|
if self.incremental.is_incremental:
|
||||||
last_commit_url = f"{self.git_provider.get_pr_url()}/commits/" \
|
last_commit_url = f"{self.git_provider.get_pr_url()}/commits/" \
|
||||||
f"{self.git_provider.incremental.first_new_commit_sha}"
|
f"{self.git_provider.incremental.first_new_commit_sha}"
|
||||||
|
last_commit_msg = self.incremental.commits_range[0].commit.message if self.incremental.commits_range else ""
|
||||||
|
incremental_review_markdown_text = f"Starting from commit {last_commit_url}"
|
||||||
|
if last_commit_msg:
|
||||||
|
incremental_review_markdown_text += f" \n_({last_commit_msg.splitlines(keepends=False)[0]})_"
|
||||||
data = OrderedDict(data)
|
data = OrderedDict(data)
|
||||||
data.update({'Incremental PR Review': {
|
data.update({'Incremental PR Review': {
|
||||||
"⏮️ Review for commits since previous PR-Agent review": f"Starting from commit {last_commit_url}"}})
|
"⏮️ Review for commits since previous PR-Agent review": incremental_review_markdown_text}})
|
||||||
data.move_to_end('Incremental PR Review', last=False)
|
data.move_to_end('Incremental PR Review', last=False)
|
||||||
|
|
||||||
markdown_text = convert_to_markdown(data, self.git_provider.is_supported("gfm_markdown"))
|
markdown_text = convert_to_markdown(data, self.git_provider.is_supported("gfm_markdown"))
|
||||||
@ -311,3 +319,26 @@ class PRReviewer:
|
|||||||
break
|
break
|
||||||
|
|
||||||
return question_str, answer_str
|
return question_str, answer_str
|
||||||
|
|
||||||
|
def _get_previous_review_comment(self):
|
||||||
|
"""
|
||||||
|
Get the previous review comment if it exists.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if get_settings().pr_reviewer.remove_previous_review_comment and hasattr(self.git_provider, "get_previous_review"):
|
||||||
|
return self.git_provider.get_previous_review(
|
||||||
|
full=not self.incremental.is_incremental,
|
||||||
|
incremental=self.incremental.is_incremental,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().exception(f"Failed to get previous review comment, error: {e}")
|
||||||
|
|
||||||
|
def _remove_previous_review_comment(self, comment):
|
||||||
|
"""
|
||||||
|
Remove the previous review comment if it exists.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if get_settings().pr_reviewer.remove_previous_review_comment and comment:
|
||||||
|
self.git_provider.remove_comment(comment)
|
||||||
|
except Exception as e:
|
||||||
|
get_logger().exception(f"Failed to remove previous review comment, error: {e}")
|
||||||
|
@ -48,9 +48,9 @@ class PRSimilarIssue:
|
|||||||
# check if index exists, and if repo is already indexed
|
# check if index exists, and if repo is already indexed
|
||||||
run_from_scratch = False
|
run_from_scratch = False
|
||||||
if run_from_scratch: # for debugging
|
if run_from_scratch: # for debugging
|
||||||
if not index_name in pinecone.list_indexes():
|
pinecone.init(api_key=api_key, environment=environment)
|
||||||
|
if index_name in pinecone.list_indexes():
|
||||||
get_logger().info('Removing index...')
|
get_logger().info('Removing index...')
|
||||||
pinecone.init(api_key=api_key, environment=environment)
|
|
||||||
pinecone.delete_index(index_name)
|
pinecone.delete_index(index_name)
|
||||||
get_logger().info('Done')
|
get_logger().info('Done')
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user