Skip to content

Commit 578225f

Browse files
committed
Enhance AI review functionality by adding user request handling and reaction to comments; refactor comment processing for better clarity and structure.
1 parent d58d9b6 commit 578225f

3 files changed

Lines changed: 148 additions & 91 deletions

File tree

handlers/webhooks.js

Lines changed: 54 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
isClosedPR,
2020
} from "../utils/validation.js";
2121
import { ALLOWED_REPO } from "../utils/constants.js";
22-
import { performAIReview } from "../utils/prReview.js";
22+
import { performAIReview, addReactionToComment } from "../utils/prReview.js";
2323

2424
/**
2525
* GitHub webhook 이벤트 처리
@@ -249,6 +249,26 @@ async function handlePullRequestEvent(payload, env) {
249249
});
250250
}
251251

252+
/**
253+
* 댓글에서 @dalestudy 멘션과 사용자 요청 추출
254+
*
255+
* @param {string} commentBody - 댓글 내용
256+
* @returns {Object|null} { isMentioned, userRequest } 또는 null
257+
*/
258+
function extractMentionAndRequest(commentBody) {
259+
const lowerBody = commentBody.toLowerCase();
260+
const isMentioned = lowerBody.includes("@dalestudy");
261+
262+
if (!isMentioned) {
263+
return null;
264+
}
265+
266+
const mentionMatch = commentBody.match(/@dalestudy\s*(.*)/i);
267+
const userRequest = mentionMatch && mentionMatch[1].trim() ? mentionMatch[1].trim() : null;
268+
269+
return { isMentioned: true, userRequest };
270+
}
271+
252272
/**
253273
* Issue Comment 이벤트 처리 (AI 코드 리뷰 요청)
254274
*/
@@ -276,21 +296,14 @@ async function handleIssueCommentEvent(payload, env) {
276296

277297
const prNumber = issue.number;
278298

279-
// 멘션 감지: @dalestudy만 체크
280-
const commentBody = comment.body;
281-
const lowerBody = commentBody.toLowerCase();
282-
const isMentioned = lowerBody.includes("@dalestudy");
283-
284-
if (!isMentioned) {
299+
// 멘션 감지 및 사용자 요청 추출
300+
const mention = extractMentionAndRequest(comment.body);
301+
if (!mention) {
285302
console.log("Ignoring: bot not mentioned");
286303
return corsResponse({ message: "Ignored: not mentioned" });
287304
}
288305

289-
// 멘션 뒤의 텍스트 추출
290-
const mentionMatch = commentBody.match(/@dalestudy\s*(.*)/i);
291-
const userRequest = mentionMatch && mentionMatch[1].trim() ? mentionMatch[1].trim() : null;
292-
293-
console.log(`AI review requested for PR #${prNumber}${userRequest ? ` - Request: ${userRequest}` : ""}`);
306+
console.log(`AI review requested for PR #${prNumber}${mention.userRequest ? ` - Request: ${mention.userRequest}` : ""}`);
294307

295308
// OPENAI_API_KEY 확인
296309
if (!env.OPENAI_API_KEY) {
@@ -303,13 +316,13 @@ async function handleIssueCommentEvent(payload, env) {
303316
const appToken = await generateGitHubAppToken(env);
304317

305318
// 👀 reaction 추가 (리뷰 시작 알림)
306-
await fetch(
307-
`https://api.github.com/repos/${repoOwner}/${repoName}/issues/comments/${comment.id}/reactions`,
308-
{
309-
method: "POST",
310-
headers: getGitHubHeaders(appToken),
311-
body: JSON.stringify({ content: "eyes" }),
312-
}
319+
await addReactionToComment(
320+
repoOwner,
321+
repoName,
322+
comment.id,
323+
"issue",
324+
"eyes",
325+
appToken
313326
);
314327

315328
await performAIReview(
@@ -319,7 +332,8 @@ async function handleIssueCommentEvent(payload, env) {
319332
issue.title,
320333
issue.body,
321334
appToken,
322-
env.OPENAI_API_KEY
335+
env.OPENAI_API_KEY,
336+
mention.userRequest
323337
);
324338

325339
console.log(`AI review completed for PR #${prNumber}`);
@@ -354,16 +368,14 @@ async function handlePullRequestReviewCommentEvent(payload, env) {
354368
const repoName = payload.repository.name;
355369
const prNumber = pullRequest.number;
356370

357-
// 멘션 감지: @dalestudy만 체크
358-
const commentBody = comment.body.toLowerCase();
359-
const isMentioned = commentBody.includes("@dalestudy");
360-
361-
if (!isMentioned) {
371+
// 멘션 감지 및 사용자 요청 추출
372+
const mention = extractMentionAndRequest(comment.body);
373+
if (!mention) {
362374
console.log("Ignoring: bot not mentioned");
363375
return corsResponse({ message: "Ignored: not mentioned" });
364376
}
365377

366-
console.log(`AI review requested for PR #${prNumber} (review comment)`);
378+
console.log(`AI review requested for PR #${prNumber} (review comment)${mention.userRequest ? ` - Request: ${mention.userRequest}` : ""}`);
367379

368380
// OPENAI_API_KEY 확인
369381
if (!env.OPENAI_API_KEY) {
@@ -376,43 +388,26 @@ async function handlePullRequestReviewCommentEvent(payload, env) {
376388
const appToken = await generateGitHubAppToken(env);
377389

378390
// 👀 reaction 추가
379-
await fetch(
380-
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls/comments/${comment.id}/reactions`,
381-
{
382-
method: "POST",
383-
headers: getGitHubHeaders(appToken),
384-
body: JSON.stringify({ content: "eyes" }),
385-
}
391+
await addReactionToComment(
392+
repoOwner,
393+
repoName,
394+
comment.id,
395+
"pull",
396+
"eyes",
397+
appToken
386398
);
387399

388-
// AI 리뷰 생성
389-
const { generateCodeReview } = await import("../utils/openai.js");
390-
const { getPRDiff } = await import("../utils/prReview.js");
391-
392-
const prDiff = await getPRDiff(repoOwner, repoName, prNumber, appToken);
393-
394-
// diff가 너무 크면 스킵
395-
const diffLines = prDiff.split("\n").length;
396-
if (diffLines > 1000) {
397-
console.log(`Skipping AI review: diff too large (${diffLines} lines)`);
398-
return corsResponse({ message: "Diff too large" });
399-
}
400-
401-
const reviewContent = await generateCodeReview(
402-
prDiff,
400+
// performAIReview 사용 (스레드 답변 모드)
401+
await performAIReview(
402+
repoOwner,
403+
repoName,
404+
prNumber,
403405
pullRequest.title,
404406
pullRequest.body,
405-
env.OPENAI_API_KEY
406-
);
407-
408-
// 스레드 답변으로 작성
409-
await fetch(
410-
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}/comments/${comment.id}/replies`,
411-
{
412-
method: "POST",
413-
headers: getGitHubHeaders(appToken),
414-
body: JSON.stringify({ body: reviewContent }),
415-
}
407+
appToken,
408+
env.OPENAI_API_KEY,
409+
mention.userRequest,
410+
comment.id // 스레드 답변으로 작성
416411
);
417412

418413
console.log(`AI review completed for PR #${prNumber} (thread reply)`);

utils/openai.js

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,6 @@ export async function generateCodeReview(
2424
? `당신은 리트코드 스터디 그룹의 AI 코치입니다.
2525
사용자가 PR의 코드에 대해 구체적인 질문을 했습니다.
2626
PR의 코드 변경 사항을 참고하여 사용자의 질문에 명확하고 도움이 되는 답변을 제공하세요.
27-
28-
답변 시:
29-
• 사용자의 질문에 직접적으로 답변하세요
30-
• 코드의 해당 부분을 구체적으로 언급하세요
31-
• 필요하면 예시나 개선 방법을 제안하세요
32-
• 격려와 학습이 되는 피드백을 함께 주세요
33-
3427
300 글자를 초과하지 말아주세요.`
3528
: `당신은 리트코드 스터디 그룹의 AI 코치입니다.
3629
아래 코드 변경 사항을 리뷰하고 건설적인 피드백을 제공하세요.
@@ -40,29 +33,29 @@ PR의 코드 변경 사항을 참고하여 사용자의 질문에 명확하고
4033
• 시간/공간 복잡도 분석이 정확한지 평가
4134
• 더 나은 접근법이나 알고리즘이 있는지 제안
4235
• 코드의 가독성 및 스타일, 베스트 프랙티스 준수 여부
43-
잠재적인 버그 또는 개선 가능성
36+
불필요한 nickpick은 피하고, 꼭 필요한 피드백만 주세요.
4437
4538
단순히 지적만 하지 말고, 격려와 학습이 되는 피드백을 함께 주세요.
4639
해당 사항없는 항목은 생략하고 자연스럽게 작성하세요.
47-
300 글자를 초과하지 말아주세요.
40+
500 글자를 초과하지 말아주세요.
4841
`;
4942

50-
let userPrompt = `# PR 제목
43+
let userPrompt = `# PR Title
5144
${prTitle}
5245
53-
# PR 설명
54-
${prBody || "설명 없음"}
46+
# PR Description
47+
${prBody || "No description provided"}
5548
56-
# 코드 변경사항
49+
# Code Changes
5750
\`\`\`diff
5851
${prDiff}
5952
\`\`\`
6053
`;
6154

6255
if (userRequest) {
63-
userPrompt += `\n# 사용자 질문\n${userRequest}`;
56+
userPrompt += `\n# User's Question\n${userRequest}`;
6457
} else {
65-
userPrompt += `\n이 PR을 리뷰해주세요.`;
58+
userPrompt += `\nPlease review this pull request.`;
6659
}
6760

6861
const response = await fetch("https://api.openai.com/v1/chat/completions", {

utils/prReview.js

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,73 @@ async function postReviewComment(
4848
reviewContent,
4949
githubToken
5050
) {
51-
const commentBody = `${reviewContent}`;
52-
5351
await fetch(
5452
`https://api.github.com/repos/${repoOwner}/${repoName}/issues/${prNumber}/comments`,
5553
{
5654
method: "POST",
5755
headers: getGitHubHeaders(githubToken),
58-
body: JSON.stringify({ body: commentBody }),
56+
body: JSON.stringify({ body: reviewContent }),
5957
}
6058
);
6159
}
6260

61+
/**
62+
* 스레드 답변으로 AI 코드 리뷰 작성
63+
*
64+
* @param {string} repoOwner - 저장소 소유자
65+
* @param {string} repoName - 저장소 이름
66+
* @param {number} prNumber - PR 번호
67+
* @param {number} commentId - 원본 댓글 ID
68+
* @param {string} reviewContent - 리뷰 내용 (마크다운)
69+
* @param {string} githubToken - GitHub 토큰
70+
*/
71+
async function postThreadReply(
72+
repoOwner,
73+
repoName,
74+
prNumber,
75+
commentId,
76+
reviewContent,
77+
githubToken
78+
) {
79+
await fetch(
80+
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}/comments/${commentId}/replies`,
81+
{
82+
method: "POST",
83+
headers: getGitHubHeaders(githubToken),
84+
body: JSON.stringify({ body: reviewContent }),
85+
}
86+
);
87+
}
88+
89+
/**
90+
* 댓글에 reaction 추가
91+
*
92+
* @param {string} repoOwner - 저장소 소유자
93+
* @param {string} repoName - 저장소 이름
94+
* @param {number} commentId - 댓글 ID
95+
* @param {string} commentType - 댓글 타입 ('issue' 또는 'pull')
96+
* @param {string} reaction - reaction 타입 (예: 'eyes')
97+
* @param {string} githubToken - GitHub 토큰
98+
*/
99+
export async function addReactionToComment(
100+
repoOwner,
101+
repoName,
102+
commentId,
103+
commentType,
104+
reaction,
105+
githubToken
106+
) {
107+
const endpoint = commentType === "issue"
108+
? `https://api.github.com/repos/${repoOwner}/${repoName}/issues/comments/${commentId}/reactions`
109+
: `https://api.github.com/repos/${repoOwner}/${repoName}/pulls/comments/${commentId}/reactions`;
110+
111+
await fetch(endpoint, {
112+
method: "POST",
113+
headers: getGitHubHeaders(githubToken),
114+
body: JSON.stringify({ content: reaction }),
115+
});
116+
}
117+
63118
/**
64119
* PR에 대해 AI 리뷰 수행
65120
*
@@ -71,6 +126,7 @@ async function postReviewComment(
71126
* @param {string} githubToken - GitHub 토큰
72127
* @param {string} openaiApiKey - OpenAI API 키
73128
* @param {string} userRequest - 사용자의 구체적인 요청 (선택사항)
129+
* @param {number} replyToCommentId - 스레드 답변으로 작성할 댓글 ID (선택사항)
74130
*/
75131
export async function performAIReview(
76132
repoOwner,
@@ -80,9 +136,10 @@ export async function performAIReview(
80136
prBody,
81137
githubToken,
82138
openaiApiKey,
83-
userRequest = null
139+
userRequest = null,
140+
replyToCommentId = null
84141
) {
85-
console.log(`Starting AI review for PR #${prNumber}`);
142+
console.log(`Starting AI review for PR #${prNumber}${userRequest ? ` - Request: ${userRequest}` : ""}`);
86143

87144
// PR diff 가져오기
88145
const prDiff = await getPRDiff(repoOwner, repoName, prNumber, githubToken);
@@ -94,22 +151,34 @@ export async function performAIReview(
94151
return;
95152
}
96153

97-
// AI 리뷰 생성
154+
// AI 리뷰 생성 (userRequest 전달)
98155
const reviewContent = await generateCodeReview(
99156
prDiff,
100157
prTitle,
101158
prBody,
102-
openaiApiKey
103-
);
104-
105-
// 리뷰 댓글 작성
106-
await postReviewComment(
107-
repoOwner,
108-
repoName,
109-
prNumber,
110-
reviewContent,
111-
githubToken
159+
openaiApiKey,
160+
userRequest
112161
);
113162

114-
console.log(`AI review posted for PR #${prNumber}`);
163+
// 리뷰 댓글 작성 (스레드 답변 또는 일반 댓글)
164+
if (replyToCommentId) {
165+
await postThreadReply(
166+
repoOwner,
167+
repoName,
168+
prNumber,
169+
replyToCommentId,
170+
reviewContent,
171+
githubToken
172+
);
173+
console.log(`AI review posted as thread reply for PR #${prNumber}`);
174+
} else {
175+
await postReviewComment(
176+
repoOwner,
177+
repoName,
178+
prNumber,
179+
reviewContent,
180+
githubToken
181+
);
182+
console.log(`AI review posted for PR #${prNumber}`);
183+
}
115184
}

0 commit comments

Comments
 (0)