Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

version 2.7.0 #193

Merged
merged 2 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
id 'io.spring.dependency-management' version '1.1.5'
id 'java'
id 'org.asciidoctor.jvm.convert' version "3.3.2"
id 'org.sonarqube' version '3.5.0.2730' // sonarqube gradle plugin 의존성
Expand All @@ -20,6 +20,8 @@ configurations {

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}

sonarqube {
Expand All @@ -30,6 +32,10 @@ sonarqube {
}
}

ext {
set('springAiVersion', "1.0.0-M1")
}

dependencies {
// Web
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -38,6 +44,10 @@ dependencies {
implementation 'org.springframework:spring-aspects'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

// AI
implementation "org.springframework.ai:spring-ai-openai-spring-boot-starter:${springAiVersion}"
implementation "org.springframework.ai:spring-ai-chroma-store-spring-boot-starter:${springAiVersion}"

// DB
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
Expand Down Expand Up @@ -93,9 +103,16 @@ dependencies {

// Test Container
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
testImplementation 'org.testcontainers:testcontainers:1.19.3'
testImplementation 'org.testcontainers:junit-jupiter:1.19.3'
testImplementation 'org.testcontainers:mariadb:1.19.3'
testImplementation 'org.testcontainers:testcontainers:1.19.8'
testImplementation 'org.testcontainers:junit-jupiter:1.19.8'
testImplementation 'org.testcontainers:mariadb:1.19.8'
testImplementation 'org.testcontainers:chromadb:1.19.8'
}

dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
}
}

// Swagger force conflict resolution
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
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;

@Tag(name = "AI-Query", description = "AI Assistant")
@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_HEADER_KEY)
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> askAIQuery(
@Parameter(description = "사용자 질문") @RequestParam("question") String question,
@RequestHeader(USER_TOKEN_HEADER_KEY) String id
) {
return ragQueryUseCase.askAiModel(question, id);
}
}
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
) {
}
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));
}
}
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 | 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("미", "리", " ", "준", "비", "된", " ",
"테", "스", "트", "질", "문", "이", " ", "아", "닙", "니", "다");
}
}
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 | local")
@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()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.kustacks.kuring.ai.adapter.out.persistence;

import com.kustacks.kuring.ai.application.port.out.CommandVectorStorePort;
import com.kustacks.kuring.ai.application.port.out.QueryVectorStorePort;
import com.kustacks.kuring.notice.domain.CategoryName;
import com.kustacks.kuring.worker.parser.notice.PageTextDto;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.ChromaVectorStore;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@Profile("prod | local")
@RequiredArgsConstructor
public class ChromaVectorStoreAdapter implements QueryVectorStorePort, CommandVectorStorePort {

private static final int TOP_K = 2;

private final ChromaVectorStore chromaVectorStore;

@Override
public List<String> findSimilarityContents(String question) {
return chromaVectorStore.similaritySearch(
SearchRequest.query(question).withTopK(TOP_K)
).stream()
.map(Document::getContent)
.toList();
}

@Override
public void embedding(List<PageTextDto> extractTextResults, CategoryName categoryName) {
TokenTextSplitter textSplitter = new TokenTextSplitter();

for (PageTextDto textResult : extractTextResults) {
if (textResult.text().isBlank()) continue;

List<Document> documents = createDocuments(categoryName, textResult);
List<Document> splitDocuments = textSplitter.apply(documents);
chromaVectorStore.accept(splitDocuments);
}
}

private List<Document> createDocuments(CategoryName categoryName, PageTextDto textResult) {
Resource resource = new ByteArrayResource(textResult.text().getBytes()) {
@Override
public String getFilename() {
return textResult.title();
}
};

TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("articleId", textResult.articleId());
textReader.getCustomMetadata().put("category", categoryName.getName());
return textReader.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.kustacks.kuring.ai.adapter.out.persistence;

import com.kustacks.kuring.ai.application.port.out.CommandVectorStorePort;
import com.kustacks.kuring.ai.application.port.out.QueryVectorStorePort;
import com.kustacks.kuring.notice.domain.CategoryName;
import com.kustacks.kuring.worker.parser.notice.PageTextDto;
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("dev | test")
@Component
@RequiredArgsConstructor
public class InMemoryVectorStoreAdapter implements QueryVectorStorePort, CommandVectorStorePort {

@Override
public List<String> findSimilarityContents(String question) {
HashMap<String, Object> metadata = createMetaData();

Document document = createDocument(metadata);

return Stream.of(document)
.map(Document::getContent)
.toList();
}

@Override
public void embedding(List<PageTextDto> extractTextResults, CategoryName categoryName) {
log.info("[InMemoryQueryVectorStoreAdapter] embedding {}", categoryName);
}

private Document createDocument(HashMap<String, Object> metadata) {
return new Document(
"a5a7414f-f676-409b-9f2e-1042f9846c97",
"""
● 등록금 전액 완납 또는 분할납부 1차분을 정해진 기간에 미납할 경우 분할납부 신청은 자동 취소되며,
미납 등록금은 이후 추가 등록기간에 전액 납부해야 함.\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;
}
}
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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.kustacks.kuring.ai.application.port.out;

import com.kustacks.kuring.notice.domain.CategoryName;
import com.kustacks.kuring.worker.parser.notice.PageTextDto;

import java.util.List;

public interface CommandVectorStorePort {
void embedding(List<PageTextDto> extractTextResults, CategoryName categoryName);
}
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);
}
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);
}
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);
}
Loading
Loading