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 @@ public record AnalysisWorkerContextResponse(
String middleClassificationName,
String detailClassificationName,
List<AnalysisWorkerQuestionItem> questions,
List<CorpusReferenceContext> corpusReferences,
List<SimilarJobPostingContext> similarJobPostings
) {
public AnalysisWorkerContextResponse(
Expand Down Expand Up @@ -41,12 +42,45 @@ public AnalysisWorkerContextResponse(
middleClassificationName,
detailClassificationName,
questions,
List.of(),
List.of()
);
}

public AnalysisWorkerContextResponse(
Long userId,
Long mockApplyId,
String companyName,
String jobTitle,
String task,
String requirements,
String preferredQualifications,
String bigClassificationName,
String middleClassificationName,
String detailClassificationName,
List<AnalysisWorkerQuestionItem> questions,
List<SimilarJobPostingContext> similarJobPostings
) {
this(
userId,
mockApplyId,
companyName,
jobTitle,
task,
requirements,
preferredQualifications,
bigClassificationName,
middleClassificationName,
detailClassificationName,
questions,
List.of(),
similarJobPostings
);
}

public AnalysisWorkerContextResponse {
questions = questions == null ? List.of() : List.copyOf(questions);
corpusReferences = corpusReferences == null ? List.of() : List.copyOf(corpusReferences);
similarJobPostings = similarJobPostings == null ? List.of() : List.copyOf(similarJobPostings);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.jobdri.jobdri_api.domain.analysis.dto.worker;

import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService.RetrievalContext;
import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService.RetrievedJobPostingReference;
import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService.RetrievedQuestionReference;
import org.springframework.util.StringUtils;

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

public record CorpusReferenceContext(
Long corpusId,
String category,
String title,
String content,
int rank
) {
public static List<CorpusReferenceContext> from(RetrievalContext retrievalContext) {
if (retrievalContext == null) {
return List.of();
}

List<CorpusReferenceContext> references = new ArrayList<>();
List<RetrievedJobPostingReference> jobPostings = retrievalContext.jobPostingReferences() == null
? List.of()
: retrievalContext.jobPostingReferences();
for (int index = 0; index < jobPostings.size(); index++) {
references.add(fromJobPosting(jobPostings.get(index), index + 1));
}

List<RetrievedQuestionReference> questions = retrievalContext.questionReferences() == null
? List.of()
: retrievalContext.questionReferences();
for (int index = 0; index < questions.size(); index++) {
references.add(fromQuestion(questions.get(index), index + 1));
}
return List.copyOf(references);
}

private static CorpusReferenceContext fromJobPosting(RetrievedJobPostingReference reference, int rank) {
return new CorpusReferenceContext(
reference.corpusId(),
"JOB_POSTING",
joinTitle(reference.companyName(), reference.roleName()),
joinContent(
line("주요 업무", reference.responsibilities()),
line("자격 요건", reference.requirements()),
line("우대 사항", reference.preferred())
),
rank
);
}

private static CorpusReferenceContext fromQuestion(RetrievedQuestionReference reference, int rank) {
return new CorpusReferenceContext(
reference.corpusId(),
"QUESTION",
joinTitle(reference.companyName(), reference.roleName(), reference.questionType()),
joinContent(
line("문항", reference.questionText()),
reference.charLimit() == null ? "" : "글자 수 제한: " + reference.charLimit()
),
rank
);
}

private static String joinTitle(String... values) {
return java.util.Arrays.stream(values)
.filter(StringUtils::hasText)
.map(String::trim)
.reduce((left, right) -> left + " - " + right)
.orElse("");
}

private static String joinContent(String... values) {
return java.util.Arrays.stream(values)
.filter(StringUtils::hasText)
.map(String::trim)
.reduce((left, right) -> left + "\n" + right)
.orElse("");
}

private static String line(String label, String value) {
return StringUtils.hasText(value) ? label + ": " + value.trim() : "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.jobdri.jobdri_api.domain.analysis.dto.worker.AnalysisWorkerCompleteRequest;
import com.jobdri.jobdri_api.domain.analysis.dto.worker.AnalysisWorkerContextResponse;
import com.jobdri.jobdri_api.domain.analysis.dto.worker.AnalysisWorkerResultStoreRequest;
import com.jobdri.jobdri_api.domain.analysis.dto.worker.CorpusReferenceContext;
import com.jobdri.jobdri_api.domain.analysis.entity.AnalysisAsyncTask;
import com.jobdri.jobdri_api.domain.analysis.entity.AnalysisAsyncTask.CreditStatus;
import com.jobdri.jobdri_api.domain.analysis.entity.AnalysisAsyncTask.FailureReason;
Expand Down Expand Up @@ -132,6 +133,7 @@ public AnalysisWorkerContextResponse getContext(String taskId, Long userId, Long
payload.jobPosting().getDetailClassification().getMiddleClassification().getMiddleName(),
payload.jobPosting().getDetailClassification().getDetailName(),
toQuestionItems(payload.questions()),
CorpusReferenceContext.from(payload.retrievalContext()),
payload.similarJobPostings()
);
task.captureExecutionSnapshot(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jobdri.jobdri_api.domain.analysis.entity.Question;
import com.jobdri.jobdri_api.domain.analysis.dto.worker.CorpusReferenceContext;
import com.jobdri.jobdri_api.domain.analysis.dto.worker.SimilarJobPostingContext;
import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService.RetrievalContext;
import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService.RetrievedJobPostingReference;
import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService.RetrievedQuestionReference;
import com.jobdri.jobdri_api.domain.analysis.service.ai.FewShotPromptProvider;
import com.jobdri.jobdri_api.domain.corpus.service.CorpusRetrievalService;
import com.jobdri.jobdri_api.domain.jobposting.entity.JobPosting;
Expand All @@ -26,8 +25,8 @@
@Component
public class AnalysisInputFingerprintProvider {

private static final String FINGERPRINT_SCHEMA_VERSION = "analysis-input-fingerprint-v2";
private static final String ANALYSIS_PROMPT_POLICY_VERSION = "analysis-prompt-policy-v2-similar-job-posting-rag";
private static final String FINGERPRINT_SCHEMA_VERSION = "analysis-input-fingerprint-v3";
private static final String ANALYSIS_PROMPT_POLICY_VERSION = "analysis-prompt-policy-v3-curated-corpus-rag";
private static final double ANALYSIS_TEMPERATURE = 0.2;

private final ObjectMapper objectMapper;
Expand Down Expand Up @@ -88,51 +87,23 @@ private Map<String, Object> retrievalPolicy() {

private Map<String, Object> retrievalContextFingerprintSource(RetrievalContext retrievalContext) {
Map<String, Object> retrievalContextSource = new LinkedHashMap<>();
if (retrievalContext == null) {
retrievalContextSource.put("jobPostingReferences", List.of());
retrievalContextSource.put("questionReferences", List.of());
return retrievalContextSource;
}

retrievalContextSource.put(
"jobPostingReferences",
retrievalContext.jobPostingReferences().stream()
.map(this::jobPostingReferenceFingerprintSource)
.toList()
);
retrievalContextSource.put(
"questionReferences",
retrievalContext.questionReferences().stream()
.map(this::questionReferenceFingerprintSource)
"corpusReferences",
CorpusReferenceContext.from(retrievalContext).stream()
.map(reference -> {
Map<String, Object> source = new LinkedHashMap<>();
source.put("corpusId", reference.corpusId());
source.put("category", reference.category());
source.put("title", reference.title());
source.put("content", reference.content());
source.put("rank", reference.rank());
return source;
})
.toList()
);
return retrievalContextSource;
}

private Map<String, Object> jobPostingReferenceFingerprintSource(RetrievedJobPostingReference reference) {
Map<String, Object> referenceSource = new LinkedHashMap<>();
referenceSource.put("corpusId", reference.corpusId());
referenceSource.put("companyName", defaultString(reference.companyName()));
referenceSource.put("roleName", defaultString(reference.roleName()));
referenceSource.put("responsibilities", defaultString(reference.responsibilities()));
referenceSource.put("requirements", defaultString(reference.requirements()));
referenceSource.put("preferred", defaultString(reference.preferred()));
referenceSource.put("distance", reference.distance());
return referenceSource;
}

private Map<String, Object> questionReferenceFingerprintSource(RetrievedQuestionReference reference) {
Map<String, Object> referenceSource = new LinkedHashMap<>();
referenceSource.put("corpusId", reference.corpusId());
referenceSource.put("companyName", defaultString(reference.companyName()));
referenceSource.put("roleName", defaultString(reference.roleName()));
referenceSource.put("questionType", defaultString(reference.questionType()));
referenceSource.put("charLimit", reference.charLimit());
referenceSource.put("questionText", defaultString(reference.questionText()));
referenceSource.put("distance", reference.distance());
return referenceSource;
}

private Map<String, Object> jobPostingFingerprintSource(JobPosting jobPosting) {
Map<String, Object> jobPostingSource = new LinkedHashMap<>();
jobPostingSource.put("companyName", defaultString(jobPosting.getCompany().getName()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,7 @@ public AnalysisResponse analyze(User user, Long mockApplyId) {
public void validateAnalysisRequest(User user, Long mockApplyId) {
MockApply mockApply = getOwnedMockApply(user, mockApplyId);
List<Question> questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId());
boolean hasAnsweredQuestion = questions.stream()
.anyMatch(question -> StringUtils.hasText(question.getAnswer()));

if (!hasAnsweredQuestion) {
throw new GeneralException(
GeneralErrorCode.INVALID_PARAMETER,
"분석할 자소서 답변이 1개 이상 필요합니다."
);
}
answeredQuestionsOrThrow(questions);
}

@Transactional
Expand All @@ -140,10 +132,16 @@ public void refundAnalysisCredit(User user, String referenceId) {

@Transactional(readOnly = true)
public AnalysisExecutionPayload prepareAnalysisExecution(User user, Long mockApplyId) {
MockApply mockApply = getOwnedMockApply(user, mockApplyId);
List<Question> questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId());
List<Question> answeredQuestions = answeredQuestionsOrThrow(questions);
return prepareAnalysisExecution(
user,
mockApplyId,
jobPostingRagContextAssembler.assemble(getOwnedMockApply(user, mockApplyId).getJobPosting().getId())
mockApply,
questions,
answeredQuestions,
retrieveAnalysisReferences(mockApply.getJobPosting(), answeredQuestions),
jobPostingRagContextAssembler.assemble(mockApply.getJobPosting().getId())
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand All @@ -155,17 +153,25 @@ public AnalysisExecutionPayload prepareAnalysisExecution(
) {
MockApply mockApply = getOwnedMockApply(user, mockApplyId);
List<Question> questions = questionRepository.findAllByMockApplyIdOrderByIdAsc(mockApply.getId());
List<Question> answeredQuestions = questions.stream()
.filter(question -> StringUtils.hasText(question.getAnswer()))
.toList();

if (answeredQuestions.isEmpty()) {
throw new GeneralException(
GeneralErrorCode.INVALID_PARAMETER,
"분석할 자소서 답변이 1개 이상 필요합니다."
);
}
List<Question> answeredQuestions = answeredQuestionsOrThrow(questions);
return prepareAnalysisExecution(
user,
mockApply,
questions,
answeredQuestions,
new RetrievalContext(List.of(), List.of()),
similarJobPostings
);
}

private AnalysisExecutionPayload prepareAnalysisExecution(
User user,
MockApply mockApply,
List<Question> questions,
List<Question> answeredQuestions,
RetrievalContext retrievalContext,
List<SimilarJobPostingContext> similarJobPostings
) {
// Initialize hierarchy before leaving the read transaction so detached payload can be used safely.
mockApply.getJobPosting().getDetailClassification().getMiddleClassification().getMiddleName();
mockApply.getJobPosting().getDetailClassification().getMiddleClassification().getClassification().getBigName();
Expand All @@ -175,16 +181,29 @@ public AnalysisExecutionPayload prepareAnalysisExecution(

return new AnalysisExecutionPayload(
user.getId(),
mockApplyId,
mockApply.getId(),
mockApply.getJobPosting(),
List.copyOf(questions),
List.copyOf(answeredQuestions),
evaluationCriteria,
retrieveAnalysisReferences(mockApply.getJobPosting(), answeredQuestions),
retrievalContext,
similarJobPostings
);
}

private List<Question> answeredQuestionsOrThrow(List<Question> questions) {
List<Question> answeredQuestions = questions.stream()
.filter(question -> StringUtils.hasText(question.getAnswer()))
.toList();
if (answeredQuestions.isEmpty()) {
throw new GeneralException(
GeneralErrorCode.INVALID_PARAMETER,
"분석할 자소서 답변이 1개 이상 필요합니다."
);
}
return answeredQuestions;
}

public AnalysisLlmResponse executeAnalysis(AnalysisExecutionPayload payload) {
return analysisAiClient.analyze(payload);
}
Expand Down Expand Up @@ -339,8 +358,8 @@ private RetrievalContext retrieveAnalysisReferences(JobPosting jobPosting, List<
try {
return corpusRetrievalService.retrieveForAnalysis(jobPosting, answeredQuestions);
} catch (Exception exception) {
log.warn("자소서 분석 fingerprint용 retrieval 실패. fallback without references. message={}", exception.getMessage());
log.debug("analysis fingerprint retrieval exception", exception);
log.warn("자소서 분석 Curated Corpus retrieval 실패. fallback without references. message={}", exception.getMessage());
log.debug("analysis Curated Corpus retrieval exception", exception);
return new RetrievalContext(List.of(), List.of());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

Expand Down Expand Up @@ -57,6 +58,7 @@ public class CorpusRetrievalService {
private final CorpusEmbeddingClient corpusEmbeddingClient;
private final DataSource dataSource;

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public RetrievalContext retrieveForAnalysis(JobPosting jobPosting, List<Question> questions) {
String jdQuery = buildAnalysisJobPostingQuery(jobPosting);
String questionQuery = buildAnalysisQuestionQuery(jobPosting, questions);
Expand Down
Loading
Loading