Skip to content

Commit

Permalink
Merge pull request #109 from KUSITMS-MOAMOA/refactor/#105
Browse files Browse the repository at this point in the history
  • Loading branch information
oosedus authored Nov 22, 2024
2 parents 050a823 + fc69bef commit baee5b8
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 73 deletions.
3 changes: 0 additions & 3 deletions src/main/java/corecord/dev/common/status/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ public enum ErrorStatus implements BaseErrorStatus {
FORBIDDEN(HttpStatus.FORBIDDEN, "E0403", "접근 권한이 없습니다."),
NOT_FOUND(HttpStatus.NOT_FOUND, "E0404", "요청한 자원을 찾을 수 없습니다."),
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "E0405", "허용되지 않은 메소드입니다."),
AI_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E0500_CHAT_AI_RESPONSE_ERROR", "AI 응답 생성 중 오류가 발생했습니다."),
AI_CLIENT_ERROR(HttpStatus.BAD_REQUEST, "E0400_AI_CLIENT_ERROR", "AI 클라이언트 요청 오류가 발생했습니다."),
AI_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E0500_CHAT_SERVER_ERROR", "AI 서버에 오류가 발생했습니다."),


/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package corecord.dev.domain.chat.application;

import corecord.dev.domain.chat.domain.entity.Chat;
import corecord.dev.domain.chat.domain.dto.response.ChatSummaryAiResponse;

import java.util.List;

