-
Notifications
You must be signed in to change notification settings - Fork 0
[GOMO-39] LLM 퀘스트 생성 마이그레이션 #97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nurdy-kim
wants to merge
18
commits into
develop
Choose a base branch
from
feature-39/quest-generator-migration
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
c124b8b
feat: 퀘스트 생성 LLM 마이그레이션
nurdy-kim b505f33
feat: 퀘스트 생성 LLM 마이그레이션
nurdy-kim 801f692
Merge branch 'feature-39/quest-generator-migration' of https://github…
nurdy-kim 59a4e37
fix: style error 수정
nurdy-kim f83f88f
feat: 리뷰 사항 반영(prompt 별도 text 파일, LlmClientPortOut interface 생성, Styl…
nurdy-kim d53c9e9
feat: 퀘스트 생성 LLM 마이그레이션
nurdy-kim db1bd16
fix: style error 수정
nurdy-kim 826c1b1
feat: 리뷰 사항 반영(prompt 별도 text 파일, LlmClientPortOut interface 생성, Styl…
nurdy-kim a7ccb3f
Merge branch 'feature-39/quest-generator-migration' of https://github…
nurdy-kim 3c496ad
feat: 리뷰 수정사항 반영
nurdy-kim 979f468
[GOMO-234] api 문서 수정 (#98)
jhl221123 6670d71
[GOMO-235] 위젯 스냅샷 조회 기능 추가 (#100)
jhl221123 90628e0
[GOMO-236] 헥사고날 아키텍처 전환 (#101)
jhl221123 2e3e5e4
[GOMO-239] 인증 정보 주입 방식 수정 (#102)
jhl221123 81f4f88
[GOMO-232] 스프링 모듈리스 도입 (#103)
jhl221123 b4f3ee8
feat: 퀘스트 생성 LLM 마이그레이션
nurdy-kim 18c0ca4
feat: 리뷰 사항 반영(prompt 별도 text 파일, LlmClientPortOut interface 생성, Styl…
nurdy-kim bd36080
feat: LLM Refactoring 관련 review 사항 반영
nurdy-kim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 8 additions & 1 deletion
9
src/main/java/com/gomo/app/support/llm/application/GenerateTextCommand.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,11 @@ | ||
| package com.gomo.app.support.llm.application; | ||
|
|
||
| public record GenerateTextCommand() { | ||
| import java.util.Map; | ||
|
|
||
| import com.gomo.app.core.quest.domain.model.quest.QuestType; | ||
|
|
||
| public record GenerateTextCommand(Map<String, Long> interests, QuestType questType, int amount) { | ||
| public static GenerateTextCommand of(Map<String, Long> interests, QuestType questType, int amount) { | ||
| return new GenerateTextCommand(interests, questType, amount); | ||
| } | ||
| } |
7 changes: 5 additions & 2 deletions
7
src/main/java/com/gomo/app/support/llm/application/GenerateTextDto.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| package com.gomo.app.support.llm.application; | ||
|
|
||
| public record GenerateTextDto() { | ||
| } | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| public record GenerateTextDto(Map<String, List<String>> generatedText) {} | ||
|
|
14 changes: 14 additions & 0 deletions
14
src/main/java/com/gomo/app/support/llm/application/LlmClientPortOut.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.gomo.app.support.llm.application; | ||
|
|
||
| import com.gomo.app.core.interest.application.port.dto.RegistrantDto; | ||
|
|
||
| public interface LlmClientPortOut { | ||
|
|
||
| /** | ||
| * Retrieves the essential details of a single registrant by id. | ||
| * | ||
| * @param {@link GenerateTextCommand} The information to generate Quests with LLM. | ||
| * @return A {@link GenerateTextDto} containing the GeneratedText data. | ||
| */ | ||
| GenerateTextDto generate(GenerateTextCommand command); | ||
| } |
22 changes: 22 additions & 0 deletions
22
src/main/java/com/gomo/app/support/llm/exception/LlmErrorCode.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| package com.gomo.app.support.llm.exception; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public enum LlmErrorCode { | ||
| GEMINI_API_ERROR(500, "LLM-GEN-001", "An error occurred while call GeminiAPI"), | ||
| EMPTY_RESPONSE(500, "LLM-GEN-002", "Gemini API response blank"), | ||
| INVALID_RESPONSE_FORMAT(500, "LLM-GEN-003", "Gemini Response format is invalid"), | ||
| INVALID_JSON_FORMAT(500, "LLM-GEN-004", "Gemini Response JSON format is invalid"), | ||
| PARSING_ERROR(500, "LLM-GEN-005", "An error occurred while parse data String to Map"); | ||
|
|
||
| private final int httpStatus; | ||
| private final String errorCode; | ||
| private final String message; | ||
|
|
||
| LlmErrorCode(int httpStatus, String errorCode, String message) { | ||
| this.httpStatus = httpStatus; | ||
| this.errorCode = errorCode; | ||
| this.message = message; | ||
| } | ||
| } |
13 changes: 13 additions & 0 deletions
13
src/main/java/com/gomo/app/support/llm/exception/LlmException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.gomo.app.support.llm.exception; | ||
|
|
||
| import com.gomo.app.common.exception.ApplicationException; | ||
|
|
||
| public class LlmException extends ApplicationException { | ||
| public LlmException(LlmErrorCode errorCode) { | ||
| super(errorCode.getHttpStatus(), errorCode.name(), errorCode.getMessage()); | ||
| } | ||
|
|
||
| public LlmException(LlmErrorCode errorCode, Throwable cause) { | ||
| super(errorCode.getHttpStatus(), errorCode.name(), errorCode.getMessage(), cause); | ||
| } | ||
| } |
97 changes: 97 additions & 0 deletions
97
src/main/java/com/gomo/app/support/llm/infrastructure/GeminiApiAdapter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package com.gomo.app.support.llm.infrastructure; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.web.client.RestClient; | ||
|
|
||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||
| import com.fasterxml.jackson.core.type.TypeReference; | ||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.gomo.app.common.arch.Adapter; | ||
| import com.gomo.app.support.llm.application.GenerateTextCommand; | ||
| import com.gomo.app.support.llm.application.GenerateTextDto; | ||
| import com.gomo.app.support.llm.application.LlmClientPortOut; | ||
| import com.gomo.app.support.llm.exception.LlmErrorCode; | ||
| import com.gomo.app.support.llm.exception.LlmException; | ||
| import com.gomo.app.support.llm.util.PromptLoader; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| @Adapter | ||
| class GeminiApiAdapter implements LlmClientPortOut { | ||
| private final RestClient restClient; | ||
|
|
||
| @Value("${spring.ai.openai.api-key}") | ||
| private String apiKey; | ||
|
|
||
| @Value("${spring.ai.openai.chat.options.model}") | ||
| private String model; | ||
|
|
||
| private PromptLoader promptLoader; | ||
|
|
||
| private static final String GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions"; | ||
|
|
||
| public GenerateTextDto generate(GenerateTextCommand command) { | ||
| try { | ||
| String apiKey = "Bearer " + this.apiKey; | ||
| GeminiRequest request = createGeminiRequest(command); | ||
|
|
||
| GeminiResponse response = restClient.post() | ||
| .uri(GEMINI_API_URL) | ||
| .header("Authorization", apiKey) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .body(request) | ||
| .retrieve() | ||
| .body(GeminiResponse.class); | ||
|
|
||
| return convertToGenerateTextDto(response); | ||
| } catch (Exception e) { | ||
| log.error("Failed to generate text with Gemini API", e); | ||
| throw new RuntimeException("Gemini API 호출 중 오류가 발생 했습니다.", e); | ||
| } | ||
| } | ||
|
|
||
| private GeminiRequest createGeminiRequest(GenerateTextCommand command) { | ||
| return GeminiRequest.createPrompt(command.interests(), command.questType(), command.amount(), promptLoader); | ||
| } | ||
|
|
||
| private GenerateTextDto convertToGenerateTextDto(GeminiResponse response) { | ||
| if (response.choices() == null || response.choices().isEmpty()) { | ||
| throw new LlmException(LlmErrorCode.EMPTY_RESPONSE); | ||
| } | ||
|
|
||
| String generatedText = response.choices().get(0).message().content(); | ||
| return new GenerateTextDto(parseDtofromText(generatedText)); | ||
| } | ||
|
|
||
| private Map<String, List<String>> parseDtofromText(String text) { | ||
| try { | ||
| String cleanText = text.trim(); | ||
|
|
||
| if (cleanText.startsWith("```json")) { | ||
| cleanText = cleanText.substring(7); | ||
| } | ||
|
|
||
| if (cleanText.endsWith("```")) { | ||
| cleanText = cleanText.substring(0, cleanText.length() - 3); | ||
| } | ||
|
|
||
| cleanText = cleanText.trim(); | ||
| ObjectMapper objectMapper = new ObjectMapper(); | ||
| TypeReference<Map<String, List<String>>> typeRef = new TypeReference<Map<String, List<String>>>() { | ||
| }; | ||
|
|
||
| return objectMapper.readValue(cleanText, typeRef); | ||
| } catch (JsonProcessingException e) { | ||
| throw new LlmException(LlmErrorCode.INVALID_JSON_FORMAT); | ||
| } catch (Exception e) { | ||
| throw new LlmException(LlmErrorCode.PARSING_ERROR); | ||
| } | ||
| } | ||
| } | ||
39 changes: 39 additions & 0 deletions
39
src/main/java/com/gomo/app/support/llm/infrastructure/GeminiRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package com.gomo.app.support.llm.infrastructure; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| import com.gomo.app.support.llm.util.PromptLoader; | ||
| import com.gomo.app.core.quest.domain.model.quest.QuestType; | ||
|
|
||
| public record GeminiRequest( | ||
|
||
| String model, | ||
| String reasoning_effort, | ||
| List<Prompt> messages | ||
| ) { | ||
| private static Prompt createSystemPrompt(PromptLoader promptLoader){ | ||
| String content = promptLoader.loadPrompt("quest/system-prompt.txt"); | ||
| return new Prompt("system", content); | ||
| } | ||
|
|
||
| private static Prompt createUserPrompt(Map<String, Long> interests, QuestType questType, int amount){ | ||
nurdy-kim marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| String interestsText = interests.entrySet().stream() | ||
| .map(entry -> entry.getKey() + "(숙련도 : " + entry.getValue() + ")") | ||
| .collect(Collectors.joining(", ")); | ||
|
|
||
| String promptText = String.format( | ||
| "**개인 정보**\n- 관심사: %s \n- 퀘스트 타입: %s \n- 퀘스트 수: %s", interestsText, questType.name(), amount | ||
| ); | ||
|
|
||
| return new Prompt("user", promptText); | ||
| } | ||
|
|
||
| public static GeminiRequest createPrompt(Map<String, Long> interests, QuestType questType, int amount, PromptLoader promptLoader){ | ||
| Prompt system = createSystemPrompt(promptLoader); | ||
| Prompt user = createUserPrompt(interests, questType, amount); | ||
| return new GeminiRequest("gemini-2.5-flash", "none", List.of(system, user)); | ||
| } | ||
|
|
||
| public record Prompt(String role, String content){} | ||
| } | ||
49 changes: 49 additions & 0 deletions
49
src/main/java/com/gomo/app/support/llm/infrastructure/GeminiResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.gomo.app.support.llm.infrastructure; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * | ||
| * @param choices: a list of generated contents. normally returns 1 content. | ||
| * @param created: response created time. (Unix timestamp, unit: s) | ||
| * @param id: Unique ID. | ||
| * @param model: Gemini Model name who create response( normally: gemini-2.5-flash ) | ||
| * @param object: API response object specification string( normally: "chat.completion") | ||
| * @param usage: Total Usage. | ||
| */ | ||
| public record GeminiResponse( | ||
nurdy-kim marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| List<Choice> choices, | ||
| int created, | ||
| String id, | ||
| String model, | ||
| String object, | ||
| GeminiUsage usage | ||
| ) { | ||
| /** | ||
| * | ||
| * @param finish_reason: reason which stopped model( ex: "stop", "length") | ||
| * @param index: index of Choice array. | ||
| * @param message: information of generated model (response and role) | ||
| */ | ||
| public record Choice(String finish_reason, int index, Message message) { | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param content: generated text contents(in this project. generated quest) | ||
| * @param role: role ("user", "assistant") | ||
| */ | ||
| public record Message(String content, String role) { | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param completion_tokens : Total usage to Response Token. | ||
| * @param prompt_tokens : Total usage to Prompt Token. | ||
| * @param total_tokens : Total usage of Token. | ||
| */ | ||
| public record GeminiUsage(int completion_tokens, int prompt_tokens, int total_tokens) { | ||
| } | ||
| } | ||
|
|
||
|
|
||
20 changes: 20 additions & 0 deletions
20
src/main/java/com/gomo/app/support/llm/util/PromptLoader.java
nurdy-kim marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.gomo.app.support.llm.util; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.nio.file.Files; | ||
|
|
||
| import org.springframework.core.io.ClassPathResource; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| public class PromptLoader { | ||
| public String loadPrompt(String promptPath) { | ||
| try { | ||
| ClassPathResource resource = new ClassPathResource("prompts/" + promptPath); | ||
| return Files.readString(resource.getFile().toPath(), StandardCharsets.UTF_8); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException("Failed to load prompt: " + promptPath, e); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # 역할: 당신은 개인 맞춤형 도전 과제(퀘스트)를 생성하는 AI 전문가입니다.\n# 목표: 주어진 '개인 정보'와 '요청'을 바탕으로, 아래 명시된 '출력 형식'을 반드시 준수하여 사용자를 위한 도전 과제를 생성합니다.\n\n## 도전 과제 생성 가이드라인\n1. 내용 생성 (Content Generation)\n- 개인화: '개인 정보'의 '관심사'와 '숙련도'를 바탕으로 도전 과제를 생성합니다.\n- 소요 시간: '퀘스트 타입'과 '숙련도'를 고려하여, 현실적으로 달성 가능한 분량의 과제를 제안합니다.\n-- Daily (30-90분): e.g., \"알고리즘 1문제 풀이\", \"TIL 작성\", \"기술 블로그 1개 정독\"\n-- Weekly (300-900분): e.g., \"기술 블로그 1개 작성\", \"개인 프로젝트 PR 1개 작업\", \"개인 프로젝트 로깅 시스템 점검\"\n-- Monthly (1000-1500분): e.g., \"서킷 브레이커 학습 후 개인 프로젝트에 적용\", \"오픈 소스 1회 기여\", \"멀티 모듈 학습 및 토이 프로젝트 진행\"\n- 문체: 내용은 30자 이내의 간결한 명사형 또는 동사형으로 끝맺습니다. (e.g., \"알고리즘 문제 풀이\", \"기술 블로그 작성\")\n\n2. 생성 규칙 (Generation Rules)\n- 고유성: '이전 수행 퀘스트' 목록을 참고하여, 중복된 과제를 생성하지 않습니다.\n- 균형: 요청된 각 '관심사'에 대해 동일한 개수의 과제를 생성해야 합니다.\n\n## 출력 형식 (Output Format)\n- [중요] 반드시 아래 명시된 JSON 형식으로만 응답해야 합니다.\n- JSON 구조:\n-- Key: '관심사' 이름 (String)\n-- Value: 해당 관심사에 대한 도전 과제 목록 (Array<String>)\n- JSON 출력 예시:\n```json\n{\n\"알고리즘\": [\n\"백준 골드 IV 문제 풀이\",\n\"해시 테이블 개념 정리 및 블로깅\"\n],\n\"백엔드\": [\n\"Spring Security 적용하여 JWT 인증 구현\",\n\"JPA N+1 문제 원인 분석 및 해결\"\n]\n}\n``` |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 클래스는 llm 호출과 응답 반환에 대한 책임만 가지는 것이 이상적이라 생각합니다.
무엇을 받고, 무엇을 생성하는지 구체적으로 알게 되면 확장성과 유지 보수성이 떨어질 것이라 생각합니다.
application 계층에서 요청에 적합한 프롬프트를 가공하고, llm 이 생성한 응답을 필요에 따라 파싱하면 조금 더 응집도가 높은 코드가 될 것 같습니다. 현재 driving, driven port 모두
GenerateTextCommand를 사용하고 있는데, 이것도 분리가 필요하겠군요!