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
Original file line number Diff line number Diff line change
@@ -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<Long, Deque<Map<String, String>>> userMemory
= new ConcurrentHashMap<>();

public void addMessage(Long userId, String role, String content) {
Deque<Map<String, String>> memory =
userMemory.computeIfAbsent(userId, id -> new ArrayDeque<>());

if (memory.size() >= MAX_SIZE) {
memory.pollFirst();
}

memory.addLast(Map.of(
"role", role,
"content", content
));
}

public List<Map<String, String>> getMessages(Long userId) {
return List.copyOf(
userMemory.getOrDefault(userId, new ArrayDeque<>())
);
}
}
94 changes: 94 additions & 0 deletions src/main/java/com/example/mosh_be/client/PromptBuilder.java
Original file line number Diff line number Diff line change
@@ -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<Map<String, String>> upstageChatPromptBuilder(
List<Map<String, String>> memory,
String userContent
) {
List<Map<String, String>> 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;
}
}
58 changes: 58 additions & 0 deletions src/main/java/com/example/mosh_be/client/UpstageChatClient.java
Original file line number Diff line number Diff line change
@@ -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<Map<String, String>> messages = promptBuilder.upstageChatPromptBuilder(
chatContext.getMessages(userId),
content
);

Map<String, Object> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Choice> 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
) {}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<ChatMessageResponse> getUpstageChatResponse(
@RequestBody UpstageChatRequest request,
@AuthenticationPrincipal Long userId
) {
ChatMessageResponse upstageChatResponse = upstageChatService.getUpstageChatResponse(userId, request.content());
return ResponseEntity.ok().body(upstageChatResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> citations;
private String traceId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.example.mosh_be.dto.request;

public record UpstageChatRequest (String content) {}
64 changes: 0 additions & 64 deletions src/main/java/com/example/mosh_be/service/ChatbotService.java

This file was deleted.

20 changes: 20 additions & 0 deletions src/main/java/com/example/mosh_be/service/UpstageChatService.java
Original file line number Diff line number Diff line change
@@ -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
);
}
}
Loading