Skip to content

Commit

Permalink
Merge pull request #48 from MARU-EGG/refactor/category-change
Browse files Browse the repository at this point in the history
[refactor] category 를 특정할 수 없는 경우 llm 서버에서 category 참고해서 저장
  • Loading branch information
Hoya324 authored Jul 23, 2024
2 parents 50d80ea + 7c6f41a commit 44720ab
Show file tree
Hide file tree
Showing 15 changed files with 103 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,17 @@ public Mono<LLMAnswerResponse> askQuestion(LLMAskQuestionRequest request) {
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
.retrieve()
.onStatus(
HttpStatusCode::isError,
response -> {
return switch (response.statusCode().value()) {
case 400 -> Mono.error(
new BadRequestWebClientException(
String.format(BAD_REQUEST_WEBCLIENT.getMessage(), "LLM 서버", request.questionType(),
request.questionCategory(), request.question())));
case 404 -> Mono.error(
new NotFoundWebClientException(String.format(NOT_FOUND_WEBCLIENT.getMessage(), "LLM 서버")));
default -> Mono.error(
new InternalServerErrorWebClientException(
String.format(INTERNAL_ERROR_WEBCLIENT.getMessage(), "LLM 서버")));
};
}
)
.onStatus(HttpStatusCode::isError, response -> {
return switch (response.statusCode().value()) {
case 400 -> Mono.error(new BadRequestWebClientException(
String.format(BAD_REQUEST_WEBCLIENT.getMessage(), "LLM 서버", request.questionType(),
request.questionCategory(), request.question())));
case 404 -> Mono.error(new NotFoundWebClientException(
String.format(NOT_FOUND_WEBCLIENT.getMessage(), "LLM 서버")));
default -> Mono.error(new InternalServerErrorWebClientException(
String.format(INTERNAL_ERROR_WEBCLIENT.getMessage(), "LLM 서버")));
};
})
.bodyToMono(LLMAnswerResponse.class);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@

@Builder
public record LLMAnswerResponse(
String questionType,
String questionCategory,
String answer
) {
public static LLMAnswerResponse from(Answer answer) {
public static LLMAnswerResponse of(String questionType, String questionCategory, Answer answer) {
return LLMAnswerResponse.builder()
.questionType(questionType)
.questionCategory(questionCategory)
.answer(answer.getContent())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public WebClient webClient(WebClient.Builder builder) {
.baseUrl(baseUrl)
.filter(logRequest())
.filter(logResponse())
.filter(logError())
.build();
}

Expand All @@ -42,4 +43,15 @@ private ExchangeFilterFunction logResponse() {
return Mono.just(clientResponse);
});
}

private ExchangeFilterFunction logError() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().isError()) {
clientResponse.bodyToMono(String.class).subscribe(body -> {
log.error("Response Error Body: {}", body);
});
}
return Mono.just(clientResponse);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public class QuestionController {
@ApiResponse(responseCode = "200", description = "질문 성공")
})
@CustomApiResponses({
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "EntityNotFoundException", status = 404, message = "type: SUSI, category: PAST_QUESTIONS, content: 수시 입학 요강에 대해 알려주세요.인 질문을 찾을 수 없습니다.", description = "질문 또는 답변을 찾지 못한 경우"),
@CustomApiResponse(error = "InternalServerError", status = 500, message = "내부 서버 오류가 발생했습니다.", description = "내부 서버 오류")
})
Expand All @@ -52,7 +52,7 @@ public QuestionResponse question(@Valid @RequestBody QuestionRequest request) {
@ApiResponse(responseCode = "200", description = "질문 성공")
})
@CustomApiResponses({
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "InternalServerError", status = 500, message = "내부 서버 오류가 발생했습니다.", description = "내부 서버 오류")
})
@GetMapping()
Expand All @@ -64,7 +64,7 @@ public List<QuestionListItemResponse> getQuestions(@Valid @ModelAttribute FindQu
@ApiResponse(responseCode = "200", description = "질문 자동완성 성공")
})
@CustomApiResponses({
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI, JAEOEGUGMIN]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "HttpMessageNotReadableException", status = 400, message = "Invalid input format: JSON parse error: Cannot deserialize value of type `mju.iphak.maru_egg.question.domain.QuestionType` from String \\\"SUSI 또는 PYEONIP 또는 JEONGSI\\\": not one of the values accepted for Enum class: [SUSI, PYEONIP, JEONGSI]", description = "validation에 맞지 않은 요청을 할 경우"),
@CustomApiResponse(error = "InternalServerError", status = 500, message = "내부 서버 오류가 발생했습니다.", description = "내부 서버 오류")
})
@GetMapping("/search")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
public class QuestionProcessingService {

private static final double STANDARD_SIMILARITY = 0.95;
private static final String UNCLASSIFIED = "";

private final QuestionRepository questionRepository;
private final AnswerService answerService;
Expand All @@ -60,22 +61,22 @@ public QuestionResponse question(final QuestionType type, final QuestionCategory

private QuestionResponse askNewQuestion(QuestionType type, QuestionCategory category, String content,
String contentToken) {
LLMAskQuestionRequest askQuestionRequest = LLMAskQuestionRequest.of(type.toString(),
LLMAskQuestionRequest askQuestionRequest = LLMAskQuestionRequest.of(type.getType(),
isCategoryNullThen(category),
content);

LLMAnswerResponse llmAnswerResponse = answerService.askQuestion(askQuestionRequest).block();

Question newQuestion = saveQuestion(type, category, content, contentToken);
Question newQuestion = saveQuestion(type,
QuestionCategory.convertToCategory(llmAnswerResponse.questionCategory()), content, contentToken);
Answer newAnswer = saveAnswer(newQuestion, llmAnswerResponse.answer());
return createQuestionResponse(newQuestion, newAnswer);
}

private static String isCategoryNullThen(final QuestionCategory category) {
if (category.toString() == null) {
return QuestionCategory.UNCLASSIFIED.getQuestionCategory();
if (category == null) {
return UNCLASSIFIED;
}
return category.toString();
return category.getCategory();
}

private Answer saveAnswer(final Question question, final String content) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,32 @@ public enum QuestionCategory {
ADMISSION_GUIDELINE("모집요강"),
PASSING_RESULT("입시결과"),
PAST_QUESTIONS("기출문제"),
UNIV_LIFE("대학생활"),
INTERVIEW_PRACTICAL_TEST("면접/실기"),
UNCLASSIFIED("미분류");
INTERVIEW_PRACTICAL_TEST("면접/실기");

private final String questionCategory;
private final String category;

@Override
public String toString() {
return this.questionCategory;
return this.category;
}

public static QuestionCategory convertToCategory(String category) {
if (category.equals(ADMISSION_GUIDELINE.getCategory())) {
return ADMISSION_GUIDELINE;
}

if (category.equals(PASSING_RESULT.getCategory())) {
return PASSING_RESULT;
}

if (category.equals(PAST_QUESTIONS.getCategory())) {
return PAST_QUESTIONS;
}

if (category.equals(INTERVIEW_PRACTICAL_TEST.getCategory())) {
return INTERVIEW_PRACTICAL_TEST;
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package mju.iphak.maru_egg.question.domain;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Schema(description = "질문 타입", enumAsRef = true)
@Getter
@RequiredArgsConstructor
public enum QuestionType {
SUSI("수시"),
JEONGSI("정시"),
PYEONIP("편입학"),
JAEOEGUGMIN("재외국민");
PYEONIP("편입학");

private final String questionType;
private final String type;

@Override
public String toString() {
return this.questionType;
return this.type;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public record FindQuestionsRequest(
QuestionType type,

@Schema(description = "질문 카테고리(모집요강, 입시결과, 기출 문제)", allowableValues = {"ADMISSION_GUIDELINE", "PASSING_RESULT",
"PAST_QUESTIONS", "UNCLASSIFIED", "UNIV_LIFE", "INTERVIEW_PRACTICAL_TEST"})
"PAST_QUESTIONS", "INTERVIEW_PRACTICAL_TEST"})
QuestionCategory category
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

@Schema(description = "질문 생성 요청 DTO", example = """
{
"type": "SUSI 또는 PYEONIP 또는 JEONGSI 또는 JAEOEGUGMIN",
"category": "PAST_QUESTIONS 또는 UNIV_LIFE 또는 INTERVIEW_PRACTICAL_TEST 또는 PASSING_RESULT 또는 ADMISSION_GUIDELINE 또는 UNCLASSIFIED",
"type": "SUSI 또는 PYEONIP 또는 JEONGSI",
"category": "PAST_QUESTIONS 또는 INTERVIEW_PRACTICAL_TEST 또는 PASSING_RESULT 또는 ADMISSION_GUIDELINE",
"content": "수시 입학 요강에 대해 알려주세요."
}
""")
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/banner.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@


░▒▓██████████████▓▒░ ░▒▓██████▓▒░ ░▒▓███████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓████████▓▒░ ░▒▓██████▓▒░ ░▒▓██████▓▒░
░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓████████▓▒░ ░▒▓███████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓█▓▒▒▓███▓▒░ ░▒▓█▓▒▒▓███▓▒░
░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░
░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░ ░▒▓██████▓▒░ ░▒▓████████▓▒░ ░▒▓██████▓▒░ ░▒▓██████▓▒░


[based on Spring Boot version ${spring-boot.version}]
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class AnswerServiceTest extends MockTest {

@Before
public void setUp() {
question = Question.of("수시 일정 알려주세요.", "수시 일정", QuestionType.JEONGSI,
question = Question.of("수시 일정 알려주세요.", "수시 일정", QuestionType.SUSI,
QuestionCategory.ADMISSION_GUIDELINE);
answer = Answer.of(question,
"수시 일정은 2024년 12월 19일(목)부터 2024년 12월 26일(목) 18:00까지 최초합격자 발표가 있고, 2025년 2월 10일(월) 10:00부터 2025년 2월 12일(수) 15:00까지 문서등록 및 등록금 납부가 진행됩니다. 등록금 납부 기간은 2024년 12월 16일(월) 10:00부터 2024년 12월 18일(수) 15:00까지이며, 방법은 입학처 홈페이지를 통한 문서등록 및 등록금 납부를 하시면 됩니다. 상세 안내는 추후 입학처 홈페이지를 통해 공지될 예정입니다.");
Expand All @@ -61,7 +61,9 @@ public void setUp() {

ClientResponse clientResponse = ClientResponse.create(HttpStatusCode.valueOf(200))
.header("Content-Type", "application/json")
.body("{\"answer\":\"" + answer.getContent() + "\"}")
.body(String.format("{\"questionType\":\"%s\",\"questionCategory\":\"%s\",\"answer\":\"%s\"}",
question.getQuestionType().getType(), question.getQuestionCategory().getCategory(),
answer.getContent()))
.build();

when(exchangeFunction.exchange(any())).thenReturn(Mono.just(clientResponse));
Expand Down Expand Up @@ -99,12 +101,13 @@ public void setUp() {
@Test
public void LLM_질문_요청() {
// given
LLMAskQuestionRequest request = LLMAskQuestionRequest.of(QuestionType.SUSI.toString(),
QuestionCategory.ADMISSION_GUIDELINE.toString(),
LLMAskQuestionRequest request = LLMAskQuestionRequest.of(QuestionType.SUSI.getType(),
QuestionCategory.ADMISSION_GUIDELINE.getCategory(),
question.getContent());

// when
LLMAnswerResponse expectedResponse = LLMAnswerResponse.from(answer);
LLMAnswerResponse expectedResponse = LLMAnswerResponse.of(QuestionType.SUSI.getType(),
QuestionCategory.ADMISSION_GUIDELINE.getCategory(), answer);
LLMAnswerResponse result = answerService.askQuestion(request).block();

// then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.springframework.beans.factory.annotation.Autowired;
Expand Down Expand Up @@ -50,8 +51,9 @@ void setUp() {
userRepository.save(user);
}

@DisplayName("회원가입 성공")
@Test
void testSignUp() throws Exception {
void 회원_성공() throws Exception {
// given
SignUpRequest signUpRequest = new SignUpRequest("[email protected]", "Password123!");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ private LLMAnswerResponse mockAskQuestion(LLMAskQuestionRequest request) {
formData.add("question", request.question());
Question testQuestion = Question.of("새로운 질문입니다.", "새로운 질문", QuestionType.SUSI,
QuestionCategory.ADMISSION_GUIDELINE);
LLMAnswerResponse expectedResponse = LLMAnswerResponse.from(Answer.of(testQuestion, "새로운 답변입니다."));
LLMAnswerResponse expectedResponse = LLMAnswerResponse.of(QuestionType.SUSI.getType(),
QuestionCategory.ADMISSION_GUIDELINE.getCategory(), Answer.of(testQuestion, "새로운 답변입니다."));

mockWebServer.enqueue(new MockResponse()
.setHeader("Content-type", MediaType.APPLICATION_JSON_VALUE)
Expand Down Expand Up @@ -205,7 +206,8 @@ void setUp() {
String contentToken = PhraseExtractionUtils.extractPhrases(content);
Question testQuestion = Question.of(content, contentToken, type, category);
Answer testAnswer = Answer.of(testQuestion, "새로운 답변입니다.");
LLMAnswerResponse expectedResponse = LLMAnswerResponse.from(testAnswer);
LLMAnswerResponse expectedResponse = LLMAnswerResponse.of(QuestionType.SUSI.getType(),
QuestionCategory.ADMISSION_GUIDELINE.getCategory(), testAnswer);

when(questionRepository.searchQuestionsByContentTokenAndTypeAndCategory(contentToken, type, category))
.thenReturn(Optional.of(Collections.emptyList()));
Expand All @@ -229,12 +231,13 @@ void setUp() {
public void MOCK_LLM_질문_요청() {
// given
startServer(new ReactorClientHttpConnector());
LLMAskQuestionRequest request = LLMAskQuestionRequest.of(QuestionType.SUSI.toString(),
QuestionCategory.ADMISSION_GUIDELINE.toString(),
LLMAskQuestionRequest request = LLMAskQuestionRequest.of(QuestionType.SUSI.getType(),
QuestionCategory.ADMISSION_GUIDELINE.getCategory(),
question.getContent());
Question testQuestion = Question.of("새로운 질문입니다.", "새로운 질문", QuestionType.SUSI,
QuestionCategory.ADMISSION_GUIDELINE);
LLMAnswerResponse expectedResponse = LLMAnswerResponse.from(Answer.of(testQuestion, "새로운 답변입니다."));
LLMAnswerResponse expectedResponse = LLMAnswerResponse.of(QuestionType.SUSI.getType(),
QuestionCategory.ADMISSION_GUIDELINE.getCategory(), Answer.of(testQuestion, "새로운 답변입니다."));

// when
LLMAnswerResponse result = mockAskQuestion(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ private LLMAnswerResponse mockAskQuestion(LLMAskQuestionRequest request) {
formData.add("question", request.question());
Question testQuestion = Question.of("새로운 질문입니다.", "새로운 질문", QuestionType.SUSI,
QuestionCategory.ADMISSION_GUIDELINE);
LLMAnswerResponse expectedResponse = LLMAnswerResponse.from(Answer.of(testQuestion, "새로운 답변입니다."));
LLMAnswerResponse expectedResponse = LLMAnswerResponse.of(QuestionType.SUSI.getType(),
QuestionCategory.ADMISSION_GUIDELINE.getCategory(), Answer.of(testQuestion, "새로운 답변입니다."));

mockWebServer.enqueue(new MockResponse()
.setHeader("Content-type", MediaType.APPLICATION_JSON_VALUE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public void setUp() throws Exception {
QuestionCategory.ADMISSION_GUIDELINE);
questionRepository.save(additionalQuestion3);
Question additionalQuestionUNCLASSIFIED = Question.of("추가4 테스트 질문 예시입니다.", "추가 테스트 질문 예시", QuestionType.SUSI,
QuestionCategory.UNCLASSIFIED);
null);
questionRepository.save(additionalQuestionUNCLASSIFIED);

answer = Answer.of(question, "테스트 답변입니다.");
Expand Down

0 comments on commit 44720ab

Please sign in to comment.