From adb9bc41f616c07810bef9e554e6969a37668d74 Mon Sep 17 00:00:00 2001 From: Jiwoo Kim Date: Sun, 1 Sep 2024 19:49:48 +0900 Subject: [PATCH] version 2.10.0 --- .../adapter/in/web/AdminCommandApiV2.java | 15 ++++++-- .../out/event/AdminAiEventAdapter.java | 18 ++++++++++ .../port/in/AdminCommandUseCase.java | 6 ++++ .../application/port/out/AiEventPort.java | 8 +++++ .../service/AdminCommandService.java | 33 +++++++++++++++++ .../in/event/DataEmbeddingEventListener.java | 28 +++++++++++++++ .../in/event/dto/DataEmbeddingEvent.java | 11 ++++++ .../persistence/ChromaVectorStoreAdapter.java | 26 ++++++++++++++ .../InMemoryVectorStoreAdapter.java | 6 ++++ .../port/in/RAGCommandUseCase.java | 8 +++++ .../port/out/CommandVectorStorePort.java | 5 +++ .../service/RAGCommandService.java | 35 +++++++++++++++++++ .../port/in/dto/DataEmbeddingCommand.java | 8 +++++ .../common/dto/ResponseCodeAndMessages.java | 1 + 14 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/kustacks/kuring/admin/adapter/out/event/AdminAiEventAdapter.java create mode 100644 src/main/java/com/kustacks/kuring/admin/application/port/out/AiEventPort.java create mode 100644 src/main/java/com/kustacks/kuring/ai/adapter/in/event/DataEmbeddingEventListener.java create mode 100644 src/main/java/com/kustacks/kuring/ai/adapter/in/event/dto/DataEmbeddingEvent.java create mode 100644 src/main/java/com/kustacks/kuring/ai/application/port/in/RAGCommandUseCase.java create mode 100644 src/main/java/com/kustacks/kuring/ai/application/service/RAGCommandService.java create mode 100644 src/main/java/com/kustacks/kuring/alert/application/port/in/dto/DataEmbeddingCommand.java diff --git a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java index 8131e05f..a66453f5 100644 --- a/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java +++ b/src/main/java/com/kustacks/kuring/admin/adapter/in/web/AdminCommandApiV2.java @@ -8,6 +8,7 @@ import com.kustacks.kuring.admin.application.port.in.dto.RealNotificationCommand; import com.kustacks.kuring.admin.domain.AdminRole; import com.kustacks.kuring.alert.application.port.in.dto.AlertCreateCommand; +import com.kustacks.kuring.alert.application.port.in.dto.DataEmbeddingCommand; import com.kustacks.kuring.auth.authorization.AuthenticationPrincipal; import com.kustacks.kuring.auth.context.Authentication; import com.kustacks.kuring.auth.secured.Secured; @@ -24,9 +25,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; -import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.ADMIN_REAL_NOTICE_CREATE_SUCCESS; -import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.ADMIN_TEST_NOTICE_CREATE_SUCCESS; +import static com.kustacks.kuring.common.dto.ResponseCodeAndMessages.*; @Tag(name = "Admin-Command", description = "관리자가 주체가 되는 정보 수정") @Validated @@ -88,6 +89,16 @@ public void cancelAlert( adminCommandUseCase.cancelAlertSchedule(id); } + @Operation(summary = "파일 임베딩", description = "어드민이 원하는 파일을 임베딩 하여 쿠링봇에서 사용할 수 있다") + @SecurityRequirement(name = "JWT") + @Secured(AdminRole.ROLE_ROOT) + @PostMapping("/embedding") + public ResponseEntity> embeddingCustomData(@RequestParam(name = "file") MultipartFile file) { + adminCommandUseCase.embeddingCustomData(new DataEmbeddingCommand(file)); + + return ResponseEntity.ok().body(new BaseResponse<>(ADMIN_EMBEDDING_NOTICE_SUCCESS, null)); + } + @Hidden @Secured(AdminRole.ROLE_ROOT) @GetMapping("/subscribe/all") diff --git a/src/main/java/com/kustacks/kuring/admin/adapter/out/event/AdminAiEventAdapter.java b/src/main/java/com/kustacks/kuring/admin/adapter/out/event/AdminAiEventAdapter.java new file mode 100644 index 00000000..742e1f0f --- /dev/null +++ b/src/main/java/com/kustacks/kuring/admin/adapter/out/event/AdminAiEventAdapter.java @@ -0,0 +1,18 @@ +package com.kustacks.kuring.admin.adapter.out.event; + +import com.kustacks.kuring.admin.application.port.out.AiEventPort; +import com.kustacks.kuring.ai.adapter.in.event.dto.DataEmbeddingEvent; +import com.kustacks.kuring.common.domain.Events; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AdminAiEventAdapter implements AiEventPort { + + @Override + public void sendDataEmbeddingEvent(String originName, String extension, String contentType, Resource resource) { + Events.raise(new DataEmbeddingEvent(originName, extension, contentType, resource)); + } +} diff --git a/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java b/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java index 18055dbc..1c227d9b 100644 --- a/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java +++ b/src/main/java/com/kustacks/kuring/admin/application/port/in/AdminCommandUseCase.java @@ -3,13 +3,19 @@ import com.kustacks.kuring.admin.application.port.in.dto.RealNotificationCommand; import com.kustacks.kuring.admin.application.port.in.dto.TestNotificationCommand; import com.kustacks.kuring.alert.application.port.in.dto.AlertCreateCommand; +import com.kustacks.kuring.alert.application.port.in.dto.DataEmbeddingCommand; public interface AdminCommandUseCase { + void createTestNotice(TestNotificationCommand command); + void createRealNoticeForAllUser(RealNotificationCommand command); + void subscribeAllUserSameTopic(); void addAlertSchedule(AlertCreateCommand command); void cancelAlertSchedule(Long id); + + void embeddingCustomData(DataEmbeddingCommand command); } diff --git a/src/main/java/com/kustacks/kuring/admin/application/port/out/AiEventPort.java b/src/main/java/com/kustacks/kuring/admin/application/port/out/AiEventPort.java new file mode 100644 index 00000000..160c7621 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/admin/application/port/out/AiEventPort.java @@ -0,0 +1,8 @@ +package com.kustacks.kuring.admin.application.port.out; + +import org.springframework.core.io.Resource; + +public interface AiEventPort { + + void sendDataEmbeddingEvent(String originName, String extension, String contentType, Resource resource); +} diff --git a/src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java b/src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java index 148e791b..e4783257 100644 --- a/src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java +++ b/src/main/java/com/kustacks/kuring/admin/application/service/AdminCommandService.java @@ -7,17 +7,23 @@ import com.kustacks.kuring.admin.application.port.out.AdminAlertEventPort; import com.kustacks.kuring.admin.application.port.out.AdminEventPort; import com.kustacks.kuring.admin.application.port.out.AdminUserFeedbackPort; +import com.kustacks.kuring.admin.application.port.out.AiEventPort; import com.kustacks.kuring.admin.domain.Admin; import com.kustacks.kuring.alert.application.port.in.dto.AlertCreateCommand; +import com.kustacks.kuring.alert.application.port.in.dto.DataEmbeddingCommand; import com.kustacks.kuring.auth.userdetails.UserDetailsServicePort; import com.kustacks.kuring.common.annotation.UseCase; import com.kustacks.kuring.common.properties.ServerProperties; import com.kustacks.kuring.notice.domain.CategoryName; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.Resource; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; @@ -33,6 +39,7 @@ public class AdminCommandService implements AdminCommandUseCase { private final AdminUserFeedbackPort adminUserFeedbackPort; private final AdminAlertEventPort adminAlertEventPort; private final AdminEventPort adminEventPort; + private final AiEventPort aiEventPort; private final NoticeProperties noticeProperties; private final ServerProperties serverProperties; private final PasswordEncoder passwordEncoder; @@ -79,6 +86,27 @@ public void cancelAlertSchedule(Long id) { adminAlertEventPort.cancelAlertSchedule(id); } + @Override + public void embeddingCustomData(DataEmbeddingCommand command) { + try { + MultipartFile file = command.file(); + + String originalFilename = file.getOriginalFilename(); + String contentType = file.getContentType(); + Resource resource = new InputStreamResource(file.getInputStream()); + String extension = extractExtension(originalFilename); + + aiEventPort.sendDataEmbeddingEvent( + originalFilename, + extension, + contentType, + resource + ); + } catch (IOException e) { + log.error("file read error", e); + } + } + /** * TODO : 1회성 API - client v2 배포 후, 단 한번 모든 사용자를 공통 topic에 구독시킨 후 제거 예정 */ @@ -100,4 +128,9 @@ public void subscribeAllUserSameTopic() { private boolean isNotMatchPassword(final String commandPassword, final String adminPassword) { return !passwordEncoder.matches(commandPassword, adminPassword); } + + private String extractExtension(String originalFilename) { + int pos = originalFilename.lastIndexOf("."); + return originalFilename.substring(pos + 1); + } } diff --git a/src/main/java/com/kustacks/kuring/ai/adapter/in/event/DataEmbeddingEventListener.java b/src/main/java/com/kustacks/kuring/ai/adapter/in/event/DataEmbeddingEventListener.java new file mode 100644 index 00000000..223d18c3 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/ai/adapter/in/event/DataEmbeddingEventListener.java @@ -0,0 +1,28 @@ +package com.kustacks.kuring.ai.adapter.in.event; + +import com.kustacks.kuring.ai.adapter.in.event.dto.DataEmbeddingEvent; +import com.kustacks.kuring.ai.application.port.in.RAGCommandUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DataEmbeddingEventListener { + + private final RAGCommandUseCase ragCommandUseCase; + + @Async + @EventListener + public void dataEmbeddingEvent( + DataEmbeddingEvent event + ) { + ragCommandUseCase.dataEmbedding( + event.fileName(), + event.extension(), + event.contentType(), + event.resource() + ); + } +} diff --git a/src/main/java/com/kustacks/kuring/ai/adapter/in/event/dto/DataEmbeddingEvent.java b/src/main/java/com/kustacks/kuring/ai/adapter/in/event/dto/DataEmbeddingEvent.java new file mode 100644 index 00000000..6de94bf9 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/ai/adapter/in/event/dto/DataEmbeddingEvent.java @@ -0,0 +1,11 @@ +package com.kustacks.kuring.ai.adapter.in.event.dto; + +import org.springframework.core.io.Resource; + +public record DataEmbeddingEvent( + String fileName, + String extension, + String contentType, + Resource resource +) { +} diff --git a/src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/ChromaVectorStoreAdapter.java b/src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/ChromaVectorStoreAdapter.java index 233d772a..3dace5fe 100644 --- a/src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/ChromaVectorStoreAdapter.java +++ b/src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/ChromaVectorStoreAdapter.java @@ -15,6 +15,8 @@ import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; +import java.io.IOException; +import java.time.LocalDateTime; import java.util.List; @Component @@ -48,6 +50,15 @@ public void embedding(List extractTextResults, CategoryName categor } } + @Override + public void embeddingSingleTextFile(String originName, Resource resource) throws IOException { + TokenTextSplitter textSplitter = new TokenTextSplitter(); + + List documents = createDocument(originName, resource); + List splitDocuments = textSplitter.apply(documents); + chromaVectorStore.accept(splitDocuments); + } + private List createDocuments(CategoryName categoryName, PageTextDto textResult) { Resource resource = new ByteArrayResource(textResult.text().getBytes()) { @Override @@ -62,4 +73,19 @@ public String getFilename() { textReader.getCustomMetadata().put("category", categoryName.getName()); return textReader.get(); } + + private List createDocument(String originName, Resource resource) throws IOException { + Resource byteResource = new ByteArrayResource(resource.getContentAsByteArray()) { + @Override + public String getFilename() { + return originName; + } + }; + + TextReader textReader = new TextReader(byteResource); + textReader.getCustomMetadata().put("articleId", ""); + textReader.getCustomMetadata().put("date", LocalDateTime.now().toString()); + textReader.getCustomMetadata().put("category", originName); + return textReader.get(); + } } diff --git a/src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/InMemoryVectorStoreAdapter.java b/src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/InMemoryVectorStoreAdapter.java index c696c2ec..ef2e0646 100644 --- a/src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/InMemoryVectorStoreAdapter.java +++ b/src/main/java/com/kustacks/kuring/ai/adapter/out/persistence/InMemoryVectorStoreAdapter.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; import org.springframework.context.annotation.Profile; +import org.springframework.core.io.Resource; import org.springframework.stereotype.Component; import java.util.Collections; @@ -39,6 +40,11 @@ public void embedding(List extractTextResults, CategoryName categor log.info("[InMemoryQueryVectorStoreAdapter] embedding {}", categoryName); } + @Override + public void embeddingSingleTextFile(String originName, Resource resource) { + log.info("[InMemoryQueryVectorStoreAdapter] embeddingSingleTextFile {}", originName); + } + private Document createDocument(HashMap metadata) { return new Document( "a5a7414f-f676-409b-9f2e-1042f9846c97", diff --git a/src/main/java/com/kustacks/kuring/ai/application/port/in/RAGCommandUseCase.java b/src/main/java/com/kustacks/kuring/ai/application/port/in/RAGCommandUseCase.java new file mode 100644 index 00000000..fe63be72 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/ai/application/port/in/RAGCommandUseCase.java @@ -0,0 +1,8 @@ +package com.kustacks.kuring.ai.application.port.in; + +import org.springframework.core.io.Resource; + +public interface RAGCommandUseCase { + + void dataEmbedding(String originName, String extension, String contentType, Resource resource); +} diff --git a/src/main/java/com/kustacks/kuring/ai/application/port/out/CommandVectorStorePort.java b/src/main/java/com/kustacks/kuring/ai/application/port/out/CommandVectorStorePort.java index f4792878..29c036ed 100644 --- a/src/main/java/com/kustacks/kuring/ai/application/port/out/CommandVectorStorePort.java +++ b/src/main/java/com/kustacks/kuring/ai/application/port/out/CommandVectorStorePort.java @@ -2,9 +2,14 @@ import com.kustacks.kuring.notice.domain.CategoryName; import com.kustacks.kuring.worker.parser.notice.PageTextDto; +import org.springframework.core.io.Resource; +import java.io.IOException; import java.util.List; public interface CommandVectorStorePort { + void embedding(List extractTextResults, CategoryName categoryName); + + void embeddingSingleTextFile(String originName, Resource resource) throws IOException; } diff --git a/src/main/java/com/kustacks/kuring/ai/application/service/RAGCommandService.java b/src/main/java/com/kustacks/kuring/ai/application/service/RAGCommandService.java new file mode 100644 index 00000000..67e98f19 --- /dev/null +++ b/src/main/java/com/kustacks/kuring/ai/application/service/RAGCommandService.java @@ -0,0 +1,35 @@ +package com.kustacks.kuring.ai.application.service; + +import com.kustacks.kuring.ai.application.port.in.RAGCommandUseCase; +import com.kustacks.kuring.ai.application.port.out.CommandVectorStorePort; +import com.kustacks.kuring.common.annotation.UseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.Resource; + +import java.io.IOException; + +@Slf4j +@UseCase +@RequiredArgsConstructor +public class RAGCommandService implements RAGCommandUseCase { + + private final CommandVectorStorePort commandVectorStorePort; + private static final String EXTENSION_PDF = "pdf"; + private static final String EXTENSION_TXT = "txt"; + + @Override + public void dataEmbedding(String originName, String extension, String contentType, Resource resource) { + try { + if (extension.equals(EXTENSION_PDF)) { + // TODO: pdf embedding + } else if (extension.equals(EXTENSION_TXT)) { + commandVectorStorePort.embeddingSingleTextFile(originName, resource); + } else { + log.warn("not supported file type : {}", extension); + } + } catch (IOException e) { + log.warn("file embedding fail : {}", originName); + } + } +} diff --git a/src/main/java/com/kustacks/kuring/alert/application/port/in/dto/DataEmbeddingCommand.java b/src/main/java/com/kustacks/kuring/alert/application/port/in/dto/DataEmbeddingCommand.java new file mode 100644 index 00000000..2953f9ec --- /dev/null +++ b/src/main/java/com/kustacks/kuring/alert/application/port/in/dto/DataEmbeddingCommand.java @@ -0,0 +1,8 @@ +package com.kustacks.kuring.alert.application.port.in.dto; + +import org.springframework.web.multipart.MultipartFile; + +public record DataEmbeddingCommand( + MultipartFile file +) { +} diff --git a/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java b/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java index dbbc3094..7dd4ca89 100644 --- a/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java +++ b/src/main/java/com/kustacks/kuring/common/dto/ResponseCodeAndMessages.java @@ -23,6 +23,7 @@ public enum ResponseCodeAndMessages { /* Admin */ ADMIN_TEST_NOTICE_CREATE_SUCCESS(HttpStatus.OK.value(), "테스트 공지 생성에 성공하였습니다"), ADMIN_REAL_NOTICE_CREATE_SUCCESS(HttpStatus.OK.value(), "실제 공지 생성에 성공하였습니다"), + ADMIN_EMBEDDING_NOTICE_SUCCESS(HttpStatus.OK.value(), "데이터 임베딩에 생성에 성공하였습니다"), /* User */ USER_REGISTER_SUCCESS(HttpStatus.OK.value(), "회원가입에 성공하였습니다"),