-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Feat] Rag system service logic #189
* setting: Spring AI 1.0.0-M1 의존성 설정 완료 * test: Spring AI 질문 인수테스트 작성 * feat(Member): Member Entity question_count 필드 추가 * feat(ErrorCode): AI관련 error code 추가 * feat(RAGConfiguration): SimpleVectorDB 구현 * feat: RAG API 구현 * feat(RAGQueryAiModelAdapter): Flux를 사용한 응답 stream 구현 * test(AiAcceptanceTest): AI에게 질문하는 인수테스트 작성 * feat(InMemoryQueryAiModelAdapter): 테스트 용도의 InMemory AI model 응답 Fake 구현 * feat(AiAcceptanceTest): 가능한 요청 횟수를 넘긴 경우 TOO_MANY_REQUESTS를 반환받는다 * fix(RAGQueryApiV2): 질문 API의 produces를 text/event-stream 으로 변경 * fix(InMemoryQueryAiModelAdapter): InMemory model의 응답 수정 * feat(AiAcceptanceTest): 질문 한도수 초과시 예외가 발행하는 인수테스트 작성 * feat(RAGEventAdapter): RAG 시스템의 이벤트 포트 별도로 분리 기존에는 service 내부에서 그냥 Event 를 raise 하였는데, 이 방식 보다는 포트에 전달하고 Adapter 에서 raise 하도록 변경 * feat: dev profile ai 설정 추가 * feat(InMemoryQueryAiModelAdapter): 기본 응답 부분을 단건 문자로 분리 * feat(RAGQueryApiV2): API를 POST에서 GET으로 변경 * chore: 테스트 코드에 필요없는 예외 제거 * test(UserRepositoryTest): 사용자 질문 토큰감소 리포지토리 테스트 추가 * feat(User): Deprecated 된 Where 에너테이션을 SQLRestriction 으로 변경 * refactor(NoticeQueryRepositoryImpl): 중복 문자열 상수로 추출
- Loading branch information
1 parent
22fca60
commit 6e89cde
Showing
36 changed files
with
919 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
src/main/java/com/kustacks/kuring/ai/adapter/in/web/RAGQueryApiV2.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package com.kustacks.kuring.ai.adapter.in.web; | ||
|
||
import com.kustacks.kuring.ai.application.port.in.RAGQueryUseCase; | ||
import com.kustacks.kuring.common.annotation.RestWebAdapter; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.security.SecurityRequirement; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestHeader; | ||
import org.springframework.web.bind.annotation.RequestParam; | ||
import reactor.core.publisher.Flux; | ||
|
||
@RequiredArgsConstructor | ||
@RestWebAdapter(path = "/api/v2/ai/messages") | ||
public class RAGQueryApiV2 { | ||
|
||
private static final String USER_TOKEN_HEADER_KEY = "User-Token"; | ||
|
||
private final RAGQueryUseCase ragQueryUseCase; | ||
|
||
@Operation(summary = "사용자 AI에 질문요청", description = "사용자가 궁금한 학교 정보를 AI에게 질문합니다.") | ||
@SecurityRequirement(name = "User-Token") | ||
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) | ||
public Flux<String> askAIQuery( | ||
@RequestParam("question") String question, | ||
@RequestHeader(USER_TOKEN_HEADER_KEY) String id | ||
) { | ||
return ragQueryUseCase.askAiModel(question, id); | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
src/main/java/com/kustacks/kuring/ai/adapter/in/web/dto/UserQuestionRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.kustacks.kuring.ai.adapter.in.web.dto; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
import jakarta.validation.constraints.Size; | ||
|
||
public record UserQuestionRequest( | ||
@NotBlank @Size(min = 5, max = 256) String question | ||
) { | ||
} |
15 changes: 15 additions & 0 deletions
15
src/main/java/com/kustacks/kuring/ai/adapter/out/event/RAGEventAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.kustacks.kuring.ai.adapter.out.event; | ||
|
||
import com.kustacks.kuring.ai.application.port.out.RAGEventPort; | ||
import com.kustacks.kuring.common.domain.Events; | ||
import com.kustacks.kuring.user.adapter.in.event.dto.UserDecreaseQuestionCountEvent; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class RAGEventAdapter implements RAGEventPort { | ||
|
||
@Override | ||
public void userDecreaseQuestionCountEvent(String userId) { | ||
Events.raise(new UserDecreaseQuestionCountEvent(userId)); | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
src/main/java/com/kustacks/kuring/ai/adapter/out/model/InMemoryQueryAiModelAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package com.kustacks.kuring.ai.adapter.out.model; | ||
|
||
import com.kustacks.kuring.ai.application.port.out.QueryAiModelPort; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.ai.chat.prompt.Prompt; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.core.io.Resource; | ||
import org.springframework.stereotype.Component; | ||
import reactor.core.publisher.Flux; | ||
|
||
@Slf4j | ||
@Component | ||
@Profile("dev | local | test") | ||
@RequiredArgsConstructor | ||
public class InMemoryQueryAiModelAdapter implements QueryAiModelPort { | ||
|
||
@Value("classpath:/ai/docs/ku-uni-register.txt") | ||
private Resource kuUniRegisterInfo; | ||
|
||
@Override | ||
public Flux<String> call(Prompt prompt) { | ||
if (prompt.getContents().contains("교내,외 장학금 및 학자금 대출 관련 전화번호들을 안내를 해줘")) { | ||
return Flux.just("학", "생", "복", "지", "처", " ", "장", "학", "복", "지", "팀", "의", | ||
" ", "전", "화", "번", "호", "는", " ", "0", "2", "-", "4", "5", "0", "-", "3", "2", "1", | ||
"1", "~", "2", "이", "며", ",", " ", "건", "국", "사", "랑", "/", "장", "학", "사", "정", | ||
"관", "장", "학", "/", "기", "금", "장", "학", "과", " ", "관", "련", "된", " ", "문", "의", | ||
"는", " ", "0", "2", "-", "4", "5", "0", "-", "3", "9", "6", "7", "로", " ", "하", "시", | ||
"면", " ", "됩", "니", "다", "." | ||
); | ||
} | ||
|
||
return Flux.just("미", "리", " ", "준", "비", "된", " ", | ||
"테", "스", "트", "질", "문", "이", " ", "아", "닙", "니", "다"); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/com/kustacks/kuring/ai/adapter/out/model/QueryAiModelAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.kustacks.kuring.ai.adapter.out.model; | ||
|
||
import com.kustacks.kuring.ai.application.port.out.QueryAiModelPort; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.ai.chat.prompt.Prompt; | ||
import org.springframework.ai.openai.OpenAiChatModel; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.stereotype.Component; | ||
import reactor.core.publisher.Flux; | ||
|
||
@Slf4j | ||
@Component | ||
@Profile("prod") | ||
@RequiredArgsConstructor | ||
public class QueryAiModelAdapter implements QueryAiModelPort { | ||
|
||
private final OpenAiChatModel openAiChatModel; | ||
|
||
@Override | ||
public Flux<String> call(Prompt prompt) { | ||
return openAiChatModel.stream(prompt) | ||
.filter(chatResponse -> chatResponse.getResult().getOutput().getContent() != null) | ||
.flatMap(chatResponse -> Flux.just(chatResponse.getResult().getOutput().getContent())) | ||
.doOnError(throwable -> log.error("[RAGQueryAiModelAdapter] {}", throwable.getMessage())); | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
.../java/com/kustacks/kuring/ai/adapter/out/persistence/InMemoryQueryVectorStoreAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package com.kustacks.kuring.ai.adapter.out.persistence; | ||
|
||
import com.kustacks.kuring.ai.application.port.out.QueryVectorStorePort; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.ai.document.Document; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.stream.Stream; | ||
|
||
@Slf4j | ||
@Profile("local | dev | test") | ||
@Component | ||
@RequiredArgsConstructor | ||
public class InMemoryQueryVectorStoreAdapter implements QueryVectorStorePort { | ||
|
||
@Override | ||
public List<String> findSimilarityContents(String question) { | ||
HashMap<String, Object> metadata = createMetaData(); | ||
|
||
Document document = createDocument(metadata); | ||
|
||
return Stream.of(document) | ||
.map(Document::getContent) | ||
.toList(); | ||
} | ||
|
||
private Document createDocument(HashMap<String, Object> metadata) { | ||
return new Document( | ||
"a5a7414f-f676-409b-9f2e-1042f9846c97", | ||
"● 등록금 전액 완납 또는 분할납부 1차분을 정해진 기간에 미납할 경우 분할납부 신청은 자동 취소되며, 미납 등록금은 이후\n" + | ||
"추가 등록기간에 전액 납부해야 함.\n", | ||
metadata); | ||
} | ||
|
||
private HashMap<String, Object> createMetaData() { | ||
HashMap<String, Object> metadata = new HashMap<>(); | ||
metadata.put("charset", "UTF-8"); | ||
metadata.put("filename", "ku-uni-register.txt"); | ||
metadata.put("source", "ku-uni-register.txt"); | ||
return metadata; | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/QueryVectorStoreAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.kustacks.kuring.ai.adapter.out.persistence; | ||
|
||
import com.kustacks.kuring.ai.application.port.out.QueryVectorStorePort; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
@Component | ||
@Profile("prod") | ||
@RequiredArgsConstructor | ||
public class QueryVectorStoreAdapter implements QueryVectorStorePort { | ||
|
||
@Override | ||
public List<String> findSimilarityContents(String question) { | ||
return Collections.emptyList(); | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/kustacks/kuring/ai/application/port/in/RAGQueryUseCase.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.kustacks.kuring.ai.application.port.in; | ||
|
||
import reactor.core.publisher.Flux; | ||
|
||
public interface RAGQueryUseCase { | ||
Flux<String> askAiModel(String question, String id); | ||
} |
8 changes: 8 additions & 0 deletions
8
src/main/java/com/kustacks/kuring/ai/application/port/out/QueryAiModelPort.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.kustacks.kuring.ai.application.port.out; | ||
|
||
import org.springframework.ai.chat.prompt.Prompt; | ||
import reactor.core.publisher.Flux; | ||
|
||
public interface QueryAiModelPort { | ||
Flux<String> call(Prompt prompt); | ||
} |
7 changes: 7 additions & 0 deletions
7
src/main/java/com/kustacks/kuring/ai/application/port/out/QueryVectorStorePort.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.kustacks.kuring.ai.application.port.out; | ||
|
||
import java.util.List; | ||
|
||
public interface QueryVectorStorePort { | ||
List<String> findSimilarityContents(String question); | ||
} |
5 changes: 5 additions & 0 deletions
5
src/main/java/com/kustacks/kuring/ai/application/port/out/RAGEventPort.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package com.kustacks.kuring.ai.application.port.out; | ||
|
||
public interface RAGEventPort { | ||
void userDecreaseQuestionCountEvent(String userId); | ||
} |
62 changes: 62 additions & 0 deletions
62
src/main/java/com/kustacks/kuring/ai/application/service/RAGQueryService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package com.kustacks.kuring.ai.application.service; | ||
|
||
import com.kustacks.kuring.ai.application.port.in.RAGQueryUseCase; | ||
import com.kustacks.kuring.ai.application.port.out.QueryAiModelPort; | ||
import com.kustacks.kuring.ai.application.port.out.QueryVectorStorePort; | ||
import com.kustacks.kuring.ai.application.port.out.RAGEventPort; | ||
import com.kustacks.kuring.common.annotation.UseCase; | ||
import com.kustacks.kuring.common.exception.InvalidStateException; | ||
import com.kustacks.kuring.common.exception.code.ErrorCode; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.ai.chat.prompt.Prompt; | ||
import org.springframework.ai.chat.prompt.PromptTemplate; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.core.io.Resource; | ||
import reactor.core.publisher.Flux; | ||
|
||
import javax.annotation.PostConstruct; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
@UseCase | ||
@RequiredArgsConstructor | ||
public class RAGQueryService implements RAGQueryUseCase { | ||
|
||
private final QueryVectorStorePort vectorStorePort; | ||
private final QueryAiModelPort ragChatModel; | ||
private final RAGEventPort ragEventPort; | ||
|
||
@Value("classpath:/ai/prompts/rag-prompt-template.st") | ||
private Resource ragPromptTemplate; | ||
private PromptTemplate promptTemplate; | ||
|
||
@Override | ||
public Flux<String> askAiModel(String question, String id) { | ||
Prompt completePrompt = buildCompletePrompt(question); | ||
ragEventPort.userDecreaseQuestionCountEvent(id); | ||
return ragChatModel.call(completePrompt); | ||
} | ||
|
||
@PostConstruct | ||
private void init() { | ||
this.promptTemplate = new PromptTemplate(ragPromptTemplate); | ||
} | ||
|
||
private Prompt buildCompletePrompt(String question) { | ||
List<String> similarDocuments = vectorStorePort.findSimilarityContents(question); | ||
if(similarDocuments.isEmpty()) { | ||
throw new InvalidStateException(ErrorCode.AI_SIMILAR_DOCUMENTS_NOT_FOUND); | ||
} | ||
|
||
Map<String, Object> promptParameters = createQuestions(question, similarDocuments); | ||
return promptTemplate.create(promptParameters); | ||
} | ||
|
||
private Map<String, Object> createQuestions(String question, List<String> contentList) { | ||
Map<String, Object> promptParameters = new HashMap<>(); | ||
promptParameters.put("input", question); | ||
promptParameters.put("documents", String.join("In", contentList)); | ||
return promptParameters; | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/com/kustacks/kuring/common/exception/InvalidStateException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.kustacks.kuring.common.exception; | ||
|
||
import com.kustacks.kuring.common.exception.code.ErrorCode; | ||
|
||
public class InvalidStateException extends BusinessException { | ||
|
||
public InvalidStateException(ErrorCode errorCode) { | ||
super(errorCode); | ||
} | ||
public InvalidStateException(ErrorCode errorCode, Exception e) { | ||
super(errorCode, e); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.