Skip to content

Commit a430e96

Browse files
authored
Merge pull request #15 from DaleStudy/perf/10-reduce-subrequests
perf: GraphQL ๋ฐฐ์น˜ ์กฐํšŒ + OpenAI ๋ฐฐ์น˜ ๋ถ„์„์œผ๋กœ subrequest ์ ˆ๊ฐ
2 parents d51353a + 92f8ca1 commit a430e96

File tree

3 files changed

+171
-49
lines changed

3 files changed

+171
-49
lines changed

โ€Žhandlers/learning-status.jsโ€Ž

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
fetchCohortUserSolutions,
1111
fetchPRSubmissions,
1212
} from "../utils/learningData.js";
13-
import { generateApproachAnalysis } from "../utils/openai.js";
13+
import { generateBatchApproachAnalysis } from "../utils/openai.js";
1414
import {
1515
formatLearningStatusComment,
1616
upsertLearningStatusComment,
@@ -105,9 +105,10 @@ export async function postLearningStatus(
105105
`[learningStatus] PR #${prNumber}: analyzing ${submissions.length} submission(s) for ${username}`
106106
);
107107

108-
// 4. ์ œ์ถœ ํŒŒ์ผ๋ณ„ AI ๋ถ„์„
108+
// 4. ์ œ์ถœ ํŒŒ์ผ ์ฝ”๋“œ ๋‹ค์šด๋กœ๋“œ (NํšŒ fetch)
109109
const submissionResults = [];
110-
const totalUsage = { prompt_tokens: 0, completion_tokens: 0 };
110+
const batchItems = [];
111+
const batchIndices = [];
111112

112113
for (const submission of submissions) {
113114
const problemInfo = categories[submission.problemName];
@@ -126,7 +127,6 @@ export async function postLearningStatus(
126127
}
127128

128129
try {
129-
// ํŒŒ์ผ ์›๋ณธ ๋‚ด์šฉ ๊ฐ€์ ธ์˜ค๊ธฐ
130130
const rawResponse = await fetch(submission.rawUrl);
131131
if (!rawResponse.ok) {
132132
throw new Error(`Failed to fetch raw file: ${rawResponse.status} ${rawResponse.statusText}`);
@@ -140,27 +140,18 @@ export async function postLearningStatus(
140140
);
141141
}
142142

143-
const analysis = await generateApproachAnalysis(
144-
fileContent,
145-
submission.problemName,
146-
problemInfo,
147-
openaiApiKey
148-
);
149-
150-
if (analysis.usage) {
151-
totalUsage.prompt_tokens += analysis.usage.prompt_tokens ?? 0;
152-
totalUsage.completion_tokens += analysis.usage.completion_tokens ?? 0;
153-
}
154-
143+
const idx = submissionResults.length;
155144
submissionResults.push({
156145
problemName: submission.problemName,
157146
difficulty: problemInfo.difficulty,
158-
matches: analysis.matches,
159-
explanation: analysis.explanation,
147+
matches: null,
148+
explanation: "",
160149
});
150+
batchItems.push({ problemName: submission.problemName, fileContent, problemInfo });
151+
batchIndices.push(idx);
161152
} catch (error) {
162153
console.error(
163-
`[learningStatus] Failed to analyze "${submission.problemName}": ${error.message}`
154+
`[learningStatus] Failed to fetch "${submission.problemName}": ${error.message}`
164155
);
165156
submissionResults.push({
166157
problemName: submission.problemName,
@@ -171,13 +162,33 @@ export async function postLearningStatus(
171162
}
172163
}
173164

174-
const hasUsage = totalUsage.prompt_tokens > 0 || totalUsage.completion_tokens > 0;
165+
// 5. AI ์ผ๊ด„ ๋ถ„์„ (1ํšŒ OpenAI ํ˜ธ์ถœ๋กœ ๋ชจ๋“  ์ œ์ถœ ํŒŒ์ผ ๋ถ„์„)
166+
let totalUsage = null;
167+
if (batchItems.length > 0) {
168+
try {
169+
console.log(
170+
`[learningStatus] PR #${prNumber}: batch analyzing ${batchItems.length} file(s) via OpenAI`
171+
);
172+
const { results: batchResults, usage } = await generateBatchApproachAnalysis(batchItems, openaiApiKey);
173+
totalUsage = usage;
174+
for (let i = 0; i < batchResults.length; i++) {
175+
submissionResults[batchIndices[i]].matches = batchResults[i].matches;
176+
submissionResults[batchIndices[i]].explanation = batchResults[i].explanation;
177+
}
178+
} catch (error) {
179+
console.error(
180+
`[learningStatus] Batch analysis failed: ${error.message}`
181+
);
182+
}
183+
}
184+
185+
const hasUsage = totalUsage != null;
175186

176-
// 5. ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง„ํ–‰๋„ ๊ณ„์‚ฐ
187+
// 6. ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง„ํ–‰๋„ ๊ณ„์‚ฐ
177188
const totalProblems = Object.keys(categories).length;
178189
const categoryProgress = buildCategoryProgress(categories, solvedProblems);
179190

180-
// 6. ๋Œ“๊ธ€ ๋ณธ๋ฌธ ํฌ๋งท
191+
// 7. ๋Œ“๊ธ€ ๋ณธ๋ฌธ ํฌ๋งท
181192
const commentBody = formatLearningStatusComment(
182193
username,
183194
submissionResults,
@@ -186,7 +197,7 @@ export async function postLearningStatus(
186197
categoryProgress
187198
);
188199

189-
// 7. ๋Œ“๊ธ€ ์ƒ์„ฑ ๋˜๋Š” ์—…๋ฐ์ดํŠธ
200+
// 8. ๋Œ“๊ธ€ ์ƒ์„ฑ ๋˜๋Š” ์—…๋ฐ์ดํŠธ
190201
await upsertLearningStatusComment(
191202
repoOwner,
192203
repoName,
@@ -196,7 +207,7 @@ export async function postLearningStatus(
196207
hasUsage ? totalUsage : null
197208
);
198209

199-
// 8. ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
210+
// 9. ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
200211
const matchedCount = submissionResults.filter((r) => r.matches === true).length;
201212
return { analyzed: submissionResults.length, matched: matchedCount };
202213
}

โ€Žutils/learningData.jsโ€Ž

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,21 @@ async function fetchActiveCohortProjectId(repoOwner, repoName, appToken) {
8080
}
8181

8282
/**
83-
* ๊ธฐ์ˆ˜ ํ”„๋กœ์ ํŠธ์—์„œ ํ•ด๋‹น ์œ ์ €๊ฐ€ ๋จธ์ง€ํ•œ PR ๋ฒˆํ˜ธ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
84-
* ํ”„๋กœ์ ํŠธ ์•„์ดํ…œ์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜๋ฉฐ author.login์œผ๋กœ ํ•„ํ„ฐ๋งํ•œ๋‹ค.
83+
* ๊ธฐ์ˆ˜ ํ”„๋กœ์ ํŠธ์˜ ์•„์ดํ…œ์„ ํŽ˜์ด์ง€๋„ค์ด์…˜ํ•˜๋ฉฐ ํ•ด๋‹น ์œ ์ €๊ฐ€ ๋จธ์ง€ํ•œ PR์˜
84+
* ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ GraphQL๋กœ ํ•œ ๋ฒˆ์— ์กฐํšŒํ•˜์—ฌ ํ’€์ดํ•œ ๋ฌธ์ œ ์ด๋ฆ„ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
85+
*
86+
* PR๋ณ„ REST ํ˜ธ์ถœ ์—†์ด GraphQL ์‘๋‹ต์— files๋ฅผ ํฌํ•จ์‹œ์ผœ subrequest๋ฅผ ์ ˆ์•ฝํ•œ๋‹ค.
8587
*
8688
* @param {string} projectId
8789
* @param {string} username
8890
* @param {string} appToken
89-
* @returns {Promise<number[]>}
91+
* @returns {Promise<string[]>}
9092
*/
91-
async function fetchUserMergedPRsInProject(projectId, username, appToken) {
92-
const prNumbers = [];
93+
async function fetchCohortSolvedFromProject(projectId, username, appToken) {
94+
const usernamePattern = new RegExp(
95+
`^([^/]+)/${escapeRegExp(username)}\\.[^/]+$`
96+
);
97+
const problemNames = new Set();
9398
let cursor = null;
9499

95100
while (true) {
@@ -103,9 +108,11 @@ async function fetchUserMergedPRsInProject(projectId, username, appToken) {
103108
nodes {
104109
content {
105110
... on PullRequest {
106-
number
107111
state
108112
author { login }
113+
files(first: 100) {
114+
nodes { path }
115+
}
109116
}
110117
}
111118
}
@@ -124,15 +131,20 @@ async function fetchUserMergedPRsInProject(projectId, username, appToken) {
124131
pr?.state === "MERGED" &&
125132
pr?.author?.login?.toLowerCase() === username.toLowerCase()
126133
) {
127-
prNumbers.push(pr.number);
134+
for (const file of pr.files?.nodes || []) {
135+
const match = file.path.match(usernamePattern);
136+
if (match) {
137+
problemNames.add(match[1]);
138+
}
139+
}
128140
}
129141
}
130142

131143
if (!pageInfo.hasNextPage) break;
132144
cursor = pageInfo.endCursor;
133145
}
134146

135-
return prNumbers;
147+
return Array.from(problemNames);
136148
}
137149

138150
/**
@@ -165,31 +177,17 @@ export async function fetchCohortUserSolutions(
165177
return fetchUserSolutions(repoOwner, repoName, username, appToken);
166178
}
167179

168-
const prNumbers = await fetchUserMergedPRsInProject(
180+
const problems = await fetchCohortSolvedFromProject(
169181
projectId,
170182
username,
171183
appToken
172184
);
173185

174186
console.log(
175-
`[fetchCohortUserSolutions] ${username} has ${prNumbers.length} merged PRs in current cohort`
187+
`[fetchCohortUserSolutions] ${username} solved ${problems.length} problems in current cohort`
176188
);
177189

178-
const problemNames = new Set();
179-
for (const prNumber of prNumbers) {
180-
const submissions = await fetchPRSubmissions(
181-
repoOwner,
182-
repoName,
183-
prNumber,
184-
username,
185-
appToken
186-
);
187-
for (const { problemName } of submissions) {
188-
problemNames.add(problemName);
189-
}
190-
}
191-
192-
return Array.from(problemNames);
190+
return problems;
193191
}
194192

195193
/**

โ€Žutils/openai.jsโ€Ž

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,119 @@ ${truncatedContent}
258258
};
259259
}
260260

261+
/**
262+
* ์—ฌ๋Ÿฌ ์†”๋ฃจ์…˜ ํŒŒ์ผ์˜ ์ ‘๊ทผ๋ฒ• ์ผ์น˜ ์—ฌ๋ถ€๋ฅผ ํ•œ ๋ฒˆ์˜ API ํ˜ธ์ถœ๋กœ ์ผ๊ด„ ๋ถ„์„.
263+
* subrequest ์ˆ˜๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ํŒŒ์ผ๋‹น ๊ฐœ๋ณ„ ํ˜ธ์ถœ ๋Œ€์‹  ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌํ•œ๋‹ค.
264+
*
265+
* @param {Array<{problemName: string, fileContent: string, problemInfo: object}>} items
266+
* @param {string} apiKey - OpenAI API ํ‚ค
267+
* @returns {Promise<{results: Array<{matches: boolean, explanation: string}>, usage: object|null}>}
268+
*/
269+
export async function generateBatchApproachAnalysis(items, apiKey) {
270+
if (items.length === 0) return { results: [], usage: null };
271+
272+
// ๋‹จ๊ฑด์ด๋ฉด ๊ธฐ์กด ํ•จ์ˆ˜ ์œ„์ž„
273+
if (items.length === 1) {
274+
const { fileContent, problemName, problemInfo } = items[0];
275+
const result = await generateApproachAnalysis(fileContent, problemName, problemInfo, apiKey);
276+
return {
277+
results: [{ matches: result.matches, explanation: result.explanation }],
278+
usage: result.usage ?? null,
279+
};
280+
}
281+
282+
const systemPrompt = `You are an algorithm analysis expert. You will receive multiple problems. For each one, determine if the submitted code matches the intended approach.
283+
284+
Respond with a JSON object containing a "results" array with exactly ${items.length} entries, in the same order as the input:
285+
{
286+
"results": [
287+
{ "matches": true, "explanation": "ํ•œ๊ตญ์–ด 1๋ฌธ์žฅ, 80์ž ์ด๋‚ด" },
288+
...
289+
]
290+
}
291+
292+
Rules:
293+
- matches=true if the core data structure or algorithm matches the intended approach
294+
- matches=false if brute force was used when an optimized approach was intended
295+
- Keep each explanation to 1 sentence in Korean, 80 characters or fewer
296+
- You MUST return exactly ${items.length} results`;
297+
298+
const MAX_BATCH_FILE_SIZE = 5000;
299+
300+
const problemSections = items.map(({ problemName, fileContent, problemInfo }, i) => {
301+
const truncated = fileContent.slice(0, MAX_BATCH_FILE_SIZE);
302+
return `## ๋ฌธ์ œ ${i + 1}: ${problemName}
303+
- ๋‚œ์ด๋„: ${problemInfo.difficulty}
304+
- ์นดํ…Œ๊ณ ๋ฆฌ: ${(problemInfo.categories || []).join(", ")}
305+
- ์˜๋„๋œ ์ ‘๊ทผ๋ฒ•: ${problemInfo.intended_approach}
306+
307+
\`\`\`
308+
${truncated}
309+
\`\`\``;
310+
});
311+
312+
const userPrompt = problemSections.join("\n\n") +
313+
`\n\n์œ„ ${items.length}๊ฐœ ์ฝ”๋“œ๊ฐ€ ๊ฐ๊ฐ ์˜๋„๋œ ์ ‘๊ทผ๋ฒ•๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ๋ถ„์„ํ•ด์ฃผ์„ธ์š”.`;
314+
315+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
316+
method: "POST",
317+
headers: {
318+
Authorization: `Bearer ${apiKey}`,
319+
"Content-Type": "application/json",
320+
},
321+
body: JSON.stringify({
322+
model: "gpt-4.1-nano",
323+
messages: [
324+
{ role: "system", content: systemPrompt },
325+
{ role: "user", content: userPrompt },
326+
],
327+
response_format: { type: "json_object" },
328+
max_tokens: 200 * items.length,
329+
temperature: 0.2,
330+
}),
331+
});
332+
333+
if (!response.ok) {
334+
const error = await response.text();
335+
throw new Error(`OpenAI batch API error: ${error}`);
336+
}
337+
338+
const data = await response.json();
339+
const content = data.choices[0]?.message?.content;
340+
341+
if (!content) {
342+
throw new Error("Empty response from OpenAI batch analysis");
343+
}
344+
345+
let parsed;
346+
try {
347+
parsed = JSON.parse(content);
348+
} catch {
349+
throw new Error(`OpenAI returned invalid JSON: ${content.slice(0, 200)}`);
350+
}
351+
352+
const rawResults = parsed.results;
353+
if (!Array.isArray(rawResults)) {
354+
throw new Error(`OpenAI did not return a results array`);
355+
}
356+
357+
if (rawResults.length !== items.length) {
358+
console.warn(
359+
`[generateBatchApproachAnalysis] Expected ${items.length} results, got ${rawResults.length}`
360+
);
361+
}
362+
363+
const results = items.map((_, i) => {
364+
const r = rawResults[i];
365+
return {
366+
matches: r?.matches === true,
367+
explanation: typeof r?.explanation === "string" ? r.explanation : "",
368+
};
369+
});
370+
371+
return { results, usage: data.usage ?? null };
372+
}
373+
261374
/**
262375
* ์†”๋ฃจ์…˜์˜ ์‹œ๊ฐ„/๊ณต๊ฐ„ ๋ณต์žก๋„ ๋ถ„์„.
263376
* ์‚ฌ์šฉ์ž๊ฐ€ ์ฝ”๋“œ ์–ด๋”˜๊ฐ€์— ์ž์œ  ํฌ๋งท์œผ๋กœ ๋‚จ๊ธด TC/SC ์ฃผ์„์„ ํ•จ๊ป˜ ์ถ”์ถœํ•˜์—ฌ

0 commit comments

Comments
ย (0)