public interface ChatAIService {
String generateChatResponse(List<Chat> chatHistory, String userContent);
ChatSummaryAiResponse generateChatSummaryResponse(List<Chat> chatHistory);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
package corecord.dev.domain.chat.application;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import corecord.dev.domain.chat.domain.converter.ChatConverter;
import corecord.dev.domain.chat.domain.dto.request.ChatRequest;
import corecord.dev.domain.chat.domain.dto.response.ChatResponse;
import corecord.dev.domain.chat.infra.clova.dto.response.ChatSummaryAiResponse;
import corecord.dev.domain.chat.domain.dto.response.ChatSummaryAiResponse;
import corecord.dev.domain.chat.domain.entity.Chat;
import corecord.dev.domain.chat.domain.entity.ChatRoom;
import corecord.dev.domain.chat.status.ChatErrorStatus;
import corecord.dev.domain.chat.exception.ChatException;
import corecord.dev.domain.chat.infra.clova.dto.request.ClovaRequest;
import corecord.dev.domain.chat.infra.clova.application.ClovaService;
import corecord.dev.domain.user.application.UserDbService;
import corecord.dev.domain.user.domain.entity.User;
import jakarta.transaction.Transactional;
Expand All @@ -27,7 +23,7 @@
public class ChatService {

private final ChatDbService chatDbService;
private final ClovaService clovaService;
private final ChatAIService chatAIService;
private final UserDbService userDbService;

/*
Expand Down Expand Up @@ -68,7 +64,8 @@ public ChatResponse.ChatsDto createChat(Long userId, Long chatRoomId, ChatReques
}

// AI 답변 생성
String aiAnswer = createChatAiAnswer(chatRoom, chatDto.getContent());
List<Chat> chatHistory = chatDbService.findChatsByChatRoom(chatRoom);
String aiAnswer = chatAIService.generateChatResponse(chatHistory, chatDto.getContent());
Chat aiChat = chatDbService.saveChat(0, aiAnswer, chatRoom);

return ChatConverter.toChatsDto(List.of(aiChat));
Expand Down Expand Up @@ -117,7 +114,7 @@ public ChatResponse.ChatSummaryDto getChatSummary(Long userId, Long chatRoomId)
validateChatList(chatList);

// 채팅 정보 요약 생성
ChatSummaryAiResponse response = generateChatSummary(chatList);
ChatSummaryAiResponse response = chatAIService.generateChatSummaryResponse(chatList);

validateResponse(response);

Expand Down Expand Up @@ -190,21 +187,6 @@ private static void validateChatList(List<Chat> chatList) {
}
}

private ChatSummaryAiResponse generateChatSummary(List<Chat> chatList) {
ClovaRequest clovaRequest = ClovaRequest.createChatSummaryRequest(chatList);
String response = clovaService.generateAiResponse(clovaRequest);
return parseChatSummaryResponse(response);
}

private ChatSummaryAiResponse parseChatSummaryResponse(String aiResponse) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.readValue(aiResponse, ChatSummaryAiResponse.class);
} catch (JsonProcessingException e) {
throw new ChatException(ChatErrorStatus.INVALID_CHAT_SUMMARY);
}
}

private void checkTmpChat(User user, ChatRoom chatRoom) {
if (user.getTmpChat() == null) {
return;
Expand All @@ -213,10 +195,4 @@ private void checkTmpChat(User user, ChatRoom chatRoom) {
user.deleteTmpChat();
}
}

private String createChatAiAnswer(ChatRoom chatRoom, String userInput) {
List<Chat> chatHistory = chatDbService.findChatsByChatRoom(chatRoom);
ClovaRequest clovaRequest = ClovaRequest.createChatRequest(chatHistory, userInput);
return clovaService.generateAiResponse(clovaRequest);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package corecord.dev.domain.chat.domain.converter;

import corecord.dev.domain.chat.infra.clova.dto.response.ChatSummaryAiResponse;
import corecord.dev.domain.chat.domain.dto.response.ChatSummaryAiResponse;
import corecord.dev.domain.chat.domain.dto.response.ChatResponse;
import corecord.dev.domain.chat.domain.entity.Chat;
import corecord.dev.domain.chat.domain.entity.ChatRoom;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package corecord.dev.domain.chat.infra.clova.dto.response;
package corecord.dev.domain.chat.domain.dto.response;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.*;

@Getter
@Setter
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatSummaryAiResponse {
@JsonProperty("title")
private String title;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package corecord.dev.domain.chat.infra.clova.application;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import corecord.dev.common.exception.GeneralException;
import corecord.dev.common.status.ErrorStatus;
import corecord.dev.domain.chat.application.ChatAIService;
import corecord.dev.domain.chat.domain.entity.Chat;
import corecord.dev.domain.chat.exception.ChatException;
import corecord.dev.domain.chat.infra.clova.dto.request.ClovaRequest;
import corecord.dev.domain.chat.domain.dto.response.ChatSummaryAiResponse;
import corecord.dev.domain.chat.status.ChatErrorStatus;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -13,10 +19,12 @@
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientException;

import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
public class ClovaService {
public class ClovaService implements ChatAIService {

private final ObjectMapper objectMapper = new ObjectMapper();
private final WebClient webClient = WebClient.create();
Expand All @@ -33,45 +41,74 @@ public class ClovaService {
@Value("${ncp.chat.request-id}")
private String chatRequestId;

public String generateAiResponse(ClovaRequest clovaRequest) {
@Override
public String generateChatResponse(List<Chat> chatHistory, String userInput) {
try {
String responseBody = webClient.post()
.uri(chatHost)
.header("X-NCP-CLOVASTUDIO-API-KEY", chatApiKey)
.header("X-NCP-APIGW-API-KEY", chatApiKeyPrimaryVal)
.header("X-NCP-CLOVASTUDIO-REQUEST-ID", chatRequestId)
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")
.bodyValue(clovaRequest)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, clientResponse -> {
log.error("클라이언트 오류 발생: 상태 코드 - {}", clientResponse.statusCode());
return clientResponse.bodyToMono(String.class)
.map(errorBody -> new GeneralException(ErrorStatus.AI_CLIENT_ERROR));
})
.onStatus(HttpStatusCode::is5xxServerError, clientResponse -> {
log.error("서버 오류 발생: 상태 코드 - {}", clientResponse.statusCode());
return clientResponse.bodyToMono(String.class)
.map(errorBody -> new GeneralException(ErrorStatus.AI_SERVER_ERROR));
})
.bodyToMono(String.class)
.block();
ClovaRequest clovaRequest = ClovaRequest.createChatRequest(chatHistory, userInput);
String responseBody = postWebClient(clovaRequest);

return parseContentFromResponse(responseBody);
} catch (WebClientException e) {
log.error("채팅 AI 응답 생성 실패", e);
throw new GeneralException(ErrorStatus.AI_RESPONSE_ERROR);
throw new ChatException(ChatErrorStatus.AI_RESPONSE_ERROR);
}
}

@Override
public ChatSummaryAiResponse generateChatSummaryResponse(List<Chat> chatHistory) {
try {
ClovaRequest clovaRequest = ClovaRequest.createChatSummaryRequest(chatHistory);
String responseBody = postWebClient(clovaRequest);
String aiResponse = parseContentFromResponse(responseBody);

return parseChatSummaryResponse(aiResponse);
} catch (WebClientException e) {
log.error("채팅 AI 응답 생성 실패", e);
throw new ChatException(ChatErrorStatus.AI_RESPONSE_ERROR);
}
}

private String postWebClient(ClovaRequest clovaRequest) {
return webClient.post()
.uri(chatHost)
.header("X-NCP-CLOVASTUDIO-API-KEY", chatApiKey)
.header("X-NCP-APIGW-API-KEY", chatApiKeyPrimaryVal)
.header("X-NCP-CLOVASTUDIO-REQUEST-ID", chatRequestId)
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")
.bodyValue(clovaRequest)
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, clientResponse -> {
log.error("클라이언트 오류 발생: 상태 코드 - {}", clientResponse.statusCode());
return clientResponse.bodyToMono(String.class)
.map(errorBody -> new ChatException(ChatErrorStatus.AI_CLIENT_ERROR));
})
.onStatus(HttpStatusCode::is5xxServerError, clientResponse -> {
log.error("서버 오류 발생: 상태 코드 - {}", clientResponse.statusCode());
return clientResponse.bodyToMono(String.class)
.map(errorBody -> new ChatException(ChatErrorStatus.AI_SERVER_ERROR));
})
.bodyToMono(String.class)
.block();
}

private String parseContentFromResponse(String responseBody) {
try {
JsonNode root = objectMapper.readTree(responseBody);
JsonNode messageContent = root.path("result").path("message").path("content");
return messageContent.asText();
} catch (Exception e) {
log.error("응답 파싱 실패", e);
throw new GeneralException(ErrorStatus.AI_RESPONSE_ERROR);
throw new ChatException(ChatErrorStatus.INVALID_CHAT_RESPONSE);
}
}

private ChatSummaryAiResponse parseChatSummaryResponse(String aiResponse) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.readValue(aiResponse, ChatSummaryAiResponse.class);
} catch (JsonProcessingException e) {
throw new ChatException(ChatErrorStatus.INVALID_CHAT_SUMMARY);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package corecord.dev.domain.chat.infra.openai.application;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import corecord.dev.common.util.ResourceLoader;
import corecord.dev.domain.chat.application.ChatAIService;
import corecord.dev.domain.chat.domain.dto.response.ChatSummaryAiResponse;
import corecord.dev.domain.chat.domain.entity.Chat;
import corecord.dev.domain.chat.exception.ChatException;
import corecord.dev.domain.chat.status.ChatErrorStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Primary
@Service
@RequiredArgsConstructor
public class OpenAiChatService implements ChatAIService {
private final OpenAiChatModel chatModel;
private static final String CHAT_SYSTEM_CONTENT = ResourceLoader.getResourceContent("chat-prompt.txt");
private static final String SUMMARY_SYSTEM_CONTENT = ResourceLoader.getResourceContent("chat-summary-prompt.txt");

@Override
public String generateChatResponse(List<Chat> chatHistory, String userContent) {
List<Map<String, String>> messages = new ArrayList<>();

// 시스템 메시지 추가
messages.add(Map.of(
"role", "system",
"content", CHAT_SYSTEM_CONTENT
));

// 기존 채팅 내역 추가
for (Chat chat : chatHistory) {
String role = chat.getAuthor() == 0 ? "assistant" : "user";
messages.add(Map.of("role", role, "content", chat.getContent()));
}

// 사용자 입력 추가
messages.add(Map.of("role", "user", "content", userContent));

return chatModel.call(String.valueOf(messages));
}

@Override
public ChatSummaryAiResponse generateChatSummaryResponse(List<Chat> chatHistory) {
List<Map<String, String>> messages = new ArrayList<>();

// 시스템 메시지 추가
messages.add(Map.of(
"role", "system",
"content", SUMMARY_SYSTEM_CONTENT
));

// 기존 채팅 내역 추가
for (Chat chat : chatHistory) {
String role = chat.getAuthor() == 0 ? "assistant" : "user";
messages.add(Map.of("role", role, "content", chat.getContent()));
}

String response = chatModel.call(String.valueOf(messages));

return parseChatSummaryResponse(response);
}

private ChatSummaryAiResponse parseChatSummaryResponse(String aiResponse) {
ObjectMapper objectMapper = new ObjectMapper();
try {
return objectMapper.readValue(aiResponse, ChatSummaryAiResponse.class);
} catch (JsonProcessingException e) {
throw new ChatException(ChatErrorStatus.INVALID_CHAT_SUMMARY);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ public enum ChatErrorStatus implements BaseErrorStatus {
OVERFLOW_SUMMARY_CONTENT(HttpStatus.BAD_REQUEST, "E0305_OVERFLOW_SUMMARY_CONTENT", "경험 요약 내용은 500자 이내여야 합니다."),
INVALID_CHAT_SUMMARY(HttpStatus.BAD_REQUEST, "E0305_INVALID_CHAT_SUMMARY", "채팅 경험 요약 파싱 중 오류가 발생했습니다."),
NO_RECORD(HttpStatus.BAD_REQUEST, "E0305_NO_RECORD", "경험 기록의 내용이 충분하지 않습니다."),
TMP_CHAT_EXIST(HttpStatus.BAD_REQUEST, "E0307_TMP_CHAT_EXIST", "임시 채팅이 이미 존재합니다."),;
TMP_CHAT_EXIST(HttpStatus.BAD_REQUEST, "E0307_TMP_CHAT_EXIST", "임시 채팅이 이미 존재합니다."),
INVALID_CHAT_RESPONSE(HttpStatus.BAD_REQUEST, "E0305_INVALID_CHAT_RESPONSE", "채팅 응답 파싱 중 오류가 발생했습니다."),
AI_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E0500_CHAT_AI_RESPONSE_ERROR", "AI 응답 생성 중 오류가 발생했습니다."),
AI_CLIENT_ERROR(HttpStatus.BAD_REQUEST, "E0400_AI_CLIENT_ERROR", "AI 클라이언트 요청 오류가 발생했습니다."),
AI_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "E0500_CHAT_SERVER_ERROR", "AI 서버에 오류가 발생했습니다."),;

private final HttpStatus httpStatus;
private final String code;
Expand Down
Loading

0 comments on commit baee5b8

Please sign in to comment.