diff --git a/apps/server/package.json b/apps/server/package.json index 6b691f5..602b137 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -105,6 +105,7 @@ "@chats/(.*)$": "/chats/$1", "@socket/(.*)$": "/socket/$1", "@logger/(.*)$": "/logger/$1", + "@ai/(.*)$": "/ai/$1", "@prisma-alias/(.*)$": "/prisma/$1", "@main/(.*)$": "/main/$1" } diff --git a/apps/server/src/ai/ai.controller.ts b/apps/server/src/ai/ai.controller.ts new file mode 100644 index 0000000..f735ddc --- /dev/null +++ b/apps/server/src/ai/ai.controller.ts @@ -0,0 +1,22 @@ +import { Body, Controller, Post, UseGuards } from '@nestjs/common'; +import { ApiBody } from '@nestjs/swagger'; + +import { AiService } from './ai.service'; +import { ImproveQuestionDto } from './dto/improve-question.dto'; +import { ImproveQuestionSwagger } from './swagger/improve-question.swagger'; + +import { SessionTokenValidationGuard } from '@common/guards/session-token-validation.guard'; +@Controller('ai') +export class AiController { + constructor(private readonly aiService: AiService) {} + + @Post('question-improve') + @ImproveQuestionSwagger() + @ApiBody({ type: ImproveQuestionDto }) + @UseGuards(SessionTokenValidationGuard) + public async improveQuestion(@Body() improveQuestionDto: ImproveQuestionDto) { + const { body: userContent } = improveQuestionDto; + const result = { question: await this.aiService.requestImproveQuestion(userContent) }; + return { result }; + } +} diff --git a/apps/server/src/ai/ai.module.ts b/apps/server/src/ai/ai.module.ts new file mode 100644 index 0000000..bc13d98 --- /dev/null +++ b/apps/server/src/ai/ai.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; + +import { AiController } from './ai.controller'; +import { AiService } from './ai.service'; + +import { SessionTokenModule } from '@common/guards/session-token.module'; + +@Module({ + imports: [SessionTokenModule], + providers: [AiService], + controllers: [AiController], +}) +export class AiModule {} diff --git a/apps/server/src/ai/ai.service.spec.ts b/apps/server/src/ai/ai.service.spec.ts new file mode 100644 index 0000000..d0fdc00 --- /dev/null +++ b/apps/server/src/ai/ai.service.spec.ts @@ -0,0 +1,19 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { AiService } from './ai.service'; + +describe('AiService', () => { + let service: AiService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AiService], + }).compile(); + + service = module.get(AiService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/apps/server/src/ai/ai.service.ts b/apps/server/src/ai/ai.service.ts new file mode 100644 index 0000000..917a908 --- /dev/null +++ b/apps/server/src/ai/ai.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from '@nestjs/common'; + +import { prompt } from '@ai/promt.constant'; + +interface ClovaApiResponse { + status: { + code: string; + message: string; + }; + result?: { + message: { + role: string; + content: string; + }; + stopReason: string; + inputLength: number; + outputLength: number; + aiFilter?: Array<{ + groupName: string; + name: string; + score: string; + }>; + }; +} + +@Injectable() +export class AiService { + private readonly CLOVA_API_URL: string; + private readonly API_KEY: string; + + constructor() { + this.CLOVA_API_URL = process.env.CLOVA_API_URL; + this.API_KEY = 'Bearer ' + process.env.CLOVA_API_KEY; + } + + public async requestImproveQuestion(userContent: string) { + return await this.requestAIResponse(userContent, prompt.improveQuestion); + } + + private async requestAIResponse(userContent: string, prompt: string) { + const headers = { + Authorization: this.API_KEY, + 'Content-Type': 'application/json', + }; + + const requestData = { + messages: [ + { + role: 'system', + content: prompt, + }, + { + role: 'user', + content: userContent, + }, + ], + topP: 0.8, + topK: 0, + maxTokens: 512, + temperature: 0.5, + repeatPenalty: 5.0, + stopBefore: [], + includeAiFilters: true, + seed: 0, + }; + + const response = await fetch(this.CLOVA_API_URL, { + method: 'POST', + headers: headers, + body: JSON.stringify(requestData), + }); + + const data: ClovaApiResponse = JSON.parse(await response.text()); + + return data.result.message.content; + } +} diff --git a/apps/server/src/ai/dto/improve-question.dto.ts b/apps/server/src/ai/dto/improve-question.dto.ts new file mode 100644 index 0000000..56c07bb --- /dev/null +++ b/apps/server/src/ai/dto/improve-question.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +import { BaseDto } from '@common/base.dto'; + +export class ImproveQuestionDto extends BaseDto { + @ApiProperty({ + example: '리마큐가 뭐임?', + description: '질문 본문 내용', + required: true, + }) + @IsString() + @IsNotEmpty({ message: '질문 본문은 필수입니다.' }) + body: string; +} diff --git a/apps/server/src/ai/promt.constant.ts b/apps/server/src/ai/promt.constant.ts new file mode 100644 index 0000000..eabe4a8 --- /dev/null +++ b/apps/server/src/ai/promt.constant.ts @@ -0,0 +1,11 @@ +export const prompt = { + improveQuestion: `당신은 질문 개선 전문가입니다. 아래 사용자가 입력한 질문을 다음 기준에 따라 재작성해 주세요. +[지침] +1. **명확성과 이해 용이성**: 원래 질문의 의도와 핵심을 파악한 후, 누구나 쉽게 이해할 수 있도록 문장을 명확하게 다듬으세요. +2. **내용의 풍부함**: 필요할 경우 추가적인 배경 정보, 예시, 세부 사항 등을 포함하여 질문의 내용을 풍부하게 확장하세요. +3. **완전한 정보 제공**: 원래 질문에 누락되었을 수 있는 중요한 내용이나 맥락을 보완하여, 어떠한 정보도 생략되지 않도록 하세요. +4. **출력 형식**: 반드시 **최종적으로 재작성된 질문 텍스트만** 출력하세요. 추가 설명, 의견, 분석 등은 절대로 포함하지 마세요. +아래에 사용자가 입력한 질문이 주어집니다: +[사용자 질문 입력] +최종적으로 재작성된 질문만 출력해 주세요.`, +} as const; diff --git a/apps/server/src/ai/swagger/improve-question.swagger.ts b/apps/server/src/ai/swagger/improve-question.swagger.ts new file mode 100644 index 0000000..29eff75 --- /dev/null +++ b/apps/server/src/ai/swagger/improve-question.swagger.ts @@ -0,0 +1,18 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; + +export const ImproveQuestionSwagger = () => + applyDecorators( + ApiOperation({ summary: '질문 개선' }), + ApiResponse({ + status: 201, + description: '질문 개선 성공', + schema: { + example: { + result: { + question: '리마큐(Remacu)란 무엇인가요?', + }, + }, + }, + }), + ); diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index ce9f214..d09e75e 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -2,14 +2,14 @@ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { APP_FILTER } from '@nestjs/core'; -import { LoggerModule } from './logger/logger.module'; - +import { AiModule } from '@ai/ai.module'; import { AuthModule } from '@auth/auth.module'; import { ChatsModule } from '@chats/chats.module'; import { GlobalExceptionFilter } from '@common/filters/global-exception.filter'; import { HttpExceptionFilter } from '@common/filters/http-exception.filter'; import { HttpLoggerMiddleware } from '@common/middlewares/http-logger.middleware'; import { RedisModule } from '@common/redis.module'; +import { LoggerModule } from '@logger/logger.module'; import { PrismaModule } from '@prisma-alias/prisma.module'; import { QuestionsModule } from '@questions/questions.module'; import { RepliesModule } from '@replies/replies.module'; @@ -37,6 +37,7 @@ import { UsersModule } from '@users/users.module'; SocketModule, ChatsModule, LoggerModule, + AiModule, ], controllers: [], providers: [ diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 3e90412..fff95c6 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -30,6 +30,7 @@ "@chats/*": ["src/chats/*"], "@socket/*": ["src/socket/*"], "@logger/*": ["src/logger/*"], + "@ai/*": ["src/ai/*"], "@prisma-alias/*": ["src/prisma/*"], "@main/*": ["src/main/*"] }