mirror of
https://github.com/freeedcom/ai-codereviewer.git
synced 2025-04-19 17:16:48 +00:00
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:
parent
751c4e51f9
commit
c30f272ef1
8 changed files with 202 additions and 187 deletions
29
src/analyzeCode.ts
Normal file
29
src/analyzeCode.ts
Normal 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
21
src/createComment.ts
Normal 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
42
src/createPrompt.ts
Normal 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")}
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
}
|
20
src/createReviewComment.ts
Normal file
20
src/createReviewComment.ts
Normal 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
48
src/getAIResponse.ts
Normal 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
16
src/getDiff.ts
Normal 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
21
src/getPRDetails.ts
Normal 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 ?? "",
|
||||||
|
};
|
||||||
|
}
|
192
src/main.ts
192
src/main.ts
|
@ -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();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue