File size: 8,175 Bytes
12ab2c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import httpx
import logging
import openai
from openai import AsyncOpenAI

import sys
import yaml
import string
import os

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from typing import Tuple

from utils import JWTGenerator

from utils import (
    get_installation_access_token,
    get_diff_url,
)

logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logger = logging.getLogger("Code Review Assistant")

class GitHubService:
    GREETING = f"""
👋 Hi, I'm @code-review-assistant, a LLM-powered GitHub app powered by 
[Open AI API Endpoints](https://api.openai.com/v1/chat/completions)
that gives you actionable feedback on your code.

Simply create a new comment in this PR that says:
`@code-review-assistant review`

and I will start my analysis. I only look at what you changed in this PR. All good? Let's get started!
    """

    def __init__(self, jwt_generator: JWTGenerator):
        self.jwt_generator = jwt_generator

    async def get_headers(self, data):
        installation_data = data['installation']
        if installation_data and installation_data.get("id"):
            installation_id = installation_data.get("id")
            jwt_token = self.jwt_generator.generate_jwt()   

            installation_access_token = await get_installation_access_token(jwt_token, installation_id)
            logger.info(f"Installation access token = {installation_access_token}")

            return {
                "Authorization": f"token {installation_access_token}",
                "User-Agent": "code-review-assistant",
                "Accept": "application/vnd.github.diff",
            }
        else:
            raise ValueError("No app installation found.")
        
    async def send_greetings(self, data):
        pr = data['pull_request']
        headers = await self.get_headers(data)

        # Greet the user and show instructions.
        async with httpx.AsyncClient() as client:
            await client.post(
                f"{pr['issue_url']}/comments",
                json={"body": self.GREETING},
                headers=headers,
            )
        return JSONResponse(content={}, status_code=200)
    
    async def handle_issue(self, data):
        issue = data['issue']
        headers = await self.get_headers(data)

        # Check if the issue is a pull request
        if "/pull/" in issue['html_url']:
           pr = issue['pull_request']

           # Get the comment body
           comment = data['comment']
           comment_body = comment['body']

           # Remove all whitespace characters except for regular spaces
           comment_body = comment_body.translate(
               str.maketrans("", "", string.whitespace.replace(" ", ""))
           )

           # Skip if the bot talks about itself
           author_handle = comment['user']['login']

            # Check if the bot is mentioned in the comment
           if (author_handle != "code-review-assistant[bot]" and "@code-review-assistant review" in comment_body):

                url = get_diff_url(pr)
                async with httpx.AsyncClient() as client:
                    response = await client.get(url, headers=headers)
                    diff = response.text
                    return (f"{comment['issue_url']}/comments", diff)

    async def post_code_review_analysis(self, data, comment_url, analysis_result, model_used):
        async with httpx.AsyncClient() as client:
            await client.post(
                            comment_url,
                            json={
                                "body": f":rocket: Code Review Assistant Analysis finished "
                                + "analysing your PR! :rocket:\n\n"
                                + "Take a look at your results:\n"
                                + f"{analysis_result}\n\n"
                                + "This bot is proudly powered by "
                                + "[Open AI API Endpoints](https://api.openai.com/v1/chat/completions).\n"
                                + f"It used the model {model_used}"
                            },
                            headers=await self.get_headers(data),
                        )
        
class CodeReviewAssistant:
    SYSTEM_CONTENT = """You’ll act as a code review assistant.
    Provide informative, constructive feedback on code quality, identify potential bugs, suggests improvements in coding style and offer explanation for suggested changes.
    You should be able to analyze code written in popular programming languages.
    Prioritize recommendations based on severity, testability and impact on maintainability.
    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.
    Consider incorporating features like highlighting specific lines of code, providing inline comments, and generating a summary report.
    When quoting variables or names from the code, use backticks (`) instead of single quote (').
    Ensure that the assistant promotes collaboration and learning among engineers while adhering to best practices in software development.
    If the content is good, don’t comment on it.
    You can use GitHub-flavored markdown syntax in your answer.
    If you encounter several files, give very concise feedback per file.
    """

    PROMPT = """Review the below code difference and give concise actionable code review comments only
    for the changed code. If code change looks good overall, just say \"change looks good\" and stop commenting more.
    Don’t try to make up comments. Don’t comment on file names or other meta data, just the actual text.
    The <content> will be in JSON format and contains file name keys and text values.
    """

    MODEL = "codellama-34b-instruct"

    def __init__(self, github_service: GitHubService):
        self.client = None  # Placeholder for AsyncOpenAI client initialization.
        self.github_service = github_service

    async def review(
            self, 
            content) -> Tuple[str]:

        client = AsyncOpenAI(
            base_url="https://api.openai.com/v1",
            api_key=os.getenv("OPENAI_API_KEY"),
        )

        response = await client.chat.completions.create(
            messages=[
                {"role": "system", "content": self.SYSTEM_CONTENT},
                {"role": "user", "content": f"This is the content: {content}. {self.PROMPT}"},
            ],
            temperature=0.7,
            model=self.MODEL,
        )
        logger.info(f"Result from Model analysis = {response}")

        return response.choices[0].message.content, self.MODEL

app = FastAPI()
try:
    jwt_generator = JWTGenerator(os.getenv("APP_ID"), os.getenv("PRIVATE_KEY"))
except FileNotFoundError as e:
    logger.error(f"Error in secret manager: {e}")

github_service = GitHubService(jwt_generator)
code_review_assistant = CodeReviewAssistant(github_service)


@app.get("/")
async def root():
    return {"message": "Code review assistant reporting for duty!"}

@app.post("/webhook")
async def handle_webhook_route(request: Request):

    data = await request.json()

    # If PR exists and is opened
    if "pull_request" in data.keys() and (data["action"] in ["opened", "reopened"]):
        await github_service.send_greetings(data)

    # Check if the event is a new or modified issue comment
    if "issue" in data.keys() and data.get("action") in ["created", "edited"]:
        try:
            result = await github_service.handle_issue(data)
            if result is not None:
                issue_comment_url, content_to_review = result
                analysis_result, model_used = await code_review_assistant.review(content_to_review)
                logger.info(f"Result from LLM Analysis = {analysis_result}")
                await github_service.post_code_review_analysis(data, issue_comment_url, analysis_result, model_used)
        except Exception as e:
            logger.error(f"Exception occured while calling github_service.handle_issue: {e}")