-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] Cohere Embed API 클라이언트 1차 연동 #227
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
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,21 @@ | ||
| caseId,sourceResultFile,analysisCount,averageRelevance,averageProblemValidity,averageSentenceTypeConsistency,averageReasonCorrectness,averageContextAwareness,averageFaithfulness,averageTenseConsistency,averageUsability,averageNonMeta,averageMeaningPreservation,noAnalysisAppropriateness,strengthsPrecision,strengthsCoverage,missingKeywordsPrecision,missingKeywordsCoverage,overallUsefulness,errorCodes,shortRationale,judgeInputTokens,judgeOutputTokens,judgeLatencyMs,failureStage | ||
| EV-01,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,1,4.0,3.0,4.0,3.0,3.0,4.0,5.0,4.0,4.0,4.0,4,5,5,5,5,4,"[""NONE""]","전반적으로 경험이 잘 드러나지만, 구체적인 절차와 결과 수치가 부족합니다.",6566,280,4806, | ||
| EV-02,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,5,5,5,5,5,4,"[""NONE""]","답변은 강점이 잘 드러나지만, 구체적인 행동과 방법론이 부족하여 일부 아쉬움이 있습니다.",6004,130,2873, | ||
| EV-03,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,2,3.0,3.0,3.0,3.0,3.0,4.0,4.0,4.0,4.0,4.0,4,5,5,5,5,4,"[""NONE""]","전반적으로 분석 역량과 경험이 잘 드러나지만, 구체적인 행동과 결과를 더 강조할 필요가 있습니다.",7600,450,6170, | ||
| EV-04,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,1,5,5,5,5,2,"[""MISSED_ANALYSIS""]",명확한 문제 문장이 존재하나 분석이 없어서 중요한 첨삭 대상을 놓쳤습니다.,7672,133,2355, | ||
| EV-05,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,2,4.0,3.0,3.0,4.0,3.0,4.0,5.0,4.0,4.0,4.0,4,5,5,5,5,4,"[""NONE""]","지원 동기와 포부가 구체적이지 않아 개선 여지가 있으며, 전반적으로 유용한 분석이 제공되었습니다.",6473,430,6139, | ||
| EV-06,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,1,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,5.0,4,5,5,5,5,5,"[""NONE""]","전반적으로 구체적인 성과와 실행 방법이 잘 드러나 있으며, 분석의 정확성이 높아 유용한 첨삭 자료로 활용될 수 있습니다.",9172,331,7780, | ||
| EV-07,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,4,5,5,5,5,3,"[""NONE""]","답변에 명확한 문제 문장이 없고, 강점 및 누락 키워드도 적절하여 분석 부재가 대체로 적합합니다.",5550,136,2995, | ||
| EV-08,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,2,4.0,3.0,3.0,4.0,3.0,4.0,4.0,3.0,4.0,4.0,4,5,5,5,5,4,"[""NONE""]","전반적으로 자기소개서의 강점이 잘 드러나지만, 구체적인 실행 방안과 방법론이 부족하여 개선 여지가 있습니다.",7503,431,5298, | ||
| EV-09,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,2,4.0,3.0,4.0,4.0,3.0,4.0,5.0,4.0,4.0,4.0,5,5,5,5,5,4,"[""NONE""]","전반적으로 구체적인 행동과 방법론이 부족하지만, 경험을 잘 설명하고 있어 유용한 분석입니다.",6496,418,12493, | ||
| EV-10,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,4,5,5,5,5,3,"[""NONE""]","답변에 명확한 문제 문장이 없고, 강점 및 누락 키워드도 적절하여 분석 부재가 대체로 적합합니다.",6709,136,2661, | ||
| EV-11,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,2,3,3,2,2,2,[],답변에서 JD의 핵심 경험 요구사항인 복지 프로그램 기획과 행정 지원이 누락되었습니다.,6763,267,4588, | ||
| EV-12,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,2,5.0,4.0,5.0,4.0,4.0,5.0,5.0,4.0,5.0,5.0,4,5,5,5,5,4,"[""NONE""]","전반적으로 직무 적합성을 잘 드러내고 있으나, 구체적인 행동이나 방법이 부족한 부분이 있습니다.",6990,424,6968, | ||
| EV-13,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,2,4.0,3.0,4.0,4.0,3.0,4.0,5.0,4.0,4.0,4.0,4,5,5,5,5,4,"[""NONE""]","전반적으로 경험과 방법론이 잘 서술되었으나, 구체성이 부족한 부분이 있어 개선 여지가 있습니다.",6620,440,5346, | ||
| EV-14,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,4,5,5,5,5,3,"[""NONE""]",지원 동기에서 구체적인 행동이나 방법이 부족하여 명확한 메시지를 전달하지 못하고 있습니다.,6673,125,2601, | ||
| EV-15,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,1,3,3,2,2,2,"[""MISSED_ANALYSIS""]","지원 동기와 직무 적합성에 대한 구체적인 사례가 부족하며, 중요한 경험 키워드가 누락되었습니다.",5595,217,4432, | ||
| EV-16,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,1,5,5,5,5,2,"[""MISSED_ANALYSIS""]","명확한 행동 계획과 방법론이 부족한 문장이 존재하나, 분석이 없어서 중요한 첨삭 대상을 놓쳤습니다.",6670,141,2356, | ||
| EV-17,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,2,4.0,3.0,4.0,4.0,3.0,4.0,5.0,4.0,4.0,4.0,4,5,5,5,5,4,"[""NONE""]","전반적으로 경험이 잘 서술되었으나, 구체적인 행동이나 방법이 부족하여 개선 여지가 있습니다.",6002,408,4717, | ||
| EV-18,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,5,3,3,2,2,3,[],"답변에서 JD의 핵심 경험 요구사항인 사내 일반 행정 지원이 누락되어 있으며, 전반적으로 구체적인 경험이 부족합니다.",6023,170,8328, | ||
| EV-19,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,2,3.0,3.0,4.0,3.0,3.0,4.0,4.0,3.0,4.0,4.0,3,5,5,5,5,3,"[""NONE""]",지원 동기와 강점에 대한 구체성이 부족하여 개선 여지가 있습니다.,6176,396,6194, | ||
| EV-20,evaluation/evaluation_ai_results_two_pass_provenance_strength_fix.csv,0,,,,,,,,,,,1,5,5,5,5,2,"[""MISSED_ANALYSIS""]",명확한 문제 문장이 존재하나 분석이 없어서 중요한 첨삭 대상을 놓쳤습니다.,6018,133,2254, |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| sourceResultFile,caseCount,successCount,judgeFailedCount,averageRelevance,averageProblemValidity,averageSentenceTypeConsistency,averageReasonCorrectness,averageContextAwareness,averageFaithfulness,averageTenseConsistency,averageUsability,averageNonMeta,averageMeaningPreservation,noAnalysisAppropriateness,strengthsPrecision,strengthsCoverage,missingKeywordsPrecision,missingKeywordsCoverage,overallUsefulness,averageJudgeInputTokens,averageJudgeOutputTokens,averageJudgeLatencyMs,averageAnalysisCount,metaImprovementRate,unsupportedFactRate,falsePositiveAnalysisRate,fatalErrorRate,errorCodeCounts | ||
| evaluation/evaluation_nlg_judge_policy_alignment.csv,20,20,0,3.85,3.35,3.85,4.1,3.35,4.1,4.6,4.0,4.1,4.1,3.35,4.7,4.7,4.55,4.55,3.45,6412.75,258.95,7965.95,0.9,0.0,0.0,0.0,0.0,"{""MISSED_ANALYSIS"":5,""MISSED_MISSING_KEYWORD"":3,""NONE"":14}" | ||
| evaluation/evaluation_nlg_judge_missing_keyword_provenance.csv,20,20,0,4.0,3.3,3.9,3.8,3.3,4.2,4.7,3.9,4.2,4.2,3.4,4.7,4.7,4.55,4.55,3.3,6663.75,279.8,5067.7,0.9,0.0,0.0,0.0,0.0,"{""MISSED_ANALYSIS"":4,""NONE"":14}" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.jobdri.jobdri_api.global.cohere; | ||
|
|
||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
| import org.springframework.context.annotation.Configuration; | ||
|
|
||
| @Configuration | ||
| @EnableConfigurationProperties(CohereProperties.class) | ||
| public class CohereConfig { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| package com.jobdri.jobdri_api.global.cohere; | ||
|
|
||
| import com.jobdri.jobdri_api.global.apiPayload.code.GeneralErrorCode; | ||
| import com.jobdri.jobdri_api.global.apiPayload.exception.GeneralException; | ||
| import com.jobdri.jobdri_api.global.cohere.dto.CohereEmbeddingRequest; | ||
| import com.jobdri.jobdri_api.global.cohere.dto.CohereEmbeddingResponse; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.http.client.SimpleClientHttpRequestFactory; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.util.StringUtils; | ||
| import org.springframework.web.client.ResourceAccessException; | ||
| import org.springframework.web.client.RestClient; | ||
| import org.springframework.web.client.RestClientException; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| @Component | ||
| @Slf4j | ||
| public class CohereEmbeddingClient { | ||
| private static final int MAX_TEXTS_PER_REQUEST = 96; | ||
| private static final String INPUT_TYPE_SEARCH_DOCUMENT = "search_document"; | ||
| private static final String INPUT_TYPE_SEARCH_QUERY = "search_query"; | ||
| private static final List<String> FLOAT_EMBEDDING_TYPE = List.of("float"); | ||
|
|
||
| private final CohereProperties properties; | ||
| private final RestClient restClient; | ||
|
|
||
| public CohereEmbeddingClient(CohereProperties properties, RestClient.Builder restClientBuilder) { | ||
| this.properties = properties; | ||
| this.restClient = restClientBuilder | ||
| .baseUrl(properties.baseUrl()) | ||
| .requestFactory(requestFactory(properties)) | ||
| .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) | ||
| .build(); | ||
| } | ||
|
|
||
| public List<float[]> embedDocuments(List<String> texts) { | ||
| return embed(texts, INPUT_TYPE_SEARCH_DOCUMENT); | ||
| } | ||
|
|
||
| public float[] embedQuery(String text) { | ||
| if (!StringUtils.hasText(text)) { | ||
| throw invalidParameter("검색 질의 텍스트는 필수입니다."); | ||
| } | ||
| List<float[]> embeddings = embed(List.of(text), INPUT_TYPE_SEARCH_QUERY); | ||
| if (embeddings.isEmpty()) { | ||
| throw unavailable("Cohere 검색 질의 임베딩 응답이 비어 있습니다."); | ||
| } | ||
| return embeddings.getFirst(); | ||
| } | ||
|
|
||
| private List<float[]> embed(List<String> texts, String inputType) { | ||
| validateApiKey(); | ||
| List<String> normalizedTexts = validateTexts(texts); | ||
| CohereEmbeddingRequest request = new CohereEmbeddingRequest( | ||
| properties.embedding().model(), | ||
| normalizedTexts, | ||
| inputType, | ||
| FLOAT_EMBEDDING_TYPE, | ||
| properties.embedding().dimension() | ||
| ); | ||
|
|
||
| CohereEmbeddingResponse response = callCohere(request); | ||
| return validateResponse(response, normalizedTexts.size()); | ||
| } | ||
|
|
||
| private CohereEmbeddingResponse callCohere(CohereEmbeddingRequest request) { | ||
| try { | ||
| return restClient.post() | ||
| .uri("/v2/embed") | ||
| .header(HttpHeaders.AUTHORIZATION, "Bearer " + properties.apiKey()) | ||
| .body(request) | ||
| .retrieve() | ||
| .onStatus( | ||
| status -> status.value() == 429 || status.is5xxServerError(), | ||
| (ignoredRequest, ignoredResponse) -> { | ||
| throw unavailable("Cohere Embed API가 일시적으로 응답할 수 없습니다."); | ||
| } | ||
| ) | ||
| .onStatus( | ||
| status -> status.value() == 400 | ||
| || status.value() == 401 | ||
| || status.value() == 403 | ||
| || status.is4xxClientError(), | ||
| (ignoredRequest, ignoredResponse) -> { | ||
| throw invalidParameter("Cohere Embed API 요청 또는 설정이 올바르지 않습니다."); | ||
| } | ||
| ) | ||
|
Comment on lines
+77
to
+91
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. 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift 429/5xx에 대한 재시도(backoff) 정책이 없습니다. 현재는 429/5xx 응답을 즉시 As per path instructions, "외부 API 호출 성능: ... 재시도 정책, rate limit 대응"을 우선순위 높게 검토해야 합니다. 🤖 Prompt for AI AgentsSource: Path instructions |
||
| .body(CohereEmbeddingResponse.class); | ||
| } catch (GeneralException e) { | ||
| throw e; | ||
| } catch (ResourceAccessException e) { | ||
| log.warn("Cohere Embed API access failed. reason=resource_access_failure, message={}", e.getMessage()); | ||
| throw new GeneralException( | ||
| GeneralErrorCode.EXTERNAL_SERVICE_TIMEOUT, | ||
| "Cohere Embed API 응답이 지연되었거나 연결할 수 없습니다.", | ||
| e | ||
| ); | ||
| } catch (RestClientException e) { | ||
| log.warn("Cohere Embed API call failed. reason=rest_client_failure, message={}", e.getMessage()); | ||
| throw unavailable("Cohere Embed API 호출에 실패했습니다.", e); | ||
| } | ||
| } | ||
|
|
||
| private List<float[]> validateResponse(CohereEmbeddingResponse response, int expectedCount) { | ||
| if (response == null || response.embeddings() == null || response.embeddings().floatValues() == null | ||
| || response.embeddings().floatValues().isEmpty()) { | ||
| throw unavailable("Cohere 임베딩 응답이 비어 있습니다."); | ||
| } | ||
|
|
||
| List<List<Double>> embeddings = response.embeddings().floatValues(); | ||
| if (embeddings.size() != expectedCount) { | ||
| throw unavailable("Cohere 임베딩 응답 개수가 요청 개수와 일치하지 않습니다."); | ||
| } | ||
|
|
||
| List<float[]> result = new ArrayList<>(); | ||
| for (List<Double> embedding : embeddings) { | ||
| if (embedding == null || embedding.size() != properties.embedding().dimension()) { | ||
| throw unavailable("Cohere 임베딩 차원이 설정값과 일치하지 않습니다."); | ||
| } | ||
| float[] vector = new float[embedding.size()]; | ||
| for (int i = 0; i < embedding.size(); i++) { | ||
| Double value = embedding.get(i); | ||
| if (value == null) { | ||
| throw unavailable("Cohere 임베딩 벡터에 비어 있는 값이 포함되어 있습니다."); | ||
| } | ||
| vector[i] = value.floatValue(); | ||
| } | ||
| result.add(vector); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| private void validateApiKey() { | ||
| if (!properties.hasApiKey()) { | ||
| throw new GeneralException( | ||
| GeneralErrorCode.SERVICE_UNAVAILABLE, | ||
| "Cohere API 키가 설정되지 않았습니다." | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| private List<String> validateTexts(List<String> texts) { | ||
| if (texts == null || texts.isEmpty()) { | ||
| throw invalidParameter("임베딩할 텍스트는 1개 이상이어야 합니다."); | ||
| } | ||
| if (texts.size() > MAX_TEXTS_PER_REQUEST) { | ||
| throw invalidParameter("Cohere 임베딩은 한 번에 최대 96개 텍스트만 요청할 수 있습니다."); | ||
| } | ||
|
|
||
| List<String> normalizedTexts = new ArrayList<>(); | ||
| for (String text : texts) { | ||
| if (!StringUtils.hasText(text)) { | ||
| throw invalidParameter("임베딩할 텍스트는 비어 있을 수 없습니다."); | ||
| } | ||
| normalizedTexts.add(text.trim()); | ||
| } | ||
| return List.copyOf(normalizedTexts); | ||
| } | ||
|
|
||
| private static SimpleClientHttpRequestFactory requestFactory(CohereProperties properties) { | ||
| SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); | ||
| requestFactory.setConnectTimeout(properties.embedding().connectTimeout()); | ||
| requestFactory.setReadTimeout(properties.embedding().readTimeout()); | ||
| return requestFactory; | ||
| } | ||
|
|
||
| private GeneralException invalidParameter(String message) { | ||
| return new GeneralException(GeneralErrorCode.INVALID_PARAMETER, message); | ||
| } | ||
|
|
||
| private GeneralException unavailable(String message) { | ||
| return new GeneralException(GeneralErrorCode.SERVICE_UNAVAILABLE, message); | ||
| } | ||
|
|
||
| private GeneralException unavailable(String message, Throwable cause) { | ||
| return new GeneralException(GeneralErrorCode.SERVICE_UNAVAILABLE, message, cause); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.jobdri.jobdri_api.global.cohere; | ||
|
|
||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| @ConfigurationProperties(prefix = "cohere") | ||
| public record CohereProperties( | ||
| String apiKey, | ||
| String baseUrl, | ||
| Embedding embedding | ||
| ) { | ||
| private static final String DEFAULT_BASE_URL = "https://api.cohere.com"; | ||
|
|
||
| public CohereProperties { | ||
| baseUrl = hasText(baseUrl) ? baseUrl : DEFAULT_BASE_URL; | ||
| embedding = embedding == null ? new Embedding(null, null, null, null) : embedding; | ||
| } | ||
|
|
||
| boolean hasApiKey() { | ||
| return hasText(apiKey); | ||
| } | ||
|
|
||
| public record Embedding( | ||
| String model, | ||
| Integer dimension, | ||
| Duration connectTimeout, | ||
| Duration readTimeout | ||
| ) { | ||
| private static final String DEFAULT_MODEL = "embed-v4.0"; | ||
| private static final int DEFAULT_DIMENSION = 1024; | ||
| private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(3); | ||
| private static final Duration DEFAULT_READ_TIMEOUT = Duration.ofSeconds(15); | ||
|
|
||
| public Embedding { | ||
| model = hasText(model) ? model : DEFAULT_MODEL; | ||
| dimension = dimension == null ? DEFAULT_DIMENSION : dimension; | ||
| connectTimeout = connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : connectTimeout; | ||
| readTimeout = readTimeout == null ? DEFAULT_READ_TIMEOUT : readTimeout; | ||
| } | ||
| } | ||
|
|
||
| private static boolean hasText(String value) { | ||
| return value != null && !value.isBlank(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.jobdri.jobdri_api.global.cohere.dto; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record CohereEmbeddingRequest( | ||
| String model, | ||
| List<String> texts, | ||
| @JsonProperty("input_type") | ||
| String inputType, | ||
| @JsonProperty("embedding_types") | ||
| List<String> embeddingTypes, | ||
| @JsonProperty("output_dimension") | ||
| Integer outputDimension | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package com.jobdri.jobdri_api.global.cohere.dto; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public record CohereEmbeddingResponse( | ||
| Embeddings embeddings | ||
| ) { | ||
| public record Embeddings( | ||
| @JsonProperty("float") | ||
| List<List<Double>> floatValues | ||
| ) { | ||
| } | ||
| } |
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.
🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win
SimpleClientHttpRequestFactory는 커넥션 풀링을 제공하지 않습니다.SimpleClientHttpRequestFactory는 JDKHttpURLConnection기반으로 요청마다 새 연결을 열고 닫는 구조라 TCP/TLS 핸드셰이크 비용이 매번 발생하고, 커넥션 풀 크기 등을 세밀하게 제어할 수 없습니다. corpus 적재처럼 대량으로embedDocuments를 호출하는 시나리오에서는 처리량 저하 요인이 될 수 있습니다.HttpComponentsClientHttpRequestFactory(Apache HttpClient) 등 풀링을 지원하는 팩토리로 교체하는 것을 권장합니다.As per path instructions, "외부 API 호출 성능: connection pooling ... 대량 요청 시 처리량 저하 가능성"을 우선순위 높게 검토해야 합니다.
♻️ 커넥션 풀링을 지원하는 팩토리로 교체하는 예시
Also applies to: 164-169
🤖 Prompt for AI Agents
Source: Path instructions