mirror of
https://github.com/qodo-ai/pr-agent.git
synced 2025-07-05 05:10:38 +08:00
Merge commit 'bdf7eff7cd0a8894c3e66e49bdf89f27da1bfcb4' into hl/incremental_review
This commit is contained in:
10
README.md
10
README.md
@ -24,25 +24,25 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review PRs
|
|||||||
|
|
||||||
<h3>Example results:</h2>
|
<h3>Example results:</h2>
|
||||||
</div>
|
</div>
|
||||||
<h4>Describe:</h4>
|
<h4>/describe:</h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img src="https://codium.ai/images/describe.gif" width="800">
|
<img src="https://codium.ai/images/describe.gif" width="800">
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h4>Review:</h4>
|
<h4>/review:</h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img src="https://codium.ai/images/review.gif" width="800">
|
<img src="https://codium.ai/images/review.gif" width="800">
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h4>Ask:</h4>
|
<h4>/ask:</h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img src="https://codium.ai/images/ask.gif" width="800">
|
<img src="https://codium.ai/images/ask.gif" width="800">
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h4>Improve:</h4>
|
<h4>/improve:</h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<p float="center">
|
<p float="center">
|
||||||
<img src="https://codium.ai/images/improve.gif" width="800">
|
<img src="https://codium.ai/images/improve.gif" width="800">
|
||||||
@ -132,7 +132,7 @@ Here are several ways to install and run PR-Agent:
|
|||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more details on how we convert a code diff to a manageable LLM prompt
|
Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more details on how we convert a code diff to a manageable LLM prompt
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ def convert_to_markdown(output_data: dict) -> str:
|
|||||||
"Focused PR": "✨",
|
"Focused PR": "✨",
|
||||||
"Security concerns": "🔒",
|
"Security concerns": "🔒",
|
||||||
"General PR suggestions": "💡",
|
"General PR suggestions": "💡",
|
||||||
|
"Insights from user's answers": "📝",
|
||||||
"Code suggestions": "🤖"
|
"Code suggestions": "🤖"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class BitbucketProvider:
|
|||||||
self.set_pr(pr_url)
|
self.set_pr(pr_url)
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
def is_supported(self, capability: str) -> bool:
|
||||||
if capability == 'get_issue_comments':
|
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -65,6 +65,12 @@ class BitbucketProvider:
|
|||||||
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):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
|
raise NotImplementedError("Bitbucket provider does not support creating inline comments yet")
|
||||||
|
|
||||||
|
def publish_inline_comments(self, comments: list[dict]):
|
||||||
|
raise NotImplementedError("Bitbucket provider does not support publishing inline comments yet")
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return self.pr.title
|
return self.pr.title
|
||||||
|
|
||||||
|
@ -44,6 +44,14 @@ class GitProvider(ABC):
|
|||||||
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):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def publish_inline_comments(self, comments: list[dict]):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def publish_code_suggestion(self, body: str, relevant_file: str,
|
def publish_code_suggestion(self, body: str, relevant_file: str,
|
||||||
relevant_lines_start: int, relevant_lines_end: int):
|
relevant_lines_start: int, relevant_lines_end: int):
|
||||||
|
@ -3,7 +3,7 @@ from datetime import datetime
|
|||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from github import AppAuthentication, Github
|
from github import AppAuthentication, Github, Auth
|
||||||
|
|
||||||
from pr_agent.config_loader import settings
|
from pr_agent.config_loader import settings
|
||||||
|
|
||||||
@ -103,6 +103,9 @@ class GithubProvider(GitProvider):
|
|||||||
self.pr.comments_list.append(response)
|
self.pr.comments_list.append(response)
|
||||||
|
|
||||||
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):
|
||||||
|
self.publish_inline_comments([self.create_inline_comment(body, relevant_file, relevant_line_in_file)])
|
||||||
|
|
||||||
|
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
self.diff_files = self.diff_files if self.diff_files else self.get_diff_files()
|
self.diff_files = self.diff_files if self.diff_files else self.get_diff_files()
|
||||||
position = -1
|
position = -1
|
||||||
for file in self.diff_files:
|
for file in self.diff_files:
|
||||||
@ -121,9 +124,16 @@ class GithubProvider(GitProvider):
|
|||||||
if position == -1:
|
if position == -1:
|
||||||
if settings.config.verbosity_level >= 2:
|
if settings.config.verbosity_level >= 2:
|
||||||
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}")
|
||||||
|
subject_type = "FILE"
|
||||||
else:
|
else:
|
||||||
|
subject_type = "LINE"
|
||||||
path = relevant_file.strip()
|
path = relevant_file.strip()
|
||||||
self.pr.create_review_comment(body=body, commit_id=self.last_commit_id, path=path, position=position)
|
# placeholder for future API support (already supported in single inline comment)
|
||||||
|
# return dict(body=body, path=path, position=position, subject_type=subject_type)
|
||||||
|
return dict(body=body, path=path, position=position) if subject_type == "LINE" else {}
|
||||||
|
|
||||||
|
def publish_inline_comments(self, comments: list[dict]):
|
||||||
|
self.pr.create_review(commit=self.last_commit_id, comments=comments)
|
||||||
|
|
||||||
def publish_code_suggestion(self, body: str,
|
def publish_code_suggestion(self, body: str,
|
||||||
relevant_file: str,
|
relevant_file: str,
|
||||||
@ -264,7 +274,7 @@ class GithubProvider(GitProvider):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"GitHub token is required when using user deployment. See: "
|
"GitHub token is required when using user deployment. See: "
|
||||||
"https://github.com/Codium-ai/pr-agent#method-2-run-from-source") from e
|
"https://github.com/Codium-ai/pr-agent#method-2-run-from-source") from e
|
||||||
return Github(token)
|
return Github(auth=Auth.Token(token))
|
||||||
|
|
||||||
def _get_repo(self):
|
def _get_repo(self):
|
||||||
return self.github_client.get_repo(self.repo)
|
return self.github_client.get_repo(self.repo)
|
||||||
|
@ -35,7 +35,7 @@ class GitLabProvider(GitProvider):
|
|||||||
self.incremental = incremental
|
self.incremental = incremental
|
||||||
|
|
||||||
def is_supported(self, capability: str) -> bool:
|
def is_supported(self, capability: str) -> bool:
|
||||||
if capability == 'get_issue_comments':
|
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments']:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -109,6 +109,12 @@ class GitLabProvider(GitProvider):
|
|||||||
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
|
self.send_inline_comment(body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
|
||||||
target_file, target_line_no)
|
target_file, target_line_no)
|
||||||
|
|
||||||
|
def create_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str):
|
||||||
|
raise NotImplementedError("Gitlab provider does not support creating inline comments yet")
|
||||||
|
|
||||||
|
def create_inline_comment(self, comments: list[dict]):
|
||||||
|
raise NotImplementedError("Gitlab provider does not support publishing inline comments yet")
|
||||||
|
|
||||||
def send_inline_comment(self, body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
|
def send_inline_comment(self, body, edit_type, found, relevant_file, relevant_line_in_file, source_line_no,
|
||||||
target_file, target_line_no):
|
target_file, target_line_no):
|
||||||
if not found:
|
if not found:
|
||||||
|
@ -24,7 +24,7 @@ class PRReviewer:
|
|||||||
self.is_answer = is_answer
|
self.is_answer = is_answer
|
||||||
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
|
if self.is_answer and not self.git_provider.is_supported("get_issue_comments"):
|
||||||
raise Exception(f"Answer mode is not supported for {settings.config.git_provider} for now")
|
raise Exception(f"Answer mode is not supported for {settings.config.git_provider} for now")
|
||||||
answer_str = question_str = self._get_user_answers()
|
answer_str, question_str = self._get_user_answers()
|
||||||
self.ai_handler = AiHandler()
|
self.ai_handler = AiHandler()
|
||||||
self.patches_diff = None
|
self.patches_diff = None
|
||||||
self.prediction = None
|
self.prediction = None
|
||||||
@ -99,6 +99,12 @@ class PRReviewer:
|
|||||||
if settings.config.git_provider != 'bitbucket' and \
|
if settings.config.git_provider != 'bitbucket' and \
|
||||||
settings.pr_reviewer.inline_code_comments and \
|
settings.pr_reviewer.inline_code_comments and \
|
||||||
'Code suggestions' in data['PR Feedback']:
|
'Code suggestions' in data['PR Feedback']:
|
||||||
|
# keeping only code suggestions that can't be submitted as inline comments
|
||||||
|
data['PR Feedback']['Code suggestions'] = [
|
||||||
|
d for d in data['PR Feedback']['Code suggestions']
|
||||||
|
if any(key not in d for key in ('relevant file', 'relevant line in file', 'suggestion content'))
|
||||||
|
]
|
||||||
|
if not data['PR Feedback']['Code suggestions']:
|
||||||
del data['PR Feedback']['Code suggestions']
|
del data['PR Feedback']['Code suggestions']
|
||||||
|
|
||||||
markdown_text = convert_to_markdown(data)
|
markdown_text = convert_to_markdown(data)
|
||||||
@ -125,16 +131,24 @@ class PRReviewer:
|
|||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
data = try_fix_json(review)
|
data = try_fix_json(review)
|
||||||
|
|
||||||
if settings.pr_reviewer.num_code_suggestions > 0:
|
comments = []
|
||||||
try:
|
|
||||||
for d in data['PR Feedback']['Code suggestions']:
|
for d in data['PR Feedback']['Code suggestions']:
|
||||||
relevant_file = d['relevant file'].strip()
|
relevant_file = d.get('relevant file', '').strip()
|
||||||
relevant_line_in_file = d['relevant line in file'].strip()
|
relevant_line_in_file = d.get('relevant line in file', '').strip()
|
||||||
content = d['suggestion content']
|
content = d.get('suggestion content', '')
|
||||||
|
if not relevant_file or not relevant_line_in_file or not content:
|
||||||
|
logging.info("Skipping inline comment with missing file/line/content")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.git_provider.is_supported("create_inline_comment"):
|
||||||
|
comment = self.git_provider.create_inline_comment(content, relevant_file, relevant_line_in_file)
|
||||||
|
if comment:
|
||||||
|
comments.append(comment)
|
||||||
|
else:
|
||||||
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
|
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
|
||||||
except KeyError:
|
|
||||||
pass
|
if comments:
|
||||||
|
self.git_provider.publish_inline_comments(comments)
|
||||||
|
|
||||||
def _get_user_answers(self):
|
def _get_user_answers(self):
|
||||||
answer_str = question_str = ""
|
answer_str = question_str = ""
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
dynaconf==3.1.12
|
dynaconf==3.1.12
|
||||||
fastapi==0.99.0
|
fastapi==0.99.0
|
||||||
PyGithub==1.58.2
|
PyGithub==1.59.*
|
||||||
retry==0.9.2
|
retry==0.9.2
|
||||||
openai==0.27.8
|
openai==0.27.8
|
||||||
Jinja2==3.1.2
|
Jinja2==3.1.2
|
||||||
|
Reference in New Issue
Block a user