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 @@ -17,8 +17,37 @@ class InterviewAiOrchestrator(
private val llmProviderRouter: LlmProviderRouter,
private val objectMapper: ObjectMapper
) {
private val interviewQuestionEndings = listOf(
"์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”",
"๋ง์”€ํ•ด ์ฃผ์„ธ์š”",
"์–˜๊ธฐํ•ด ์ฃผ์„ธ์š”",
"์ด์•ผ๊ธฐํ•ด ์ฃผ์„ธ์š”",
"์•Œ๋ ค ์ฃผ์„ธ์š”",
"๊ณต์œ ํ•ด ์ฃผ์„ธ์š”",
"์ •๋ฆฌํ•ด ์ฃผ์„ธ์š”",
"์„ค๋ช…ํ•ด์ฃผ์„ธ์š”",
"๋ง์”€ํ•ด์ฃผ์„ธ์š”",
"์–˜๊ธฐํ•ด์ฃผ์„ธ์š”",
"์ด์•ผ๊ธฐํ•ด์ฃผ์„ธ์š”",
"์•Œ๋ ค์ฃผ์„ธ์š”",
"๊ณต์œ ํ•ด์ฃผ์„ธ์š”",
"์ •๋ฆฌํ•ด์ฃผ์„ธ์š”",
"๋ฌด์—‡์ธ๊ฐ€์š”",
"์™œ ๊ทธ๋Ÿฐ๊ฐ€์š”",
"์–ด๋–ป๊ฒŒ ์ƒ๊ฐํ•˜์‹œ๋‚˜์š”",
"์–ด๋–ป๊ฒŒ ๋ณด์‹œ๋‚˜์š”",
"ํ•ด์ฃผ์‹ค ์ˆ˜ ์žˆ๋‚˜์š”",
"๋งํ•ด์ฃผ์‹ค ์ˆ˜ ์žˆ๋‚˜์š”",
"์„ค๋ช…ํ•ด์ฃผ์‹ค ์ˆ˜ ์žˆ๋‚˜์š”"
)

private val logger = LoggerFactory.getLogger(javaClass)

data class DocumentQuestionValidationResult(
val accepted: List<GeneratedDocumentQuestion>,
val rejectedReasons: List<String>
)

fun evaluateTechAnswer(
question: QaQuestion?,
userAnswer: String,
Expand Down Expand Up @@ -69,8 +98,18 @@ class InterviewAiOrchestrator(
try {
val generated = llmProviderRouter.generateJson(prompt, temperature = temperature)
val parsed = parseGeneratedDocumentQuestions(generated.text, fileTypeLabel)
val validated = validateGeneratedDocumentQuestions(parsed, fileTypeLabel)
validated.forEach { item ->
val validation = validateGeneratedDocumentQuestions(parsed, fileTypeLabel)
if (validation.rejectedReasons.isNotEmpty()) {
logger.info(
"document question validation summary fileType={} round={} accepted={} rejected={} reasons={}",
fileTypeLabel,
round + 1,
validation.accepted.size,
validation.rejectedReasons.size,
summarizeRejectedReasons(validation.rejectedReasons)
)
}
validation.accepted.forEach { item ->
val key = item.questionText.trim().lowercase()
if (key.isNotBlank() && !collected.containsKey(key)) {
collected[key] = item.copy(questionNo = collected.size + 1)
Expand Down Expand Up @@ -697,48 +736,90 @@ class InterviewAiOrchestrator(
private fun validateGeneratedDocumentQuestions(
generated: List<GeneratedDocumentQuestion>,
fileTypeLabel: String
): List<GeneratedDocumentQuestion> {
): DocumentQuestionValidationResult {
val seen = linkedSetOf<String>()
return generated.mapNotNull { item ->
val accepted = mutableListOf<GeneratedDocumentQuestion>()
val rejectedReasons = mutableListOf<String>()
generated.forEach { item ->
val normalizedQuestionType = normalizeDocumentQuestionType(item.questionType, fileTypeLabel)
val normalizedEvidenceKind = normalizeEvidenceKind(item.evidenceKind)
val normalizedQuestion = item.questionText.replace(Regex("\\s+"), " ").trim()
if (!isUsableDocumentQuestion(normalizedQuestion, normalizedQuestionType, normalizedEvidenceKind)) return@mapNotNull null
if (!isUsableDocumentQuestion(normalizedQuestion, normalizedQuestionType, normalizedEvidenceKind)) {
rejectedReasons += "unusable_question"
return@forEach
}

val fingerprint = normalizedQuestion
.lowercase()
.replace(Regex("[^a-z0-9๊ฐ€-ํžฃ]+"), "")
if (!seen.add(fingerprint)) return@mapNotNull null
if (!seen.add(fingerprint)) {
rejectedReasons += "duplicate_question"
return@forEach
}

if (!isAllowedDocumentQuestionType(normalizedQuestionType, fileTypeLabel, normalizedEvidenceKind)) return@mapNotNull null
if (!isCompatibleQuestionForEvidenceKind(normalizedQuestion, normalizedQuestionType, normalizedEvidenceKind)) return@mapNotNull null
if (!isAllowedDocumentQuestionType(normalizedQuestionType, fileTypeLabel, normalizedEvidenceKind)) {
rejectedReasons += "question_type_mismatch"
return@forEach
}
if (!isCompatibleQuestionForEvidenceKind(normalizedQuestion, normalizedQuestionType, normalizedEvidenceKind)) {
rejectedReasons += "question_evidence_kind_mismatch"
return@forEach
}

val normalizedAnswer = item.referenceAnswer
?.replace(Regex("\\s+"), " ")
?.trim()
?.takeIf {
it.isNotBlank() &&
!isGuideLikeModelAnswer(it) &&
isDocumentAnswerLinkedToQuestion(it, normalizedQuestion, normalizedQuestionType) &&
isCompatibleReferenceAnswer(it, normalizedQuestionType, normalizedEvidenceKind)
?: run {
rejectedReasons += "missing_reference_answer"
return@forEach
}
?: return@mapNotNull null
if (normalizedAnswer.isBlank()) {
rejectedReasons += "blank_reference_answer"
return@forEach
}
if (isGuideLikeModelAnswer(normalizedAnswer)) {
rejectedReasons += "guide_like_reference_answer"
return@forEach
}
if (!isDocumentAnswerLinkedToQuestion(normalizedAnswer, normalizedQuestion, normalizedQuestionType)) {
rejectedReasons += "reference_answer_not_linked"
return@forEach
}
if (!isCompatibleReferenceAnswer(normalizedAnswer, normalizedQuestionType, normalizedEvidenceKind)) {
rejectedReasons += "reference_answer_evidence_kind_mismatch"
return@forEach
}

val normalizedEvidence = item.evidence
.map { it.replace(Regex("\\s+"), " ").trim() }
.filter { it.length >= 8 }
.distinct()
.take(4)
if (normalizedEvidence.isEmpty()) return@mapNotNull null
if (normalizedEvidence.isEmpty()) {
rejectedReasons += "empty_evidence"
return@forEach
}

item.copy(
accepted += item.copy(
questionText = normalizedQuestion,
questionType = normalizedQuestionType,
evidenceKind = normalizedEvidenceKind,
referenceAnswer = normalizedAnswer,
evidence = normalizedEvidence
)
}
return DocumentQuestionValidationResult(
accepted = accepted,
rejectedReasons = rejectedReasons
)
}

private fun summarizeRejectedReasons(reasons: List<String>): String {
return reasons.groupingBy { it }
.eachCount()
.entries
.sortedByDescending { it.value }
.joinToString(", ") { (reason, count) -> "$reason=$count" }
}

private fun parseGeneratedTechQuestions(raw: String): List<GeneratedTechQuestion> {
Expand Down Expand Up @@ -897,10 +978,18 @@ class InterviewAiOrchestrator(
commonDomainHints
}
if (domainHints.none { lowered.contains(it) }) return false
if (!questionText.trim().endsWith("?")) return false
if (!hasInterviewQuestionEnding(questionText)) return false
return true
}

private fun hasInterviewQuestionEnding(questionText: String): Boolean {
val trimmed = questionText.trim()
if (trimmed.endsWith("?")) return true

val normalized = trimmed.removeSuffix(".").removeSuffix("!").trim()
return interviewQuestionEndings.any { normalized.endsWith(it) }
}

private fun documentQuestionTypeRules(fileTypeKey: String): List<String> {
return when (fileTypeKey) {
"RESUME" -> listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ class DocumentInterviewService(
}

if (results.isNotEmpty()) {
return results
return DocumentQuestionGenerationPolicy.prioritizeSnippets(file.fileType, results)
.map(::sanitizePromptSnippet)
.filter { isUsablePromptSnippet(file.fileType, it) }
.map { it.take(420) }
Expand Down Expand Up @@ -913,9 +913,9 @@ class DocumentInterviewService(

val base = when (fileType) {
FileType.RESUME -> listOf(
"์ด๋ ฅ์„œ์—์„œ ์‹ค์ œ๋กœ ์ˆ˜ํ–‰ํ•œ ์—ญํ• ๊ณผ ์ฑ…์ž„์ด ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ฒฝํ—˜",
"์ด๋ ฅ์„œ์—์„œ ์„ฑ๊ณผ๋‚˜ ๊ฐœ์„  ๊ฒฐ๊ณผ๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ฒฝํ—˜",
"์ด๋ ฅ์„œ์—์„œ ํ˜‘์—…, ๋ฌธ์ œ ํ•ด๊ฒฐ, ์˜์‚ฌ๊ฒฐ์ •์ด ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ฒฝํ—˜"
"์ด๋ ฅ์„œ์—์„œ ์ง๋ฌด ๊ด€๋ จ ๊ฒฝ๋ ฅ, ์ธํ„ด, ํ”„๋กœ์ ํŠธ, ์—ฐ๊ตฌ์‹ค ํ™œ๋™์ฒ˜๋Ÿผ ์‹ค์ œ๋กœ ์ˆ˜ํ–‰ํ•œ ์—ญํ• ๊ณผ ์ฑ…์ž„์ด ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ฒฝํ—˜",
"์ด๋ ฅ์„œ์—์„œ ์„ฑ๊ณผ, ์ˆ˜์ƒ, ๊ธฐ์ˆ ์  ๋ฌธ์ œ ํ•ด๊ฒฐ, ๊ฐœ์„  ๊ฒฐ๊ณผ๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ฒฝํ—˜",
"์ด๋ ฅ์„œ์™€ ์ง€์›์„œ ๋ฌธํ•ญ์—์„œ ์ง€์› ๋™๊ธฐ, ์„ฑ์žฅ ๊ณผ์ •, ์–ด๋ ค์›€ ๊ทน๋ณต, ๊ธฐ์ˆ ์  ๋„์ „์ด ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ฒฝํ—˜"
)
FileType.PORTFOLIO -> listOf(
"ํฌํŠธํด๋ฆฌ์˜ค์—์„œ ํ”„๋กœ์ ํŠธ ์—ญํ• , ๊ธฐ์ˆ  ์„ ํƒ, ๊ตฌํ˜„ ์ฑ…์ž„์ด ๋“œ๋Ÿฌ๋‚˜๋Š” ๋‚ด์šฉ",
Expand Down Expand Up @@ -946,21 +946,25 @@ class DocumentInterviewService(
return chunks.take(min(1, snippetBudget)).map { it.take(500) }
}

val prioritized = DocumentQuestionGenerationPolicy.prioritizeSnippets(fileType, normalized)

val indexes = when {
normalized.size <= snippetBudget -> normalized.indices.toList()
prioritized.size <= snippetBudget -> prioritized.indices.toList()
else -> buildList {
val candidateCount = min(prioritized.size, max(snippetBudget, snippetBudget * 2))
val candidateLastIndex = candidateCount - 1
add(0)
val step = max(1, normalized.lastIndex / max(1, snippetBudget - 1))
val step = max(1, candidateLastIndex / max(1, snippetBudget - 1))
var current = step
while (size < snippetBudget - 1 && current < normalized.lastIndex) {
while (size < snippetBudget - 1 && current < candidateLastIndex) {
add(current)
current += step
}
add(normalized.lastIndex)
add(candidateLastIndex)
}
}.distinct()

return indexes.map { normalized[it].take(500) }
return indexes.map { prioritized[it].take(500) }
}

private fun resolveTechCandidates(userId: Long, categoryId: Long, difficulty: QuestionDifficulty?) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ internal data class ClassifiedPromptSnippet(
}

internal object DocumentQuestionGenerationPolicy {
private val resumeRoleSignals = listOf(
"๊ฒฝ๋ ฅ", "์ง๋ฌด๊ด€๋ จ ๊ฒฝ๋ ฅ", "์ธํ„ด", "ํ”„๋กœ์ ํŠธ", "์—ฐ๊ตฌ์‹ค", "ํ•™๋ถ€์—ฐ๊ตฌ์ƒ", "๋Œ€์™ธ ํ™œ๋™",
"์ˆ˜์ƒ", "ํ•ด์ปคํ†ค", "๋ฐฑ์—”๋“œ", "๊ฐœ๋ฐœ", "๊ตฌํ˜„", "์„ค๊ณ„", "๊ฐœ์„ ", "๋ฌธ์ œ", "ํ•ด๊ฒฐ",
"์„ฑ๊ณผ", "๊ฒฐ๊ณผ", "์ง€์›ํ•œ ์ด์œ ", "์ง€์›๋™๊ธฐ", "์ž…์‚ฌ ํ›„", "์„ฑ์žฅ๊ณผ์ •", "์—ญ๊ฒฝ", "๊ธฐ์ˆ ์  ๋„์ „",
"intern", "project", "award", "hackathon", "backend", "implemented", "designed",
"improved", "result", "motivation", "growth", "challenge"
)
private val resumeNoiseSignals = listOf(
"์ง€์›์ž", "ํœด๋Œ€ํฐ ๋ฒˆํ˜ธ", "์ „ํ™” ๋ฒˆํ˜ธ", "ํฌ๋ง๊ทผ๋ฌด์ง€์—ญ", "์ฃผ์†Œ", "์˜๋ฌธ", "์ธ์ ์‚ฌํ•ญ",
"ํ•™์ ", "ํ‰์ ", "ํ•™๋ฒˆ", "์ทจ๋“ ํ•™์ ", "๊ณผ๋ชฉ๋ช…", "์„ฑ์ ", "์ˆ˜๊ฐ•์—ฐ๋„", "ํ•™๊ธฐ", "๊ต์–‘", "์ „๊ณต",
"phone", "address", "gpa", "grade", "semester", "course"
)
private val introduceAspirationSignals = listOf(
"์ง€์›๋™๊ธฐ", "ํฌ๋ถ€", "๊ฐ€์น˜๊ด€", "๊ณ„ํš", "์„ฑ์žฅ", "motivation", "value", "plan"
)
private val aspirationMarkers = listOf(
"์‹ถ", "ํ•˜๊ณ ์ž", "ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค", "๊ฐ€์ง€๊ฒ ์Šต๋‹ˆ๋‹ค", "ํฌ๋ง", "ํฌ๋ถ€", "์ง€์› ๋™๊ธฐ", "์ง€์›๋™๊ธฐ",
"๊ธฐ์—ฌ", "์„ฑ์žฅ", "๋ฐฐ์šฐ", "๋ชฉํ‘œ", "๊ด€์‹ฌ", "๊ฟˆ", "๋˜๊ณ ์ž", "๋˜๊ฒ ์Šต๋‹ˆ๋‹ค",
Expand Down Expand Up @@ -91,6 +106,10 @@ internal object DocumentQuestionGenerationPolicy {
FileType.PROFILE_IMAGE -> 2
}

fun prioritizeSnippets(fileType: FileType, snippets: List<String>): List<String> {
return snippets.sortedByDescending { promptSnippetScore(fileType, it) }
}

fun classifySnippets(fileType: FileType, snippets: List<String>): List<ClassifiedPromptSnippet> {
return snippets.map { snippet ->
ClassifiedPromptSnippet(
Expand Down Expand Up @@ -148,6 +167,30 @@ internal object DocumentQuestionGenerationPolicy {
FileType.PROFILE_IMAGE -> 0.5
}

private fun promptSnippetScore(fileType: FileType, snippet: String): Int {
val lowered = snippet.lowercase()
return when (fileType) {
FileType.RESUME -> {
val signalHits = resumeRoleSignals.count { lowered.contains(it) } * 18
val noiseHits = resumeNoiseSignals.count { lowered.contains(it) } * 16
val numberBonus = if (Regex("""\d+[%๊ฑด๋ช…๋ฐฐํšŒ]""").containsMatchIn(snippet)) 12 else 0
val lengthBonus = min(20, snippet.length / 40)
signalHits + numberBonus + lengthBonus - noiseHits
}
FileType.INTRODUCE -> {
val signalHits = resumeRoleSignals.count { lowered.contains(it) } * 10
val aspirationHits = introduceAspirationSignals.count { lowered.contains(it) } * 14
signalHits + aspirationHits + min(16, snippet.length / 50)
}
FileType.PORTFOLIO -> {
val signalHits = resumeRoleSignals.count { lowered.contains(it) } * 15
val numberBonus = if (Regex("""\d+[%๊ฑด๋ช…๋ฐฐํšŒ]""").containsMatchIn(snippet)) 10 else 0
signalHits + numberBonus + min(18, snippet.length / 45)
}
FileType.PROFILE_IMAGE -> snippet.length / 10
}
}
Comment on lines +170 to +192

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

promptSnippetScore ํ•จ์ˆ˜ ๋‚ด์— ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ์ €ํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ๋งค์ง ๋„˜๋ฒ„(magic number)๊ฐ€ ์‚ฌ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ ์ˆซ์ž์˜ ์˜๋ฏธ๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ  ํ–ฅํ›„ ๊ฐ€์ค‘์น˜ ํŠœ๋‹์„ ์šฉ์ดํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด, ์ด ๊ฐ’๋“ค์„ ์˜๋ฏธ ์žˆ๋Š” ์ด๋ฆ„์˜ ์ง€์—ญ ์ƒ์ˆ˜๋กœ ์„ ์–ธํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    private fun promptSnippetScore(fileType: FileType, snippet: String): Int {
        val lowered = snippet.lowercase()
        return when (fileType) {
            FileType.RESUME -> {
                val signalWeight = 18
                val noiseWeight = 16
                val numberBonusValue = 12
                val maxLengthBonus = 20
                val lengthBonusDivisor = 40

                val signalHits = resumeRoleSignals.count { lowered.contains(it) } * signalWeight
                val noiseHits = resumeNoiseSignals.count { lowered.contains(it) } * noiseWeight
                val numberBonus = if (Regex("""\d+[%๊ฑด๋ช…๋ฐฐํšŒ]""").containsMatchIn(snippet)) numberBonusValue else 0
                val lengthBonus = min(maxLengthBonus, snippet.length / lengthBonusDivisor)
                signalHits + numberBonus + lengthBonus - noiseHits
            }
            FileType.INTRODUCE -> {
                val signalWeight = 10
                val aspirationWeight = 14
                val maxLengthBonus = 16
                val lengthBonusDivisor = 50

                val signalHits = resumeRoleSignals.count { lowered.contains(it) } * signalWeight
                val aspirationHits = introduceAspirationSignals.count { lowered.contains(it) } * aspirationWeight
                signalHits + aspirationHits + min(maxLengthBonus, snippet.length / lengthBonusDivisor)
            }
            FileType.PORTFOLIO -> {
                val signalWeight = 15
                val numberBonusValue = 10
                val maxLengthBonus = 18
                val lengthBonusDivisor = 45

                val signalHits = resumeRoleSignals.count { lowered.contains(it) } * signalWeight
                val numberBonus = if (Regex("""\d+[%๊ฑด๋ช…๋ฐฐํšŒ]""").containsMatchIn(snippet)) numberBonusValue else 0
                signalHits + numberBonus + min(maxLengthBonus, snippet.length / lengthBonusDivisor)
            }
            FileType.PROFILE_IMAGE -> {
                val lengthBonusDivisor = 10
                snippet.length / lengthBonusDivisor
            }
        }
    }


private fun countMarkerHits(text: String, markers: List<String>): Int {
return markers.count { marker -> text.contains(marker.lowercase()) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,41 @@ class InterviewAiOrchestratorTests {
assertThat(capturedPrompt).contains("INTRODUCE_MOTIVATION, INTRODUCE_VALUE, INTRODUCE_FUTURE_PLAN, INTRODUCE_EXPERIENCE")
}

@Test
fun `๋ฌธ์„œ ์งˆ๋ฌธ์€ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”๋กœ ๋๋‚˜๋Š” ํ•œ๊ตญ์–ด ๋ฉด์ ‘ ๋ฌธ์žฅ๋„ ํ—ˆ์šฉํ•œ๋‹ค`() {
given(llmProviderRouter.generateJson(anyString(), nullable(Double::class.java))).willReturn(
LlmGenerationResult(
model = "gemini",
modelVersion = "v1",
text = """
{
"questions": [
{
"questionText": "JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์„ ํƒํ•œ ์ด์œ ์™€ ์‹ค๋ฌด ์ ์šฉ ์‹œ ์žฅ๋‹จ์ ์„ ์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”.",
"questionType": "RESUME_EXPERIENCE",
"evidenceKind": "ACTUAL_EXPERIENCE",
"referenceAnswer": "ํ”„๋กœ์ ํŠธ์—์„œ ์ธ์ฆ ์ฒด๊ณ„๋ฅผ ๋น ๋ฅด๊ฒŒ ๊ตฌ์ถ•ํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹น์‹œ ์ €๋Š” ์„œ๋ฒ„ API ์„ค๊ณ„์™€ ์ธ์ฆ ๋กœ์ง ๊ตฌํ˜„์„ ๋‹ด๋‹นํ–ˆ์Šต๋‹ˆ๋‹ค. JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์„ ํƒํ•œ ์ด์œ ๋Š” ์„ธ์…˜ ๋ฐฉ์‹๋ณด๋‹ค ํ™•์žฅ์„ฑ๊ณผ ํด๋ผ์ด์–ธํŠธ ๋ถ„๋ฆฌ ์ธก๋ฉด์—์„œ ์œ ๋ฆฌํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์‹ค๋ฌด ์ ์šฉ์—์„œ๋Š” ํ† ํฐ ํƒˆ์ทจ์™€ ๋ฌดํšจํ™” ๊ฐ™์€ ์žฅ๋‹จ์ ์„ ํ•จ๊ป˜ ๊ณ ๋ คํ•ด Refresh Token๊ณผ Redis ๊ธฐ๋ฐ˜ ์„ธ์…˜ ์ถ”์ ์„ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ฒฐ๊ณผ ์ธ์ฆ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ์œ ์—ฐ์„ฑ๊ณผ ๋ณด์•ˆ ํ†ต์ œ๋ฅผ ๋™์‹œ์— ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.",
"evidence": [
"JWT, Refresh Token, Redis ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ตฌ์กฐ๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•œ ๊ฒฝํ—˜์ด ์žˆ์Œ"
]
}
]
}
""".trimIndent()
)
)

val generated = orchestrator.generateDocumentQuestions(
fileTypeLabel = "RESUME",
difficulty = null,
questionCount = 1,
contextSnippets = listOf("JWT, Refresh Token, Redis ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ตฌ์กฐ๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•œ ๊ฒฝํ—˜์ด ์žˆ๋‹ค.")
)

assertThat(generated).hasSize(1)
assertThat(generated.first().questionText).endsWith("์„ค๋ช…ํ•ด ์ฃผ์„ธ์š”.")
}

@Test
fun `์˜์–ด ๋ฌธ์„œ ๋‹ต๋ณ€ ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ๋Š” ํ‰๊ฐ€ ์ถœ๋ ฅ์€ ํ•œ๊ตญ์–ด๋กœ ์œ ์ง€ํ•˜๊ณ  ์˜์–ด ๋ฌธ๋ฒ• ๊ธฐ์ค€์„ ๋ช…์‹œํ•œ๋‹ค`() {
var capturedPrompt = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,20 @@ class DocumentQuestionGenerationPolicyTests {

assertThat(classified.single().kind).isNotEqualTo(DocumentSnippetKind.PROJECT_OR_RESULT)
}

@Test
fun `์ด๋ ฅ์„œ snippet ์šฐ์„ ์ˆœ์œ„๋Š” ๊ณผ๋ชฉํ‘œ๋ณด๋‹ค ๊ฒฝ๋ ฅ๊ณผ ์ˆ˜์ƒ ๊ฒฝํ—˜์„ ์•ž์„ธ์šด๋‹ค`() {
val prioritized = DocumentQuestionGenerationPolicy.prioritizeSnippets(
fileType = FileType.RESUME,
snippets = listOf(
"ํ•™ ์‚ฌ ์ปดํ“จํ„ฐ๊ณตํ•™ 2023 1 ์ „๊ณต ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค 3 A+ ์šด์˜์ฒด์ œ 3 A ๊ณผ๋ชฉ๋ช… ์ทจ๋“ ํ•™์  ์„ฑ์ ",
"๋งˆ์ŒAI์—์„œ SW ๊ฐœ๋ฐœ ์ธํ„ด์œผ๋กœ ์Œ์„ฑ๋ด‡ ๊ณ ๋„ํ™” ์ž‘์—…๊ณผ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์„ ๋‹ด๋‹นํ–ˆ์Šต๋‹ˆ๋‹ค.",
"AWS ๋ฃจํ‚ค ์ฑ”ํ”ผ์–ธ์‹ญ์—์„œ Slack ์•Œ๋ฆผ ๋ด‡์„ ์ฃผ์ œ๋กœ OCR ๋ถ„์„๊ณผ ๋ฒˆ์—ญ ๊ธฐ๋Šฅ์„ ์ ‘๋ชฉํ•œ ์„œ๋น„์Šค๋ฅผ ์ œ์ž‘ํ•ด ์ˆ˜์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
)
)

assertThat(prioritized.first()).contains("๋งˆ์ŒAI")
assertThat(prioritized[1]).contains("AWS ๋ฃจํ‚ค ์ฑ”ํ”ผ์–ธ์‹ญ")
assertThat(prioritized.last()).contains("๊ณผ๋ชฉ๋ช…")
}
}
Loading