-
Notifications
You must be signed in to change notification settings - Fork 0
반환 로직 수정 #355
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
반환 로직 수정 #355
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| toolchainVersion=17 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| package com.samhap.kokomen.global.external.bedrock; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.samhap.kokomen.global.exception.ExternalApiException; | ||
| import java.util.List; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ContentBlock; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ConverseRequest; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.InferenceConfiguration; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.Message; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.StopReason; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.SystemContentBlock; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ToolConfiguration; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ToolUseBlock; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| public class BedrockConverseClient { | ||
|
|
||
| private final BedrockRuntimeClient bedrockRuntimeClient; | ||
| private final BedrockConverseProperties properties; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| public BedrockConverseClient( | ||
| BedrockRuntimeClient bedrockRuntimeClient, | ||
| BedrockConverseProperties properties, | ||
| ObjectMapper objectMapper | ||
| ) { | ||
| this.bedrockRuntimeClient = bedrockRuntimeClient; | ||
| this.properties = properties; | ||
| this.objectMapper = objectMapper; | ||
| } | ||
|
|
||
| public ConverseResponse converse( | ||
| List<SystemContentBlock> systemMessages, | ||
| List<Message> messages, | ||
| ToolConfiguration toolConfiguration, | ||
| int maxTokens | ||
| ) { | ||
| ConverseRequest request = ConverseRequest.builder() | ||
| .modelId(properties.modelId()) | ||
| .system(systemMessages) | ||
| .messages(messages) | ||
| .toolConfig(toolConfiguration) | ||
| .inferenceConfig(InferenceConfiguration.builder() | ||
| .maxTokens(maxTokens) | ||
| .temperature(properties.temperature()) | ||
| .build()) | ||
| .build(); | ||
| return bedrockRuntimeClient.converse(request); | ||
| } | ||
|
|
||
| public ToolUseBlock extractToolUse(ConverseResponse response, String expectedToolName) { | ||
| if (response.stopReason() != StopReason.TOOL_USE) { | ||
| throw new ExternalApiException( | ||
| "Bedrock 응답이 tool_use가 아닙니다. stopReason=" + response.stopReason() | ||
| + ", expected=" + expectedToolName); | ||
| } | ||
| return response.output().message().content().stream() | ||
| .filter(content -> content.toolUse() != null) | ||
| .map(ContentBlock::toolUse) | ||
| .filter(toolUse -> expectedToolName.equals(toolUse.name())) | ||
| .findFirst() | ||
| .orElseThrow(() -> new ExternalApiException( | ||
| "Bedrock 응답에 tool_use 블록이 없습니다. expected=" + expectedToolName)); | ||
| } | ||
|
|
||
| public <T> T parseToolInput(ToolUseBlock toolUse, Class<T> type) { | ||
| try { | ||
| Object javaObject = DocumentJsonConverter.toJavaObject(toolUse.input()); | ||
| return objectMapper.convertValue(javaObject, type); | ||
| } catch (Exception e) { | ||
| throw new ExternalApiException("Bedrock toolUse 파싱 실패: toolName=" + toolUse.name(), e); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.samhap.kokomen.global.external.bedrock; | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| @ConfigurationProperties(prefix = "aws.bedrock") | ||
| public record BedrockConverseProperties( | ||
| String modelId, | ||
| Integer proceedMaxTokens, | ||
| Integer endMaxTokens, | ||
| Integer answerFeedbackMaxTokens, | ||
| Integer resumeQuestionMaxTokens, | ||
| Integer resumeEvaluationMaxTokens, | ||
| Float temperature | ||
| ) { | ||
|
Comment on lines
+5
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# validation 관련 의존성/사용 여부를 읽기 전용으로 확인
fd -i "build.gradle*" | xargs -I{} rg -n "spring-boot-starter-validation|jakarta.validation|hibernate-validator" {}
rg -n "`@ConfigurationProperties`|`@Validated`|jakarta.validation.constraints"Repository: samhap-soft/kokomen-backend Length of output: 3433 🏁 Script executed: # Search for other `@ConfigurationProperties` usage to see if they use validation
rg -A 10 "`@ConfigurationProperties`" --type java | head -60Repository: samhap-soft/kokomen-backend Length of output: 1374 🏁 Script executed: cat -n src/main/java/com/samhap/kokomen/global/external/bedrock/BedrockConverseProperties.javaRepository: samhap-soft/kokomen-backend Length of output: 641 Bedrock 설정값에 바인딩 단계 검증을 추가하세요. 현재는 🔧 제안 수정안 import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+@Validated
`@ConfigurationProperties`(prefix = "aws.bedrock")
public record BedrockConverseProperties(
- String modelId,
- Integer proceedMaxTokens,
- Integer endMaxTokens,
- Integer answerFeedbackMaxTokens,
- Integer resumeQuestionMaxTokens,
- Integer resumeEvaluationMaxTokens,
- Float temperature
+ `@NotBlank` String modelId,
+ `@NotNull` `@Positive` Integer proceedMaxTokens,
+ `@NotNull` `@Positive` Integer endMaxTokens,
+ `@NotNull` `@Positive` Integer answerFeedbackMaxTokens,
+ `@NotNull` `@Positive` Integer resumeQuestionMaxTokens,
+ `@NotNull` `@Positive` Integer resumeEvaluationMaxTokens,
+ `@NotNull` `@Min`(0) `@Max`(1) Float temperature
) {
}🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.samhap.kokomen.global.external.bedrock; | ||
|
|
||
| import com.samhap.kokomen.global.exception.ExternalApiException; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.Map; | ||
| import software.amazon.awssdk.core.document.Document; | ||
|
|
||
| public final class DocumentJsonConverter { | ||
|
|
||
| private DocumentJsonConverter() { | ||
| } | ||
|
|
||
| public static Object toJavaObject(Document document) { | ||
| if (document == null || document.isNull()) { | ||
| return null; | ||
| } | ||
| if (document.isBoolean()) { | ||
| return document.asBoolean(); | ||
| } | ||
| if (document.isString()) { | ||
| return document.asString(); | ||
| } | ||
| if (document.isNumber()) { | ||
| return document.asNumber().bigDecimalValue(); | ||
| } | ||
| if (document.isList()) { | ||
| return document.asList().stream() | ||
| .map(DocumentJsonConverter::toJavaObject) | ||
| .toList(); | ||
| } | ||
| if (document.isMap()) { | ||
| Map<String, Object> map = new LinkedHashMap<>(); | ||
| document.asMap().forEach((key, value) -> map.put(key, toJavaObject(value))); | ||
| return map; | ||
| } | ||
| throw new ExternalApiException("알 수 없는 Document 타입입니다."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package com.samhap.kokomen.interview.external; | ||
|
|
||
| import com.samhap.kokomen.answer.domain.AnswerRank; | ||
| import com.samhap.kokomen.global.annotation.ExecutionTimer; | ||
| import com.samhap.kokomen.global.exception.ExternalApiException; | ||
| import com.samhap.kokomen.global.external.bedrock.BedrockConverseClient; | ||
| import com.samhap.kokomen.global.external.bedrock.BedrockConverseProperties; | ||
| import com.samhap.kokomen.interview.external.dto.request.InterviewBedrockRequestFactory; | ||
| import com.samhap.kokomen.interview.external.dto.response.AnswerFeedbackOnlyResponse; | ||
| import com.samhap.kokomen.interview.tool.QuestionAndAnswers; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ToolUseBlock; | ||
|
|
||
| @Slf4j | ||
| @ExecutionTimer | ||
| @Component | ||
| public class AnswerFeedbackBedrockClient { | ||
|
|
||
| private final BedrockConverseClient converseClient; | ||
| private final BedrockConverseProperties properties; | ||
|
|
||
| public AnswerFeedbackBedrockClient( | ||
| BedrockConverseClient converseClient, | ||
| BedrockConverseProperties properties | ||
| ) { | ||
| this.converseClient = converseClient; | ||
| this.properties = properties; | ||
| } | ||
|
|
||
| public String requestAnswerFeedback(QuestionAndAnswers questionAndAnswers, AnswerRank curAnswerRank) { | ||
| ConverseResponse response = converseClient.converse( | ||
| InterviewBedrockRequestFactory.createAnswerFeedbackSystem(), | ||
| InterviewBedrockRequestFactory.createAnswerFeedbackMessages(questionAndAnswers, curAnswerRank), | ||
| InterviewBedrockRequestFactory.createAnswerFeedbackToolConfig(), | ||
| properties.answerFeedbackMaxTokens()); | ||
| ToolUseBlock toolUse = converseClient.extractToolUse(response, InterviewBedrockRequestFactory.ANSWER_FEEDBACK_TOOL_NAME); | ||
| AnswerFeedbackOnlyResponse parsed = converseClient.parseToolInput(toolUse, AnswerFeedbackOnlyResponse.class); | ||
| if (parsed.feedback() == null || parsed.feedback().isBlank()) { | ||
| throw new ExternalApiException("Bedrock 답변 피드백 응답이 비어있습니다."); | ||
| } | ||
| return parsed.feedback(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| package com.samhap.kokomen.interview.external; | ||
|
|
||
| import com.samhap.kokomen.global.annotation.ExecutionTimer; | ||
| import com.samhap.kokomen.global.external.bedrock.BedrockConverseClient; | ||
| import com.samhap.kokomen.global.external.bedrock.BedrockConverseProperties; | ||
| import com.samhap.kokomen.interview.external.dto.request.InterviewBedrockRequestFactory; | ||
| import com.samhap.kokomen.interview.external.dto.response.BedrockConverseResponse; | ||
| import com.samhap.kokomen.interview.tool.QuestionAndAnswers; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; | ||
| import software.amazon.awssdk.services.bedrockruntime.model.ToolUseBlock; | ||
|
|
||
| @Slf4j | ||
| @ExecutionTimer | ||
| @Component | ||
| public class InterviewProceedBedrockClient { | ||
|
|
||
| private final BedrockConverseClient converseClient; | ||
| private final BedrockConverseProperties properties; | ||
|
|
||
| public InterviewProceedBedrockClient( | ||
| BedrockConverseClient converseClient, | ||
| BedrockConverseProperties properties | ||
| ) { | ||
| this.converseClient = converseClient; | ||
| this.properties = properties; | ||
| } | ||
|
|
||
| public BedrockConverseResponse requestToBedrock(QuestionAndAnswers questionAndAnswers) { | ||
| if (questionAndAnswers.isProceedRequest()) { | ||
| return requestProceed(questionAndAnswers); | ||
| } | ||
| return requestEnd(questionAndAnswers); | ||
| } | ||
|
|
||
| private BedrockConverseResponse requestProceed(QuestionAndAnswers questionAndAnswers) { | ||
| ConverseResponse response = converseClient.converse( | ||
| InterviewBedrockRequestFactory.createProceedSystem(), | ||
| InterviewBedrockRequestFactory.createProceedMessages(questionAndAnswers), | ||
| InterviewBedrockRequestFactory.createProceedToolConfig(), | ||
| properties.proceedMaxTokens()); | ||
| ToolUseBlock toolUse = converseClient.extractToolUse(response, InterviewBedrockRequestFactory.PROCEED_TOOL_NAME); | ||
| return new BedrockConverseResponse(toolUse.input()); | ||
| } | ||
|
|
||
| private BedrockConverseResponse requestEnd(QuestionAndAnswers questionAndAnswers) { | ||
| ConverseResponse response = converseClient.converse( | ||
| InterviewBedrockRequestFactory.createEndSystem(), | ||
| InterviewBedrockRequestFactory.createProceedMessages(questionAndAnswers), | ||
| InterviewBedrockRequestFactory.createEndToolConfig(), | ||
| properties.endMaxTokens()); | ||
| ToolUseBlock toolUse = converseClient.extractToolUse(response, InterviewBedrockRequestFactory.END_TOOL_NAME); | ||
| return new BedrockConverseResponse(toolUse.input()); | ||
| } | ||
| } |
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.
response.output().message().content()를 호출할 때 중간 객체(output,message)가 null일 경우NullPointerException이 발생할 수 있습니다.stopReason이TOOL_USE이더라도 방어적인 코드를 위해 null 체크를 추가하는 것이 안전합니다.