Elizabeth Thomas commited on
Commit
12ab2c5
·
1 Parent(s): 5d6a540
Files changed (9) hide show
  1. .gitignore +1 -0
  2. Dockerfile +14 -0
  3. README.md +8 -11
  4. app.py +0 -43
  5. full_app.py +0 -153
  6. main.py +197 -0
  7. requirements.txt +4 -2
  8. secret.yml +36 -0
  9. 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
- title: Code Reviewer
3
- emoji: 🐠
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
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
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
- ray[serve]
 
 
 
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}"