Skip to content

Commit 8c723a7

Browse files
committed
[#50] feat: 프롬프트 설정 및 템플릿 고도화
1 parent 4188403 commit 8c723a7

File tree

12 files changed

+596
-144
lines changed

12 files changed

+596
-144
lines changed
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package com.nexters.teamace.conversation.application;
22

3-
import java.util.List;
3+
import com.nexters.teamace.conversation.domain.ConversationContextType;
4+
import java.util.Map;
45

5-
public record ConversationContext(String sessionKey, List<String> previousMessages) {
6+
public record ConversationContext(
7+
String sessionKey, Map<ConversationContextType, String> variables) {
68

79
public ConversationContext {
810
if (sessionKey == null || sessionKey.trim().isEmpty()) {
911
throw new IllegalArgumentException("SessionKey cannot be null or empty");
1012
}
11-
if (previousMessages == null) {
12-
previousMessages = List.of();
13+
if (variables == null) {
14+
variables = Map.of();
1315
}
1416
}
1517
}
Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
package com.nexters.teamace.conversation.application;
22

3-
import static com.nexters.teamace.conversation.domain.ConversationContextType.MESSAGE;
4-
import static com.nexters.teamace.conversation.domain.ConversationContextType.PREVIOUS_CONVERSATIONS;
5-
6-
import com.nexters.teamace.conversation.domain.ConversationContextType;
73
import com.nexters.teamace.conversation.domain.ConversationScript;
84
import com.nexters.teamace.conversation.domain.ConversationScriptRepository;
95
import com.nexters.teamace.conversation.domain.ConversationType;
10-
import java.util.Map;
116
import lombok.RequiredArgsConstructor;
127
import org.springframework.stereotype.Service;
138

@@ -20,19 +15,4 @@ class ConversationScriptService {
2015
public ConversationScript getPromptTemplate(ConversationType type) {
2116
return conversationScriptRepository.getByType(type);
2217
}
23-
24-
public String renderScript(
25-
ConversationType type, ConversationContext context, String userMessage) {
26-
final ConversationScript template = getPromptTemplate(type);
27-
final Map<ConversationContextType, String> variables =
28-
createConversationVariables(context, userMessage);
29-
return template.render(variables);
30-
}
31-
32-
private Map<ConversationContextType, String> createConversationVariables(
33-
ConversationContext context, String userMessage) {
34-
return Map.ofEntries(
35-
Map.entry(MESSAGE, userMessage),
36-
Map.entry(PREVIOUS_CONVERSATIONS, String.join("\n", context.previousMessages())));
37-
}
3818
}
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,40 @@
11
package com.nexters.teamace.conversation.application;
22

3+
import com.nexters.teamace.conversation.domain.ConversationScript;
34
import com.nexters.teamace.conversation.domain.ConversationType;
5+
import java.util.Objects;
46
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
58
import org.springframework.stereotype.Service;
69

10+
@Slf4j
711
@Service
812
@RequiredArgsConstructor
913
public class ConversationService {
1014

1115
private final ConversationClient conversationClient;
1216
private final ConversationScriptService conversationScriptService;
1317

18+
public <T> T chat(
19+
final Class<T> type,
20+
final ConversationType conversationType,
21+
final ConversationContext context) {
22+
// No userMessage for cases like EMOTION_ANALYSIS
23+
return chat(type, conversationType, context, null);
24+
}
25+
1426
public <T> T chat(
1527
final Class<T> type,
1628
final ConversationType conversationType,
1729
final ConversationContext context,
18-
final String message) {
19-
final String script =
20-
conversationScriptService.renderScript(conversationType, context, message);
21-
return conversationClient.chat(type, script, message);
30+
final String userMessage) {
31+
final ConversationScript script =
32+
conversationScriptService.getPromptTemplate(conversationType);
33+
34+
script.validateVariables(context.variables());
35+
final String renderedScript = script.render(context.variables());
36+
37+
return conversationClient.chat(
38+
type, renderedScript, Objects.isNull(userMessage) ? "" : userMessage);
2239
}
2340
}

src/main/java/com/nexters/teamace/conversation/domain/ConversationContextType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
public enum ConversationContextType {
44
PREVIOUS_CONVERSATIONS,
5-
MESSAGE
5+
CONVERSATION_STAGE,
66
}

src/main/java/com/nexters/teamace/conversation/domain/ConversationScript.java

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.nexters.teamace.conversation.domain;
22

3+
import java.util.HashSet;
34
import java.util.Map;
5+
import java.util.Set;
46
import java.util.regex.Matcher;
57
import java.util.regex.Pattern;
8+
import java.util.stream.Collectors;
69
import lombok.EqualsAndHashCode;
710
import lombok.Getter;
811
import lombok.ToString;
12+
import lombok.extern.slf4j.Slf4j;
913

14+
@Slf4j
1015
@EqualsAndHashCode
1116
@ToString
1217
public class ConversationScript {
@@ -27,20 +32,101 @@ public ConversationScript(final ConversationType type, final String content) {
2732
}
2833

2934
public String render(final Map<ConversationContextType, String> variables) {
30-
if (variables == null || variables.isEmpty()) {
31-
return content;
32-
}
33-
3435
String result = content;
3536
Matcher matcher = VARIABLE_PATTERN.matcher(content);
3637

3738
while (matcher.find()) {
3839
String variableName = matcher.group(1);
39-
String variableValue =
40-
variables.getOrDefault(ConversationContextType.valueOf(variableName), "");
40+
String variableValue = resolveVariableValue(variableName, variables);
4141
result = result.replace("{{" + variableName + "}}", variableValue);
4242
}
4343

4444
return result;
4545
}
46+
47+
private String resolveVariableValue(
48+
String variableName, Map<ConversationContextType, String> variables) {
49+
try {
50+
final ConversationContextType contextType =
51+
ConversationContextType.valueOf(variableName);
52+
return getVariableValue(variableName, contextType, variables);
53+
} catch (IllegalArgumentException e) {
54+
return handleUnknownVariable(variableName);
55+
}
56+
}
57+
58+
private String getVariableValue(
59+
String variableName,
60+
ConversationContextType contextType,
61+
Map<ConversationContextType, String> variables) {
62+
final String variableValue = variables.getOrDefault(contextType, "");
63+
64+
if (variableValue.isEmpty()) {
65+
log.warn(
66+
"Empty value provided for variable '{}' in template type '{}'",
67+
variableName,
68+
type);
69+
}
70+
71+
return variableValue;
72+
}
73+
74+
private String handleUnknownVariable(String variableName) {
75+
log.error(
76+
"Unknown variable '{}' in template type '{}'. Variable names must be defined in ConversationContextType enum.",
77+
variableName,
78+
type);
79+
return "{{" + variableName + "}}";
80+
}
81+
82+
public void validateVariables(final Map<ConversationContextType, String> providedVariables) {
83+
final Set<String> requiredVariables = extractRequiredVariables();
84+
final Set<String> missingVariables =
85+
findMissingVariables(requiredVariables, providedVariables);
86+
87+
throwExceptionIfVariablesMissing(missingVariables);
88+
}
89+
90+
private Set<String> extractRequiredVariables() {
91+
final Set<String> variables = new HashSet<>();
92+
final Matcher matcher = VARIABLE_PATTERN.matcher(content);
93+
94+
while (matcher.find()) {
95+
variables.add(matcher.group(1));
96+
}
97+
98+
return variables;
99+
}
100+
101+
private Set<String> findMissingVariables(
102+
final Set<String> requiredVariables,
103+
final Map<ConversationContextType, String> providedVariables) {
104+
return requiredVariables.stream()
105+
.filter(this::isValidContextType)
106+
.filter(
107+
required ->
108+
!providedVariables.containsKey(
109+
ConversationContextType.valueOf(required)))
110+
.collect(Collectors.toSet());
111+
}
112+
113+
private boolean isValidContextType(final String variable) {
114+
try {
115+
ConversationContextType.valueOf(variable);
116+
return true;
117+
} catch (IllegalArgumentException e) {
118+
log.warn("Template '{}' contains unknown variable: {}", type, variable);
119+
return false;
120+
}
121+
}
122+
123+
private void throwExceptionIfVariablesMissing(final Set<String> missingVariables) {
124+
if (!missingVariables.isEmpty()) {
125+
final String variableNames = String.join(", ", missingVariables);
126+
throw new IllegalArgumentException(
127+
String.format(
128+
"Missing required variables for template '%s': %s",
129+
type, variableNames));
130+
}
131+
}
46132
}

src/main/java/com/nexters/teamace/conversation/infrastructure/chatmodel/GeminiChatModel.java

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package com.nexters.teamace.conversation.infrastructure.chatmodel;
22

3+
import static com.google.genai.types.Content.fromParts;
4+
35
import com.google.genai.Client;
6+
import com.google.genai.types.GenerateContentConfig;
47
import com.google.genai.types.GenerateContentResponse;
8+
import com.google.genai.types.Part;
59
import java.util.List;
6-
import java.util.stream.Collectors;
710
import org.springframework.ai.chat.messages.AssistantMessage;
811
import org.springframework.ai.chat.model.ChatModel;
912
import org.springframework.ai.chat.model.ChatResponse;
1013
import org.springframework.ai.chat.model.Generation;
1114
import org.springframework.ai.chat.prompt.Prompt;
15+
import org.springframework.http.MediaType;
1216
import org.springframework.stereotype.Component;
1317

1418
@Component
@@ -24,21 +28,33 @@ public GeminiChatModel(final GeminiProperties properties) {
2428

2529
@Override
2630
public ChatResponse call(Prompt prompt) {
27-
String userMessage =
28-
prompt.getInstructions().stream()
29-
.map(org.springframework.ai.content.Content::getText)
30-
.collect(Collectors.joining("\n"));
31-
32-
GenerateContentResponse response =
33-
client.models.generateContent(modelName, userMessage, null);
34-
35-
String responseText = response.text();
31+
// Use Spring AI's built-in methods for cleaner code
32+
final String systemMessage = prompt.getSystemMessage().getText();
33+
final String userMessage = prompt.getUserMessage().getText();
34+
35+
// Create config with system instruction and optimized settings
36+
final var systemInstruction = fromParts(Part.fromText(systemMessage));
37+
final GenerateContentConfig config =
38+
GenerateContentConfig.builder()
39+
.systemInstruction(systemInstruction)
40+
.responseMimeType(MediaType.APPLICATION_JSON_VALUE) // JSON 응답 보장
41+
.temperature(0.7f)
42+
.topP(0.9f)
43+
.topK(40f)
44+
.maxOutputTokens(2048)
45+
.candidateCount(1)
46+
.build();
47+
48+
final GenerateContentResponse response =
49+
client.models.generateContent(modelName, userMessage, config);
50+
51+
final String responseText = response.text();
3652
if (responseText == null || responseText.isEmpty()) {
3753
return new ChatResponse(List.of());
3854
}
3955

40-
Generation generation = new Generation(new AssistantMessage(responseText));
41-
List<Generation> generations = List.of(generation);
56+
final Generation generation = new Generation(new AssistantMessage(responseText));
57+
final List<Generation> generations = List.of(generation);
4258

4359
return new ChatResponse(generations);
4460
}

src/main/java/com/nexters/teamace/fairy/application/FairyService.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.nexters.teamace.common.presentation.UserInfo;
66
import com.nexters.teamace.conversation.application.ConversationContext;
77
import com.nexters.teamace.conversation.application.ConversationService;
8+
import com.nexters.teamace.conversation.domain.ConversationContextType;
89
import com.nexters.teamace.conversation.domain.ConversationType;
910
import com.nexters.teamace.conversation.domain.EmotionSelectConversation;
1011
import com.nexters.teamace.fairy.application.dto.FairyInfo;
@@ -16,6 +17,7 @@
1617
import com.nexters.teamace.fairy.infrastructure.AcquiredFairyJpaRepository;
1718
import com.nexters.teamace.fairy.infrastructure.dto.FairyProjection;
1819
import java.util.List;
20+
import java.util.Map;
1921
import java.util.Set;
2022
import java.util.stream.Collectors;
2123
import lombok.RequiredArgsConstructor;
@@ -40,12 +42,17 @@ public FairyResult getFairy(UserInfo user, Long chatRoomId) {
4042
* 4. ...
4143
* */
4244
ConversationType type = ConversationType.EMOTION_ANALYSIS;
43-
ConversationContext context = new ConversationContext("질의응답", List.of());
45+
46+
Map<ConversationContextType, String> variables =
47+
Map.of(
48+
ConversationContextType.PREVIOUS_CONVERSATIONS,
49+
"" // This should be populated from chat room history
50+
);
51+
ConversationContext context = new ConversationContext("질의응답", variables);
4452

4553
// 2. 질의응답 기반으로 ai call 해서 감정 후보 획득
4654
EmotionSelectConversation emotionSelectConversation =
47-
(EmotionSelectConversation)
48-
conversationService.chat(type.getType(), type, context, "");
55+
(EmotionSelectConversation) conversationService.chat(type.getType(), type, context);
4956

5057
// 3. 감정 후보 기반으로 fairy 후보 조회
5158
List<FairyProjection> fairyProjections =

0 commit comments

Comments
 (0)