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
Expand Up @@ -14,6 +14,7 @@
import com.shyashyashya.refit.global.gemini.GeminiClient;
import com.shyashyashya.refit.global.gemini.GeminiGenerateRequest;
import com.shyashyashya.refit.global.gemini.GeminiGenerateResponse;
import com.shyashyashya.refit.global.gemini.GenerateModel;
import com.shyashyashya.refit.global.gemini.StarAnalysisGeminiResponse;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
Expand Down Expand Up @@ -43,7 +44,10 @@ public CompletableFuture<StarAnalysisDto> createStarAnalysis(Long qnaSetId) {

log.info("Send star analysis generate request to gemini. qnaSetId: {}", qnaSetId);
CompletableFuture<GeminiGenerateResponse> reqFuture =
geminiClient.sendAsyncRequest(requestBody, STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC);
// geminiClient.sendAsyncRequest(requestBody, GenerateModel.GEMINI_2_5_FLASH_LITE,
// STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC);
geminiClient.sendAsyncTextGenerateRequest(
requestBody, GenerateModel.GEMMA_3_27B_IT, STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC);

return reqFuture
.thenApplyAsync(
Expand Down Expand Up @@ -72,6 +76,12 @@ private StarAnalysisDto processSuccessRequest(

private StarAnalysisGeminiResponse parseStarAnalysisGeminiResponse(String text) {
try {
if (text.startsWith("```json\n")) {
text = text.substring("```json\n".length());
}
if (text.endsWith("```")) {
text = text.substring(0, text.length() - "```".length());
}
Comment on lines +79 to +84
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이 파트가 전에 말씀하셨던 모델별 응답 형식의 차이? 와 관련된 로직일까요? 아니면 테스트 용으로 로직을 작성하는 과정에서 필요하여 작성된 로직일까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

특정 모델이 응답에 ```json ```을 붙이는 것을 해결하기 위한 로직입니다!

return objectMapper.readValue(text, StarAnalysisGeminiResponse.class);
} catch (JsonProcessingException e) {
throw new CustomException(STAR_ANALYSIS_PARSING_FAILED);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,47 @@
package com.shyashyashya.refit.domain.user.api;

import static com.shyashyashya.refit.domain.qnaset.constant.StarAnalysisConstant.STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC;
import static com.shyashyashya.refit.global.exception.ErrorCode.TEXT_EMBEDDING_CREATE_FAILED;
import static com.shyashyashya.refit.global.exception.ErrorCode.USER_NOT_FOUND;
import static com.shyashyashya.refit.global.model.ResponseCode.COMMON200;
import static com.shyashyashya.refit.global.model.ResponseCode.COMMON204;

import com.shyashyashya.refit.domain.user.repository.UserRepository;
import com.shyashyashya.refit.global.auth.repository.RefreshTokenRepository;
import com.shyashyashya.refit.global.dto.ApiResponse;
import com.shyashyashya.refit.global.exception.CustomException;
import com.shyashyashya.refit.global.gemini.GeminiClient;
import com.shyashyashya.refit.global.gemini.GeminiEmbeddingRequest;
import com.shyashyashya.refit.global.gemini.GeminiEmbeddingResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Test Auth/User API", description = "개발용 테스트 인증/인가 API입니다.")
@Tag(name = "Test API", description = "개발용 테스트 API입니다.")
@RestController
@RequestMapping("/test/user")
@RequiredArgsConstructor
@Slf4j
public class TestUserController {

private final UserRepository userRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final GeminiClient geminiClient;
private final Executor geminiPostProcessExecutor;

@Operation(summary = "(테스트용) 유저를 이메일로 찾아 삭제합니다.")
@DeleteMapping
Expand Down Expand Up @@ -61,4 +76,26 @@ public ResponseEntity<ApiResponse<Void>> deleteUserById(@PathVariable Long userI
var body = ApiResponse.success(COMMON204);
return ResponseEntity.ok(body);
}

// TODO API 삭제: Gemini Embedding 생성 테스트용 임시 메소드
@Operation(summary = "(테스트) 요청 텍스트의 임베딩값을 생성합니다.")
@PostMapping("/test-embedding")
public CompletableFuture<ResponseEntity<ApiResponse<GeminiEmbeddingResponse>>> getGeminiEmbedding(
@RequestBody @NotBlank String text) {

GeminiEmbeddingRequest requestBody = GeminiEmbeddingRequest.of(
text, GeminiEmbeddingRequest.TaskType.CLUSTERING, GeminiEmbeddingRequest.OutputDimensionality.D128);

CompletableFuture<GeminiEmbeddingResponse> reqFuture =
geminiClient.sendAsyncEmbeddingRequest(requestBody, STAR_ANALYSIS_CREATE_REQUEST_TIMEOUT_SEC);

CompletableFuture<GeminiEmbeddingResponse> result = reqFuture
.thenApplyAsync(response -> response, geminiPostProcessExecutor)
.exceptionally(e -> {
log.error(e.getMessage(), e);
throw new CustomException(TEXT_EMBEDDING_CREATE_FAILED);
});

return result.thenApply(rsp -> ResponseEntity.ok(ApiResponse.success(COMMON200, rsp)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public enum ErrorCode {
STAR_ANALYSIS_CREATE_FAILED(INTERNAL_SERVER_ERROR, "스타 분석 생성 중 오류가 발생하였습니다."),
STAR_ANALYSIS_COMPLETE_FAILED(INTERNAL_SERVER_ERROR, "스타 분석 업데이트 중 오류가 발생하였습니다."),
STAR_ANALYSIS_DELETE_NOT_ALLOWED_STATUS(BAD_REQUEST, "진행 중(IN_PROGRESS)인 스타 분석만 삭제할 수 있습니다."),
;

TEXT_EMBEDDING_CREATE_FAILED(INTERNAL_SERVER_ERROR, "임베딩 생성에 실패하였습니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,11 @@ public class GeminiClient {
private final GeminiProperty geminiProperty;
private final WebClient webClient;

// TODO 요청 URL 상수로 분리, 임베딩 요청 고려
private static final String GEMINI_API_URL =
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent";
// "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent";
// "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent";

public CompletableFuture<GeminiGenerateResponse> sendAsyncRequest(
GeminiGenerateRequest requestBody, Long timeoutSec) {
public CompletableFuture<GeminiGenerateResponse> sendAsyncTextGenerateRequest(
GeminiGenerateRequest requestBody, GenerateModel model, Long timeoutSec) {
return webClient
.post()
.uri(GEMINI_API_URL)
.uri(model.endpoint())
.header("x-goog-api-key", geminiProperty.apiKey())
.accept(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
Expand All @@ -34,4 +28,21 @@ public CompletableFuture<GeminiGenerateResponse> sendAsyncRequest(
.timeout(Duration.ofSeconds(timeoutSec))
.toFuture();
}

private static final String EMBEDDING_ENDPOINT =
"https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent";

public CompletableFuture<GeminiEmbeddingResponse> sendAsyncEmbeddingRequest(
GeminiEmbeddingRequest requestBody, Long timeoutSec) {
return webClient
.post()
.uri(EMBEDDING_ENDPOINT)
.header("x-goog-api-key", geminiProperty.apiKey())
.accept(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(GeminiEmbeddingResponse.class)
.timeout(Duration.ofSeconds(timeoutSec))
.toFuture();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.shyashyashya.refit.global.gemini;

import com.fasterxml.jackson.annotation.JsonValue;
import java.util.List;

public record GeminiEmbeddingRequest(TaskType taskType, Content content, OutputDimensionality outputDimensionality) {

public enum TaskType {
SEMANTIC_SIMILARITY,
CLASSIFICATION,
CLUSTERING
}

public enum OutputDimensionality {
D2048(2048),
D1536(1536),
D768(768),
D512(512),
D256(256),
D128(128);

private final int value;

OutputDimensionality(int value) {
this.value = value;
}

@JsonValue
public int value() {
return value;
}
}

public record Content(List<Part> parts) {}

public record Part(String text) {}

public static GeminiEmbeddingRequest of(String text, TaskType taskType, OutputDimensionality outputDimensionality) {
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("text must not be blank");
}

Content content = new Content(List.of(new Part(text)));
return new GeminiEmbeddingRequest(taskType, content, outputDimensionality);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.shyashyashya.refit.global.gemini;

import java.util.List;

public record GeminiEmbeddingResponse(Embedding embedding) {

public record Embedding(List<Float> values) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.shyashyashya.refit.global.gemini;

public enum GenerateModel {
GEMINI_2_5_PRO("gemini-2.5-pro"),
GEMINI_2_5_FLASH_LITE("gemini-2.5-flash-lite"),
GEMINI_2_5_FLASH("gemini-2.5-flash"),
GEMINI_3_FLASH("gemini-3-flash-preview"),
GEMINI_3_PRO("gemini-3-pro-preview"),

GEMMA_3_1B_IT("gemma-3-1b-it"),
GEMMA_3_4B_IT("gemma-3-4b-it"),
GEMMA_3_12B_IT("gemma-3-12b-it"),
GEMMA_3_27B_IT("gemma-3-27b-it");

private static final String PREFIX = "https://generativelanguage.googleapis.com/v1beta/models/";
private static final String SUFFIX = ":generateContent";

private final String name;
private final String endpoint;

GenerateModel(String name) {
this.name = name;
this.endpoint = PREFIX + name + SUFFIX;
}

public String id() {
return name;
}

public String endpoint() {
return endpoint;
}
}
Loading