From 24b203cddf144fb6801ed0ab5d13fcd5ace542dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EC=A7=80=ED=98=B8?= Date: Mon, 10 Feb 2025 18:59:31 +0900 Subject: [PATCH] feat(be): create prompt result insertion API (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: create prompt table Co-authored-by: shl0501 Co-authored-by: 유영재 Co-authored-by: Choi Jeongmin * feat: create history insert API Co-authored-by: shl0501 Co-authored-by: 유영재 Co-authored-by: Choi Jeongmin * fix: remove ai test file --------- Co-authored-by: shl0501 Co-authored-by: 유영재 Co-authored-by: Choi Jeongmin --- .../20250210082219_prompt/migration.sql | 21 +++++++ .../migration.sql | 14 +++++ apps/server/prisma/schema.prisma | 17 +++++ apps/server/src/ai/ai.controller.ts | 10 +++ apps/server/src/ai/ai.module.ts | 3 +- apps/server/src/ai/ai.service.spec.ts | 19 ------ apps/server/src/ai/ai.service.ts | 18 ++++-- apps/server/src/ai/dto/create-history.dto.ts | 44 +++++++++++++ apps/server/src/ai/promt.constant.ts | 63 ------------------- .../src/ai/swagger/create-history.swagger.ts | 11 ++++ 10 files changed, 133 insertions(+), 87 deletions(-) create mode 100644 apps/server/prisma/migrations/20250210082219_prompt/migration.sql create mode 100644 apps/server/prisma/migrations/20250210083749_update_history_column_name/migration.sql delete mode 100644 apps/server/src/ai/ai.service.spec.ts create mode 100644 apps/server/src/ai/dto/create-history.dto.ts delete mode 100644 apps/server/src/ai/promt.constant.ts create mode 100644 apps/server/src/ai/swagger/create-history.swagger.ts diff --git a/apps/server/prisma/migrations/20250210082219_prompt/migration.sql b/apps/server/prisma/migrations/20250210082219_prompt/migration.sql new file mode 100644 index 0000000..6815882 --- /dev/null +++ b/apps/server/prisma/migrations/20250210082219_prompt/migration.sql @@ -0,0 +1,21 @@ +-- CreateTable +CREATE TABLE "Prompt" ( + "name" TEXT NOT NULL, + "content" TEXT NOT NULL, + + CONSTRAINT "Prompt_pkey" PRIMARY KEY ("name") +); + +-- CreateTable +CREATE TABLE "PromptHistory" ( + "history_id" SERIAL NOT NULL, + "before" TEXT NOT NULL, + "after" TEXT NOT NULL, + "prompt_name" TEXT NOT NULL, + "result" TEXT NOT NULL, + + CONSTRAINT "PromptHistory_pkey" PRIMARY KEY ("history_id") +); + +-- AddForeignKey +ALTER TABLE "PromptHistory" ADD CONSTRAINT "PromptHistory_prompt_name_fkey" FOREIGN KEY ("prompt_name") REFERENCES "Prompt"("name") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/server/prisma/migrations/20250210083749_update_history_column_name/migration.sql b/apps/server/prisma/migrations/20250210083749_update_history_column_name/migration.sql new file mode 100644 index 0000000..fff7c7a --- /dev/null +++ b/apps/server/prisma/migrations/20250210083749_update_history_column_name/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `after` on the `PromptHistory` table. All the data in the column will be lost. + - You are about to drop the column `before` on the `PromptHistory` table. All the data in the column will be lost. + - Added the required column `request` to the `PromptHistory` table without a default value. This is not possible if the table is not empty. + - Added the required column `response` to the `PromptHistory` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "PromptHistory" DROP COLUMN "after", +DROP COLUMN "before", +ADD COLUMN "request" TEXT NOT NULL, +ADD COLUMN "response" TEXT NOT NULL; diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index b74ad76..677092f 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -136,3 +136,20 @@ model Chatting { @@index([sessionId]) @@index([sessionId, chattingId(Sort.desc)]) } + +model Prompt { + name String @id + content String + + PromptHistory PromptHistory[] @relation("promptHistories") +} + +model PromptHistory { + historyId Int @id @default(autoincrement()) @map("history_id") + request String + response String + promptName String @map("prompt_name") + result String + + prompt Prompt @relation("promptHistories", fields: [promptName], references: [name]) +} \ No newline at end of file diff --git a/apps/server/src/ai/ai.controller.ts b/apps/server/src/ai/ai.controller.ts index c82d67e..81a7a04 100644 --- a/apps/server/src/ai/ai.controller.ts +++ b/apps/server/src/ai/ai.controller.ts @@ -2,12 +2,15 @@ import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { AiService } from './ai.service'; +import { CreateHistoryDto } from './dto/create-history.dto'; import { ImproveQuestionDto } from './dto/improve-question.dto'; import { ShortenQuestionDto } from './dto/shorten-question.dto'; +import { CreateHistorySwagger } from './swagger/create-history.swagger'; import { ImproveQuestionSwagger } from './swagger/improve-question.swagger'; import { ShortenQuestionSwagger } from './swagger/shorten-question.swagger'; import { SessionTokenValidationGuard } from '@common/guards/session-token-validation.guard'; + @Controller('ai') export class AiController { constructor(private readonly aiService: AiService) {} @@ -31,4 +34,11 @@ export class AiController { const result = { question: await this.aiService.requestShortenQuestion(userContent) }; return { result }; } + + @Post('history') + @CreateHistorySwagger() + @ApiBody({ type: CreateHistoryDto }) + public createHistory(@Body() createHistoryDto: CreateHistoryDto) { + this.aiService.createHistory(createHistoryDto); + } } diff --git a/apps/server/src/ai/ai.module.ts b/apps/server/src/ai/ai.module.ts index bc13d98..1c2e666 100644 --- a/apps/server/src/ai/ai.module.ts +++ b/apps/server/src/ai/ai.module.ts @@ -4,9 +4,10 @@ import { AiController } from './ai.controller'; import { AiService } from './ai.service'; import { SessionTokenModule } from '@common/guards/session-token.module'; +import { PrismaModule } from '@prisma-alias/prisma.module'; @Module({ - imports: [SessionTokenModule], + imports: [PrismaModule, SessionTokenModule], providers: [AiService], controllers: [AiController], }) diff --git a/apps/server/src/ai/ai.service.spec.ts b/apps/server/src/ai/ai.service.spec.ts deleted file mode 100644 index d0fdc00..0000000 --- a/apps/server/src/ai/ai.service.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -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 index 8cc2b42..3f483dc 100644 --- a/apps/server/src/ai/ai.service.ts +++ b/apps/server/src/ai/ai.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { prompt } from '@ai/promt.constant'; +import { CreateHistoryDto } from './dto/create-history.dto'; + +import { PrismaService } from '@prisma-alias/prisma.service'; interface ClovaApiResponse { status: { @@ -28,17 +30,25 @@ export class AiService { private readonly CLOVA_API_URL: string; private readonly API_KEY: string; - constructor() { + constructor(private readonly prisma: PrismaService) { 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); + const prompt = await this.prisma.prompt.findUnique({ where: { name: 'IMPROVE_QUESTION' } }); + return await this.requestAIResponse(userContent, prompt.content); } public async requestShortenQuestion(userContent: string) { - return await this.requestAIResponse(userContent, prompt.shortenQuestion); + const prompt = await this.prisma.prompt.findUnique({ where: { name: 'SHORTEN_QUESTION' } }); + return await this.requestAIResponse(userContent, prompt.content); + } + + public async createHistory({ request, response, promptName, result }: CreateHistoryDto) { + await this.prisma.promptHistory.create({ + data: { request, response, promptName, result }, + }); } private async requestAIResponse(userContent: string, prompt: string) { diff --git a/apps/server/src/ai/dto/create-history.dto.ts b/apps/server/src/ai/dto/create-history.dto.ts new file mode 100644 index 0000000..86a5c0f --- /dev/null +++ b/apps/server/src/ai/dto/create-history.dto.ts @@ -0,0 +1,44 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn, IsNotEmpty, IsString } from 'class-validator'; + +export class CreateHistoryDto { + @ApiProperty({ + example: 'IMPROVE_QUESTION', + description: '프롬프트 종류', + required: true, + }) + @IsIn(['IMPROVE_QUESTION', 'SHORTEN_QUESTION'], { + message: 'promptName은 IMPROVE_QUESTION 또는 SHORTEN_QUESTION이어야 합니다.', + }) + @IsNotEmpty() + promptName: string; + + @ApiProperty({ + example: '호날두 VS 메시', + description: 'AI 수정 전의 원본', + required: true, + }) + @IsString() + @IsNotEmpty() + request: string; + + @ApiProperty({ + example: '호날두와 메시 중 누가 더 뛰어난 축구선수인가요?', + description: 'AI 수정 후의 결과', + required: true, + }) + @IsString() + @IsNotEmpty() + response: string; + + @ApiProperty({ + example: 'ACCEPT', + description: '사용자 반응', + required: true, + }) + @IsIn(['ACCEPT', 'REJECT'], { + message: 'result는 ACCEPT 또는 REJECT이어야 합니다.', + }) + @IsNotEmpty() + result: string; +} diff --git a/apps/server/src/ai/promt.constant.ts b/apps/server/src/ai/promt.constant.ts deleted file mode 100644 index 7a6f7ac..0000000 --- a/apps/server/src/ai/promt.constant.ts +++ /dev/null @@ -1,63 +0,0 @@ -export const prompt = { - improveQuestion: `당신은 질문 개선 전문가입니다. 아래 사용자가 입력한 질문을 다음 기준에 따라 명확하고 완전하게 재작성해 주세요. - -[지침] -0. **마크다운 유지**: 원본 질문에 포함된 모든 링크, 이미지, 코드 블록 등 모든 마크다운 요소를 그대로 보존하세요. 수정 과정 중 형식, URL, 이미지 주소 등이 변경되거나 누락되지 않도록 주의하세요. -1. **출력 형식 준수**: 최종 결과로 **재작성된 질문 텍스트만** 출력하세요. 추가 설명, 의견, 분석 등은 포함하지 않고, 모든 마크다운 요소(링크, 이미지 등)는 그대로 유지하세요. -2. **명확성과 이해 용이성**: 원래 질문의 의도와 핵심을 정확히 파악한 후, 누구나 쉽게 이해할 수 있도록 문장을 명확하게 다듬으세요. -3. **완전한 정보 제공**: 원래 질문에 누락되었을 수 있는 중요한 내용이나 맥락을 보완하여, 어떠한 정보도 빠지지 않도록 하세요. -4. **사용자가 제공한 정보만 활용**: 사용자가 제공하지 않은 정보를 임의로 생성하지 마세요. 오직 사용자가 제공한 정보만을 토대로 질문을 재작성해주세요. - -[사용자 질문 입력] -최종적으로 재작성된 질문만 출력해 주세요. - ---- -아래는 입출력 예시입니다. - -[질문 예시1] -메시 vs 호날두 -![두 선수의 매서운 눈빛](https://private-user-images.githubusercontent.com/11) -- [메시](https://private-user-images.githubusercontent.com/112055561) -- [호날두](https://private-user-images.githubusercontent.com/112055562) - -[응답 예시1] -# 누가 더 위대한 축구선수인가: 메시 vs 호날두 -메시와 호날두 중에서 누가 더 위대한 선수일까요? -![두 선수의 매서운 눈빛](https://private-user-images.githubusercontent.com/11) -- [메시](https://private-user-images.githubusercontent.com/112055561) -- [호날두](https://private-user-images.githubusercontent.com/112055562) - -[질문 예시2] -결혼 몇 살에 할까요? - -[응답 예시2] -# 결혼 적령 시기 -- 결혼 적령 시기에 대해 설명해주세요.`, - - shortenQuestion: `당신은 질문 요약 전문가입니다. 아래의 기준에 따라 사용자가 입력한 질문을 분석하고, 짧게 요약해 주세요 -0. **마크다운 유지**: 원본 질문에 포함된 모든 링크, 이미지, 코드 블록 등 모든 마크다운 요소를 그대로 보존하세요. 수정 과정 중 형식, URL, 이미지 주소 등이 변경되거나 누락되지 않도록 주의하세요. -1. **글자 수 제한**: 원본 질문이 링크를 제외하고 500자 이하가 될 수 있도록 하세요. '![](링크 주소)'와 같은 형태는 500자에 포함되지 않으니 링크는 반드시 포함하세요. -2. 요약의 핵심: 질문의 주요 요소와 의도를 파악하여, 불필요한 부분은 제거하되 핵심 내용은 그대로 반영해 주세요. -3. 정보 누락 방지: 중요한 정보나 세부 사항이 빠지지 않도록 주의해 주세요. -4. 명확성과 이해 용이성: 요약된 질문이 누구에게나 명확하고 쉽게 이해될 수 있도록 작성해 주세요. -5. **질문 속 URL, 이미지 주소 유지**: 원본 질문에 포함된 모든 링크, 이미지, 코드 블록 등 모든 마크다운 요소를 그대로 보존하세요. 수정 과정 중 형식, URL, 이미지 주소 등이 변경되거나 누락되지 않도록 주의하세요. 링크 형식은 '![]()'으로 되어 있습니다. 반드시 포함하세요. -최종적으로 개선된, 짧고 명료한 질문을 출력해 주세요. ---- -아래는 입출력 예시입니다. - -[질문 예시1] -# 누가 더 위대한 축구선수인가: 메시 vs 호날두 -메시와 호날두 중에서 누가 더 위대한 선수일까요? -![두 선수의 매서운 눈빛](https://private-user-images.githubusercontent.com/11) -- [메시](https://private-user-images.githubusercontent.com/112055561) -- [호날두](https://private-user-images.githubusercontent.com/112055562) - -[응답 예시1] -# 메시 vs 호날두 -![두 선수의 매서운 눈빛](https://private-user-images.githubusercontent.com/11) -- [메시](https://private-user-images.githubusercontent.com/112055561) -- [호날두](https://private-user-images.githubusercontent.com/112055562) - - -`, -} as const; diff --git a/apps/server/src/ai/swagger/create-history.swagger.ts b/apps/server/src/ai/swagger/create-history.swagger.ts new file mode 100644 index 0000000..de1651d --- /dev/null +++ b/apps/server/src/ai/swagger/create-history.swagger.ts @@ -0,0 +1,11 @@ +import { applyDecorators } from '@nestjs/common'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; + +export const CreateHistorySwagger = () => + applyDecorators( + ApiOperation({ summary: 'AI 제안을 사용자가 accept/rejet했는지를 저장합니다.' }), + ApiResponse({ + status: 201, + description: 'history 저장 성공', + }), + );