feat: Refactor code to separate functions for creating review comments, analyzing code, getting PR details, getting AI response, creating comments, and getting diffs.

This commit is contained in:
Will Hohyon Ryu 2024-03-07 19:16:09 -08:00
parent 751c4e51f9
commit c30f272ef1
8 changed files with 202 additions and 187 deletions

29
src/analyzeCode.ts Normal file
View file

@ -0,0 +1,29 @@
import {File} from "parse-diff";
import {createPrompt, PRDetails} from "./createPrompt";
import {getAIResponse} from "./getAIResponse";
import {createComment} from "./createComment";
export async function analyzeCode(
parsedDiff: File[],
prDetails: PRDetails
): Promise<Array<{ body: string; path: string; line: number }>> {
const comments: Array<{ body: string; path: string; line: number }> = [];
for (const file of parsedDiff) {
if (file.to === "/dev/null") continue; // Ignore deleted files
for (const chunk of file.chunks) {
// Ignore diffs longer than 2000 characters
if (chunk.content.length > 4000) continue;
const prompt = createPrompt(file, chunk, prDetails);
const aiResponse = await getAIResponse(prompt);
if (aiResponse) {
const newComments = createComment(file, chunk, aiResponse);
if (newComments) {
comments.push(...newComments);
}
}
}
}
return comments;
}

21
src/createComment.ts Normal file
View file

@ -0,0 +1,21 @@
import {Chunk, File} from "parse-diff";
export function createComment(
file: File,
chunk: Chunk,
aiResponses: Array<{
lineNumber: string;
reviewComment: string;
}>
): Array<{ body: string; path: string; line: number }> {
return aiResponses.flatMap((aiResponse) => {
if (!file.to) {
return [];
}
return {
body: aiResponse.reviewComment,
path: file.to,
line: Number(aiResponse.lineNumber),
};
});
}

42
src/createPrompt.ts Normal file
View file

@ -0,0 +1,42 @@
import {Chunk, File} from "parse-diff";
export interface PRDetails {
owner: string;
repo: string;
pull_number: number;
title: string;
description: string;
}
export function createPrompt(file: File, chunk: Chunk, prDetails: PRDetails): string {
return `Your task is to review pull requests. Instructions:
- Provide the response in following JSON format: {"reviews": [{"lineNumber": <line_number>, "reviewComment": "<review comment>"}]}
- Do not give positive comments or compliments.
- Provide review in Korean language.
- Provide comments and suggestions ONLY if there is something to improve, otherwise "reviews" should be an empty array.
- Write the comment in GitHub Markdown format.
- Use the given description only for the overall context and only comment the code.
- IMPORTANT: NEVER suggest adding comments to the code.
Review the following code diff in the file "${
file.to
}" and take the pull request title and description into account when writing the response.
Pull request title: ${prDetails.title}
Pull request description:
---
${prDetails.description}
---
Git diff to review:
\`\`\`diff
${chunk.content}
${chunk.changes
// @ts-expect-error - ln and ln2 exists where needed
.map((c) => `${c.ln ? c.ln : c.ln2} ${c.content}`)
.join("\n")}
\`\`\`
`;
}

View file

@ -0,0 +1,20 @@
import * as core from "@actions/core";
import {Octokit} from "@octokit/rest";
const GITHUB_TOKEN: string = core.getInput("GITHUB_TOKEN");
export const octokit = new Octokit({auth: GITHUB_TOKEN});
export async function createReviewComment(
owner: string,
repo: string,
pull_number: number,
comments: Array<{ body: string; path: string; line: number }>
): Promise<void> {
await octokit.pulls.createReview({
owner,
repo,
pull_number,
comments,
event: "COMMENT",
});
}

48
src/getAIResponse.ts Normal file
View file

@ -0,0 +1,48 @@
import * as core from "@actions/core";
import OpenAI from "openai";
const OPENAI_API_KEY: string = core.getInput("OPENAI_API_KEY");
const OPENAI_API_MODEL: string = core.getInput("OPENAI_API_MODEL");
const openai = new OpenAI({
apiKey: OPENAI_API_KEY,
});
export async function getAIResponse(prompt: string): Promise<Array<{
lineNumber: string;
reviewComment: string;
}> | null> {
const queryConfig = {
model: OPENAI_API_MODEL,
temperature: 0.2,
max_tokens: 700,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
};
const response =
await openai.chat.completions.create(
{
...queryConfig,
// return JSON if the model supports it:
...(OPENAI_API_MODEL === "gpt-4-1106-preview"
? {response_format: {type: "json_object"}}
: {}),
messages: [
{
role: "system",
content: prompt,
},
],
});
const res = response.choices[0].message?.content?.trim() || "{}";
console.log("GPT response:")
console.log(res);
const cleanupResponse = res
.replace(/```json/g, "")
.replace(/```/g, "");
return JSON.parse(cleanupResponse).reviews;
}

16
src/getDiff.ts Normal file
View file

@ -0,0 +1,16 @@
import {octokit} from "./createReviewComment";
export async function getDiff(
owner: string,
repo: string,
pull_number: number
): Promise<string | null> {
const response = await octokit.pulls.get({
owner,
repo,
pull_number,
mediaType: {format: "diff"},
});
// @ts-expect-error - response.data is a string
return response.data;
}

21
src/getPRDetails.ts Normal file
View file

