From 2cc6512ad8b172031c9ade80c0302f663ff1d941 Mon Sep 17 00:00:00 2001 From: hjh Date: Fri, 10 Oct 2025 22:32:44 +0900 Subject: [PATCH] =?UTF-8?q?chat=20api=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ChatService.java | 4 +- .../service/ChatServiceImpl.java | 170 +++++++++++++----- 2 files changed, 131 insertions(+), 43 deletions(-) diff --git a/src/main/java/Helloworld/helloworld_webflux/service/ChatService.java b/src/main/java/Helloworld/helloworld_webflux/service/ChatService.java index 1c31880..1c1b770 100644 --- a/src/main/java/Helloworld/helloworld_webflux/service/ChatService.java +++ b/src/main/java/Helloworld/helloworld_webflux/service/ChatService.java @@ -18,7 +18,9 @@ public interface ChatService { Mono saveMessage(ChatMessageDTO message); Flux getRecentMessages(String roomId); Mono translateToKorean(String text); - Mono translateFromKorean(String text, String targetLanguage); +// Mono translateFromKorean(String text, String targetLanguage); + Flux translateFromKorean(String text, String targetLanguage); + Mono getChatbotResponse(JsonNode prompt); Mono saveTranslatedMessage(String roomId, String sender, String content); Flux getRecentTranslatedMessages(String roomId); diff --git a/src/main/java/Helloworld/helloworld_webflux/service/ChatServiceImpl.java b/src/main/java/Helloworld/helloworld_webflux/service/ChatServiceImpl.java index 9e4c6f9..171530e 100644 --- a/src/main/java/Helloworld/helloworld_webflux/service/ChatServiceImpl.java +++ b/src/main/java/Helloworld/helloworld_webflux/service/ChatServiceImpl.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; @@ -29,6 +30,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Service @@ -65,22 +67,49 @@ private Flux getRoomAndProcessMessages(String updatedRoomId, String ques ); } - private Flux processResponse(String roomId, String question, String koreanQuestion, String botResponse, String language) { - return translateFromKorean(botResponse, language) - .flatMapMany(userResponse -> - Flux.just( - Mono.defer(() -> saveTranslatedMessage(roomId, "user", koreanQuestion)), - Mono.defer(() -> saveTranslatedMessage(roomId, "bot", botResponse)), - Mono.defer(() -> saveMessage(new ChatMessageDTO(null, roomId, "user", question, LocalDateTime.now()))), - Mono.defer(() -> saveMessage(new ChatMessageDTO(null, roomId, "bot", userResponse, LocalDateTime.now()))) - ).concatMap(mono -> mono) - .thenMany(Flux.fromStream(userResponse.chars() - .mapToObj(c -> String.valueOf((char) c))) - .delayElements(Duration.ofMillis(25)) - .concatWith(Flux.just("Room ID: " + roomId)) - ) - ); - } +// private Flux processResponse(String roomId, String question, String koreanQuestion, String botResponse, String language) { +// return translateFromKorean(botResponse, language) +// .flatMapMany(userResponse -> +// Flux.just( +// Mono.defer(() -> saveTranslatedMessage(roomId, "user", koreanQuestion)), +// Mono.defer(() -> saveTranslatedMessage(roomId, "bot", botResponse)), +// Mono.defer(() -> saveMessage(new ChatMessageDTO(null, roomId, "user", question, LocalDateTime.now()))), +// Mono.defer(() -> saveMessage(new ChatMessageDTO(null, roomId, "bot", userResponse, LocalDateTime.now()))) +// ).concatMap(mono -> mono) +// .thenMany(Flux.fromStream(userResponse.chars() +// .mapToObj(c -> String.valueOf((char) c))) +// .delayElements(Duration.ofMillis(25)) +// .concatWith(Flux.just("Room ID: " + roomId)) +// ) +// ); +// } +private Flux processResponse(String roomId, String question, String koreanQuestion, String botResponse, String language) { + Flux.merge( + saveTranslatedMessage(roomId, "user", koreanQuestion).then(), + saveTranslatedMessage(roomId, "bot", botResponse).then(), + saveMessage(new ChatMessageDTO(null, roomId, "user", question, LocalDateTime.now())).then() + ).subscribe(); // ← 즉시 비동기 실행 + + StringBuilder translatedBuffer = new StringBuilder(); + + Flux translationStream = translateFromKorean(botResponse, language) + .doOnNext(chunk -> translatedBuffer.append(chunk)) + .doOnSubscribe(s -> System.out.println("streaming 번역 시작 -> ")) + .doOnError(e -> System.err.println("번역 stream 에러: " + e.getMessage())) + .doOnComplete(() -> { + String fullTranslated = translatedBuffer.toString(); + + saveMessage(new ChatMessageDTO(null, roomId, "bot", fullTranslated, LocalDateTime.now())) + .doOnSuccess(v -> System.out.println("번역 저장 성공")) + .doOnError(e -> System.err.println("번역 저장 실패 에러: " + e.getMessage())) + .subscribe(); + }); + + return translationStream.concatWith(Flux.just("\nRoom ID: " + roomId)); +} + + + @Override @@ -115,39 +144,96 @@ public Mono translateToKorean(String text) { .map(response -> response.getChoices().get(0).getMessage().getContent()); } - @Override - public Mono translateFromKorean(String text, String targetLanguage) { - GPTRequest.Message systemMessage = new GPTRequest.Message("system", "You are a translator."); - GPTRequest.Message userMessage = new GPTRequest.Message("user", - "Translate the following Korean text into " + targetLanguage + " accurately and naturally. " + - "Respond with only the translated sentence in " + targetLanguage + ". Do not include any other text, explanation, or labels." + - "Respond with **plain text only**:\n\n" + - "Text: " + text); - GPTRequest request = new GPTRequest("gpt-4o-mini", List.of(systemMessage, userMessage), 1000); +// @Override +// public Mono translateFromKorean(String text, String targetLanguage) { +// GPTRequest.Message systemMessage = new GPTRequest.Message("system", "You are a translator."); +// GPTRequest.Message userMessage = new GPTRequest.Message("user", +// "Translate the following Korean text into " + targetLanguage + " accurately and naturally. " + +// "Respond with only the translated sentence in " + targetLanguage + ". Do not include any other text, explanation, or labels." + +// "Respond with **plain text only**:\n\n" + +// "Text: " + text); +// GPTRequest request = new GPTRequest("gpt-4o-mini", List.of(systemMessage, userMessage), 1000); +// +// return webClient.post() +// .uri("https://api.openai.com/v1/chat/completions") +// .header("Authorization", "Bearer " + openaiApiKey) +// .header("Content-Type", "application/json") +// .bodyValue(request) +// .retrieve() +// .bodyToMono(GPTResponse.class) +// .map(response -> response.getChoices().get(0).getMessage().getContent()); +// } +@Override +public Flux translateFromKorean(String text, String targetLanguage) { + GPTRequest.Message systemMessage = new GPTRequest.Message("system", "You are a translator."); + GPTRequest.Message userMessage = new GPTRequest.Message("user", + "Translate the following Korean text into " + targetLanguage + " accurately and naturally. " + + "Respond with only the translated sentence in " + targetLanguage + ". Do not include any other text, explanation, or labels." + + "Respond with **plain text only**:\n\n" + + "Text: " + text); + + Map requestBody = Map.of( + "model", "gpt-4o-mini", + "messages", List.of( + Map.of("role", systemMessage.getRole(), "content", systemMessage.getContent()), + Map.of("role", userMessage.getRole(), "content", userMessage.getContent()) + ), + "stream", true + ); + + ObjectMapper mapper = new ObjectMapper(); + + return webClient.post() + .uri("https://api.openai.com/v1/chat/completions") + .header("Authorization", "Bearer " + openaiApiKey) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(requestBody) + .retrieve() + .bodyToFlux(org.springframework.core.io.buffer.DataBuffer.class) + .map(dataBuffer -> { + byte[] bytes = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(bytes); + DataBufferUtils.release(dataBuffer); // 안전하게 메모리 해제 + return new String(bytes); + }) + + .flatMap(line -> Flux.fromArray(line.split("\n"))) + .filter(line -> line.trim().startsWith("data: ")) + .map(line -> line.trim().replaceFirst("data: ", "")) + .doOnNext(line -> System.out.println("🔹 Stream line: " + line)) + .takeUntil(line -> line.equals("[DONE]")) + .filter(line -> !line.equals("[DONE]")) + .flatMap(line -> { + try { + JsonNode node = mapper.readTree(line); + JsonNode delta = node.at("/choices/0/delta/content"); + if (!delta.isMissingNode() && delta.isTextual()) { + return Mono.just(delta.asText()); + } + return Mono.empty(); + } catch (Exception e) { + return Mono.empty(); + } + }); +} + + - return webClient.post() - .uri("https://api.openai.com/v1/chat/completions") - .header("Authorization", "Bearer " + openaiApiKey) - .header("Content-Type", "application/json") - .bodyValue(request) - .retrieve() - .bodyToMono(GPTResponse.class) - .map(response -> response.getChoices().get(0).getMessage().getContent()); - } @Override public Mono getChatbotResponse(JsonNode prompt) { - // WebClient를 사용하여 JSON 요청을 보냅니다. - Mono web = webClient.post() - .uri("https://helloworld-func-app.azurewebsites.net/api/question") // Flask 서버 URI + return webClient.post() + .uri("https://helloworld-func-app.azurewebsites.net/api/question") .contentType(MediaType.APPLICATION_JSON) - .bodyValue(prompt) // JSON 형식으로 요청 본문 설정 - .retrieve() // 응답을 검색 - .bodyToMono(String.class); // 응답을 String으로 변환 - System.out.println("fuck"); - return web; + .bodyValue(prompt) + .retrieve() + .bodyToMono(String.class) + .doOnNext(resp -> System.out.println("AI 서버 응답: " + resp)) + .doOnError(e -> System.err.println("AI 서버 에러: " + e.getMessage())); } + @Override public Mono saveTranslatedMessage(String roomId, String sender, String content) { TranslateLog log = new TranslateLog();