Skip to content
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
3 changes: 3 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'
implementation 'org.springframework.boot:spring-boot-starter-batch'

// API Documentation (문서화)
implementation 'org.apache.commons:commons-lang3:3.18.0'
Expand Down Expand Up @@ -78,6 +79,8 @@ dependencies {
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
implementation 'org.springframework.ai:spring-ai-advisors-vector-store'
implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc'
implementation 'org.springframework.ai:spring-ai-starter-model-ollama'
implementation 'org.springframework.ai:spring-ai-starter-model-huggingface'

// Testing (테스트)
testImplementation 'org.springframework.boot:spring-boot-starter-test'
Expand Down
20 changes: 20 additions & 0 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ services:
timeout: 5s
retries: 10

ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
entrypoint: [ "/bin/sh", "-c" ]
command: >
"ollama serve &
sleep 5 &&
ollama pull daynice/kure-v1:567m &&
wait"
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:11434/api/version" ]
interval: 10s
timeout: 5s
retries: 10

volumes:
mysql-data:
redis-data:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static class TitleExtractionDto {
@AllArgsConstructor
@NoArgsConstructor
public static class KeywordExtractionDto {
private List<String> keyword;
private String keyword;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,13 @@ public class ChatBotService {
// 멤버 조회 -> 벡터 검색 (판례, 법령) -> 프롬프트 생성 (시스템, 유저) -> 채팅 클라이언트 호출 (스트림) -> 응답 저장, 제목/키워드 추출
public Flux<ChatResponse> sendMessage(Long memberId, ChatRequest chatChatRequestDto, Long roomId) {

if(memberId == null) {
log.error("해당 멤버는 존재하지 않거나, accessToken이 만료되거나 잘못되었습니다.");
}

Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")
);

// 벡터 검색 (판례, 법령)
List<Document> similarCaseDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "판례", 3);
List<Document> similarLawDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "법령", 2);
List<Document> similarCaseDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "판례");
List<Document> similarLawDocuments = qdrantService.searchDocument(chatChatRequestDto.getMessage(), "type", "법령");

// 판례와 법령 정보를 구분 있게 포맷팅
String caseContext = formatting(similarCaseDocuments);
Expand Down Expand Up @@ -167,18 +163,19 @@ private void handlerTasks(ChatRequest chatDto, History history, String fullRespo
private void extractAndUpdateKeywordRanks(String message) {
KeywordExtractionDto keywordResponse = keywordExtract(message, keywordExtraction, KeywordExtractionDto.class);

for (String keyword : keywordResponse.getKeyword()) {
KeywordRank keywordRank = keywordRankRepository.findByKeyword(keyword);
if (keywordRank == null) {
keywordRank = KeywordRank.builder()
.keyword(keyword)
.score(1L)
.build();
} else {
keywordRank.setScore(keywordRank.getScore() + 1);
}
keywordRankRepository.save(keywordRank);
KeywordRank keywordRank = keywordRankRepository.findByKeyword(keywordResponse.getKeyword());

if (keywordRank == null) {
keywordRank = KeywordRank.builder()
.keyword(keywordResponse.getKeyword())
.score(1L)
.build();
} else {
keywordRank.setScore(keywordRank.getScore() + 1);
}

keywordRankRepository.save(keywordRank);

}

private void setHistoryTitle(ChatRequest chatDto, History history, String fullResponse) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.ai.lawyer.global.batch;

/*@Slf4j
@Component
@EnableScheduling
@RequiredArgsConstructor
public class BatchScheduler {

private final JobLauncher jobLauncher;
private final Job dataVectorizationJob;

@Scheduled(cron = "#{${batch.scheduler.run-every-minute} ? '* * * * * *' : '* * 2 * * *'}")
public void runVectorizationJob() {
log.info("전체 데이터(판례, 법령) 벡터화 스케줄러 실행...");
try {
JobParameters jobParameters = new JobParametersBuilder()
.addString("requestDate", LocalDateTime.now().toString())
.toJobParameters();

jobLauncher.run(dataVectorizationJob, jobParameters); // Job 실행
} catch (Exception e) {
log.error("전체 데이터 벡터화 배치 작업 실행 중 오류 발생", e);
}
}
}*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package com.ai.lawyer.global.batch;

/*@Slf4j
@Configuration
@RequiredArgsConstructor
public class DataVectorizationJobConfig {

private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final EntityManagerFactory entityManagerFactory;
private final VectorStore vectorStore;

private final JangRepository jangRepository;
private final JoRepository joRepository;
private final HangRepository hangRepository;
private final HoRepository hoRepository;

private final TokenTextSplitter tokenSplitter = TokenTextSplitter.builder()
.withChunkSize(800)
.withMinChunkSizeChars(0)
.withMinChunkLengthToEmbed(5)
.withMaxNumChunks(10000)
.withKeepSeparator(true)
.build();

private static final int CHUNK_SIZE = 10; // 배치 처리 시 한 번에 읽어올 데이터 수

@Value("${batch.page.size.precedent}")
private int precedentPageSize; // 하루에 처리할 판례 수

@Value("${batch.page.size.law}")
private int lawPageSize; // 하루에 처리할 법령 수

@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-thread-");
executor.initialize();
return executor;
}

// -------------- 전체 데이터 벡터화 정의 --------------
@Bean
public Job dataVectorizationJob() {
return new JobBuilder("dataVectorizationJob", jobRepository)
.start(precedentVectorizationStep()) // 판례 벡터화 Step 실행
.next(lawVectorizationStep()) // 법령 벡터화 Step 실행
.build();
}

// -------------- 판례 벡터화 ---------------
@Bean
public Step precedentVectorizationStep() {
log.info(">>>>>> 판례 벡터화 시작");
return new StepBuilder("precedentVectorizationStep", jobRepository)
.<Precedent, List<Document>>chunk(CHUNK_SIZE, transactionManager)
.reader(precedentItemReader())
.processor(precedentItemProcessor())
.writer(documentItemWriter())
.taskExecutor(taskExecutor())
.build();
}

@Bean
public JpaPagingItemReader<Precedent> precedentItemReader() {
return new JpaPagingItemReaderBuilder<Precedent>()
.name("precedentItemReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(CHUNK_SIZE)
.maxItemCount(precedentPageSize)
.queryString("SELECT p FROM Precedent p ORDER BY p.id ASC")
.build();
}

@Bean
public ItemProcessor<Precedent, List<Document>> precedentItemProcessor() {

return precedent -> {
String content = precedent.getPrecedentContent();
if (content == null || content.isBlank()) return null;

Document originalDoc = new Document(content, Map.of(
"type", "판례",
"caseNumber", precedent.getCaseNumber(),
"court", precedent.getCourtName(),
"caseName", precedent.getCaseName()
));

List<Document> chunkDocs = tokenSplitter.split(originalDoc);
List<Document> finalChunks = new ArrayList<>();

// 청크별로 메타데이터에 인덱스 추가 -> 구분 용도
for (int i = 0; i < chunkDocs.size(); i++) {
Document chunk = chunkDocs.get(i);
Map<String, Object> newMetadata = new HashMap<>(chunk.getMetadata());
newMetadata.put("chunkIndex", i);
finalChunks.add(new Document(chunk.getText(), newMetadata));
}
return finalChunks;
};
}

// -------------- 법령 백터화 ---------------
@Bean
public Step lawVectorizationStep() {
log.info(">>>>>> 법령 벡터화 시작");
return new StepBuilder("lawVectorizationStep", jobRepository)
.<Law, List<Document>>chunk(CHUNK_SIZE, transactionManager) // 법령은 한 번에 10개씩 처리
.reader(lawItemReader())
.processor(lawItemProcessor())
.writer(documentItemWriter())
.taskExecutor(taskExecutor())
.build();
}

@Bean
public JpaPagingItemReader<Law> lawItemReader() {
return new JpaPagingItemReaderBuilder<Law>()
.name("lawItemReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(CHUNK_SIZE)
.maxItemCount(lawPageSize)
.queryString("SELECT l FROM Law l ORDER BY l.id ASC")
.build();
}

@Bean
public ItemProcessor<Law, List<Document>> lawItemProcessor() {
return law -> {
List<Document> finalChunks = new ArrayList<>();

List<Jang> jangs = jangRepository.findByLaw(law);

for (Jang jang : jangs) {

StringBuilder contentBuilder = new StringBuilder();

contentBuilder.append(law.getLawName()).append("\n");

if (jang.getContent() != null && !jang.getContent().isBlank()) {
contentBuilder.append(jang.getContent()).append("\n");
}

List<Jo> jos = joRepository.findByJang(jang);
for (Jo jo : jos) {

if (jo.getContent() != null && !jo.getContent().isBlank()) {
contentBuilder.append(jo.getContent()).append("\n");
}

List<Hang> hangs = hangRepository.findByJo(jo);
for (Hang hang : hangs) {
if (hang.getContent() != null && !hang.getContent().isBlank()) {
contentBuilder.append(hang.getContent()).append("\n");
}

List<Ho> hos = hoRepository.findByHang(hang);
for (Ho ho : hos) {
if (ho.getContent() != null && !ho.getContent().isBlank()) {
contentBuilder.append(ho.getContent()).append("\n");
}
}
}
}

// === Jang 단위로 문서화 ===
String finalContent = contentBuilder.toString();

if (!finalContent.isBlank()) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("type", "법령");
metadata.put("lawName", law.getLawName());
metadata.put("jangId", jang.getId());

Document originalDoc = new Document(finalContent, metadata);

List<Document> chunkDocs = tokenSplitter.split(originalDoc);

for (int i = 0; i < chunkDocs.size(); i++) {
Document chunk = chunkDocs.get(i);
Map<String, Object> newMetadata = new HashMap<>(chunk.getMetadata());
newMetadata.put("chunkIndex", i);
finalChunks.add(new Document(chunk.getText(), newMetadata));
}
}
}

return finalChunks.isEmpty() ? null : finalChunks;
};
}

@Bean
public ItemWriter<List<Document>> documentItemWriter() {
return chunk -> {
List<Document> totalDocuments = chunk.getItems().stream()
.flatMap(List::stream)
.collect(Collectors.toList());

if (!totalDocuments.isEmpty()) {
vectorStore.add(totalDocuments);
log.info(">>>>>> {}개의 Document 청크를 벡터 저장소에 저장했습니다.", totalDocuments.size());
}
};
}
}*/

16 changes: 10 additions & 6 deletions backend/src/main/java/com/ai/lawyer/global/config/AIConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,24 @@
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemoryRepository;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;

@Configuration
public class AIConfig {

@Bean
@Primary
public EmbeddingModel primaryOllamaEmbeddingModel(OllamaEmbeddingModel ollamaEmbeddingModel) {
return ollamaEmbeddingModel;
}

@Bean
public ChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate, PlatformTransactionManager transactionManager) {
return JdbcChatMemoryRepository.builder()
Expand All @@ -26,9 +34,5 @@ public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
return ChatClient.create(openAiChatModel);
}

@Bean
public TokenTextSplitter tokenTextSplitter() {
return new TokenTextSplitter(500, 150, 5, 10000, true);
}

}

Loading