Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Main에 ai 저장소 합치기 #268

Merged
merged 7 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@

/.idea

/ai/generate-server/node_modules

ai/generate-server/.env
=======
# 디폴트 무시된 파일
/.idea/shelf/
/.idea/workspace.xml
Expand All @@ -8,3 +15,4 @@
/dataSources.local.xml

/.idea

19 changes: 19 additions & 0 deletions ai/evaluate-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# compiled output
/node_modules

# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/extensions.json

.env
30 changes: 30 additions & 0 deletions ai/evaluate-server/RedisPub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const redis = require("redis");
require("dotenv").config();

// RedisPub 클래스
class RedisPub {
constructor() {
this.init();
}

async init() {
try {
this.publisher = redis.createClient({
url: process.env.REDIS_URL,
});
await this.publisher.connect();
console.log("Redis publisher connected");
} catch (err) {
console.error("An error occurred:", err);
}
}
async send(channel, message) {
await this.publisher.publish(channel, message);
}
async close() {
await this.publisher.quit();
console.log("connection closed");
}
}

module.exports = RedisPub;
188 changes: 188 additions & 0 deletions ai/evaluate-server/evaluate-api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
require("dotenv").config();
const axios = require("axios");
const RedisPub = require("./RedisPub");

const publisher = new RedisPub();

// 체크리스트를 평가하는 메인 함수
async function processAiResult(categoryDto, checklistDto, maxRetries = 10) {
let retryCount = 0;
while (retryCount < maxRetries) {
try {
const result = await evaluateChecklistItem(categoryDto, checklistDto);
const { select, reason } = await aiResultParser(result);
await checkValidResult(select, reason, checklistDto);
await publisher.send(
"ai_evaluate",
JSON.stringify({ message: "result", body: { select, reason } })
);
console.log("select:", select);
console.log("reason:", reason);
return { select, reason };
} catch (error) {
// 429 에러인 경우, 2초에서 5초 사이의 랜덤한 시간 동안 대기 후 재시도
if (error?.response?.status === 429) {
console.error("Too many requests");
const delayTime = getRandomDelay(2000, 10000); // 2초에서 5초 사이의 랜덤한 시간
console.log(`Waiting for ${delayTime}ms before retry...`);
await publisher.send(
"ai_evaluate_error",
`Too many requests::Waiting for ${delayTime}ms before retry...`
);
await delay(delayTime);
continue; // 다음 시도로 이동
}
console.error("Error:", error.message);
await publisher.send("ai_evaluate_error", error.message);
console.log("retryCount:", retryCount + 1);
await publisher.send(
"ai_evaluate_error",
`retryCount: ${retryCount + 1}`
);
retryCount++;
}
}
if (retryCount === maxRetries) {
console.error("모든 재시도 실패");
await publisher.send("ai_evaluate_error", "모든 재시도 실패");
return { select: undefined, reason: undefined };
}
}

const AI_OPTIONS = {
topP: 0.8,
topK: 0,
maxTokens: 256,
temperature: 1,
repeatPenalty: 7.0,
stopBefore: [],
includeAiFilters: true,
};

const CLOVA_API_URL =
"https://clovastudio.stream.ntruss.com/testapp/v1/chat-completions/HCX-002";

const SYSTEM_ROLE = {
role: "system",
content: `너는 평가자야
사용자가 대 중 소 카테고리를 기반으로 추천 체크리스트를 만들었어
체크리스트에는 10개의 항목이 있어. 이 중에서 카테고리에 잘 어울리는 체크리스트 3개를 선택해서 번호와 이유를 JSON 형식으로 제공해 줘.
오직 JSON 형식으로만 출력해야 해.

사용자가 입력할 정보
1. 대 중 소 카테고리 JSON
{
"mainCategory": "대",
"subCategory": "중",
"minorCategory": "소"
}

2. 체크리스트 JSON
{
"1": "생성된 체크리스트1",
"2": " 생성된 체크리스트2",
"3": "생성된 체크리스트3",
"4": "생성된 체크리스트4",
"5": "생성된 체크리스트5",
"6": "생성된 체크리스트6",
"7": "생성된 체크리스트7",
"8": "생성된 체크리스트8",
"9": "생성된 체크리스트9",
"10": "생성된 체크리스트10"
}


{
"select": [
"number1",
"number2",
"number3"
],
"reason": {
"number1": "reason1",
"number2": "reason2",
"number3": "reason3"
}
}
`,
};

// api 요청을 보내는 함수
async function sendRequestToClova(data) {
const headers = {
"X-NCP-CLOVASTUDIO-API-KEY": process.env.X_NCP_CLOVASTUDIO_API_KEY,
"X-NCP-APIGW-API-KEY": process.env.X_NCP_APIGW_API_KEY,
"X-NCP-CLOVASTUDIO-REQUEST-ID": process.env.X_NCP_CLOVASTUDIO_REQUEST_ID,
"Content-Type": "application/json",
Accept: "application/json",
};

const response = await axios.post(CLOVA_API_URL, data, { headers });
return response.data;
}

// 카테고리 기반으로 사용자 역할을 가진 메시지를 생성하는 함수
function getUserRole(categoryDto, checklistDto) {
return {
role: "user",
content: `
1. 대 중 소 카테고리 JSON
${JSON.stringify(categoryDto)}
2. 체크리스트 JSON
${JSON.stringify(checklistDto)}`,
};
}

// clova api를 통해 체크리스트를 평가를 요청하고 결과를 반환하는 함수
async function evaluateChecklistItem(categoryDto, checklistDto) {
const requestData = {
messages: [SYSTEM_ROLE, getUserRole(categoryDto, checklistDto)],
...AI_OPTIONS,
};

const result = await sendRequestToClova(requestData);
return result;
}

