Skip to content

Commit

Permalink
feat(be): ai question enhancement (#58)
Browse files Browse the repository at this point in the history
* feat(be): add ai-question-enhancement

* docs: add question enhancement API swagger

Co-authored-by: shl0501 <[email protected]>
Co-authored-by: 유영재 <[email protected]>

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: yu-yj215 <[email protected]>
Co-authored-by: shl0501 <[email protected]>
Co-authored-by: 유영재 <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
5 people authored Feb 4, 2025
1 parent 5a9ed71 commit fa6b01d
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 2 deletions.
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"@chats/(.*)$": "<rootDir>/chats/$1",
"@socket/(.*)$": "<rootDir>/socket/$1",
"@logger/(.*)$": "<rootDir>/logger/$1",
"@ai/(.*)$": "<rootDir>/ai/$1",
"@prisma-alias/(.*)$": "<rootDir>/prisma/$1",
"@main/(.*)$": "<rootDir>/main/$1"
}
Expand Down
22 changes: 22 additions & 0 deletions apps/server/src/ai/ai.controller.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
}
13 changes: 13 additions & 0 deletions apps/server/src/ai/ai.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
19 changes: 19 additions & 0 deletions apps/server/src/ai/ai.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(AiService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
77 changes: 77 additions & 0 deletions apps/server/src/ai/ai.service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
15 changes: 15 additions & 0 deletions apps/server/src/ai/dto/improve-question.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
11 changes: 11 additions & 0 deletions apps/server/src/ai/promt.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const prompt = {
improveQuestion: `당신은 질문 개선 전문가입니다. 아래 사용자가 입력한 질문을 다음 기준에 따라 재작성해 주세요.
[지침]
1. **명확성과 이해 용이성**: 원래 질문의 의도와 핵심을 파악한 후, 누구나 쉽게 이해할 수 있도록 문장을 명확하게 다듬으세요.
2. **내용의 풍부함**: 필요할 경우 추가적인 배경 정보, 예시, 세부 사항 등을 포함하여 질문의 내용을 풍부하게 확장하세요.
3. **완전한 정보 제공**: 원래 질문에 누락되었을 수 있는 중요한 내용이나 맥락을 보완하여, 어떠한 정보도 생략되지 않도록 하세요.
4. **출력 형식**: 반드시 **최종적으로 재작성된 질문 텍스트만** 출력하세요. 추가 설명, 의견, 분석 등은 절대로 포함하지 마세요.
아래에 사용자가 입력한 질문이 주어집니다:
[사용자 질문 입력]
최종적으로 재작성된 질문만 출력해 주세요.`,
} as const;
18 changes: 18 additions & 0 deletions apps/server/src/ai/swagger/improve-question.swagger.ts
Original file line number Diff line number Diff line change
@@ -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)란 무엇인가요?',
},
},
},
}),
);
5 changes: 3 additions & 2 deletions apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -37,6 +37,7 @@ import { UsersModule } from '@users/users.module';
SocketModule,
ChatsModule,
LoggerModule,
AiModule,
],
controllers: [],
providers: [
Expand Down
1 change: 1 addition & 0 deletions apps/server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@chats/*": ["src/chats/*"],
"@socket/*": ["src/socket/*"],
"@logger/*": ["src/logger/*"],
"@ai/*": ["src/ai/*"],
"@prisma-alias/*": ["src/prisma/*"],
"@main/*": ["src/main/*"]
}
Expand Down

0 comments on commit fa6b01d

Please sign in to comment.