Spaces:
Configuration error
Configuration error
Elizabeth Thomas commited on
Commit ·
12ab2c5
1
Parent(s): 5d6a540
Refactor
Browse files- .gitignore +1 -0
- Dockerfile +14 -0
- README.md +8 -11
- app.py +0 -43
- full_app.py +0 -153
- main.py +197 -0
- requirements.txt +4 -2
- secret.yml +36 -0
- utils.py +51 -0
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use an official Python runtime as a parent image
|
| 2 |
+
FROM python:3.8-slim
|
| 3 |
+
|
| 4 |
+
# Set the working directory to /app
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy the current directory contents into the container at /app
|
| 8 |
+
COPY . /app
|
| 9 |
+
|
| 10 |
+
# Install any needed packages specified in requirements.txt
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Run app.py when the container launches
|
| 14 |
+
CMD ["python", "./app/main.py"]
|
README.md
CHANGED
|
@@ -1,12 +1,9 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
colorFrom: yellow
|
| 5 |
-
colorTo: yellow
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 4.15.0
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LLM-powered Code Review Assistant
|
| 2 |
+
=========================================================================
|
| 3 |
+
Drawing inspiration from a [blog](https://www.anyscale.com/blog/building-an-llm-powered-github-bot-to-improve-your-pull-requests) published by Anyscale, this Github app is developed to help engineers get a first hand review of the pull requests before inviting external reviews. Additionally, it can also help the reviewers address the gap in review which could potentially be missed by a human reviewer.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
This Code Review Assistant Github app is setup to use Github Enterprise APIs to post a greeting/welcome message to every PR that is opened on the repository/organization for which this bot is enabled/installed. Review analysis of the code could be triggered by issuing
|
| 6 |
+
|
| 7 |
+
`@code-review-assistant review`
|
| 8 |
+
|
| 9 |
+
command. Once the above comment is typed in PR, the app calls the Open AI API Endpoints with the diff content of the PR to obtain results of review. The review analysis is then posted onto the PR as a comment.
|
app.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
import httpx
|
| 2 |
-
import ray
|
| 3 |
-
|
| 4 |
-
from fastapi import FastAPI, Request
|
| 5 |
-
from fastapi.responses import JSONResponse
|
| 6 |
-
from ray import serve
|
| 7 |
-
|
| 8 |
-
app = FastAPI()
|
| 9 |
-
headers = {
|
| 10 |
-
"Content-Type": "application/json"
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
async def handle_webhook(request: Request):
|
| 14 |
-
data = await request.json()
|
| 15 |
-
if "pull_request" in data.keys() and (
|
| 16 |
-
data["action"] in ["opened", "reopened"]
|
| 17 |
-
): # use "synchronize" for tracking new commits
|
| 18 |
-
pr = data.get("pull_request")
|
| 19 |
-
|
| 20 |
-
# Greet the user and show instructions.
|
| 21 |
-
async with httpx.AsyncClient() as client:
|
| 22 |
-
await client.post(
|
| 23 |
-
f"{pr['issue_url']}/comments",
|
| 24 |
-
json={"body": "Hello from code review assistant"},
|
| 25 |
-
headers=headers,
|
| 26 |
-
)
|
| 27 |
-
return JSONResponse(content={}, status_code=200)
|
| 28 |
-
|
| 29 |
-
@serve.deployment
|
| 30 |
-
@serve.ingress(app)
|
| 31 |
-
class ServeBot:
|
| 32 |
-
@app.get("/")
|
| 33 |
-
async def root(self):
|
| 34 |
-
return {"message": "Docu Mentor reporting for duty!"}
|
| 35 |
-
|
| 36 |
-
@app.post("/webhook/")
|
| 37 |
-
async def handle_webhook_route(self, request: Request):
|
| 38 |
-
return await handle_webhook(request)
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
# Run with: serve run main:bot
|
| 42 |
-
bot = ServeBot.bind()
|
| 43 |
-
serve.run(bot, route_prefix="/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
full_app.py
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
from fastapi import FastAPI, Request
|
| 2 |
-
from fastapi.responses import JSONResponse
|
| 3 |
-
import httpx
|
| 4 |
-
from dotenv import load_dotenv
|
| 5 |
-
import os
|
| 6 |
-
import openai
|
| 7 |
-
import logging
|
| 8 |
-
import string
|
| 9 |
-
import sys
|
| 10 |
-
import ray
|
| 11 |
-
from ray import serve
|
| 12 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
-
|
| 14 |
-
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
| 15 |
-
logger = logging.getLogger("Code Review Assistant")
|
| 16 |
-
|
| 17 |
-
load_dotenv()
|
| 18 |
-
|
| 19 |
-
# If the app was installed, retrieve the installation access token through the App's
|
| 20 |
-
# private key and app ID, by generating an intermediary JWT token.
|
| 21 |
-
APP_ID = os.environ.get("APP_ID")
|
| 22 |
-
PRIVATE_KEY = os.environ.get("PRIVATE_KEY", "")
|
| 23 |
-
|
| 24 |
-
OPENAI_API_ENDPOINT = "https://api.openai.com/v1"
|
| 25 |
-
openai.api_base = OPENAI_API_ENDPOINT
|
| 26 |
-
openai.api_key = os.environ.get("OPENAI_API_KEY")
|
| 27 |
-
|
| 28 |
-
GREETING = """
|
| 29 |
-
👋 Hi, I'm @code-review-assistant, an LLM powered app
|
| 30 |
-
that gives you actionable feedback.
|
| 31 |
-
|
| 32 |
-
All good? Let's get started!
|
| 33 |
-
"""
|
| 34 |
-
|
| 35 |
-
SYSTEM_CONTENT = """You are a code review assistant.
|
| 36 |
-
Improve the following <content>.
|
| 37 |
-
Provide constructive feedback on code quality, identify potential bugs,
|
| 38 |
-
suggest improvements in coding style, and offer explanations for suggested changes.
|
| 39 |
-
You should be able to analyze code written in popular programming languages
|
| 40 |
-
and prioritize recommendations based on severity and impact on maintainability.
|
| 41 |
-
Consider incorporating features like highlighting specific lines of code, providing inline comments,
|
| 42 |
-
and generating a summary report. Ensure that the assistant promotes collaboration
|
| 43 |
-
and learning among developers while adhering to best practices in software development.
|
| 44 |
-
"""
|
| 45 |
-
|
| 46 |
-
PROMPT = """Improve this content.
|
| 47 |
-
Don't comment on file names or other meta data, just the actual text.
|
| 48 |
-
The <content> will be in JSON format and contains file name keys and text values.
|
| 49 |
-
Make sure to give very concise feedback per file.
|
| 50 |
-
"""
|
| 51 |
-
|
| 52 |
-
def mentor(
|
| 53 |
-
content,
|
| 54 |
-
model="codellama/CodeLlama-34b-Instruct-hf",
|
| 55 |
-
system_content=SYSTEM_CONTENT,
|
| 56 |
-
prompt=PROMPT
|
| 57 |
-
):
|
| 58 |
-
result = openai.ChatCompletion.create(
|
| 59 |
-
model=model,
|
| 60 |
-
messages=[
|
| 61 |
-
{"role": "system", "content": system_content},
|
| 62 |
-
{"role": "user", "content": f"This is the content: {content}. {prompt}"},
|
| 63 |
-
],
|
| 64 |
-
temperature=0,
|
| 65 |
-
)
|
| 66 |
-
usage = result.get("usage")
|
| 67 |
-
prompt_tokens = usage.get("prompt_tokens")
|
| 68 |
-
completion_tokens = usage.get("completion_tokens")
|
| 69 |
-
content = result["choices"][0]["message"]["content"]
|
| 70 |
-
|
| 71 |
-
return content, model, prompt_tokens, completion_tokens
|
| 72 |
-
|
| 73 |
-
try:
|
| 74 |
-
ray.init()
|
| 75 |
-
except:
|
| 76 |
-
logger.info("Ray init failed.")
|
| 77 |
-
|
| 78 |
-
@ray.remote
|
| 79 |
-
def mentor_task(content, model, system_content, prompt):
|
| 80 |
-
return mentor(content, model, system_content, prompt)
|
| 81 |
-
|
| 82 |
-
def ray_mentor(
|
| 83 |
-
content: dict,
|
| 84 |
-
model="codellama/CodeLlama-34b-Instruct-hf",
|
| 85 |
-
system_content=SYSTEM_CONTENT,
|
| 86 |
-
prompt="Improve this content."
|
| 87 |
-
):
|
| 88 |
-
futures = [
|
| 89 |
-
mentor_task.remote(v, model, system_content, prompt)
|
| 90 |
-
for v in content.values()
|
| 91 |
-
]
|
| 92 |
-
suggestions = ray.get(futures)
|
| 93 |
-
content = {k: v[0] for k, v in zip(content.keys(), suggestions)}
|
| 94 |
-
prompt_tokens = sum(v[2] for v in suggestions)
|
| 95 |
-
completion_tokens = sum(v[3] for v in suggestions)
|
| 96 |
-
|
| 97 |
-
print_content = ""
|
| 98 |
-
for k, v in content.items():
|
| 99 |
-
print_content += f"{k}:\n\t\{v}\n\n"
|
| 100 |
-
logger.info(print_content)
|
| 101 |
-
|
| 102 |
-
return print_content, model, prompt_tokens, completion_tokens
|
| 103 |
-
|
| 104 |
-
app = FastAPI()
|
| 105 |
-
app.add_middleware(
|
| 106 |
-
CORSMiddleware,
|
| 107 |
-
# Restrict this to the domains you want to allow
|
| 108 |
-
# access to your service.
|
| 109 |
-
allow_origins=["*"],
|
| 110 |
-
allow_credentials=True,
|
| 111 |
-
allow_methods=["*"],
|
| 112 |
-
allow_headers=["*"],
|
| 113 |
-
)
|
| 114 |
-
|
| 115 |
-
async def handle_webhook(request: Request):
|
| 116 |
-
data = await request.json()
|
| 117 |
-
print(f"data =", data)
|
| 118 |
-
headers = {
|
| 119 |
-
"User-Agent": "code-review-bot",
|
| 120 |
-
"Content-Type": "application/json",
|
| 121 |
-
"Accept": "application/vnd.github.VERSION.diff",
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
# If PR exists and is opened
|
| 125 |
-
if "pull_request" in data.keys() and (
|
| 126 |
-
data["action"] in ["opened", "reopened"]
|
| 127 |
-
): # use "synchronize" for tracking new commits
|
| 128 |
-
pr = data.get("pull_request")
|
| 129 |
-
|
| 130 |
-
# Greet the user and show instructions.
|
| 131 |
-
async with httpx.AsyncClient() as client:
|
| 132 |
-
await client.post(
|
| 133 |
-
f"{pr['issue_url']}/comments",
|
| 134 |
-
json={"body": GREETING},
|
| 135 |
-
headers=headers,
|
| 136 |
-
)
|
| 137 |
-
return JSONResponse(content={}, status_code=200)
|
| 138 |
-
|
| 139 |
-
@serve.deployment
|
| 140 |
-
@serve.ingress(app)
|
| 141 |
-
class ServeBot:
|
| 142 |
-
@app.get("/")
|
| 143 |
-
async def root(self):
|
| 144 |
-
return {"message": "Docu Mentor reporting for duty!"}
|
| 145 |
-
|
| 146 |
-
@app.post("/webhook/")
|
| 147 |
-
async def handle_webhook_route(self, request: Request):
|
| 148 |
-
return await handle_webhook(request)
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
# Run with: serve run main:bot
|
| 152 |
-
bot = ServeBot.bind()
|
| 153 |
-
serve.run(bot, route_prefix="/")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
import logging
|
| 3 |
+
import openai
|
| 4 |
+
from openai import AsyncOpenAI
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
import yaml
|
| 8 |
+
import string
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
from fastapi import FastAPI, Request
|
| 12 |
+
from fastapi.responses import JSONResponse
|
| 13 |
+
from typing import Tuple
|
| 14 |
+
|
| 15 |
+
from utils import JWTGenerator
|
| 16 |
+
|
| 17 |
+
from utils import (
|
| 18 |
+
get_installation_access_token,
|
| 19 |
+
get_diff_url,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
| 23 |
+
logger = logging.getLogger("Code Review Assistant")
|
| 24 |
+
|
| 25 |
+
class GitHubService:
|
| 26 |
+
GREETING = f"""
|
| 27 |
+
👋 Hi, I'm @code-review-assistant, a LLM-powered GitHub app powered by
|
| 28 |
+
[Open AI API Endpoints](https://api.openai.com/v1/chat/completions)
|
| 29 |
+
that gives you actionable feedback on your code.
|
| 30 |
+
|
| 31 |
+
Simply create a new comment in this PR that says:
|
| 32 |
+
`@code-review-assistant review`
|
| 33 |
+
|
| 34 |
+
and I will start my analysis. I only look at what you changed in this PR. All good? Let's get started!
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
def __init__(self, jwt_generator: JWTGenerator):
|
| 38 |
+
self.jwt_generator = jwt_generator
|
| 39 |
+
|
| 40 |
+
async def get_headers(self, data):
|
| 41 |
+
installation_data = data['installation']
|
| 42 |
+
if installation_data and installation_data.get("id"):
|
| 43 |
+
installation_id = installation_data.get("id")
|
| 44 |
+
jwt_token = self.jwt_generator.generate_jwt()
|
| 45 |
+
|
| 46 |
+
installation_access_token = await get_installation_access_token(jwt_token, installation_id)
|
| 47 |
+
logger.info(f"Installation access token = {installation_access_token}")
|
| 48 |
+
|
| 49 |
+
return {
|
| 50 |
+
"Authorization": f"token {installation_access_token}",
|
| 51 |
+
"User-Agent": "code-review-assistant",
|
| 52 |
+
"Accept": "application/vnd.github.diff",
|
| 53 |
+
}
|
| 54 |
+
else:
|
| 55 |
+
raise ValueError("No app installation found.")
|
| 56 |
+
|
| 57 |
+
async def send_greetings(self, data):
|
| 58 |
+
pr = data['pull_request']
|
| 59 |
+
headers = await self.get_headers(data)
|
| 60 |
+
|
| 61 |
+
# Greet the user and show instructions.
|
| 62 |
+
async with httpx.AsyncClient() as client:
|
| 63 |
+
await client.post(
|
| 64 |
+
f"{pr['issue_url']}/comments",
|
| 65 |
+
json={"body": self.GREETING},
|
| 66 |
+
headers=headers,
|
| 67 |
+
)
|
| 68 |
+
return JSONResponse(content={}, status_code=200)
|
| 69 |
+
|
| 70 |
+
async def handle_issue(self, data):
|
| 71 |
+
issue = data['issue']
|
| 72 |
+
headers = await self.get_headers(data)
|
| 73 |
+
|
| 74 |
+
# Check if the issue is a pull request
|
| 75 |
+
if "/pull/" in issue['html_url']:
|
| 76 |
+
pr = issue['pull_request']
|
| 77 |
+
|
| 78 |
+
# Get the comment body
|
| 79 |
+
comment = data['comment']
|
| 80 |
+
comment_body = comment['body']
|
| 81 |
+
|
| 82 |
+
# Remove all whitespace characters except for regular spaces
|
| 83 |
+
comment_body = comment_body.translate(
|
| 84 |
+
str.maketrans("", "", string.whitespace.replace(" ", ""))
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# Skip if the bot talks about itself
|
| 88 |
+
author_handle = comment['user']['login']
|
| 89 |
+
|
| 90 |
+
# Check if the bot is mentioned in the comment
|
| 91 |
+
if (author_handle != "code-review-assistant[bot]" and "@code-review-assistant review" in comment_body):
|
| 92 |
+
|
| 93 |
+
url = get_diff_url(pr)
|
| 94 |
+
async with httpx.AsyncClient() as client:
|
| 95 |
+
response = await client.get(url, headers=headers)
|
| 96 |
+
diff = response.text
|
| 97 |
+
return (f"{comment['issue_url']}/comments", diff)
|
| 98 |
+
|
| 99 |
+
async def post_code_review_analysis(self, data, comment_url, analysis_result, model_used):
|
| 100 |
+
async with httpx.AsyncClient() as client:
|
| 101 |
+
await client.post(
|
| 102 |
+
comment_url,
|
| 103 |
+
json={
|
| 104 |
+
"body": f":rocket: Code Review Assistant Analysis finished "
|
| 105 |
+
+ "analysing your PR! :rocket:\n\n"
|
| 106 |
+
+ "Take a look at your results:\n"
|
| 107 |
+
+ f"{analysis_result}\n\n"
|
| 108 |
+
+ "This bot is proudly powered by "
|
| 109 |
+
+ "[Open AI API Endpoints](https://api.openai.com/v1/chat/completions).\n"
|
| 110 |
+
+ f"It used the model {model_used}"
|
| 111 |
+
},
|
| 112 |
+
headers=await self.get_headers(data),
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
class CodeReviewAssistant:
|
| 116 |
+
SYSTEM_CONTENT = """You’ll act as a code review assistant.
|
| 117 |
+
Provide informative, constructive feedback on code quality, identify potential bugs, suggests improvements in coding style and offer explanation for suggested changes.
|
| 118 |
+
You should be able to analyze code written in popular programming languages.
|
| 119 |
+
Prioritize recommendations based on severity, testability and impact on maintainability.
|
| 120 |
+
Prioritize suggestions that address major problems, issues and bugs in the PR code. As a second priority, suggestions should focus on enhancement, best practice, performance, maintainability, and other aspects.
|
| 121 |
+
Consider incorporating features like highlighting specific lines of code, providing inline comments, and generating a summary report.
|
| 122 |
+
When quoting variables or names from the code, use backticks (`) instead of single quote (').
|
| 123 |
+
Ensure that the assistant promotes collaboration and learning among engineers while adhering to best practices in software development.
|
| 124 |
+
If the content is good, don’t comment on it.
|
| 125 |
+
You can use GitHub-flavored markdown syntax in your answer.
|
| 126 |
+
If you encounter several files, give very concise feedback per file.
|
| 127 |
+
"""
|
| 128 |
+
|
| 129 |
+
PROMPT = """Review the below code difference and give concise actionable code review comments only
|
| 130 |
+
for the changed code. If code change looks good overall, just say \"change looks good\" and stop commenting more.
|
| 131 |
+
Don’t try to make up comments. Don’t comment on file names or other meta data, just the actual text.
|
| 132 |
+
The <content> will be in JSON format and contains file name keys and text values.
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
MODEL = "codellama-34b-instruct"
|
| 136 |
+
|
| 137 |
+
def __init__(self, github_service: GitHubService):
|
| 138 |
+
self.client = None # Placeholder for AsyncOpenAI client initialization.
|
| 139 |
+
self.github_service = github_service
|
| 140 |
+
|
| 141 |
+
async def review(
|
| 142 |
+
self,
|
| 143 |
+
content) -> Tuple[str]:
|
| 144 |
+
|
| 145 |
+
client = AsyncOpenAI(
|
| 146 |
+
base_url="https://api.openai.com/v1",
|
| 147 |
+
api_key=os.getenv("OPENAI_API_KEY"),
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
response = await client.chat.completions.create(
|
| 151 |
+
messages=[
|
| 152 |
+
{"role": "system", "content": self.SYSTEM_CONTENT},
|
| 153 |
+
{"role": "user", "content": f"This is the content: {content}. {self.PROMPT}"},
|
| 154 |
+
],
|
| 155 |
+
temperature=0.7,
|
| 156 |
+
model=self.MODEL,
|
| 157 |
+
)
|
| 158 |
+
logger.info(f"Result from Model analysis = {response}")
|
| 159 |
+
|
| 160 |
+
return response.choices[0].message.content, self.MODEL
|
| 161 |
+
|
| 162 |
+
app = FastAPI()
|
| 163 |
+
try:
|
| 164 |
+
jwt_generator = JWTGenerator(os.getenv("APP_ID"), os.getenv("PRIVATE_KEY"))
|
| 165 |
+
except FileNotFoundError as e:
|
| 166 |
+
logger.error(f"Error in secret manager: {e}")
|
| 167 |
+
|
| 168 |
+
github_service = GitHubService(jwt_generator)
|
| 169 |
+
code_review_assistant = CodeReviewAssistant(github_service)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
@app.get("/")
|
| 173 |
+
async def root():
|
| 174 |
+
return {"message": "Code review assistant reporting for duty!"}
|
| 175 |
+
|
| 176 |
+
@app.post("/webhook")
|
| 177 |
+
async def handle_webhook_route(request: Request):
|
| 178 |
+
|
| 179 |
+
data = await request.json()
|
| 180 |
+
|
| 181 |
+
# If PR exists and is opened
|
| 182 |
+
if "pull_request" in data.keys() and (data["action"] in ["opened", "reopened"]):
|
| 183 |
+
await github_service.send_greetings(data)
|
| 184 |
+
|
| 185 |
+
# Check if the event is a new or modified issue comment
|
| 186 |
+
if "issue" in data.keys() and data.get("action") in ["created", "edited"]:
|
| 187 |
+
try:
|
| 188 |
+
result = await github_service.handle_issue(data)
|
| 189 |
+
if result is not None:
|
| 190 |
+
issue_comment_url, content_to_review = result
|
| 191 |
+
analysis_result, model_used = await code_review_assistant.review(content_to_review)
|
| 192 |
+
logger.info(f"Result from LLM Analysis = {analysis_result}")
|
| 193 |
+
await github_service.post_code_review_analysis(data, issue_comment_url, analysis_result, model_used)
|
| 194 |
+
except Exception as e:
|
| 195 |
+
logger.error(f"Exception occured while calling github_service.handle_issue: {e}")
|
| 196 |
+
|
| 197 |
+
|
requirements.txt
CHANGED
|
@@ -5,7 +5,10 @@ python-dotenv
|
|
| 5 |
openai
|
| 6 |
pyjwt
|
| 7 |
cryptography
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# Styling
|
| 11 |
black
|
|
@@ -13,4 +16,3 @@ flake8
|
|
| 13 |
Flake8-pyproject
|
| 14 |
isort
|
| 15 |
pyupgrade
|
| 16 |
-
pre-commit
|
|
|
|
| 5 |
openai
|
| 6 |
pyjwt
|
| 7 |
cryptography
|
| 8 |
+
pyyaml
|
| 9 |
+
ruamel.yaml
|
| 10 |
+
requests
|
| 11 |
+
aiohttp==3.9.3
|
| 12 |
|
| 13 |
# Styling
|
| 14 |
black
|
|
|
|
| 16 |
Flake8-pyproject
|
| 17 |
isort
|
| 18 |
pyupgrade
|
|
|
secret.yml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
APP_ID: "262"
|
| 2 |
+
PRIVATE_KEY: "-----BEGIN RSA PRIVATE KEY-----
|
| 3 |
+
MIIEpgIBAAKCAQEAupgXDpLp5En/GSRh2uSPK2wFZ6aKvmrjJg4PrBiB7EwJ2Uit
|
| 4 |
+
Ayx3T4zuV+UpGUW3cIEebyCFqamil9p462Rgm7eeyvANdmAr9eO/MA1j++H6O2X1
|
| 5 |
+
PireBQdy3sSn3p72HqAR6OXVP1o8xwCH9ccYacHnCMc7o7Yq2A6PK904wF4DczrC
|
| 6 |
+
A+L0UW/WePgWO7dHmkh9LSVJXSzgGtxiu6mRvSzfH6j7M3oasgsUJ3CCeeNqlTwf
|
| 7 |
+
zUaTDgaJZBPm68Ez8T3r5ECyK7A8EjjVGPj7qDkT+R0F9mmTXoXGg7Vib6tcJ4Q6
|
| 8 |
+
vOOU7O+qrnw0Z7FD5E+MnJNaSk6lYEruYaE1hQIDAQABAoIBAQC6kkJTrzAwtJBe
|
| 9 |
+
mlNB5FEHMM5CsnJ+LTAMp/ihsiuOkwUx/ooH51kw8JCM0DUE8QGXe3Nr2A/t8hEC
|
| 10 |
+
V0+axlYWvUYIUniTiVvLVDqhmHIFtBFZfPv3ejNewfDor6fOYBFl09W0ksJjwx2M
|
| 11 |
+
OYq4hKdzb49L5rJKYmZ6fCxJxrvtExzHM7PQM1CnWcNOUL2Qqe6G6GGlTDn6DpWV
|
| 12 |
+
NhM4ZrJJXNhuQ4b3b5qPgkrXeTm9F9cJnNqafAyg6cBYup4wgb4GCKQEUs1PNHK9
|
| 13 |
+
Vtw5s1eJ0D1hzvnxe0l+hx2pXaeUNpwlk05mHTebjUj09PzESFaWbtuzTm7x7UcB
|
| 14 |
+
IIxo4oahAoGBAPA7diAkO0fNVoxOH8qfVJbiW25w0LrcWiWuJfLUv6smXuOocFTq
|
| 15 |
+
pqqrtEnAeY5shAgoOTjzsKHwOw9aIOSDF9s/iWsvkEb5LcCnYoxu5Fmx6XzXqRH/
|
| 16 |
+
6cPtfdMCZNwAJhj3NXPhiOB+lSGXxTwQdjzxqkBEk3rQ64Hu7LH3ky+5AoGBAMbX
|
| 17 |
+
XYQJ77OhoK42gJFU4z0K7GsqLbAqdc4+dYOx+olbMjhpx1L0lZBWHB5yfzOQymY4
|
| 18 |
+
qZQw/K3P/fgo6uREtm6FhqFmnncS09uyrqYEEoaqaLyfbgoMpS9816n+0qQMdl1r
|
| 19 |
+
1vObKFVU+/L+XlwxsOVM9FJe/+45birQKkZ05WItAoGBAKOLV3e6MsFHAUyzQuFm
|
| 20 |
+
ZufxYd1l4DPWH2jXje4q9/FERgUmfpLQzHYUPsCW0CotphUHjS1AeVdFfG+PJCVt
|
| 21 |
+
OaiBMMRPtSEcMhGd4nFIbRzDCfl7uBYQ6sv/ulEUqCU91LHaWgFx4QU0J8Ke2B9z
|
| 22 |
+
9Yq32ve1t9E8uZfTWEAwE3vBAoGBAJ2yYDA/0SMdpFmGUCDyueXHrAixwtpcUmHn
|
| 23 |
+
lzuDA7e74/Bps/NOlu+J23MqS0eSJXM8rQEieMNAmaMekGvJMwYkT8nhoPu+qtcq
|
| 24 |
+
tuhjgm3a6IXvy02dCcTHtiLUPips19Lvm+JHw40pgUgOBLgJkMnKZlqNjVxZn83E
|
| 25 |
+
mkKWovVhAoGBAKIfkSlVmLKlLdHwNZSsGqEWLcC8I29VkXMmElmU2lnJJZx311li
|
| 26 |
+
9duCQ8tqsEtvosGAxpSaWoOVeatBa9cy+7B5RrvkSDa2kmMM9lV8aR4FZR6tuuOb
|
| 27 |
+
kqNIxPQbZKkQhPF0DuE/EwWOmmaeV+F/7Ek9n7lRhmFUgdDK9xZDmNBi
|
| 28 |
+
-----END RSA PRIVATE KEY-----"
|
| 29 |
+
OAUTH_URL: "https://oauth.iam.perf.target.com/auth/oauth/v2/token"
|
| 30 |
+
OAUTH_NUID_USERNAME: "SVSREGENAI"
|
| 31 |
+
OAUTH_NUID_PASSWORD: "JBai971926261)@"
|
| 32 |
+
OAUTH_CLIENT_ID: "codereviewer_ropc"
|
| 33 |
+
OAUTH_CLIENT_SECRET: "xiuFcaMq1D4bl9svsi8LirunPiN9xaDxZhwXguMfhYTWT5GwWd0bqXMYNylSpAZc"
|
| 34 |
+
|
| 35 |
+
GENAI_URL: "https://stgapi-internal.target.com/gen_ai_model_requests/v1"
|
| 36 |
+
API_KEY: "3e779b11599d49243f84c7b387aa6561b522eb80"
|
utils.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import logging
|
| 3 |
+
import time
|
| 4 |
+
import base64
|
| 5 |
+
import jwt
|
| 6 |
+
import httpx
|
| 7 |
+
|
| 8 |
+
from cryptography.hazmat.primitives.serialization import load_der_private_key
|
| 9 |
+
|
| 10 |
+
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
| 11 |
+
logger = logging.getLogger("Code Review Assistant")
|
| 12 |
+
|
| 13 |
+
BASE_GITHUB_URL = "https://api.github.com"
|
| 14 |
+
|
| 15 |
+
class JWTGenerator:
|
| 16 |
+
def __init__(self, app_id, private_key):
|
| 17 |
+
self.app_id = app_id
|
| 18 |
+
self.private_key = private_key
|
| 19 |
+
|
| 20 |
+
def generate_jwt(self):
|
| 21 |
+
payload = {
|
| 22 |
+
"iat": int(time.time()),
|
| 23 |
+
"exp": int(time.time()) + (10 * 60),
|
| 24 |
+
"iss": self.app_id,
|
| 25 |
+
}
|
| 26 |
+
if self.private_key:
|
| 27 |
+
private_key_cleaned = self.private_key.replace("-----BEGIN RSA PRIVATE KEY-----", "").replace("\n", "").replace("-----END RSA PRIVATE KEY-----", "")
|
| 28 |
+
secret = base64.b64decode(private_key_cleaned)
|
| 29 |
+
|
| 30 |
+
private_rsa_key = load_der_private_key(secret, password=None)
|
| 31 |
+
jwt_token = jwt.encode(payload, private_rsa_key, algorithm="RS256")
|
| 32 |
+
return jwt_token
|
| 33 |
+
raise ValueError("PRIVATE_KEY not found.")
|
| 34 |
+
|
| 35 |
+
async def get_installation_access_token(jwt, installation_id):
|
| 36 |
+
url = f"{BASE_GITHUB_URL}/app/installations/{installation_id}/access_tokens"
|
| 37 |
+
headers = {
|
| 38 |
+
"Authorization": f"Bearer {jwt}",
|
| 39 |
+
"Accept": "application/vnd.github.v3+json",
|
| 40 |
+
}
|
| 41 |
+
async with httpx.AsyncClient() as client:
|
| 42 |
+
response = await client.post(url, headers=headers)
|
| 43 |
+
return response.json()["token"]
|
| 44 |
+
|
| 45 |
+
def get_diff_url(pr):
|
| 46 |
+
"""GitHub 302s to this URL."""
|
| 47 |
+
original_url = pr.get("url")
|
| 48 |
+
parts = original_url.split("/")
|
| 49 |
+
owner, repo, pr_number = parts[-4], parts[-3], parts[-1]
|
| 50 |
+
return f"https://patch-diff.githubusercontent.com/raw/{owner}/{repo}/pull/{pr_number}.diff"
|
| 51 |
+
# return f"{BASE_GITHUB_URL}/repos/{owner}/{repo}/pulls/{pr_number}"
|