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 @@ -5,11 +5,13 @@
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.jobdri.jobdri_api.domain.analysis.dto.external.llm.AnalysisLlmResponse;
import com.jobdri.jobdri_api.domain.analysis.dto.response.MissingKeywordSource;
import com.jobdri.jobdri_api.domain.evaluation.analysis.mapper.EvaluationLlmSnapshotParser;
import com.jobdri.jobdri_api.domain.evaluation.analysis.mapper.EvaluationMissingKeywordSourceMapper;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationMissingKeyword;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationMissingKeywordSource;
import com.jobdri.jobdri_api.domain.analysis.service.sanitization.AnalysisSanitizationRules;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
Expand All @@ -28,7 +30,6 @@
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
class NlgEvaluationBatchService {
private static final int MIN_SCORE = 1;
Expand Down Expand Up @@ -64,6 +65,17 @@ class NlgEvaluationBatchService {

private final NlgEvaluationAiClient nlgEvaluationAiClient;
private final ObjectMapper objectMapper;
private final EvaluationLlmSnapshotParser evaluationLlmSnapshotParser;

@Autowired
NlgEvaluationBatchService(
NlgEvaluationAiClient nlgEvaluationAiClient,
ObjectMapper objectMapper
) {
this.nlgEvaluationAiClient = nlgEvaluationAiClient;
this.objectMapper = objectMapper;
this.evaluationLlmSnapshotParser = new EvaluationLlmSnapshotParser(objectMapper);
}

NlgEvaluationSummary run(Path inputPath, Path outputPath) throws IOException {
validateDifferentFiles(inputPath, outputPath);
Expand Down Expand Up @@ -140,7 +152,9 @@ private NlgEvaluationAiClient.NlgJudgeInput buildJudgeInput(
requiredResolvedValue(row, sourceCaseRows, resolvedHeaders, "question", caseId),
requiredResolvedValue(row, sourceCaseRows, resolvedHeaders, "answer", caseId),
questionAnalysesJson,
readKeyStrengthsJson(resolvedValue(row, sourceCaseRows, resolvedHeaders, "rawLlmResponseJson")),
writeJson(evaluationLlmSnapshotParser.parseRawLlmResponse(
resolvedValue(row, sourceCaseRows, resolvedHeaders, "rawLlmResponseJson")
).keyStrengthQuotes()),
missingKeywordsJson,
value(row, "rawCandidateResponseJson"),
value(row, "sanitizedCandidateResponseJson"),
Expand Down Expand Up @@ -389,8 +403,8 @@ private List<NlgEvaluationResponse.MissingKeywordMissEvaluation> validMissingKey
if (evaluations == null || evaluations.isEmpty()) {
return List.of();
}
List<AnalysisLlmResponse.MissingKeywordItem> actualMissingKeywords =
readActualMissingKeywords(input.missingKeywordsJson(), input.caseId());
List<EvaluationMissingKeyword> actualMissingKeywords =
evaluationLlmSnapshotParser.parseMissingKeywords(input.missingKeywordsJson(), input.caseId());
List<NlgEvaluationResponse.MissingKeywordMissEvaluation> valid = new ArrayList<>();
for (NlgEvaluationResponse.MissingKeywordMissEvaluation evaluation : evaluations) {
Optional<MissingKeywordMissInvalidReason> invalidReason =
Expand All @@ -413,7 +427,7 @@ private List<NlgEvaluationResponse.MissingKeywordMissEvaluation> validMissingKey

private Optional<MissingKeywordMissInvalidReason> missingKeywordMissInvalidReason(
NlgEvaluationAiClient.NlgJudgeInput input,
List<AnalysisLlmResponse.MissingKeywordItem> actualMissingKeywords,
List<EvaluationMissingKeyword> actualMissingKeywords,
NlgEvaluationResponse.MissingKeywordMissEvaluation evaluation
) {
if (evaluation == null) {
Expand All @@ -428,23 +442,23 @@ private Optional<MissingKeywordMissInvalidReason> missingKeywordMissInvalidReaso
if (!StringUtils.hasText(evaluation.reason())) {
return Optional.of(MissingKeywordMissInvalidReason.BLANK_REASON);
}
Optional<MissingKeywordSource> source = parseJudgeMissingKeywordSource(evaluation.source());
if (source.isEmpty() || source.get() == MissingKeywordSource.PREFERENCE) {
Optional<EvaluationMissingKeywordSource> source = parseJudgeMissingKeywordSource(evaluation.source());
if (source.isEmpty() || source.get() == EvaluationMissingKeywordSource.PREFERENCE) {
return Optional.of(MissingKeywordMissInvalidReason.INVALID_SOURCE);
}
if (AnalysisSanitizationRules.isStructuredQualificationKeyword(evaluation.keyword())
|| AnalysisSanitizationRules.isStructuredQualificationKeyword(evaluation.relatedRequirement())) {
return Optional.of(MissingKeywordMissInvalidReason.STRUCTURED_QUALIFICATION);
}
String sourceText = source.get() == MissingKeywordSource.MAIN_TASK
String sourceText = source.get() == EvaluationMissingKeywordSource.MAIN_TASK
? input.mainTasks()
: input.qualifications();
if (!containsNormalized(sourceText, evaluation.relatedRequirement())) {
return Optional.of(MissingKeywordMissInvalidReason.RELATED_REQUIREMENT_NOT_IN_JD);
}
if (!AnalysisSanitizationRules.isValidMissingKeyword(
evaluation.keyword(),
source.get(),
EvaluationMissingKeywordSourceMapper.toAnalysisSource(source.get()),
input.mainTasks(),
input.qualifications()
)) {
Expand All @@ -456,30 +470,18 @@ private Optional<MissingKeywordMissInvalidReason> missingKeywordMissInvalidReaso
return Optional.empty();
}

private Optional<MissingKeywordSource> parseJudgeMissingKeywordSource(String source) {
if (!StringUtils.hasText(source)) {
return Optional.empty();
}
String normalized = source.trim();
if ("MAIN_TASK".equalsIgnoreCase(normalized) || "MAIN_TASKS".equalsIgnoreCase(normalized)) {
return Optional.of(MissingKeywordSource.MAIN_TASK);
}
if ("QUALIFICATION".equalsIgnoreCase(normalized) || "QUALIFICATIONS".equalsIgnoreCase(normalized)) {
return Optional.of(MissingKeywordSource.QUALIFICATION);
}
return MissingKeywordSource.from(normalized);
private Optional<EvaluationMissingKeywordSource> parseJudgeMissingKeywordSource(String source) {
return EvaluationMissingKeywordSource.from(source);
}

private boolean sameMissingKeyword(
AnalysisLlmResponse.MissingKeywordItem actual,
EvaluationMissingKeyword actual,
NlgEvaluationResponse.MissingKeywordMissEvaluation evaluation,
MissingKeywordSource source
EvaluationMissingKeywordSource source
) {
return actual != null
&& normalize(actual.keyword()).equals(normalize(evaluation.keyword()))
&& parseJudgeMissingKeywordSource(actual.source())
.map(actualSource -> actualSource == source)
.orElse(false);
&& actual.source() == source;
}

private List<NlgEvaluationErrorCode> sanitizeErrorCodes(
Expand Down Expand Up @@ -601,22 +603,6 @@ private int readJsonArraySize(String json, String fieldName, String caseId) {
}
}

private List<AnalysisLlmResponse.MissingKeywordItem> readActualMissingKeywords(String json, String caseId) {
if (!StringUtils.hasText(json)) {
return List.of();
}
try {
List<AnalysisLlmResponse.MissingKeywordItem> values = objectMapper.readValue(
json,
new TypeReference<>() {
}
);
return values == null ? List.of() : values;
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("aiMissingKeywordsJson must be a JSON array. caseId=" + caseId, e);
}
}

private int readMissingKeywordCandidateCount(String sanitizedCandidateResponseJson) {
if (!StringUtils.hasText(sanitizedCandidateResponseJson)) {
return 0;
Expand All @@ -634,18 +620,6 @@ private int readMissingKeywordCandidateCount(String sanitizedCandidateResponseJs
}
}

private String readKeyStrengthsJson(String rawLlmResponseJson) {
if (!StringUtils.hasText(rawLlmResponseJson)) {
return "[]";
}
try {
AnalysisLlmResponse response = objectMapper.readValue(rawLlmResponseJson, AnalysisLlmResponse.class);
return writeJson(response == null || response.keyStrengths() == null ? List.of() : response.keyStrengths());
} catch (JsonProcessingException e) {
return "[]";
}
}

private SourceCaseRows loadSourceCaseRowsIfNeeded(Path inputPath, List<String> inputHeaders) throws IOException {
boolean hasAllContextHeaders = List.of("mainTasks", "qualifications", "question", "answer").stream()
.allMatch(field -> directHeader(inputHeaders, field).isPresent());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.jobdri.jobdri_api.domain.evaluation.analysis.mapper;

import com.jobdri.jobdri_api.domain.analysis.dto.external.llm.AnalysisCandidateResponse;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationCandidateSnapshot;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationMissingKeywordCandidate;

import java.util.List;

public final class EvaluationCandidateSnapshotMapper {
private EvaluationCandidateSnapshotMapper() {
}

public static EvaluationCandidateSnapshot from(AnalysisCandidateResponse response) {
if (response == null || response.missingKeywordCandidates() == null) {
return new EvaluationCandidateSnapshot(List.of());
}
List<EvaluationMissingKeywordCandidate> missingKeywordCandidates = response.missingKeywordCandidates().stream()
.map(candidate -> EvaluationMissingKeywordSourceMapper.fromAnalysisSource(candidate.source())
.map(source -> new EvaluationMissingKeywordCandidate(
candidate.keyword(),
source,
candidate.relatedRequirement()
))
.orElse(null))
.filter(candidate -> candidate != null)
.toList();
return new EvaluationCandidateSnapshot(missingKeywordCandidates);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.jobdri.jobdri_api.domain.evaluation.analysis.mapper;

import com.jobdri.jobdri_api.domain.analysis.dto.external.llm.AnalysisLlmResponse;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationLlmSnapshot;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationMissingKeyword;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationQuestionAnalysis;

import java.util.List;
import java.util.Optional;

public final class EvaluationLlmSnapshotMapper {
private EvaluationLlmSnapshotMapper() {
}

public static EvaluationLlmSnapshot from(AnalysisLlmResponse response) {
if (response == null) {
return new EvaluationLlmSnapshot(List.of(), List.of(), List.of());
}
List<String> keyStrengthQuotes = response.keyStrengths() == null
? List.of()
: response.keyStrengths().stream()
.map(AnalysisLlmResponse.HighlightItem::quote)
.toList();
List<EvaluationMissingKeyword> missingKeywords = response.missingKeywords() == null
? List.of()
: response.missingKeywords().stream()
.map(EvaluationMissingKeywordMapper::from)
.flatMap(Optional::stream)
.toList();
List<EvaluationQuestionAnalysis> questionAnalyses = response.questionAnalyses() == null
? List.of()
: response.questionAnalyses().stream()
.map(EvaluationQuestionAnalysisMapper::from)
.filter(questionAnalysis -> questionAnalysis != null)
.toList();
return new EvaluationLlmSnapshot(keyStrengthQuotes, missingKeywords, questionAnalyses);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.jobdri.jobdri_api.domain.evaluation.analysis.mapper;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationLlmSnapshot;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationMissingKeyword;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationMissingKeywordSource;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationQuestionAnalysis;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;

public class EvaluationLlmSnapshotParser {
private final ObjectMapper objectMapper;

public EvaluationLlmSnapshotParser(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

public EvaluationLlmSnapshot parseRawLlmResponse(String rawLlmResponseJson) {
if (!StringUtils.hasText(rawLlmResponseJson)) {
return emptySnapshot();
}
try {
JsonNode root = objectMapper.readTree(rawLlmResponseJson);
return new EvaluationLlmSnapshot(
readKeyStrengthQuotes(root.path("keyStrengths")),
readMissingKeywords(root.path("missingKeywords")),
readQuestionAnalyses(root.path("questionAnalyses"))
);
} catch (JsonProcessingException e) {
return emptySnapshot();
}
}

public List<EvaluationMissingKeyword> parseMissingKeywords(String json, String caseId) {
if (!StringUtils.hasText(json)) {
return List.of();
}
try {
JsonNode root = objectMapper.readTree(json);
if (!root.isArray()) {
throw new IllegalArgumentException("aiMissingKeywordsJson must be a JSON array. caseId=" + caseId);
}
return readMissingKeywords(root);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("aiMissingKeywordsJson must be a JSON array. caseId=" + caseId, e);
}
}

private EvaluationLlmSnapshot emptySnapshot() {
return new EvaluationLlmSnapshot(List.of(), List.of(), List.of());
}

private List<String> readKeyStrengthQuotes(JsonNode keyStrengthsNode) {
if (!keyStrengthsNode.isArray()) {
return List.of();
}
List<String> quotes = new ArrayList<>();
for (JsonNode item : keyStrengthsNode) {
String quote = item.path("quote").asText(null);
if (StringUtils.hasText(quote)) {
quotes.add(quote);
}
}
return quotes;
}

private List<EvaluationMissingKeyword> readMissingKeywords(JsonNode missingKeywordsNode) {
if (!missingKeywordsNode.isArray()) {
return List.of();
}
List<EvaluationMissingKeyword> missingKeywords = new ArrayList<>();
for (JsonNode item : missingKeywordsNode) {
String keyword = item.path("keyword").asText(null);
String source = item.path("source").asText(null);
if (!StringUtils.hasText(keyword)) {
continue;
}
EvaluationMissingKeywordSource.from(source)
.ifPresent(value -> missingKeywords.add(new EvaluationMissingKeyword(keyword, value)));
}
return missingKeywords;
}

private List<EvaluationQuestionAnalysis> readQuestionAnalyses(JsonNode questionAnalysesNode) {
if (!questionAnalysesNode.isArray()) {
return List.of();
}
List<EvaluationQuestionAnalysis> questionAnalyses = new ArrayList<>();
for (JsonNode item : questionAnalysesNode) {
questionAnalyses.add(new EvaluationQuestionAnalysis(
item.path("questionId").isNumber() ? item.path("questionId").longValue() : null,
item.path("sentence").asText(null),
item.path("status").asText(null),
item.path("reason").asText(null),
item.path("improvement").asText(null)
));
}
return questionAnalyses;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.jobdri.jobdri_api.domain.evaluation.analysis.mapper;

import com.jobdri.jobdri_api.domain.analysis.dto.external.llm.AnalysisLlmResponse;
import com.jobdri.jobdri_api.domain.evaluation.analysis.model.EvaluationMissingKeyword;

import java.util.Optional;

public final class EvaluationMissingKeywordMapper {
private EvaluationMissingKeywordMapper() {
}

public static Optional<EvaluationMissingKeyword> from(AnalysisLlmResponse.MissingKeywordItem item) {
if (item == null) {
return Optional.empty();
}
return EvaluationMissingKeywordSourceMapper.fromAnalysisSource(item.source())
.map(source -> new EvaluationMissingKeyword(item.keyword(), source));
}
}
Loading
Loading