Compare commits
17 Commits
enhancemen
...
enhancemen
Author | SHA1 | Date | |
---|---|---|---|
93311a9d9b | |||
704030230f | |||
60bce8f049 | |||
e394cb7ddb | |||
a0e4fb01af | |||
eb9190efa1 | |||
8cc37d6f59 | |||
6cc9fe3d06 | |||
0acf423450 | |||
7958786b4c | |||
719f3a9dd8 | |||
71efd84113 | |||
25e46a99fd | |||
2531849b73 | |||
19f11f99ce | |||
87f978e816 | |||
7488eb8c9e |
2
.github/workflows/review.yaml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: Codium-ai/pr-agent@feature/github_action
|
||||
uses: Codium-ai/pr-agent@main
|
||||
env:
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
OPENAI_ORG: ${{ secrets.OPENAI_ORG }}
|
||||
|
1
Dockerfile.github_action_dockerhub
Normal file
@ -0,0 +1 @@
|
||||
FROM codiumai/pr-agent:github_action
|
51
README.md
@ -9,18 +9,40 @@
|
||||
|
||||
[](https://github.com/Codium-ai/pr-agent/blob/main/LICENSE)
|
||||
[](https://discord.com/channels/1057273017547378788/1126104260430528613)
|
||||
|
||||
CodiumAI `pr-agent` is an open-source tool aiming to help developers review PRs faster and more efficiently. It automatically analyzes the PR, provides feedback and suggestions, and can answer free-text questions.
|
||||
|
||||
</div>
|
||||
<div align="left">
|
||||
|
||||
CodiumAI `pr-agent` is an open-source tool aiming to help developers review PRs faster and more efficiently. It automatically analyzes the PR and can provide several types of feedback:
|
||||
|
||||
**Auto-Description**: Automatically generating PR description - name, type, summary, and code walkthrough.
|
||||
\
|
||||
**PR Review**: Feedback about the PR main theme, type, relevant tests, security issues, focused, and various suggestions for the PR content.
|
||||
\
|
||||
**Question Answering**: Answering free-text questions about the PR.
|
||||
\
|
||||
**Code Suggestion**: Committable code suggestions for improving the PR.
|
||||
|
||||
Example results:
|
||||
</div>
|
||||
<div align="center">
|
||||
<p float="center">
|
||||
<img src="./pics/pr_reviewer_1.png" width="800">
|
||||
</p>
|
||||
<p float="center">
|
||||
<img src="./pics/pr_code_suggestions.png" width="800">
|
||||
</p>
|
||||
</div>
|
||||
<div align="left">
|
||||
|
||||
- [Live demo](#live-demo)
|
||||
- [Quickstart](#Quickstart)
|
||||
- [Overview](#overview)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Usage and tools](#usage-and-tools)
|
||||
- [Configuration](#Configuration)
|
||||
- [Configuration](#configuration)
|
||||
- [How it works](#how-it-works)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Similar projects](#similar-projects)
|
||||
</div>
|
||||
|
||||
## Live demo
|
||||
|
||||
@ -31,6 +53,25 @@ Experience GPT-4 powered PR review on your public GitHub repository with our hos
|
||||
To set up your own pr-agent, see the [Quickstart](#Quickstart) section
|
||||
|
||||
---
|
||||
## Overview
|
||||
`pr-agent` offers extensive pull request functionalities across various git providers:
|
||||
| | | Github | Gitlab | Bitbucket |
|
||||
|-------|---------------------------------------------|--------|--------|-----------|
|
||||
| TOOLS | Review | ✓ | ✓ | ✓ |
|
||||
| | ⮑ Inline review | ✓ | ✓ | |
|
||||
| | Ask | ✓ | ✓ | |
|
||||
| | Auto-Description | ✓ | | |
|
||||
| | Improve Code | ✓ | | |
|
||||
| | | | | |
|
||||
| USAGE | CLI | ✓ | ✓ | ✓ |
|
||||
| | Tagging bot | ✓ | ✓ | |
|
||||
| | Actions | ✓ | | |
|
||||
| | | | | |
|
||||
| CORE | PR compression | ✓ | ✓ | ✓ |
|
||||
| | Repo language prioritization | ✓ | ✓ | ✓ |
|
||||
| | Adaptive and token-aware<br />file patch fitting | ✓ | ✓ | ✓ |
|
||||
|
||||
|
||||
|
||||
## Quickstart
|
||||
|
||||
|
@ -2,4 +2,4 @@ name: 'PR Agent'
|
||||
description: 'Summarize, review and suggest improvements for pull requests'
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile.github_action'
|
||||
image: 'Dockerfile.github_action_dockerhub'
|
||||
|
BIN
pics/Icon-7.png
Before Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 102 KiB |
BIN
pics/main_pic_4_tools.gif
Normal file
After Width: | Height: | Size: 260 KiB |
BIN
pics/pr_auto_description.png
Normal file
After Width: | Height: | Size: 335 KiB |
BIN
pics/pr_code_suggestions.png
Normal file
After Width: | Height: | Size: 193 KiB |
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 161 KiB |
Before Width: | Height: | Size: 267 KiB |
BIN
pics/pr_reviewer_1.png
Normal file
After Width: | Height: | Size: 185 KiB |
BIN
pics/pr_reviewer_2.png
Normal file
After Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 42 KiB |
@ -1,5 +1,7 @@
|
||||
import re
|
||||
|
||||
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
||||
from pr_agent.tools.pr_description import PRDescription
|
||||
from pr_agent.tools.pr_questions import PRQuestions
|
||||
from pr_agent.tools.pr_reviewer import PRReviewer
|
||||
|
||||
@ -8,17 +10,20 @@ class PRAgent:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def handle_request(self, pr_url, request):
|
||||
if 'please review' in request.lower() or 'review' == request.lower().strip() or len(request) == 0:
|
||||
reviewer = PRReviewer(pr_url)
|
||||
await reviewer.review()
|
||||
|
||||
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()
|
||||
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"]):
|
||||
await PRCodeSuggestions(pr_url).suggest()
|
||||
elif any(cmd in request for cmd in ["/ask", "/ask_question"]):
|
||||
pattern = r'(/ask|/ask_question)\s*(.*)'
|
||||
matches = re.findall(pattern, request, re.IGNORECASE)
|
||||
if matches:
|
||||
question = matches[0][1]
|
||||
await PRQuestions(pr_url, question).answer()
|
||||
else:
|
||||
if "please answer" in request.lower():
|
||||
question = re.split(r'(?i)please answer', request)[1].strip()
|
||||
elif request.lower().strip().startswith("answer"):
|
||||
question = re.split(r'(?i)answer', request)[1].strip()
|
||||
else:
|
||||
question = request
|
||||
answerer = PRQuestions(pr_url, question)
|
||||
await answerer.answer()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -1,8 +1,11 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
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_questions import PRQuestions
|
||||
from pr_agent.tools.pr_reviewer import PRReviewer
|
||||
|
||||
@ -16,10 +19,11 @@ async def run_action():
|
||||
if not GITHUB_EVENT_PATH:
|
||||
print("GITHUB_EVENT_PATH not set")
|
||||
return
|
||||
event_payload = json.load(open(GITHUB_EVENT_PATH, 'r'))
|
||||
RUNNER_DEBUG = os.environ.get('RUNNER_DEBUG', None)
|
||||
if not RUNNER_DEBUG:
|
||||
print("RUNNER_DEBUG not set")
|
||||
try:
|
||||
event_payload = json.load(open(GITHUB_EVENT_PATH, 'r'))
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
print(f"Failed to parse JSON: {e}")
|
||||
return
|
||||
OPENAI_KEY = os.environ.get('OPENAI_KEY', None)
|
||||
if not OPENAI_KEY:
|
||||
print("OPENAI_KEY not set")
|
||||
@ -48,10 +52,21 @@ async def run_action():
|
||||
if comment_body:
|
||||
pr_url = event_payload.get("issue", {}).get("pull_request", {}).get("url", None)
|
||||
if pr_url:
|
||||
if comment_body.strip().lower() == "review":
|
||||
body = comment_body.strip().lower()
|
||||
if any(cmd in body for cmd in ["/review", "/review_pr"]):
|
||||
await PRReviewer(pr_url).review()
|
||||
elif comment_body.lstrip().lower().startswith("answer"):
|
||||
await PRQuestions(pr_url, comment_body).answer()
|
||||
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}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -56,7 +56,7 @@ async def handle_request(body):
|
||||
api_url = pull_request.get("url", None)
|
||||
if api_url is None:
|
||||
return {}
|
||||
await agent.handle_request(api_url, "please review")
|
||||
await agent.handle_request(api_url, "/review")
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@ -8,6 +9,11 @@ import aiohttp
|
||||
from pr_agent.agent.pr_agent import PRAgent
|
||||
from pr_agent.config_loader import settings
|
||||
from pr_agent.git_providers import get_git_provider
|
||||
from pr_agent.servers.help import bot_help_text
|
||||
from pr_agent.tools.pr_code_suggestions import PRCodeSuggestions
|
||||
from pr_agent.tools.pr_description import PRDescription
|
||||
from pr_agent.tools.pr_questions import PRQuestions
|
||||
from pr_agent.tools.pr_reviewer import PRReviewer
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
|
||||
NOTIFICATION_URL = "https://api.github.com/notifications"
|
||||
@ -25,6 +31,7 @@ async def polling_loop():
|
||||
last_modified = [None]
|
||||
git_provider = get_git_provider()()
|
||||
user_id = git_provider.get_user_id()
|
||||
agent = PRAgent()
|
||||
try:
|
||||
deployment_type = settings.github.deployment_type
|
||||
token = settings.github.user_token
|
||||
@ -83,8 +90,13 @@ async def polling_loop():
|
||||
if user_tag not in comment_body:
|
||||
continue
|
||||
rest_of_comment = comment_body.split(user_tag)[1].strip()
|
||||
agent = PRAgent()
|
||||
await agent.handle_request(pr_url, rest_of_comment)
|
||||
|
||||
success = await agent.handle_request(pr_url, rest_of_comment)
|
||||
if not success:
|
||||
git_provider.set_pr(pr_url)
|
||||
git_provider.publish_comment("### How to user PR-Agent\n" +
|
||||
bot_help_text(user_id))
|
||||
|
||||
elif response.status != 304:
|
||||
print(f"Failed to fetch notifications. Status code: {response.status}")
|
||||
|
||||
|
14
pr_agent/servers/help.py
Normal file
@ -0,0 +1,14 @@
|
||||
commands_text = "> /review - Ask for a new review after your update the PR\n" \
|
||||
"> /describe - Modify the PR title and description based " \
|
||||
"on the PR's contents.\n" \
|
||||
"> /improve - Suggest improvements to the code in the PR as pull " \
|
||||
"request comments ready to commit.\n" \
|
||||
"> /ask <QUESTION> - Ask a question about the PR.\n"
|
||||
|
||||
|
||||
def bot_help_text(user: str):
|
||||
return f"> Tag me in a comment '@{user}' and add one of the following commands:\n" + commands_text
|
||||
|
||||
|
||||
actions_help_text = "> Add a comment to to invoke PR-Agent, use one of the following commands:\n" + \
|
||||
commands_text
|
@ -11,6 +11,7 @@ from pr_agent.algo.utils import convert_to_markdown, try_fix_json
|
||||
from pr_agent.config_loader import settings
|
||||
from pr_agent.git_providers import get_git_provider
|
||||
from pr_agent.git_providers.git_provider import get_main_pr_language
|
||||
from pr_agent.servers.help import bot_help_text, actions_help_text
|
||||
|
||||
|
||||
class PRReviewer:
|
||||
@ -43,7 +44,7 @@ class PRReviewer:
|
||||
async def review(self):
|
||||
logging.info('Reviewing PR...')
|
||||
if settings.config.publish_review:
|
||||
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...')
|
||||
@ -97,14 +98,9 @@ class PRReviewer:
|
||||
if not self.cli_mode:
|
||||
markdown_text += "\n### How to use\n"
|
||||
if user and '[bot]' not in user:
|
||||
markdown_text += f"> Tag me in a comment '@{user}' to ask for a new review after you update the PR.\n"
|
||||
markdown_text += "> You can also tag me and ask any question, " \
|
||||
f"for example '@{user} is the PR ready for merge?'"
|
||||
markdown_text += bot_help_text(user)
|
||||
else:
|
||||
markdown_text += "> Add a comment that says 'review' to ask for a new review " \
|
||||
"after you update the PR.\n"
|
||||
markdown_text += "> You can also add a comment that says 'answer QUESTION', " \
|
||||
"for example 'answer is the PR ready for merge?'"
|
||||
markdown_text += actions_help_text
|
||||
|
||||
if settings.config.verbosity_level >= 2:
|
||||
logging.info(f"Markdown response:\n{markdown_text}")
|
||||
|