// clova api의 결과를 파싱하는 함수
async function aiResultParser(result) {
const content = result?.result?.message?.content;
const parsedContent = JSON.parse(content);
const { select, reason } = parsedContent;

return { select, reason };
}

// clova api의 결과가 유효한지 검사하는 함수
async function checkValidResult(select, reason, checklistDto) {
if (select.length !== 3) {
throw new Error("select 항목이 3개가 아닙니다.");
}
if (Object.keys(reason).length !== 3) {
throw new Error("reason 항목이 3개가 아닙니다.");
}
if (Object.keys(reason).some((item) => isNaN(item))) {
throw new Error("reason의 키 항목이 숫자가 아닙니다.");
}
if (select.some((item) => isNaN(item))) {
throw new Error("select 항목이 숫자가 아닙니다.");
}
const checklistDtoKeys = Object.keys(checklistDto);
if (!Object.keys(reason).every((key) => checklistDtoKeys.includes(key))) {
throw new Error("reason의 키에 해당하는 checklistDto의 키가 없습니다.");
}
}

// 지정된 시간만큼 대기하는 함수
function delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}

// 지정된 범위의 랜덤한 시간을 반환하는 함수
function getRandomDelay(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}

module.exports = {
processAiResult,
};
140 changes: 140 additions & 0 deletions ai/evaluate-server/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
const { PromisePool } = require("@supercharge/promise-pool");
const RedisPub = require("./RedisPub");
const redis = require("redis");
const {
getChecklistItemsByEvaluateCount,
incrementCounts,
insertReasons,
setFinalScore,
pool,
} = require("./postgres.js");
const { processAiResult } = require("./evaluate-api.js");

const subscriber = redis.createClient({
url: process.env.REDIS_URL,
});

const publisher = new RedisPub();

// 메인 함수
// processAiEvaluate 메시지를 받으면 ai_evaluate를 시작하는 함수
async function main() {
const redisSub = await subscriber.connect();
redisSub.subscribe("ai_generate", async function (data) {
try {
const { message, body } = parseJsonData(data);
if (message === "processAiEvaluate") {
publisher.send(
"ai_evaluate",
`received: ${message} ${body} processAiEvaluate start`
);
const evaluateCountMax = parseInt(body);
const checklistItemsByEvaluateCount =
await getChecklistItemsByEvaluateCount(evaluateCountMax);
const result = transformAndChunkItems(checklistItemsByEvaluateCount);
const evaluateCycle = result.length;
publisher.send("ai_evaluate", `expected count: ${evaluateCycle}`);

await processResultsConcurrency(result); // 작업이 완료될 때까지 기다림
console.log("모든 평가가 완료되었습니다.");
publisher.send("ai_evaluate", `모든 평가가 완료되었습니다.`);
await setFinalScore();
console.log("final_score가 업데이트되었습니다.");
publisher.send("ai_evaluate", `final_score가 업데이트되었습니다.`);
}
} catch (error) {
console.error("An error occurred:", error);
publisher.send("ai_evaluate", `An error occurred: ${error}`);
}
});
}

// JSON 데이터를 파싱하는 함수
function parseJsonData(data) {
try {
return JSON.parse(data);
} catch (error) {
return { message: "", body: "0" };
}
}

// 카테고리별로 아이템을 묶고, 10개씩 묶어서 반환하는 함수
function transformAndChunkItems(items, chunkSize = 10) {
const categoryMap = {};
items.forEach((item) => {
if (!categoryMap[item.category_id]) {
categoryMap[item.category_id] = {
category: {
maincategory: item.maincategory,
subcategory: item.subcategory,
minorcategory: item.minorcategory,
},
contents: [],
};
}
categoryMap[item.category_id].contents.push({
[item.item_id]: item.content,
});
});

const result = [];
Object.values(categoryMap).forEach((cat) => {
for (let i = 0; i < cat.contents.length; i += chunkSize) {
const chunk = cat.contents.slice(i, i + chunkSize);
result.push({
category: cat.category,
contents: Object.assign({}, ...chunk),
});
}
});

return result;
}

// ai evaluate를 동시에 여러개 처리하는 함수
async function processResultsConcurrency(result) {
let successCount = 0;
let failureCount = 0;
const proccessCycle = result.length;

const { results, errors } = await PromisePool.withConcurrency(10) // 동시에 처리할 작업 수를 10개로 설정
.for(result)
.process(async (item) => {
const { category, contents } = item;
const contentIDs = Object.keys(contents);
const { select, reason } = await processAiResult(category, contents);

if (select === undefined || reason === undefined) {
failureCount++;
console.log("Failure:", failureCount);
} else {
try {
await incrementCounts(contentIDs, "evaluated");
await incrementCounts(Object.keys(reason), "selected");
await insertReasons(reason);
successCount++;
console.log(`Success: ${successCount} / ${proccessCycle}`);
publisher.send(
"ai_evaluate",
`Success: ${successCount} / ${proccessCycle}`
);
} catch (error) {
failureCount++;
console.error("An error occurred:", error);
publisher.send("ai_evaluate_error", `An error occurred: ${error}`);
console.log(`Failure: ${failureCount} / ${proccessCycle}`);
publisher.send(
"ai_evaluate",
`Failure: ${failureCount} / ${proccessCycle}`
);
}
}
});

// 오류 로그
if (errors.length) {
console.log("Errors:", errors);
}
}

main();
Loading