@ -0,0 +1,21 @@
import {PRDetails} from "./createPrompt";
import {readFileSync} from "fs";
import {octokit} from "./createReviewComment";
export async function getPRDetails(): Promise<PRDetails> {
const {repository, number} = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8")
);
const prResponse = await octokit.pulls.get({
owner: repository.owner.login,
repo: repository.name,
pull_number: number,
});
return {
owner: repository.owner.login,
repo: repository.name,
pull_number: number,
title: prResponse.data.title ?? "",
description: prResponse.data.body ?? "",
};
}

View file

@ -1,193 +1,11 @@
import {readFileSync} from "fs"; import {readFileSync} from "fs";
import * as core from "@actions/core"; import * as core from "@actions/core";
import OpenAI from "openai"; import parseDiff from "parse-diff";
import {Octokit} from "@octokit/rest";
import parseDiff, {Chunk, File} from "parse-diff";
import minimatch from "minimatch"; import minimatch from "minimatch";
import {createReviewComment, octokit} from "./createReviewComment";
const GITHUB_TOKEN: string = core.getInput("GITHUB_TOKEN"); import {analyzeCode} from "./analyzeCode";
const OPENAI_API_KEY: string = core.getInput("OPENAI_API_KEY"); import {getPRDetails} from "./getPRDetails";
const OPENAI_API_MODEL: string = core.getInput("OPENAI_API_MODEL"); import {getDiff} from "./getDiff";
const octokit = new Octokit({auth: GITHUB_TOKEN});
const openai = new OpenAI({
apiKey: OPENAI_API_KEY,
});
interface PRDetails {
owner: string;
repo: string;
pull_number: number;
title: string;
description: string;
}
async function getPRDetails(): Promise<PRDetails> {
const {repository, number} = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH || "", "utf8")
);
const prResponse = await octokit.pulls.get({
owner: repository.owner.login,
repo: repository.name,
pull_number: number,
});
return {
owner: repository.owner.login,
repo: repository.name,
pull_number: number,
title: prResponse.data.title ?? "",
description: prResponse.data.body ?? "",
};
}
async function getDiff(
owner: string,
repo: string,
pull_number: number
): Promise<string | null> {
const response = await octokit.pulls.get({
owner,
repo,
pull_number,
mediaType: {format: "diff"},
});
// @ts-expect-error - response.data is a string
return response.data;
}
async function analyzeCode(
parsedDiff: File[],
prDetails: PRDetails
): Promise<Array<{ body: string; path: string; line: number }>> {
const comments: Array<{ body: string; path: string; line: number }> = [];
for (const file of parsedDiff) {
if (file.to === "/dev/null") continue; // Ignore deleted files
for (const chunk of file.chunks) {
// Ignore diffs longer than 2000 characters
if (chunk.content.length > 4000) continue;
const prompt = createPrompt(file, chunk, prDetails);
const aiResponse = await getAIResponse(prompt);
if (aiResponse) {
const newComments = createComment(file, chunk, aiResponse);
if (newComments) {
comments.push(...newComments);
}
}
}
}
return comments;
}
function createPrompt(file: File, chunk: Chunk, prDetails: PRDetails): string {
return `Your task is to review pull requests. Instructions:
- Provide the response in following JSON format: {"reviews": [{"lineNumber": <line_number>, "reviewComment": "<review comment>"}]}
- Do not give positive comments or compliments.
- Provide review in Korean language.
- Provide comments and suggestions ONLY if there is something to improve, otherwise "reviews" should be an empty array.
- Write the comment in GitHub Markdown format.
- Use the given description only for the overall context and only comment the code.
- IMPORTANT: NEVER suggest adding comments to the code.
Review the following code diff in the file "${
file.to
}" and take the pull request title and description into account when writing the response.
Pull request title: ${prDetails.title}
Pull request description:
---
${prDetails.description}
---
Git diff to review:
\`\`\`diff
${chunk.content}
${chunk.changes
// @ts-expect-error - ln and ln2 exists where needed
.map((c) => `${c.ln ? c.ln : c.ln2} ${c.content}`)
.join("\n")}
\`\`\`
`;
}
async function getAIResponse(prompt: string): Promise<Array<{
lineNumber: string;
reviewComment: string;
}> | null> {
const queryConfig = {
model: OPENAI_API_MODEL,
temperature: 0.2,
max_tokens: 700,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0,
};
const response =
await openai.chat.completions.create(
{
...queryConfig,
// return JSON if the model supports it:
...(OPENAI_API_MODEL === "gpt-4-1106-preview"
? {response_format: {type: "json_object"}}
: {}),
messages: [
{
role: "system",
content: prompt,
},
],
});
const res = response.choices[0].message?.content?.trim() || "{}";
console.log("GPT response:")
console.log(res);
const cleanupResponse = res
.replace(/```json/g, "")
.replace(/```/g, "");
return JSON.parse(cleanupResponse).reviews;
}
function createComment(
file: File,
chunk: Chunk,
aiResponses: Array<{
lineNumber: string;
reviewComment: string;
}>
): Array<{ body: string; path: string; line: number }> {
return aiResponses.flatMap((aiResponse) => {
if (!file.to) {
return [];
}
return {
body: aiResponse.reviewComment,
path: file.to,
line: Number(aiResponse.lineNumber),
};
});
}
async function createReviewComment(
owner: string,
repo: string,
pull_number: number,
comments: Array<{ body: string; path: string; line: number }>
): Promise<void> {
await octokit.pulls.createReview({
owner,
repo,
pull_number,
comments,
event: "COMMENT",
});
}
async function main() { async function main() {
const prDetails = await getPRDetails(); const prDetails = await getPRDetails();