Merge commit 'bdf7eff7cd0a8894c3e66e49bdf89f27da1bfcb4' into hl/incremental_review

This commit is contained in:
Hussam.lawen
2023-07-18 23:28:43 +03:00
8 changed files with 69 additions and 24 deletions

View File

@ -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
![PR-Agent Tools](https://codium.ai/images/pr_agent_overview.png) ![PR-Agent Tools](https://www.codium.ai/wp-content/uploads/2023/07/pr-agent-schema-updated.png)
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

View File

@ -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": "🤖"
} }

View File

@ -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

View File

@ -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):

View File

@ -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:
path = relevant_file.strip() subject_type = "LINE"
self.pr.create_review_comment(body=body, commit_id=self.last_commit_id, path=path, position=position) path = relevant_file.strip()
# 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)

View File

@ -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:

View File

@ -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
@ -51,7 +51,7 @@ class PRReviewer:
async def review(self): async def review(self):
logging.info('Reviewing PR...') logging.info('Reviewing PR...')
if settings.config.publish_output: if settings.config.publish_output:
self.git_provider.publish_comment("Preparing review...", is_temporary=True) self.git_provider.publish_comment("Preparing review...", is_temporary=True)
logging.info('Getting PR diff...') logging.info('Getting PR diff...')
self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) self.patches_diff = get_pr_diff(self.git_provider, self.token_handler)
logging.info('Getting AI prediction...') logging.info('Getting AI prediction...')
@ -99,7 +99,13 @@ 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']:
del data['PR Feedback']['Code suggestions'] # 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']
markdown_text = convert_to_markdown(data) markdown_text = convert_to_markdown(data)
user = self.git_provider.get_user_id() user = self.git_provider.get_user_id()
@ -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.get('relevant file', '').strip()
relevant_file = d['relevant file'].strip() relevant_line_in_file = d.get('relevant line in file', '').strip()
relevant_line_in_file = d['relevant line in file'].strip() content = d.get('suggestion content', '')
content = d['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
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) if self.git_provider.is_supported("create_inline_comment"):
except KeyError: comment = self.git_provider.create_inline_comment(content, relevant_file, relevant_line_in_file)
pass if comment:
comments.append(comment)
else:
self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
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 = ""

View File

@ -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