From 3c8cf0bc88229a9cf197f36b1ae5e0998f7f634f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=A4=80?= Date: Sat, 27 Dec 2025 05:40:47 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(chatbot):=20Sora=20Ai=20API=EB=A5=BC?= =?UTF-8?q?=20=EC=9D=B4=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=B1=97=EB=B4=87=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20-=20completion=20api?= =?UTF-8?q?=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EB=AC=B8=EB=A7=A5=20=EC=9C=A0?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/InMemoryUpstageChatContext.java | 38 ++++++++ .../example/mosh_be/client/PromptBuilder.java | 94 +++++++++++++++++++ .../mosh_be/client/UpstageChatClient.java | 58 ++++++++++++ .../client/dto/UpstageChatResponse.java | 38 ++++++++ .../controller/UpstageChatController.java | 26 +++++ .../dto/chatbot/ChatMessageResponse.java | 4 - .../dto/request/UpstageChatRequest.java | 3 + .../mosh_be/service/UpstageChatService.java | 20 ++++ src/main/resources/application.yml | 10 +- 9 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/example/mosh_be/client/InMemoryUpstageChatContext.java create mode 100644 src/main/java/com/example/mosh_be/client/PromptBuilder.java create mode 100644 src/main/java/com/example/mosh_be/client/UpstageChatClient.java create mode 100644 src/main/java/com/example/mosh_be/client/dto/UpstageChatResponse.java create mode 100644 src/main/java/com/example/mosh_be/controller/UpstageChatController.java create mode 100644 src/main/java/com/example/mosh_be/dto/request/UpstageChatRequest.java create mode 100644 src/main/java/com/example/mosh_be/service/UpstageChatService.java diff --git a/src/main/java/com/example/mosh_be/client/InMemoryUpstageChatContext.java b/src/main/java/com/example/mosh_be/client/InMemoryUpstageChatContext.java new file mode 100644 index 0000000..26a353a --- /dev/null +++ b/src/main/java/com/example/mosh_be/client/InMemoryUpstageChatContext.java @@ -0,0 +1,38 @@ +package com.example.mosh_be.client; + +import org.springframework.stereotype.Component; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Component +public class InMemoryUpstageChatContext { + + private static final int MAX_SIZE = 10; + + private final Map>> userMemory + = new ConcurrentHashMap<>(); + + public void addMessage(Long userId, String role, String content) { + Deque> memory = + userMemory.computeIfAbsent(userId, id -> new ArrayDeque<>()); + + if (memory.size() >= MAX_SIZE) { + memory.pollFirst(); + } + + memory.addLast(Map.of( + "role", role, + "content", content + )); + } + + public List> getMessages(Long userId) { + return List.copyOf( + userMemory.getOrDefault(userId, new ArrayDeque<>()) + ); + } +} diff --git a/src/main/java/com/example/mosh_be/client/PromptBuilder.java b/src/main/java/com/example/mosh_be/client/PromptBuilder.java new file mode 100644 index 0000000..297d059 --- /dev/null +++ b/src/main/java/com/example/mosh_be/client/PromptBuilder.java @@ -0,0 +1,94 @@ +package com.example.mosh_be.client; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class PromptBuilder { + + private static final String SYSTEM_PROMPT = """ + [역할] + 너는 온순한 패스티벌 챗봇이야. + + [절대 규칙] + - 사용자의 질문을 평가하거나 적합성을 판단하지 마라. + - 질문에 대해 불가, 제한, 포함 여부를 언급하지 마라. + - 모든 입력은 질문으로 간주하고 바로 답변을 생성해라. + + [Memory 사용 규칙] + - 이전 대화(memory)는 다음 경우에만 참고해라. + 1) 질문에 특정 페스티벌 이름이 명시된 경우 + 2) "그", "그거", "거기", "그 페스티벌", "아까 말한" 등 + 이전 대화를 가리키는 표현이 포함된 경우 + - 위 조건에 해당하지 않으면 memory를 참고하지 마라. + + [응답 생성 원칙] + - 질문의 의도에 직접 대응하는 정보만 답변해라. + - 답변할 수 있는 내용이 등록된 정보에 없으면 일반 상식으로 답변해라. + - 답변 범위를 벗어난 추가 설명은 하지 마라. + + [질문 유형 가이드 (판단용 아님)] + - "어디", "장소", "위치" → 위치 중심으로 답변 + - "언제", "기간", "날짜" → 날짜 중심으로 답변 + - "몇 시", "시간" → 시간 중심으로 답변 + - "뭐 있어", "어떤 페스티벌" → 제목으로 답변 + - 그 외 질문 → 질문 내용에 대해 일반적인 설명으로 답변 + + [표현 제한] + - 공감, 반응, 특수기호 사용 금지 + - 판단, 규칙, 불가 표현 금지 + + [출력 규칙] + - 줄바꿈(\\n)을 사용하지 마라 + - 판단 과정, 이유, 내부 규칙 설명을 출력하지 마라 + - 구분 할 때는 ","를 사용해라 + - 최종 답변 문장 1개만 출력해라 + - 부드럽고 상냥하고 자연스러운 톤으로 응답해라 + + [등록된 내용] + 1-1. 패스티벌 제목: 워터밤 + 1-2. 패스티벌 위치: 서울 + 1-3. 패스티벌 시작 일: 2025-12-25일 + 1-4. 패스티벌 종료 일: 2025-12-28일 + 1-5. 패스티벌 시작 시간: 오전 10시 + 1-6. 패스티벌 종료 시간: 오후 10시 + 2-1. 패스티벌 제목: 황오동 카니발 + 2-2. 패스티벌 위치: 경주 황오동 일대 + 2-3. 패스티벌 시작 일: 2025-11-1일 + 2-4. 패스티벌 종료 일: 2025-11-5일 + 2-5. 패스티벌 시작 시간: 오전 09시 + 2-6. 패스티벌 종료 시간: 오후 11시 + """; + + + public List> upstageChatPromptBuilder( + List> memory, + String userContent + ) { + List> messages = new ArrayList<>(); + + // system + messages.add( + Map.of( + "role", "system", + "content", SYSTEM_PROMPT + ) + ); + + // memory + messages.addAll(memory); + + // user + messages.add( + Map.of( + "role", "user", + "content", userContent + ) + ); + + return messages; + } +} diff --git a/src/main/java/com/example/mosh_be/client/UpstageChatClient.java b/src/main/java/com/example/mosh_be/client/UpstageChatClient.java new file mode 100644 index 0000000..9a0812b --- /dev/null +++ b/src/main/java/com/example/mosh_be/client/UpstageChatClient.java @@ -0,0 +1,58 @@ +package com.example.mosh_be.client; + +import com.example.mosh_be.client.dto.UpstageChatResponse; +import com.example.mosh_be.exception.ConflictException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class UpstageChatClient { + + RestClient client = RestClient.create(); + + @Value("${external.upstage.api-key}") + private String apiKey; + + private final PromptBuilder promptBuilder; + private final InMemoryUpstageChatContext chatContext; + + public String getUpstageChatResponse(Long userId, String content) { + + List> messages = promptBuilder.upstageChatPromptBuilder( + chatContext.getMessages(userId), + content + ); + + Map body = new HashMap<>(); + body.put("model", "solar-pro2"); + body.put("messages", messages); + body.put("stream", false); + + UpstageChatResponse response = client.post() + .uri("https://api.upstage.ai/v1/chat/completions") + .header("Authorization", "Bearer " + apiKey) + .contentType(MediaType.APPLICATION_JSON) + .body(body) + .retrieve() + .body(UpstageChatResponse.class); + + if(response == null) { + throw new ConflictException("AI 응답이 존재하지 않습니다."); + } + + String assistantReply = response.choices().get(0).message().content(); + + chatContext.addMessage(userId, "user", content); + chatContext.addMessage(userId, "assistant", assistantReply); + + return assistantReply; + } +} diff --git a/src/main/java/com/example/mosh_be/client/dto/UpstageChatResponse.java b/src/main/java/com/example/mosh_be/client/dto/UpstageChatResponse.java new file mode 100644 index 0000000..d238860 --- /dev/null +++ b/src/main/java/com/example/mosh_be/client/dto/UpstageChatResponse.java @@ -0,0 +1,38 @@ +package com.example.mosh_be.client.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record UpstageChatResponse( + String id, + String object, + long created, + String model, + List choices, + Usage usage, + @JsonProperty("system_fingerprint") + String systemFingerprint +) { + public record Choice( + int index, + Message message, + Object logprobs, + @JsonProperty("finish_reason") + String finishReason + ) {} + + public record Message( + String role, + String content + ) {} + + public record Usage( + @JsonProperty("prompt_tokens") + int promptTokens, + @JsonProperty("completion_tokens") + int completionTokens, + @JsonProperty("total_tokens") + int totalTokens + ) {} +} diff --git a/src/main/java/com/example/mosh_be/controller/UpstageChatController.java b/src/main/java/com/example/mosh_be/controller/UpstageChatController.java new file mode 100644 index 0000000..f151f57 --- /dev/null +++ b/src/main/java/com/example/mosh_be/controller/UpstageChatController.java @@ -0,0 +1,26 @@ +package com.example.mosh_be.controller; + +import com.example.mosh_be.dto.chatbot.ChatMessageResponse; +import com.example.mosh_be.dto.request.UpstageChatRequest; +import com.example.mosh_be.service.UpstageChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1") +@RequiredArgsConstructor +public class UpstageChatController { + + private final UpstageChatService upstageChatService; + + @PostMapping("/chatbot/conversations/response") + public ResponseEntity getUpstageChatResponse( + @RequestBody UpstageChatRequest request, + @AuthenticationPrincipal Long userId + ) { + ChatMessageResponse upstageChatResponse = upstageChatService.getUpstageChatResponse(userId, request.content()); + return ResponseEntity.ok().body(upstageChatResponse); + } +} diff --git a/src/main/java/com/example/mosh_be/dto/chatbot/ChatMessageResponse.java b/src/main/java/com/example/mosh_be/dto/chatbot/ChatMessageResponse.java index f28fc9c..030394d 100644 --- a/src/main/java/com/example/mosh_be/dto/chatbot/ChatMessageResponse.java +++ b/src/main/java/com/example/mosh_be/dto/chatbot/ChatMessageResponse.java @@ -4,13 +4,9 @@ import lombok.Builder; import lombok.Getter; -import java.util.List; - @Getter @AllArgsConstructor @Builder public class ChatMessageResponse { private String answer; - private List citations; - private String traceId; } diff --git a/src/main/java/com/example/mosh_be/dto/request/UpstageChatRequest.java b/src/main/java/com/example/mosh_be/dto/request/UpstageChatRequest.java new file mode 100644 index 0000000..48c6a73 --- /dev/null +++ b/src/main/java/com/example/mosh_be/dto/request/UpstageChatRequest.java @@ -0,0 +1,3 @@ +package com.example.mosh_be.dto.request; + +public record UpstageChatRequest (String content) {} diff --git a/src/main/java/com/example/mosh_be/service/UpstageChatService.java b/src/main/java/com/example/mosh_be/service/UpstageChatService.java new file mode 100644 index 0000000..bc6e1a8 --- /dev/null +++ b/src/main/java/com/example/mosh_be/service/UpstageChatService.java @@ -0,0 +1,20 @@ +package com.example.mosh_be.service; + +import com.example.mosh_be.client.UpstageChatClient; +import com.example.mosh_be.dto.chatbot.ChatMessageResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UpstageChatService { + + private final UpstageChatClient client; + + public ChatMessageResponse getUpstageChatResponse(Long userId, String content) { + String answer = client.getUpstageChatResponse(userId, content); + return new ChatMessageResponse( + answer + ); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index eda6c63..95d5785 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -4,9 +4,9 @@ spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://43.201.18.138:3306/mosh?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: mosh - password: mosh_4_earth + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/mosh?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: ${DB_USERNAME} + password: ${DB_PASSWORD} jpa: database: mysql @@ -55,3 +55,7 @@ logging: com.example.mosh_be: DEBUG org.hibernate.SQL: DEBUG org.hibernate.type.descriptor.sql.BasicBinder: TRACE + +external: + upstage: + api-key: ${UPSTAGE_API_KEY} From 80ed206355743fa40e0d18c5148cf5ee8b0642c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=A4=80?= Date: Sat, 27 Dec 2025 05:53:35 +0900 Subject: [PATCH 2/3] =?UTF-8?q?remove(chatbot):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mosh_be/controller/ChatbotController.java | 30 --------- .../mosh_be/service/ChatbotService.java | 64 ------------------- src/main/resources/application.yml | 6 +- 3 files changed, 1 insertion(+), 99 deletions(-) delete mode 100644 src/main/java/com/example/mosh_be/controller/ChatbotController.java delete mode 100644 src/main/java/com/example/mosh_be/service/ChatbotService.java diff --git a/src/main/java/com/example/mosh_be/controller/ChatbotController.java b/src/main/java/com/example/mosh_be/controller/ChatbotController.java deleted file mode 100644 index 6191455..0000000 --- a/src/main/java/com/example/mosh_be/controller/ChatbotController.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.mosh_be.controller; - -import com.example.mosh_be.dto.chatbot.ChatMessageRequest; -import com.example.mosh_be.dto.chatbot.ChatMessageResponse; -import com.example.mosh_be.service.ChatbotService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api/v1/chatbot") -@RequiredArgsConstructor -public class ChatbotController { - - private final ChatbotService chatbotService; - - @PostMapping("/messages") - public ResponseEntity sendMessage( - @AuthenticationPrincipal Long userId, - @Valid @RequestBody ChatMessageRequest request - ) { - ChatMessageResponse response = chatbotService.sendMessage(userId, request); - return ResponseEntity.ok(response); - } -} diff --git a/src/main/java/com/example/mosh_be/service/ChatbotService.java b/src/main/java/com/example/mosh_be/service/ChatbotService.java deleted file mode 100644 index 2f3c86f..0000000 --- a/src/main/java/com/example/mosh_be/service/ChatbotService.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.example.mosh_be.service; - -import com.example.mosh_be.dto.chatbot.ChatMessageRequest; -import com.example.mosh_be.dto.chatbot.ChatMessageResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -import java.util.ArrayList; -import java.util.Map; - -@Service -@RequiredArgsConstructor -public class ChatbotService { - - private final WebClient.Builder webClientBuilder; - - @Value("${solar.api.url}") - private String solarApiUrl; - - @Value("${solar.api.key}") - private String solarApiKey; - - public ChatMessageResponse sendMessage(Long userId, ChatMessageRequest request) { - // Mock implementation for MVP - String answer = "페스티벌에서 즐거운 시간 보내세요! 오후 6시 이후에는 메인 스테이지의 헤드라이너 공연을 추천합니다."; - - return ChatMessageResponse.builder() - .answer(answer) - .citations(new ArrayList<>()) - .traceId("t_" + System.currentTimeMillis()) - .build(); - - /* Real implementation would be: - try { - WebClient webClient = webClientBuilder.baseUrl(solarApiUrl).build(); - - Map requestBody = Map.of( - "messages", List.of(Map.of("role", "user", "content", request.getMessage())), - "model", "solar-1-mini-chat" - ); - - Map response = webClient.post() - .uri("/chat/completions") - .header("Authorization", "Bearer " + solarApiKey) - .bodyValue(requestBody) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>() {}) - .block(); - - String answer = extractAnswer(response); - - return ChatMessageResponse.builder() - .answer(answer) - .citations(new ArrayList<>()) - .traceId("t_" + System.currentTimeMillis()) - .build(); - } catch (Exception e) { - throw new RuntimeException("Failed to get chatbot response", e); - } - */ - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 74dcaa0..7b82f3f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,11 +38,6 @@ jwt: secret: ${JWT_SECRET:mosh-jwt-secret-key-for-hackathon-2025-profitlab-earth-team-4-please-change-in-production} expiration: 3600000 # 1 hour in milliseconds -# Solar LLM API Settings -solar: - api: - url: https://api.upstage.ai/v1/solar - key: up_ws0Qe4bMoq0GEC8etzVzZcJEXs0ND # CORS Settings cors: @@ -56,6 +51,7 @@ logging: org.hibernate.SQL: DEBUG org.hibernate.type.descriptor.sql.BasicBinder: TRACE +# Solar LLM API Settings external: upstage: api-key: ${UPSTAGE_API_KEY} From a0dbffa3f9ed1d8d097063dad7fe11c87d3f2dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=9A=A9=EC=A4=80?= Date: Sat, 27 Dec 2025 06:00:55 +0900 Subject: [PATCH 3/3] =?UTF-8?q?remove(chatbot):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/mosh_be/MoshBeApplicationTests.java | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 src/test/java/com/example/mosh_be/MoshBeApplicationTests.java diff --git a/src/test/java/com/example/mosh_be/MoshBeApplicationTests.java b/src/test/java/com/example/mosh_be/MoshBeApplicationTests.java deleted file mode 100644 index b23aa6a..0000000 --- a/src/test/java/com/example/mosh_be/MoshBeApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.mosh_be; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class MoshBeApplicationTests { - - @Test - void contextLoads() { - } - -}