From b09635ce8f9dc652db48fe6552838b46734608e3 Mon Sep 17 00:00:00 2001 From: koosco Date: Wed, 4 Dec 2024 20:58:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20open=20ai=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orbit/config/openai/AiFeedbackPrompt.java | 42 --------- .../config/openai/AiFeedbackRequestDto.java | 92 ------------------- .../config/openai/AiFeedbackResponseDto.java | 27 ------ .../config/openai/GoalRecommendPrompt.java | 30 ------ .../openai/GoalRecommendRequestDto.java | 76 --------------- .../openai/GoalRecommendResponseDto.java | 27 ------ .../orbit/config/openai/OpenAiClient.java | 21 ----- .../config/openai/OpenAiClientConfig.java | 21 ----- .../config/openai/QuestRecommendPrompt.java | 30 ------ .../openai/QuestRecommendRequestDto.java | 76 --------------- .../openai/QuestRecommendResponseDto.java | 27 ------ 11 files changed, 469 deletions(-) delete mode 100644 src/main/java/com/groom/orbit/config/openai/AiFeedbackPrompt.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/AiFeedbackRequestDto.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/AiFeedbackResponseDto.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/GoalRecommendPrompt.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/GoalRecommendRequestDto.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/GoalRecommendResponseDto.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/OpenAiClient.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/OpenAiClientConfig.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/QuestRecommendPrompt.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/QuestRecommendRequestDto.java delete mode 100644 src/main/java/com/groom/orbit/config/openai/QuestRecommendResponseDto.java diff --git a/src/main/java/com/groom/orbit/config/openai/AiFeedbackPrompt.java b/src/main/java/com/groom/orbit/config/openai/AiFeedbackPrompt.java deleted file mode 100644 index a327411..0000000 --- a/src/main/java/com/groom/orbit/config/openai/AiFeedbackPrompt.java +++ /dev/null @@ -1,42 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// public class AiFeedbackPrompt { -// private static final String SYSTEM_PROMPT = -// "내 관심 직무 분야는 {job}인데 이에 관한 내 활동사항들이야. 이 항목 대해 피드백을 해줘"; -// private static final String USER_PROMPT = -// """ -// {academy} -// -// 이건 내 학력이고, -// -// {career} -// -// 이건 내 경력이야. -// -// {qualification} -// -// 이건 내 자격, 어학, 수상에 관련된 내용들이고 -// -// {experience} -// -// 이건 내 경험, 활동, 교육에 관한 내용들이야. -// -// {etc} -// -// 이건 기타사항이야 -// """; -// -// public static String getSystemPrompt(String job) { -// return SYSTEM_PROMPT.replace("{job}", job); -// } -// -// public static String getUserPrompt( -// String academy, String career, String qualification, String experience, String etc) { -// USER_PROMPT.replace("{academy}", academy); -// USER_PROMPT.replace("{career}", career); -// USER_PROMPT.replace("{qualification}", qualification); -// USER_PROMPT.replace("{experience}", experience); -// USER_PROMPT.replace("{etc}", etc); -// return USER_PROMPT; -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/AiFeedbackRequestDto.java b/src/main/java/com/groom/orbit/config/openai/AiFeedbackRequestDto.java deleted file mode 100644 index a15f46d..0000000 --- a/src/main/java/com/groom/orbit/config/openai/AiFeedbackRequestDto.java +++ /dev/null @@ -1,92 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// import java.util.List; -// -// import com.fasterxml.jackson.annotation.JsonInclude; -// import com.fasterxml.jackson.annotation.JsonValue; -// -// import lombok.AccessLevel; -// import lombok.AllArgsConstructor; -// import lombok.Getter; -// import lombok.RequiredArgsConstructor; -// -// @Getter -// public class AiFeedbackRequestDto { -// -// private String model = "gpt-4o-mini"; -// -// private List messages; -// -// public AiFeedbackRequestDto(List messages) { -// this.messages = messages; -// } -// -// @Getter -// @AllArgsConstructor(access = AccessLevel.PROTECTED) -// public static class Message { -// private Role role; -// private List content; -// } -// -// @RequiredArgsConstructor -// @Getter -// public enum Role { -// USER("user"), -// SYSTEM("system"); -// @JsonValue private final String value; -// } -// -// @AllArgsConstructor(access = AccessLevel.PROTECTED) -// @Getter -// @JsonInclude(JsonInclude.Include.NON_NULL) -// public static class Content { -// private Type type; -// -// private String text; -// -// public static Content textContent(String text) { -// return new Content(Type.TEXT, text); -// } -// } -// -// @RequiredArgsConstructor -// @Getter -// public enum Type { -// TEXT("text"); -// @JsonValue private final String value; -// } -// -// public static AiFeedbackRequestDto from( -// String job, -// String academy, -// String career, -// String qualification, -// String experience, -// String etc) { -// return new AiFeedbackRequestDto( -// createMessages(job, academy, career, qualification, experience, etc)); -// } -// -// private static List createMessages( -// String job, -// String academy, -// String career, -// String qualification, -// String experience, -// String etc) { -// return List.of( -// new Message(Role.SYSTEM, createSystemContents(job)), -// new Message(Role.USER, createContents(academy, career, qualification, experience, etc))); -// } -// -// private static List createSystemContents(String job) { -// return List.of(Content.textContent(AiFeedbackPrompt.getSystemPrompt(job))); -// } -// -// private static List createContents( -// String academy, String career, String qualification, String experience, String etc) { -// return List.of( -// Content.textContent( -// AiFeedbackPrompt.getUserPrompt(academy, career, qualification, experience, etc))); -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/AiFeedbackResponseDto.java b/src/main/java/com/groom/orbit/config/openai/AiFeedbackResponseDto.java deleted file mode 100644 index b71f4b3..0000000 --- a/src/main/java/com/groom/orbit/config/openai/AiFeedbackResponseDto.java +++ /dev/null @@ -1,27 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// import java.util.List; -// -// import lombok.Getter; -// -// @Getter -// public class AiFeedbackResponseDto { -// -// private List choices; -// -// @Getter -// public static class Choice { -// private Message message; -// private Integer index; -// } -// -// @Getter -// public static class Message { -// String role; -// String content; -// } -// -// public String getAnswer() { -// return choices.get(0).message.content; -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/GoalRecommendPrompt.java b/src/main/java/com/groom/orbit/config/openai/GoalRecommendPrompt.java deleted file mode 100644 index e7f91ac..0000000 --- a/src/main/java/com/groom/orbit/config/openai/GoalRecommendPrompt.java +++ /dev/null @@ -1,30 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// public class GoalRecommendPrompt { -// -// private static final String SYSTEM_PROMPT = "내 관심 직무는 {job}이야"; -// -// private static final String USER_PROMPT = -// """ -// -// {goalList} -// -// 이건 사람들과 내가 해왔고 달성하기 위해 노력하는 목표인데 어떤 목표를 더 달성하면 좋을까? -// -// 자격·어학·수상, 경험·활동·교육, 경력, 기타 이게 목표의 카테고리야 -// -// 저 카테고리 중 하나와 그에 맞는 카테고리를 ~하기라고 하나만 추천해줘. 대답은 저기 중 카테고리 하나와 ~하기라고만 해 -// -// 답변의 예시는 "자격·어학·수상,~하기"야 -// 예시처럼 답변해 -// -// """; -// -// public static String getSystemPrompt(String job) { -// return SYSTEM_PROMPT.replace("{job}", job); -// } -// -// public static String getUserPrompt(String goalList) { -// return USER_PROMPT.replace("{goalList}", goalList); -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/GoalRecommendRequestDto.java b/src/main/java/com/groom/orbit/config/openai/GoalRecommendRequestDto.java deleted file mode 100644 index 0d9b636..0000000 --- a/src/main/java/com/groom/orbit/config/openai/GoalRecommendRequestDto.java +++ /dev/null @@ -1,76 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// import java.util.List; -// -// import com.fasterxml.jackson.annotation.JsonInclude; -// import com.fasterxml.jackson.annotation.JsonValue; -// -// import lombok.AccessLevel; -// import lombok.AllArgsConstructor; -// import lombok.Getter; -// import lombok.RequiredArgsConstructor; -// -// @Getter -// public class GoalRecommendRequestDto { -// -// private String model = "gpt-4o-mini"; -// -// private List messages; -// -// public GoalRecommendRequestDto(List messages) { -// this.messages = messages; -// } -// -// @Getter -// @AllArgsConstructor(access = AccessLevel.PROTECTED) -// public static class Message { -// private Role role; -// private List content; -// } -// -// @RequiredArgsConstructor -// @Getter -// public enum Role { -// USER("user"), -// SYSTEM("system"); -// @JsonValue private final String value; -// } -// -// @AllArgsConstructor(access = AccessLevel.PROTECTED) -// @Getter -// @JsonInclude(JsonInclude.Include.NON_NULL) -// public static class Content { -// private Type type; -// -// private String text; -// -// public static Content textContent(String text) { -// return new Content(Type.TEXT, text); -// } -// } -// -// @RequiredArgsConstructor -// @Getter -// public enum Type { -// TEXT("text"); -// @JsonValue private final String value; -// } -// -// public static GoalRecommendRequestDto from(String job, String goalList) { -// return new GoalRecommendRequestDto(createMessage(job, goalList)); -// } -// -// private static List createMessage(String job, String goalList) { -// return List.of( -// new Message(Role.SYSTEM, createSystemContents(job)), -// new Message(Role.USER, createContents(goalList))); -// } -// -// private static List createSystemContents(String job) { -// return List.of(Content.textContent(GoalRecommendPrompt.getSystemPrompt(job))); -// } -// -// private static List createContents(String goalList) { -// return List.of(Content.textContent(GoalRecommendPrompt.getUserPrompt(goalList))); -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/GoalRecommendResponseDto.java b/src/main/java/com/groom/orbit/config/openai/GoalRecommendResponseDto.java deleted file mode 100644 index 3245ca1..0000000 --- a/src/main/java/com/groom/orbit/config/openai/GoalRecommendResponseDto.java +++ /dev/null @@ -1,27 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// import java.util.List; -// -// import lombok.Getter; -// -// @Getter -// public class GoalRecommendResponseDto { -// -// private List choices; -// -// @Getter -// public static class Choice { -// private Message message; -// private Integer index; -// } -// -// @Getter -// public static class Message { -// String role; -// String content; -// } -// -// public String getAnswer() { -// return choices.get(0).message.content; -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/OpenAiClient.java b/src/main/java/com/groom/orbit/config/openai/OpenAiClient.java deleted file mode 100644 index 2b8f798..0000000 --- a/src/main/java/com/groom/orbit/config/openai/OpenAiClient.java +++ /dev/null @@ -1,21 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// import org.springframework.cloud.openfeign.FeignClient; -// import org.springframework.web.bind.annotation.PostMapping; -// import org.springframework.web.bind.annotation.RequestBody; -// -// @FeignClient( -// name = "open-ai-client", -// url = "https://api.openai.com/v1/chat/completions", -// configuration = OpenAiClientConfig.class) -// public interface OpenAiClient { -// @PostMapping() -// AiFeedbackResponseDto createAiFeedback(@RequestBody AiFeedbackRequestDto requestDto); -// -// @PostMapping() -// GoalRecommendResponseDto createGoalRecommend(@RequestBody GoalRecommendRequestDto requestDto); -// -// @PostMapping() -// QuestRecommendResponseDto createQuestRecommend(@RequestBody QuestRecommendRequestDto -// requestDto); -// } diff --git a/src/main/java/com/groom/orbit/config/openai/OpenAiClientConfig.java b/src/main/java/com/groom/orbit/config/openai/OpenAiClientConfig.java deleted file mode 100644 index 92f6a4c..0000000 --- a/src/main/java/com/groom/orbit/config/openai/OpenAiClientConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// import org.springframework.beans.factory.annotation.Value; -// import org.springframework.context.annotation.Bean; -// -// import feign.Logger; -// import feign.Logger.Level; -// import feign.RequestInterceptor; -// -// public class OpenAiClientConfig { -// -// @Bean -// public RequestInterceptor requestInterceptor(@Value("${openai.api-key}") String apiKey) { -// return template -> template.header("Authorization", "Bearer " + apiKey); -// } -// -// @Bean -// public Logger.Level loggerLevel() { -// return Level.FULL; -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/QuestRecommendPrompt.java b/src/main/java/com/groom/orbit/config/openai/QuestRecommendPrompt.java deleted file mode 100644 index 1578355..0000000 --- a/src/main/java/com/groom/orbit/config/openai/QuestRecommendPrompt.java +++ /dev/null @@ -1,30 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// public class QuestRecommendPrompt { -// -// private static final String SYSTEM_PROMPT = "내 관심 직무는 {job}고 내 폭표는 {goal}야"; -// -// private static final String USER_PROMPT = -// """ -// -// {questList} -// -// 이건 같은 목표를 갖고 있는 사람들이 했던 퀘스트인데 어떤거를 하면 좋을까? -// -// 하나만 추천해줘. 저 중 하나만 골라서 이야기해줘 딱 저 중 하나만 이야기하면 되 -// -// 답변의 예시로는 "데이터 분석 참여"야 -// 이런 식으로 답변만 이야기해 -// -// """; -// -// public static String getSystemPrompt(String job, String goal) { -// SYSTEM_PROMPT.replace("{job}", job); -// SYSTEM_PROMPT.replace("{goal}", goal); -// return SYSTEM_PROMPT; -// } -// -// public static String getUserPrompt(String questList) { -// return USER_PROMPT.replace("{questList}", questList); -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/QuestRecommendRequestDto.java b/src/main/java/com/groom/orbit/config/openai/QuestRecommendRequestDto.java deleted file mode 100644 index 839e86a..0000000 --- a/src/main/java/com/groom/orbit/config/openai/QuestRecommendRequestDto.java +++ /dev/null @@ -1,76 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// import java.util.List; -// -// import com.fasterxml.jackson.annotation.JsonInclude; -// import com.fasterxml.jackson.annotation.JsonValue; -// -// import lombok.AccessLevel; -// import lombok.AllArgsConstructor; -// import lombok.Getter; -// import lombok.RequiredArgsConstructor; -// -// @Getter -// public class QuestRecommendRequestDto { -// -// private String model = "gpt-4o-mini"; -// -// private List messages; -// -// public QuestRecommendRequestDto(List messages) { -// this.messages = messages; -// } -// -// @Getter -// @AllArgsConstructor(access = AccessLevel.PROTECTED) -// public static class Message { -// private Role role; -// private List content; -// } -// -// @RequiredArgsConstructor -// @Getter -// public enum Role { -// USER("user"), -// SYSTEM("system"); -// @JsonValue private final String value; -// } -// -// @AllArgsConstructor(access = AccessLevel.PROTECTED) -// @Getter -// @JsonInclude(JsonInclude.Include.NON_NULL) -// public static class Content { -// private Type type; -// -// private String text; -// -// public static Content textContent(String text) { -// return new Content(Type.TEXT, text); -// } -// } -// -// @RequiredArgsConstructor -// @Getter -// public enum Type { -// TEXT("text"); -// @JsonValue private final String value; -// } -// -// public static QuestRecommendRequestDto from(String job, String goal, String questList) { -// return new QuestRecommendRequestDto(createMessage(job, goal, questList)); -// } -// -// private static List createMessage(String job, String goal, String questList) { -// return List.of( -// new Message(Role.SYSTEM, createSystemContents(job, goal)), -// new Message(Role.USER, createContents(questList))); -// } -// -// private static List createSystemContents(String job, String goal) { -// return List.of(Content.textContent(QuestRecommendPrompt.getSystemPrompt(job, goal))); -// } -// -// private static List createContents(String questList) { -// return List.of(Content.textContent(QuestRecommendPrompt.getUserPrompt(questList))); -// } -// } diff --git a/src/main/java/com/groom/orbit/config/openai/QuestRecommendResponseDto.java b/src/main/java/com/groom/orbit/config/openai/QuestRecommendResponseDto.java deleted file mode 100644 index 56ed324..0000000 --- a/src/main/java/com/groom/orbit/config/openai/QuestRecommendResponseDto.java +++ /dev/null @@ -1,27 +0,0 @@ -// package com.groom.orbit.config.openai; -// -// import java.util.List; -// -// import lombok.Getter; -// -// @Getter -// public class QuestRecommendResponseDto { -// -// private List choices; -// -// @Getter -// public static class Choice { -// private Message message; -// private Integer index; -// } -// -// @Getter -// public static class Message { -// String role; -// String content; -// } -// -// public String getAnswer() { -// return choices.get(0).message.content; -// } -// } From a4f67f90dddf76e4e6e2ec527f22ea6957599968 Mon Sep 17 00:00:00 2001 From: koosco Date: Wed, 4 Dec 2024 21:27:16 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20quest=20=EC=B6=94=EC=B2=9C=20api=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/groom/orbit/ai/app/AiService.java | 3 + .../orbit/ai/app/openai/OpenAiService.java | 36 +++++++ .../goal/app/command/QuestCommandService.java | 21 ---- .../app/command/QuestRecommendService.java | 19 ++++ .../response/RecommendQuestResponseDto.java | 7 +- .../command/QuestCommandController.java | 6 -- .../command/QuestRecommendController.java | 25 +++++ .../templates/quest-recommend-prompt.txt | 99 +++++++++++++++++-- 8 files changed, 177 insertions(+), 39 deletions(-) create mode 100644 src/main/java/com/groom/orbit/goal/app/command/QuestRecommendService.java create mode 100644 src/main/java/com/groom/orbit/goal/controller/command/QuestRecommendController.java diff --git a/src/main/java/com/groom/orbit/ai/app/AiService.java b/src/main/java/com/groom/orbit/ai/app/AiService.java index d9d0518..06e0709 100644 --- a/src/main/java/com/groom/orbit/ai/app/AiService.java +++ b/src/main/java/com/groom/orbit/ai/app/AiService.java @@ -1,6 +1,7 @@ package com.groom.orbit.ai.app; import com.groom.orbit.goal.app.dto.request.CreateGoalRequestDto; +import com.groom.orbit.goal.app.dto.response.RecommendQuestResponseDto; import com.groom.orbit.member.app.dto.response.GetFeedbackResponseDto; import com.groom.orbit.resume.app.dto.GetResumeResponseDto; @@ -9,4 +10,6 @@ public interface AiService { GetFeedbackResponseDto getMemberFeedback(String interestJobs, GetResumeResponseDto dto); CreateGoalRequestDto recommendGoal(Long memberId); + + RecommendQuestResponseDto recommendQuest(Long memberId); } diff --git a/src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java b/src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java index 10bd7a7..f9f5552 100644 --- a/src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java +++ b/src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java @@ -16,6 +16,7 @@ import com.groom.orbit.ai.app.VectorService; import com.groom.orbit.ai.dao.vector.Vector; import com.groom.orbit.goal.app.dto.request.CreateGoalRequestDto; +import com.groom.orbit.goal.app.dto.response.RecommendQuestResponseDto; import com.groom.orbit.member.app.dto.response.GetFeedbackResponseDto; import com.groom.orbit.resume.app.dto.GetResumeResponseDto; import com.groom.orbit.resume.app.dto.ResumeResponseDto; @@ -37,6 +38,9 @@ public class OpenAiService implements AiService { @Value("classpath:/templates/goal-recommend-prompt.txt") private Resource goalRecommendPrompt; + @Value("classpath:/templates/quest-recommend-prompt.txt") + private Resource questRecommendPrompt; + private static final String PARAMETER_NEW_LINE_LIST_DELIMITER = "\n -"; private static final String PARAMETER_LIST_DELIMITER = ","; @@ -85,6 +89,38 @@ public CreateGoalRequestDto recommendGoal(Long memberId) { return converter.convert(response); } + @Override + public RecommendQuestResponseDto recommendQuest(Long memberId) { + BeanOutputConverter converter = + getConverter(RecommendQuestResponseDto.class); + String format = converter.getFormat(); + List similarVectors = vectorService.findSimilarVector(memberId); + + Vector myVector = similarVectors.getFirst(); + List othersVector = similarVectors.subList(1, similarVectors.size()); + + PromptTemplate promptTemplate = new PromptTemplate(goalRecommendPrompt); + String response = + callChatModel( + promptTemplate, + Map.of( + "job", + String.join(PARAMETER_LIST_DELIMITER, myVector.interestJobs()), + "myQuest", + String.join(PARAMETER_LIST_DELIMITER, myVector.quests()), + "questList", + String.join( + PARAMETER_LIST_DELIMITER, + othersVector.stream().flatMap(vector -> vector.goals().stream()).toList()), + "format", + format)); + return converter.convert(response); + } + + private PromptTemplate createPromptTemplate(Resource resource) { + return new PromptTemplate(resource); + } + private BeanOutputConverter getConverter(Class converterClass) { return new BeanOutputConverter<>(converterClass); } diff --git a/src/main/java/com/groom/orbit/goal/app/command/QuestCommandService.java b/src/main/java/com/groom/orbit/goal/app/command/QuestCommandService.java index 505615d..4b8915b 100644 --- a/src/main/java/com/groom/orbit/goal/app/command/QuestCommandService.java +++ b/src/main/java/com/groom/orbit/goal/app/command/QuestCommandService.java @@ -72,27 +72,6 @@ private static void updateSequence(List quests, Integer removeSequence) { } } - // public RecommendQuestResponseDto recommendQuest(Long memberId, Long goalId) { - // - // Goal goal = goalQueryService.findGoal(goalId); - // Member member = memberQueryService.findMember(memberId); - // MemberGoal memberGoal = memberGoalService.findMemberGoal(memberId, goalId); - // - // List quest = questQueryService.getRecommendedQuests(memberGoal.getMemberGoalId()); - // - // String questList = String.join(",", quest); - // - // QuestRecommendResponseDto questRecommendResponseDto = - // openAiClient.createQuestRecommend( - // QuestRecommendRequestDto.from( - // member.getInterestJobs().get(0).getJob().getName(), goal.getTitle(), - // questList)); - // - // String title = questRecommendResponseDto.getAnswer(); - // - // return RecommendQuestResponseDto.from(title); - // } - public CommonSuccessDto deleteOneQuest(Long questId) { questRepository.delete(questQueryService.findQuest(questId)); diff --git a/src/main/java/com/groom/orbit/goal/app/command/QuestRecommendService.java b/src/main/java/com/groom/orbit/goal/app/command/QuestRecommendService.java new file mode 100644 index 0000000..23aba8d --- /dev/null +++ b/src/main/java/com/groom/orbit/goal/app/command/QuestRecommendService.java @@ -0,0 +1,19 @@ +package com.groom.orbit.goal.app.command; + +import org.springframework.stereotype.Service; + +import com.groom.orbit.ai.app.AiService; +import com.groom.orbit.goal.app.dto.response.RecommendQuestResponseDto; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class QuestRecommendService { + + private final AiService aiService; + + public RecommendQuestResponseDto recommendQuest(Long memberId) { + return aiService.recommendQuest(memberId); + } +} diff --git a/src/main/java/com/groom/orbit/goal/app/dto/response/RecommendQuestResponseDto.java b/src/main/java/com/groom/orbit/goal/app/dto/response/RecommendQuestResponseDto.java index 1910064..72fa1ac 100644 --- a/src/main/java/com/groom/orbit/goal/app/dto/response/RecommendQuestResponseDto.java +++ b/src/main/java/com/groom/orbit/goal/app/dto/response/RecommendQuestResponseDto.java @@ -1,8 +1,3 @@ package com.groom.orbit.goal.app.dto.response; -public record RecommendQuestResponseDto(String title) { - - public static RecommendQuestResponseDto from(String title) { - return new RecommendQuestResponseDto(title); - } -} +public record RecommendQuestResponseDto(String title) {} diff --git a/src/main/java/com/groom/orbit/goal/controller/command/QuestCommandController.java b/src/main/java/com/groom/orbit/goal/controller/command/QuestCommandController.java index 2ed2b80..c176110 100644 --- a/src/main/java/com/groom/orbit/goal/controller/command/QuestCommandController.java +++ b/src/main/java/com/groom/orbit/goal/controller/command/QuestCommandController.java @@ -44,12 +44,6 @@ public ResponseDto updateQuestsSequence( return ResponseDto.ok(questUpdateService.updateQuestSequence(memberId, dtos)); } - // @GetMapping("/recommend/{memberGoalId}") - // public ResponseDto recommendQuest( - // @AuthMember Long memberId, @PathVariable Long memberGoalId) { - // return ResponseDto.ok(questCommandService.recommendQuest(memberId, memberGoalId)); - // } - @DeleteMapping("/{quest_id}") public ResponseDto deleteQuest( @AuthMember Long memberId, @PathVariable("quest_id") Long questId) { diff --git a/src/main/java/com/groom/orbit/goal/controller/command/QuestRecommendController.java b/src/main/java/com/groom/orbit/goal/controller/command/QuestRecommendController.java new file mode 100644 index 0000000..43cf610 --- /dev/null +++ b/src/main/java/com/groom/orbit/goal/controller/command/QuestRecommendController.java @@ -0,0 +1,25 @@ +package com.groom.orbit.goal.controller.command; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.groom.orbit.common.annotation.AuthMember; +import com.groom.orbit.common.dto.ResponseDto; +import com.groom.orbit.goal.app.command.QuestRecommendService; +import com.groom.orbit.goal.app.dto.response.RecommendQuestResponseDto; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/quest") +public class QuestRecommendController { + + private final QuestRecommendService questRecommendService; + + @PostMapping("/recommend/{memberGoalId}") + public ResponseDto recommendQuest(@AuthMember Long memberId) { + return ResponseDto.ok(questRecommendService.recommendQuest(memberId)); + } +} diff --git a/src/main/resources/templates/quest-recommend-prompt.txt b/src/main/resources/templates/quest-recommend-prompt.txt index cdd9d54..5752a51 100644 --- a/src/main/resources/templates/quest-recommend-prompt.txt +++ b/src/main/resources/templates/quest-recommend-prompt.txt @@ -1,8 +1,95 @@ -저의 관심 직무는 {job}입니다. 저의 폭표는 {goal}입니다. +# 상황 +저의 관심 직무는 {job}입니다. 저의 목표는 {goal}입니다. -{questList}는 같은 목표를 갖고 있는 사람들이 했던 퀘스트입니다. -{questList}에서 제가 수행하지 않은 퀘스트를 진행하려고 합니다. -{questList}에서 하나를 추천해주세요. +- {myQuest}는 제가 여태까지 작성했던 목표입니다. +- {questList}에 포함된 퀘스트 중 제가 아직 수행하지 않은 퀘스트를 선택하려고 합니다. +- {questList}에 포함된 퀘스트 중 저의 직무, 목표와 관련성이 가장 높은 퀘스트를 세 개 선택해주세요. + - {questList}에서 선택한 퀘스트가 {{job}}, {{goal}}과 관련이 있는지 판단해주세요. + - 만약 선택된 퀘스트가 {{job}} 또는 {{goal}} 과 관련이 없다면, {{job}}과 {{myGoal}}, {{myQuest}}을 기준으로 새로운 퀘스트를 만들어주세요. + - 새로운 퀘스트는 {myQuest}에 해당되면 안됩니다. 해당되지 않는다는 것은 비슷한 내용도 포함됩니다. -답변의 예시로는 "데이터 분석 참여"야 -이런 식으로 답변만 이야기해 \ No newline at end of file +- 퀘스트는 {{goal}}을 성취하기 위한 더 작은 목표입니다. + 예를 들어, + 1) + - input + ``` + { + "goal": "Spring Security 학습하기" + } + ``` + - output + ``` + { + "quests": [ + { + "title": "SecurityFilterChain에 대해 공부하기" + }, + { + "title": "SecurityContextHolder 이해하기" + }, + { + "title": "AuthenticationManager 이해하기" + } + ] + } + ``` + + 2) + - input + ``` + { + "goal": ""미적분학 A+ 받기" + } + ``` + - output + ``` + { + "quests": [ + { + "title": "대수학과 기본 함수 개념 복습하기" + }, + { + "title": "극한과 연속성의 개념 이해하기" + }, + { + "title": "미분의 정의와 기본 규칙 학습하기" + } + ] + } + ``` + + 3) + - input + ``` + { + "goal": "어플 디자인 기초 마스터하기" + } + ``` + - output + ``` + { + "quests": [ + { + "title": "UI/UX 디자인 기본 원칙 학습하기" + }, + { + "title": "디자인 툴(Figma, Sketch 등) 사용법 익히기" + }, + { + "title": "모바일 앱 디자인 사례 분석 및 모방 연습하기" + } + ] + } + ``` + +- 새롭게 제시되는 퀘스트는 {myQuest}에 있는 퀘스트보다 쉬운 내용이면 안됩니다. +- 새롭게 제시되는 퀘스트는 {myQuest}에 있는 내용을 복습하는 내용을 포함할 수 있습니다. + - 복습하는 내용을 제시하는 경우에는 {myQuest}에 있는 값 하나를 선택하고 "~ 복습하기"라는 형태여야 합니다. + - 복습하는 내용은 반환되는 3개의 퀘스트 중 최대 하나만 오거나 아예 오지 않을 수 있습니다. + +# 조건 +- 답변하기 전에 한 번 더 검토를 하고 {{questList}}에서 선택한 퀘스트가 {{job}}, {{goal}}과 관련이 있는지 판단해주세요. + + +# 응답 포맷 +{format} \ No newline at end of file From b1cd73d06ce323d62a28bf4fe3c15608f0fe0b1a Mon Sep 17 00:00:00 2001 From: koosco Date: Thu, 5 Dec 2024 00:52:02 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EC=B6=94=EC=B2=9C=20=ED=80=98?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EB=A5=BC=203=EA=B0=9C=20=EB=B0=98=ED=99=98?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/groom/orbit/ai/app/AiService.java | 4 +- .../orbit/ai/app/openai/OpenAiService.java | 43 ++++++---- .../ai/app/util/CustomOutputConverter.java | 54 +++++++++++++ .../app/command/QuestRecommendService.java | 4 +- .../RecommendQuestListResponseDto.java | 5 ++ .../command/QuestRecommendController.java | 6 +- .../templates/quest-recommend-prompt.txt | 80 ++++--------------- 7 files changed, 108 insertions(+), 88 deletions(-) create mode 100644 src/main/java/com/groom/orbit/ai/app/util/CustomOutputConverter.java create mode 100644 src/main/java/com/groom/orbit/goal/app/dto/response/RecommendQuestListResponseDto.java diff --git a/src/main/java/com/groom/orbit/ai/app/AiService.java b/src/main/java/com/groom/orbit/ai/app/AiService.java index 06e0709..556ecdc 100644 --- a/src/main/java/com/groom/orbit/ai/app/AiService.java +++ b/src/main/java/com/groom/orbit/ai/app/AiService.java @@ -1,7 +1,7 @@ package com.groom.orbit.ai.app; import com.groom.orbit.goal.app.dto.request.CreateGoalRequestDto; -import com.groom.orbit.goal.app.dto.response.RecommendQuestResponseDto; +import com.groom.orbit.goal.app.dto.response.RecommendQuestListResponseDto; import com.groom.orbit.member.app.dto.response.GetFeedbackResponseDto; import com.groom.orbit.resume.app.dto.GetResumeResponseDto; @@ -11,5 +11,5 @@ public interface AiService { CreateGoalRequestDto recommendGoal(Long memberId); - RecommendQuestResponseDto recommendQuest(Long memberId); + RecommendQuestListResponseDto recommendQuest(Long memberId); } diff --git a/src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java b/src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java index f9f5552..862b208 100644 --- a/src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java +++ b/src/main/java/com/groom/orbit/ai/app/openai/OpenAiService.java @@ -16,7 +16,7 @@ import com.groom.orbit.ai.app.VectorService; import com.groom.orbit.ai.dao.vector.Vector; import com.groom.orbit.goal.app.dto.request.CreateGoalRequestDto; -import com.groom.orbit.goal.app.dto.response.RecommendQuestResponseDto; +import com.groom.orbit.goal.app.dto.response.RecommendQuestListResponseDto; import com.groom.orbit.member.app.dto.response.GetFeedbackResponseDto; import com.groom.orbit.resume.app.dto.GetResumeResponseDto; import com.groom.orbit.resume.app.dto.ResumeResponseDto; @@ -49,7 +49,7 @@ public GetFeedbackResponseDto getMemberFeedback(String interestJobs, GetResumeRe getConverter(GetFeedbackResponseDto.class); String format = converter.getFormat(); - PromptTemplate promptTemplate = new PromptTemplate(aiFeedbackPrompt); + PromptTemplate promptTemplate = createPromptTemplate(aiFeedbackPrompt); String response = callChatModel( promptTemplate, @@ -74,46 +74,51 @@ public CreateGoalRequestDto recommendGoal(Long memberId) { Vector myVector = similarVectors.getFirst(); List othersVector = similarVectors.subList(1, similarVectors.size()); - PromptTemplate promptTemplate = new PromptTemplate(goalRecommendPrompt); + PromptTemplate promptTemplate = createPromptTemplate(goalRecommendPrompt); String response = callChatModel( promptTemplate, Map.of( - "job", String.join(PARAMETER_LIST_DELIMITER, myVector.interestJobs()), - "myGoal", String.join(PARAMETER_LIST_DELIMITER, myVector.goals()), + "job", + String.join(PARAMETER_LIST_DELIMITER, myVector.interestJobs()), + "myGoal", + String.join(PARAMETER_LIST_DELIMITER, myVector.goals()), "goalList", - String.join( - PARAMETER_LIST_DELIMITER, - othersVector.stream().flatMap(vector -> vector.goals().stream()).toList()), - "format", format)); + String.join( + PARAMETER_LIST_DELIMITER, + othersVector.stream().flatMap(vector -> vector.goals().stream()).toList()), + "format", + format)); return converter.convert(response); } @Override - public RecommendQuestResponseDto recommendQuest(Long memberId) { - BeanOutputConverter converter = - getConverter(RecommendQuestResponseDto.class); + public RecommendQuestListResponseDto recommendQuest(Long memberId) { + BeanOutputConverter converter = + getConverter(RecommendQuestListResponseDto.class); String format = converter.getFormat(); List similarVectors = vectorService.findSimilarVector(memberId); Vector myVector = similarVectors.getFirst(); List othersVector = similarVectors.subList(1, similarVectors.size()); - PromptTemplate promptTemplate = new PromptTemplate(goalRecommendPrompt); + PromptTemplate promptTemplate = createPromptTemplate(questRecommendPrompt); String response = callChatModel( promptTemplate, Map.of( "job", - String.join(PARAMETER_LIST_DELIMITER, myVector.interestJobs()), + convertListToString(myVector.interestJobs()), + "goal", + String.join(PARAMETER_LIST_DELIMITER, myVector.goals()), "myQuest", - String.join(PARAMETER_LIST_DELIMITER, myVector.quests()), + convertListToString(myVector.quests()), "questList", - String.join( - PARAMETER_LIST_DELIMITER, + convertListToString( othersVector.stream().flatMap(vector -> vector.goals().stream()).toList()), "format", format)); + log.info("response is {}", response); return converter.convert(response); } @@ -125,6 +130,10 @@ private BeanOutputConverter getConverter(Class converterClass) { return new BeanOutputConverter<>(converterClass); } + private String convertListToString(List data) { + return String.join(PARAMETER_LIST_DELIMITER, data); + } + private String convertResumeDtoToString(List data) { return String.join( PARAMETER_NEW_LINE_LIST_DELIMITER, data.stream().map(ResumeResponseDto::title).toList()); diff --git a/src/main/java/com/groom/orbit/ai/app/util/CustomOutputConverter.java b/src/main/java/com/groom/orbit/ai/app/util/CustomOutputConverter.java new file mode 100644 index 0000000..97482fa --- /dev/null +++ b/src/main/java/com/groom/orbit/ai/app/util/CustomOutputConverter.java @@ -0,0 +1,54 @@ +package com.groom.orbit.ai.app.util; + +import java.util.List; + +import org.springframework.ai.converter.BeanOutputConverter; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class CustomOutputConverter { + + private final BeanOutputConverter converter; + private final ObjectMapper objectMapper; + private final Class targetType; + + public CustomOutputConverter(Class converterClass) { + this.converter = new BeanOutputConverter<>(converterClass); + this.objectMapper = new ObjectMapper(); + this.targetType = converterClass; + } + + public T convertToObject(String text) { + return converter.convert(text); + } + + public List convertToList(String text) { + try { + // JSON 문자열을 파싱하여 리스트 형태로 변환 + text = preprocessJson(text); + return objectMapper.readValue(text, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to convert text to List: " + text, e); + } + } + + public String getFormat() { + return converter.getFormat(); + } + + private String preprocessJson(String text) { + text = text.trim(); + if (text.startsWith("```") && text.endsWith("```")) { + String[] lines = text.split("\n", 2); + if (lines[0].trim().equalsIgnoreCase("```json")) { + text = lines.length > 1 ? lines[1] : ""; + } else { + text = text.substring(3); + } + text = text.substring(0, text.length() - 3); + } + return text.trim(); + } +} diff --git a/src/main/java/com/groom/orbit/goal/app/command/QuestRecommendService.java b/src/main/java/com/groom/orbit/goal/app/command/QuestRecommendService.java index 23aba8d..ada8b6c 100644 --- a/src/main/java/com/groom/orbit/goal/app/command/QuestRecommendService.java +++ b/src/main/java/com/groom/orbit/goal/app/command/QuestRecommendService.java @@ -3,7 +3,7 @@ import org.springframework.stereotype.Service; import com.groom.orbit.ai.app.AiService; -import com.groom.orbit.goal.app.dto.response.RecommendQuestResponseDto; +import com.groom.orbit.goal.app.dto.response.RecommendQuestListResponseDto; import lombok.RequiredArgsConstructor; @@ -13,7 +13,7 @@ public class QuestRecommendService { private final AiService aiService; - public RecommendQuestResponseDto recommendQuest(Long memberId) { + public RecommendQuestListResponseDto recommendQuest(Long memberId) { return aiService.recommendQuest(memberId); } } diff --git a/src/main/java/com/groom/orbit/goal/app/dto/response/RecommendQuestListResponseDto.java b/src/main/java/com/groom/orbit/goal/app/dto/response/RecommendQuestListResponseDto.java new file mode 100644 index 0000000..82bd6ae --- /dev/null +++ b/src/main/java/com/groom/orbit/goal/app/dto/response/RecommendQuestListResponseDto.java @@ -0,0 +1,5 @@ +package com.groom.orbit.goal.app.dto.response; + +import java.util.List; + +public record RecommendQuestListResponseDto(List items) {} diff --git a/src/main/java/com/groom/orbit/goal/controller/command/QuestRecommendController.java b/src/main/java/com/groom/orbit/goal/controller/command/QuestRecommendController.java index 43cf610..3b77466 100644 --- a/src/main/java/com/groom/orbit/goal/controller/command/QuestRecommendController.java +++ b/src/main/java/com/groom/orbit/goal/controller/command/QuestRecommendController.java @@ -7,7 +7,7 @@ import com.groom.orbit.common.annotation.AuthMember; import com.groom.orbit.common.dto.ResponseDto; import com.groom.orbit.goal.app.command.QuestRecommendService; -import com.groom.orbit.goal.app.dto.response.RecommendQuestResponseDto; +import com.groom.orbit.goal.app.dto.response.RecommendQuestListResponseDto; import lombok.RequiredArgsConstructor; @@ -18,8 +18,8 @@ public class QuestRecommendController { private final QuestRecommendService questRecommendService; - @PostMapping("/recommend/{memberGoalId}") - public ResponseDto recommendQuest(@AuthMember Long memberId) { + @PostMapping("/recommend") + public ResponseDto recommendQuest(@AuthMember Long memberId) { return ResponseDto.ok(questRecommendService.recommendQuest(memberId)); } } diff --git a/src/main/resources/templates/quest-recommend-prompt.txt b/src/main/resources/templates/quest-recommend-prompt.txt index 5752a51..a76c2c3 100644 --- a/src/main/resources/templates/quest-recommend-prompt.txt +++ b/src/main/resources/templates/quest-recommend-prompt.txt @@ -4,83 +4,35 @@ - {myQuest}는 제가 여태까지 작성했던 목표입니다. - {questList}에 포함된 퀘스트 중 제가 아직 수행하지 않은 퀘스트를 선택하려고 합니다. - {questList}에 포함된 퀘스트 중 저의 직무, 목표와 관련성이 가장 높은 퀘스트를 세 개 선택해주세요. - - {questList}에서 선택한 퀘스트가 {{job}}, {{goal}}과 관련이 있는지 판단해주세요. - - 만약 선택된 퀘스트가 {{job}} 또는 {{goal}} 과 관련이 없다면, {{job}}과 {{myGoal}}, {{myQuest}}을 기준으로 새로운 퀘스트를 만들어주세요. + - {questList}에서 선택한 퀘스트가 {job}, {goal}과 관련이 있는지 판단해주세요. + - 만약 선택된 퀘스트가 {job} 또는 {goal} 과 관련이 없다면, {job}과 {goal}, {myQuest}을 기준으로 새로운 퀘스트를 만들어주세요. - 새로운 퀘스트는 {myQuest}에 해당되면 안됩니다. 해당되지 않는다는 것은 비슷한 내용도 포함됩니다. -- 퀘스트는 {{goal}}을 성취하기 위한 더 작은 목표입니다. +- 퀘스트는 {goal}을 성취하기 위한 더 작은 목표입니다. 예를 들어, 1) - input - ``` - { - "goal": "Spring Security 학습하기" - } - ``` + - "goal": "Spring Security 학습하기" - output - ``` - { - "quests": [ - { - "title": "SecurityFilterChain에 대해 공부하기" - }, - { - "title": "SecurityContextHolder 이해하기" - }, - { - "title": "AuthenticationManager 이해하기" - } - ] - } - ``` + - "title": "SecurityFilterChain에 대해 공부하기" + - "title": "SecurityContextHolder 이해하기" + - "title": "AuthenticationManager 이해하기" 2) - input - ``` - { - "goal": ""미적분학 A+ 받기" - } - ``` + - "goal": ""미적분학 A+ 받기" - output - ``` - { - "quests": [ - { - "title": "대수학과 기본 함수 개념 복습하기" - }, - { - "title": "극한과 연속성의 개념 이해하기" - }, - { - "title": "미분의 정의와 기본 규칙 학습하기" - } - ] - } - ``` + - "title": "대수학과 기본 함수 개념 복습하기" + - "title": "극한과 연속성의 개념 이해하기" + - "title": "미분의 정의와 기본 규칙 학습하기" 3) - input - ``` - { - "goal": "어플 디자인 기초 마스터하기" - } - ``` + - "goal": "어플 디자인 기초 마스터하기" - output - ``` - { - "quests": [ - { - "title": "UI/UX 디자인 기본 원칙 학습하기" - }, - { - "title": "디자인 툴(Figma, Sketch 등) 사용법 익히기" - }, - { - "title": "모바일 앱 디자인 사례 분석 및 모방 연습하기" - } - ] - } - ``` + - "title": "UI/UX 디자인 기본 원칙 학습하기" + - "title": "디자인 툴(Figma, Sketch 등) 사용법 익히기" + - "title": "모바일 앱 디자인 사례 분석 및 모방 연습하기" - 새롭게 제시되는 퀘스트는 {myQuest}에 있는 퀘스트보다 쉬운 내용이면 안됩니다. - 새롭게 제시되는 퀘스트는 {myQuest}에 있는 내용을 복습하는 내용을 포함할 수 있습니다. @@ -88,7 +40,7 @@ - 복습하는 내용은 반환되는 3개의 퀘스트 중 최대 하나만 오거나 아예 오지 않을 수 있습니다. # 조건 -- 답변하기 전에 한 번 더 검토를 하고 {{questList}}에서 선택한 퀘스트가 {{job}}, {{goal}}과 관련이 있는지 판단해주세요. +- 답변하기 전에 한 번 더 검토를 하고 {questList}에서 선택한 퀘스트가 {job}, {goal}과 관련이 있는지 판단해주세요. # 응답 포맷