Skip to content

Commit f04faaf

Browse files
committed
feat[chat]: ai채팅 비동기 처리
1 parent 73e258a commit f04faaf

File tree

7 files changed

+195
-173
lines changed

7 files changed

+195
-173
lines changed

backend/src/main/java/com/ai/lawyer/BackendApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
66
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
7+
import org.springframework.scheduling.annotation.EnableAsync;
78

9+
@EnableAsync
810
@SpringBootApplication
911
@EnableJpaAuditing
1012
@ConfigurationPropertiesScan

backend/src/main/java/com/ai/lawyer/domain/chatbot/controller/HistoryController.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.ai.lawyer.domain.chatbot.dto.ChatDto.ChatHistoryDto;
44
import com.ai.lawyer.domain.chatbot.dto.HistoryDto;
5-
import com.ai.lawyer.domain.chatbot.service.ChatService;
65
import com.ai.lawyer.domain.chatbot.service.HistoryService;
76
import io.swagger.v3.oas.annotations.Operation;
87
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -21,7 +20,6 @@
2120
public class HistoryController {
2221

2322
private final HistoryService historyService;
24-
private final ChatService chatService;
2523

2624
@Operation(summary = "채팅방 제목 목록 조회")
2725
@GetMapping("/")
@@ -32,7 +30,7 @@ public ResponseEntity<List<HistoryDto>> getHistoryTitles(@AuthenticationPrincipa
3230
@Operation(summary = "채팅 조회")
3331
@GetMapping("/{historyId}")
3432
public ResponseEntity<List<ChatHistoryDto>> getChatHistory(@AuthenticationPrincipal Long memberId, @PathVariable("historyId") Long roomId) {
35-
return chatService.getChatHistory(memberId, roomId);
33+
return historyService.getChatHistory(memberId, roomId);
3634
}
3735

3836
@Operation(summary = "채팅방 삭제")

backend/src/main/java/com/ai/lawyer/domain/chatbot/dto/ChatDto.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ public static class ChatResponse {
3434
@Schema(description = "채팅방 ID", example = "1")
3535
private Long roomId;
3636

37-
@Schema(description = "History 방 제목", example = "손해배상 청구 관련 문의")
38-
private String title;
39-
4037
@Schema(description = "AI 챗봇의 응답 메시지", example = "네, 관련 법령과 판례를 바탕으로 답변해 드리겠습니다.")
4138
private String message;
4239

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.ai.lawyer.domain.chatbot.service;
2+
3+
import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.KeywordExtractionDto;
4+
import com.ai.lawyer.domain.chatbot.dto.ExtractionDto.TitleExtractionDto;
5+
import com.ai.lawyer.domain.chatbot.entity.*;
6+
import com.ai.lawyer.domain.chatbot.repository.*;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.ai.chat.memory.ChatMemory;
10+
import org.springframework.ai.chat.memory.ChatMemoryRepository;
11+
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
12+
import org.springframework.ai.chat.messages.AssistantMessage;
13+
import org.springframework.ai.chat.messages.MessageType;
14+
import org.springframework.ai.document.Document;
15+
import org.springframework.beans.factory.annotation.Value;
16+
import org.springframework.scheduling.annotation.Async;
17+
import org.springframework.stereotype.Service;
18+
import org.springframework.transaction.annotation.Transactional;
19+
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
23+
@Slf4j
24+
@Service
25+
@RequiredArgsConstructor
26+
public class AsyncPostChatProcessingService {
27+
28+
private final KeywordService keywordService;
29+
private final HistoryRepository historyRepository;
30+
private final ChatRepository chatRepository;
31+
private final KeywordRankRepository keywordRankRepository;
32+
private final ChatMemoryRepository chatMemoryRepository;
33+
private final ChatPrecedentRepository chatPrecedentRepository;
34+
private final ChatLawRepository chatLawRepository;
35+
36+
@Value("${custom.ai.title-extraction}")
37+
private String titleExtraction;
38+
@Value("{$custom.ai.keyword-extraction}")
39+
private String keywordExtraction;
40+
41+
@Async
42+
@Transactional
43+
public void processHandlerTasks(Long historyId, String userMessage, String fullResponse, List<Document> similarCaseDocuments, List<Document> similarLawDocuments) {
44+
try {
45+
History history = historyRepository.findById(historyId)
46+
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채팅방입니다. historyId: " + historyId));
47+
48+
// 1. 메시지 기억 저장
49+
ChatMemory chatMemory = MessageWindowChatMemory.builder()
50+
.maxMessages(10)
51+
.chatMemoryRepository(chatMemoryRepository)
52+
.build();
53+
54+
chatMemory.add(String.valueOf(history.getHistoryId()), new AssistantMessage(fullResponse));
55+
chatMemoryRepository.saveAll(String.valueOf(history.getHistoryId()), chatMemory.get(String.valueOf(history.getHistoryId())));
56+
57+
// 2. 채팅방 제목 설정 / 및 필터
58+
setHistoryTitle(userMessage, history, fullResponse);
59+
60+
// 3. 채팅 기록 저장
61+
saveChatWithDocuments(history, MessageType.USER, userMessage, similarCaseDocuments, similarLawDocuments);
62+
saveChatWithDocuments(history, MessageType.ASSISTANT, fullResponse, similarCaseDocuments, similarLawDocuments);
63+
64+
// 4. 키워드 추출 및 랭킹 업데이트
65+
if (!fullResponse.contains("해당 질문은 법률")) {
66+
extractAndUpdateKeywordRanks(userMessage);
67+
}
68+
} catch (Exception e) {
69+
log.error("에러 발생: {}", historyId, e);
70+
}
71+
}
72+
73+
private void setHistoryTitle(String userMessage, History history, String fullResponse) {
74+
String targetText = fullResponse.contains("해당 질문은 법률") ? userMessage : fullResponse;
75+
TitleExtractionDto titleDto = keywordService.keywordExtract(targetText, titleExtraction, TitleExtractionDto.class);
76+
history.setTitle(titleDto.getTitle());
77+
historyRepository.save(history); // @Transactional 어노테이션으로 인해 메소드 종료 시 자동 저장되지만, 명시적으로 호출할 수도 있습니다.
78+
}
79+
80+
private void extractAndUpdateKeywordRanks(String message) {
81+
KeywordExtractionDto keywordResponse = keywordService.keywordExtract(message, keywordExtraction, KeywordExtractionDto.class);
82+
if (keywordResponse == null || keywordResponse.getKeyword() == null) {
83+
return;
84+
}
85+
86+
KeywordRank keywordRank = keywordRankRepository.findByKeyword(keywordResponse.getKeyword());
87+
88+
if (keywordRank == null) {
89+
keywordRank = KeywordRank.builder()
90+
.keyword(keywordResponse.getKeyword())
91+
.score(1L)
92+
.build();
93+
} else {
94+
keywordRank.setScore(keywordRank.getScore() + 1);
95+
}
96+
keywordRankRepository.save(keywordRank);
97+
}
98+
99+
private void saveChatWithDocuments(History history, MessageType type, String message, List<Document> similarCaseDocuments, List<Document> similarLawDocuments) {
100+
Chat chat = chatRepository.save(Chat.builder()
101+
.historyId(history)
102+
.type(type)
103+
.message(message)
104+
.build());
105+
106+
// Ai 메시지가 저장될 때 관련 문서 저장
107+
if (type == MessageType.ASSISTANT) {
108+
if (similarCaseDocuments != null && !similarCaseDocuments.isEmpty()) {
109+
List<ChatPrecedent> chatPrecedents = similarCaseDocuments.stream()
110+
.map(doc -> ChatPrecedent.builder()
111+
.chatId(chat)
112+
.precedentContent(doc.getText())
113+
.caseNumber(doc.getMetadata().get("caseNumber").toString())
114+
.caseName(doc.getMetadata().get("caseName").toString())
115+
.build())
116+
.collect(Collectors.toList());
117+
chatPrecedentRepository.saveAll(chatPrecedents);
118+
}
119+
120+
if (similarLawDocuments != null && !similarLawDocuments.isEmpty()) {
121+
List<ChatLaw> chatLaws = similarLawDocuments.stream()
122+
.map(doc -> ChatLaw.builder()
123+
.chatId(chat)
124+
.content(doc.getText())
125+
.lawName(doc.getMetadata().get("lawName").toString())
126+
.build())
127+
.collect(Collectors.toList());
128+
chatLawRepository.saveAll(chatLaws);
129+
}
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)