diff --git a/README.md b/README.md index 27262326..0728bbce 100644 --- a/README.md +++ b/README.md @@ -24,25 +24,25 @@ CodiumAI `PR-Agent` is an open-source tool aiming to help developers review PRs

Example results:

-

Describe:

+

/describe:

-

Review:

+

/review:

-

Ask:

+

/ask:

-

Improve:

+

/improve:

@@ -132,7 +132,7 @@ Here are several ways to install and run PR-Agent: ## 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 diff --git a/pr_agent/algo/utils.py b/pr_agent/algo/utils.py index 1d85b1d6..0803f874 100644 --- a/pr_agent/algo/utils.py +++ b/pr_agent/algo/utils.py @@ -21,6 +21,7 @@ def convert_to_markdown(output_data: dict) -> str: "Focused PR": "✨", "Security concerns": "🔒", "General PR suggestions": "💡", + "Insights from user's answers": "📝", "Code suggestions": "🤖" } diff --git a/pr_agent/git_providers/bitbucket_provider.py b/pr_agent/git_providers/bitbucket_provider.py index 1f3f9d6e..1470ce78 100644 --- a/pr_agent/git_providers/bitbucket_provider.py +++ b/pr_agent/git_providers/bitbucket_provider.py @@ -27,7 +27,7 @@ class BitbucketProvider: self.set_pr(pr_url) 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 True @@ -65,6 +65,12 @@ class BitbucketProvider: def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): 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): return self.pr.title diff --git a/pr_agent/git_providers/git_provider.py b/pr_agent/git_providers/git_provider.py index 55d2bf34..f7c7fa98 100644 --- a/pr_agent/git_providers/git_provider.py +++ b/pr_agent/git_providers/git_provider.py @@ -44,6 +44,14 @@ class GitProvider(ABC): def publish_inline_comment(self, body: str, relevant_file: str, relevant_line_in_file: str): 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 def publish_code_suggestion(self, body: str, relevant_file: str, relevant_lines_start: int, relevant_lines_end: int): diff --git a/pr_agent/git_providers/github_provider.py b/pr_agent/git_providers/github_provider.py index c125b2d3..415daa06 100644 --- a/pr_agent/git_providers/github_provider.py +++ b/pr_agent/git_providers/github_provider.py @@ -3,7 +3,7 @@ from datetime import datetime from typing import Optional, Tuple from urllib.parse import urlparse -from github import AppAuthentication, Github +from github import AppAuthentication, Github, Auth from pr_agent.config_loader import settings @@ -103,6 +103,9 @@ class GithubProvider(GitProvider): self.pr.comments_list.append(response) 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() position = -1 for file in self.diff_files: @@ -121,9 +124,16 @@ class GithubProvider(GitProvider): if position == -1: if settings.config.verbosity_level >= 2: logging.info(f"Could not find position for {relevant_file} {relevant_line_in_file}") + subject_type = "FILE" else: - path = relevant_file.strip() - self.pr.create_review_comment(body=body, commit_id=self.last_commit_id, path=path, position=position) + subject_type = "LINE" + 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, relevant_file: str, @@ -264,7 +274,7 @@ class GithubProvider(GitProvider): raise ValueError( "GitHub token is required when using user deployment. See: " "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): return self.github_client.get_repo(self.repo) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 17927dd1..95246647 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -35,7 +35,7 @@ class GitLabProvider(GitProvider): self.incremental = incremental 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 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, 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, target_file, target_line_no): if not found: diff --git a/pr_agent/tools/pr_reviewer.py b/pr_agent/tools/pr_reviewer.py index 38f918b9..4e147ace 100644 --- a/pr_agent/tools/pr_reviewer.py +++ b/pr_agent/tools/pr_reviewer.py @@ -24,7 +24,7 @@ class PRReviewer: self.is_answer = is_answer 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") - answer_str = question_str = self._get_user_answers() + answer_str, question_str = self._get_user_answers() self.ai_handler = AiHandler() self.patches_diff = None self.prediction = None @@ -51,7 +51,7 @@ class PRReviewer: async def review(self): logging.info('Reviewing PR...') 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...') self.patches_diff = get_pr_diff(self.git_provider, self.token_handler) logging.info('Getting AI prediction...') @@ -99,7 +99,13 @@ class PRReviewer: if settings.config.git_provider != 'bitbucket' and \ settings.pr_reviewer.inline_code_comments and \ '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) user = self.git_provider.get_user_id() @@ -125,16 +131,24 @@ class PRReviewer: except json.decoder.JSONDecodeError: data = try_fix_json(review) - if settings.pr_reviewer.num_code_suggestions > 0: - try: - for d in data['PR Feedback']['Code suggestions']: - relevant_file = d['relevant file'].strip() - relevant_line_in_file = d['relevant line in file'].strip() - content = d['suggestion content'] + comments = [] + for d in data['PR Feedback']['Code suggestions']: + relevant_file = d.get('relevant file', '').strip() + relevant_line_in_file = d.get('relevant line in file', '').strip() + 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 - self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file) - except KeyError: - pass + 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) + + if comments: + self.git_provider.publish_inline_comments(comments) def _get_user_answers(self): answer_str = question_str = "" diff --git a/requirements.txt b/requirements.txt index 8695f709..399cd3ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ dynaconf==3.1.12 fastapi==0.99.0 -PyGithub==1.58.2 +PyGithub==1.59.* retry==0.9.2 openai==0.27.8 Jinja2==3.1.2