Skip to content

Commit

Permalink
Merge pull request qodo-ai#78 from Codium-ai/tr/agent_logic
Browse files Browse the repository at this point in the history
Enhancement of PR Agent with User Interaction
  • Loading branch information
okotek authored Jul 18, 2023
2 parents bd9d002 + aeeb218 commit 111bf11
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 34 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ To set up your own PR-Agent, see the [Quickstart](#Quickstart) section
| | Ask | :white_check_mark: | :white_check_mark: | |
| | Auto-Description | :white_check_mark: | | |
| | Improve Code | :white_check_mark: | :white_check_mark: | |
| | Reflect and Review | :white_check_mark: | | |
| | | | | |
| USAGE | CLI | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| | Tagging bot | :white_check_mark: | :white_check_mark: | |
Expand All @@ -92,6 +93,7 @@ Examples for invoking the different tools via the [CLI](#quickstart):
- **Describe**: python cli.py --pr-url=<pr_url> describe
- **Improve**: python cli.py --pr-url=<pr_url> improve
- **Ask**: python cli.py --pr-url=<pr_url> ask "Write me a poem about this PR"
- **Reflect**: python cli.py --pr-url=<pr_url> reflect

"<pr_url>" is the url of the relevant PR (for example: https://github.com/Codium-ai/pr-agent/pull/50).

Expand Down Expand Up @@ -120,12 +122,13 @@ Here are several ways to install and run PR-Agent:

## Usage and Tools

**PR-Agent** provides four types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"` and `"PR Code Sueggestions"`.
**PR-Agent** provides five types of interactions ("tools"): `"PR Reviewer"`, `"PR Q&A"`, `"PR Description"`, `"PR Code Sueggestions"` and `"PR Reflect and Review"`.

- The "PR Reviewer" tool automatically analyzes PRs, and provides various types of feedback.
- The "PR Q&A" tool answers free-text questions about the PR.
- The "PR Description" tool automatically sets the PR Title and body.
- The "PR Code Suggestion" tool provide inline code suggestions for the PR that can be applied and committed.
- The "PR Reflect and Review" tool first initiates a dialog with the user and asks them to reflect on the PR, and then provides a review.

## How it works

Expand All @@ -138,11 +141,11 @@ Check out the [PR Compression strategy](./PR_COMPRESSION.md) page for more detai
- [ ] Support open-source models, as a replacement for openai models. (Note - a minimal requirement for each open-source model is to have 8k+ context, and good support for generating json as an output)
- [x] Support other Git providers, such as Gitlab and Bitbucket.
- [ ] Develop additional logics for handling large PRs, and compressing git patches
- [ ] Dedicated tools and sub-tools for specific programming languages (Python, Javascript, Java, C++, etc)
- [ ] Add additional context to the prompt. For example, repo (or relevant files) summarization, with tools such a [ctags](https://github.com/universal-ctags/ctags)
- [ ] Adding more tools. Possible directions:
- [x] PR description
- [x] Inline code suggestions
- [x] Reflect and review
- [ ] Enforcing CONTRIBUTING.md guidelines
- [ ] Performance (are there any performance issues)
- [ ] Documentation (is the PR properly documented)
Expand Down
Binary file removed pics/.DS_Store
Binary file not shown.
11 changes: 9 additions & 2 deletions pr_agent/agent/pr_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@

from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer
from pr_agent.config_loader import settings


class PRAgent:
def __init__(self):
pass

async def handle_request(self, pr_url, request) -> bool:
if any(cmd in request for cmd in ["/review", "/review_pr"]):
await PRReviewer(pr_url).review()
if any(cmd in request for cmd in ["/answer"]):
await PRReviewer(pr_url, is_answer=True).review()
elif any(cmd in request for cmd in ["/review", "/review_pr", "/reflect_and_review"]):
if settings.pr_reviewer.ask_and_reflect or "/reflect_and_review" in request:
await PRInformationFromUser(pr_url).generate_questions()
else:
await PRReviewer(pr_url).review()
elif any(cmd in request for cmd in ["/describe", "/describe_pr"]):
await PRDescription(pr_url).describe()
elif any(cmd in request for cmd in ["/improve", "/improve_code"]):
Expand Down
11 changes: 9 additions & 2 deletions pr_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,22 @@ def run():
- cli.py --pr-url=... describe
- cli.py --pr-url=... improve
- cli.py --pr-url=... ask "write me a poem about this PR"
- cli.py --pr-url=... reflect
Supported commands:
review / review_pr - Add a review that includes a summary of the PR and specific suggestions for improvement.
ask / ask_question [question] - Ask a question about the PR.
describe / describe_pr - Modify the PR title and description based on the PR's contents.
improve / improve_code - Suggest improvements to the code in the PR as pull request comments ready to commit.
reflect - Ask the PR author questions about the PR.
""")
parser.add_argument('--pr_url', type=str, help='The URL of the PR to review', required=True)
parser.add_argument('command', type=str, help='The', choices=['review', 'review_pr',
'ask', 'ask_question',
'describe', 'describe_pr',
'improve', 'improve_code',
'user_questions'], default='review')
'reflect', 'review_after_reflect'],
default='review')
parser.add_argument('rest', nargs=argparse.REMAINDER, default=[])
args = parser.parse_args()
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
Expand All @@ -56,10 +59,14 @@ def run():
print(f"Reviewing PR: {args.pr_url}")
reviewer = PRReviewer(args.pr_url, cli_mode=True)
asyncio.run(reviewer.review())
elif command in ['user_questions']:
elif command in ['reflect']:
print(f"Asking the PR author questions: {args.pr_url}")
reviewer = PRInformationFromUser(args.pr_url)
asyncio.run(reviewer.generate_questions())
elif command in ['review_after_reflect']:
print(f"Processing author's answers and sending review: {args.pr_url}")
reviewer = PRReviewer(args.pr_url, cli_mode=True, is_answer=True)
asyncio.run(reviewer.review())
else:
print(f"Unknown command: {command}")
parser.print_help()
Expand Down
8 changes: 8 additions & 0 deletions pr_agent/git_providers/bitbucket_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ def __init__(self, pr_url: Optional[str] = None):
if pr_url:
self.set_pr(pr_url)

def is_supported(self, capability: str) -> bool:
if capability == 'get_issue_comments':
return False
return True

def set_pr(self, pr_url: str):
self.workspace_slug, self.repo_slug, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr()
Expand Down Expand Up @@ -74,6 +79,9 @@ def get_pr_description(self):
def get_user_id(self):
return 0

def get_issue_comments(self):
raise NotImplementedError("Bitbucket provider does not support issue comments yet")

@staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url)
Expand Down
8 changes: 8 additions & 0 deletions pr_agent/git_providers/git_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class FilePatchInfo:


class GitProvider(ABC):
@abstractmethod
def is_supported(self, capability: str) -> bool:
pass

@abstractmethod
def get_diff_files(self) -> list[FilePatchInfo]:
pass
Expand Down Expand Up @@ -62,6 +66,10 @@ def get_user_id(self):
def get_pr_description(self):
pass

@abstractmethod
def get_issue_comments(self):
pass


def get_main_pr_language(languages, files) -> str:
"""
Expand Down
6 changes: 6 additions & 0 deletions pr_agent/git_providers/github_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ def __init__(self, pr_url: Optional[str] = None):
self.set_pr(pr_url)
self.last_commit_id = list(self.pr.get_commits())[-1]

def is_supported(self, capability: str) -> bool:
return True

def set_pr(self, pr_url: str):
self.repo, self.pr_num = self._parse_pr_url(pr_url)
self.pr = self._get_pr()
Expand Down Expand Up @@ -161,6 +164,9 @@ def get_notifications(self, since: datetime):
notifications = self.github_client.get_user().get_notifications(since=since)
return notifications

def get_issue_comments(self):
return self.pr.get_issue_comments()

@staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url)
Expand Down
8 changes: 8 additions & 0 deletions pr_agent/git_providers/gitlab_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ def __init__(self, merge_request_url: Optional[str] = None):
self.RE_HUNK_HEADER = re.compile(
r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)")

def is_supported(self, capability: str) -> bool:
if capability == 'get_issue_comments':
return False
return True

@property
def pr(self):
'''The GitLab terminology is merge request (MR) instead of pull request (PR)'''
Expand Down Expand Up @@ -210,6 +215,9 @@ def get_pr_branch(self):
def get_pr_description(self):
return self.mr.description

def get_issue_comments(self):
raise NotImplementedError("GitLab provider does not support issue comments yet")

def _parse_merge_request_url(self, merge_request_url: str) -> Tuple[int, int]:
parsed_url = urlparse(merge_request_url)

Expand Down
17 changes: 3 additions & 14 deletions pr_agent/servers/github_action_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import os
import re

from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import settings
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
from pr_agent.tools.pr_description import PRDescription
from pr_agent.tools.pr_information_from_user import PRInformationFromUser
from pr_agent.tools.pr_questions import PRQuestions
from pr_agent.tools.pr_reviewer import PRReviewer

Expand Down Expand Up @@ -53,20 +55,7 @@ async def run_action():
pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None)
if pr_url:
body = comment_body.strip().lower()
if any(cmd in body for cmd in ["/review", "/review_pr"]):
await PRReviewer(pr_url).review()
elif any(cmd in body for cmd in ["/describe", "/describe_pr"]):
await PRDescription(pr_url).describe()
elif any(cmd in body for cmd in ["/improve", "/improve_code"]):
await PRCodeSuggestions(pr_url).suggest()
elif any(cmd in body for cmd in ["/ask", "/ask_question"]):
pattern = r'(/ask|/ask_question)\s*(.*)'
matches = re.findall(pattern, comment_body, re.IGNORECASE)
if matches:
question = matches[0][1]
await PRQuestions(pr_url, question).answer()
else:
print(f"Unknown command: {body}")
await PRAgent().handle_request(pr_url, body)


if __name__ == '__main__':
Expand Down
1 change: 1 addition & 0 deletions pr_agent/settings/configuration.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require_tests_review=true
require_security_review=true
num_code_suggestions=3
inline_code_comments = true
ask_and_reflect=false

[pr_description]
publish_description_as_comment=false
Expand Down
9 changes: 5 additions & 4 deletions pr_agent/settings/pr_information_from_user_prompts.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
[pr_information_from_user_prompt]
system="""You are CodiumAI-PR-Reviewer, a language model designed to review git pull requests.
Given the PR Info and the PR Git Diff, generate 4 questions about the PR for the PR author.
Given the PR Info and the PR Git Diff, generate 3 short questions about the PR code for the PR author.
The goal of the questions is to help the language model understand the PR better, so the questions should be insightful, informative, non-trivial, and relevant to the PR.
Prefer yes\\no or multiple choice questions. If you have to ask open-ended questions, make sure they are not too difficult, and can be answered in a sentence or two.
You should prefer asking yes\\no questions, or multiple choice questions. Also add at least one open-ended question, but make sure they are not too difficult, and can be answered in a sentence or two.
Example output:
'
Questions to better understand the PR:
1. ...
2. ...
1) ...
2) ...
...
'
"""

user="""PR Info:
Expand Down
16 changes: 16 additions & 0 deletions pr_agent/settings/pr_reviewer_prompts.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ You must use the following JSON schema to format your answer:
"description": "yes\\no question: does this PR have relevant tests ?"
},
{%- endif %}
{%- if question_str %}
"Insights from user's answer": {
"type": "string",
"description": "shortly summarize the insights you gained from the user's answers to the questions"
},
{%- endif %}
{%- if require_focused %}
"Focused PR": {
"type": "string",
Expand Down Expand Up @@ -115,6 +121,16 @@ Description: '{{description}}'
Main language: {{language}}
{%- endif %}
{%- if question_str %}
######
Here are questions to better understand the PR. Use the answers to provide better feedback.
{{question_str|trim}}
User answers:
{{answer_str|trim}}
######
{%- endif %}
The PR Git Diff:
```
Expand Down
6 changes: 3 additions & 3 deletions pr_agent/tools/pr_information_from_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self, pr_url: str):
async def generate_questions(self):
logging.info('Generating question to the user...')
if settings.config.publish_output:
self.git_provider.publish_comment("Preparing answer...", is_temporary=True)
self.git_provider.publish_comment("Preparing questions...", 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...')
Expand Down Expand Up @@ -66,6 +66,6 @@ def _prepare_pr_answer(self) -> str:
model_output = self.prediction.strip()
if settings.config.verbosity_level >= 2:
logging.info(f"answer_str:\n{model_output}")
answer_str = f"{model_output}\n\n Please respond to the question above in the following format:\n\n" + \
f"/answer <question_id> <answer>\n\n" + f"Example:\n'\n/answer\n1. Yes, because ...\n2. No, because ...\n'"
answer_str = f"{model_output}\n\n Please respond to the questions above in the following format:\n\n" +\
f"\n>/answer\n>1) ...\n>2) ...\n>...\n"
return answer_str
38 changes: 31 additions & 7 deletions pr_agent/tools/pr_reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@


class PRReviewer:
def __init__(self, pr_url: str, cli_mode=False):
def __init__(self, pr_url: str, cli_mode=False, is_answer: bool = False):

self.git_provider = get_git_provider()(pr_url)
self.main_language = get_main_pr_language(
self.git_provider.get_languages(), self.git_provider.get_files()
)
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()
self.ai_handler = AiHandler()
self.patches_diff = None
self.prediction = None
Expand All @@ -35,6 +39,9 @@ def __init__(self, pr_url: str, cli_mode=False):
"require_security": settings.pr_reviewer.require_security_review,
"require_focused": settings.pr_reviewer.require_focused_review,
'num_code_suggestions': settings.pr_reviewer.num_code_suggestions,
#
'question_str': question_str,
'answer_str': answer_str,
}
self.token_handler = TokenHandler(self.git_provider.pr,
self.vars,
Expand Down Expand Up @@ -118,9 +125,26 @@ def _publish_inline_code_comments(self):
except json.decoder.JSONDecodeError:
data = try_fix_json(review)

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']

self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
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']

self.git_provider.publish_inline_comment(content, relevant_file, relevant_line_in_file)
except KeyError:
pass

def _get_user_answers(self):
answer_str = question_str = ""
if self.is_answer:
discussion_messages = self.git_provider.get_issue_comments()
for message in discussion_messages.reversed:
if "Questions to better understand the PR:" in message.body:
question_str = message.body
elif '/answer' in message.body:
answer_str = message.body
if answer_str and question_str:
break
return question_str, answer_str

0 comments on commit 111bf11

Please sign in to comment.