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
5 changes: 3 additions & 2 deletions ai/src/ai_server/chain/prompts/question_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
)

HUMAN_PROMPT = (
"직군: {job_category}\n"
"직군(복수 가능): {job_categories}\n"
"면접 모드: {mode}\n"
"최대 질문 수: {max_questions}\n\n"
"지원자 컨텍스트 (이력서/레포 분석):\n"
Expand All @@ -25,10 +25,11 @@
"1. 정확히 {max_questions}개의 질문을 생성하되, **서로 다른 주제/영역**을 다룹니다 "
"(각 질문이 일반질문 1개의 씨앗이 되고 뒤에 꼬리질문이 붙으므로 같은 주제 반복 금지).\n"
"2. 각 질문에 적절한 category 를 부여합니다.\n"
"3. 직군({job_category})·면접 모드({mode}) 에 맞는 비중으로 카테고리 분배:\n"
"3. 직군({job_categories})·면접 모드({mode}) 에 맞는 비중으로 카테고리 분배:\n"
" - TECHNICAL: CS_FUNDAMENTAL + TECH_CHOICE + PROJECT_DEEP_DIVE 중심.\n"
" - PERSONALITY: BEHAVIORAL 중심.\n"
" - INTEGRATED: 모두 균형.\n"
" 직군이 여러 개면 각 직군에 질문이 고르게 분배되도록 합니다.\n"
" 자료가 받쳐주는 만큼만 분배하고, 근거 없는 카테고리를 억지로 채우지 마세요.\n"
"4. 각 질문에 target_evidence 와 expected_signal 을 채웁니다:\n"
" - target_evidence: 질문의 근거가 된 컨텍스트 원문을 짧게 인용. "
Expand Down
6 changes: 3 additions & 3 deletions ai/src/ai_server/chain/question_generation_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class QuestionGenerator(Protocol):
async def generate(
self,
*,
job_category: str,
job_categories: list[str],
mode: str,
max_questions: int,
context: str,
Expand All @@ -43,15 +43,15 @@ def __init__(self, chain: Runnable) -> None:
async def generate(
self,
*,
job_category: str,
job_categories: list[str],
mode: str,
max_questions: int,
context: str,
recent_questions: list[str] | None = None,
) -> GeneratedQuestionPool:
result = await self._chain.ainvoke(
{
"job_category": job_category,
"job_categories": ", ".join(job_categories),
"mode": mode,
"max_questions": max_questions,
"context": context,
Expand Down
4 changes: 2 additions & 2 deletions ai/src/ai_server/messaging/consumers/questions_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ async def handle(self, message: AbstractIncomingMessage) -> None:

context_text = await self._build_context(req)
pool = await self._generator.generate(
job_category=req.job_category,
job_categories=req.job_categories,
mode=req.mode,
max_questions=effective_pool_size,
context=context_text,
Expand Down Expand Up @@ -173,7 +173,7 @@ def _build_context(documents: list[DocumentContext]) -> str:
def _build_initial_rag_query(req: GenerateQuestionsRequest) -> str:
parts = [
f"mode: {req.mode}",
f"job category: {req.job_category}",
f"job categories: {', '.join(req.job_categories)}",
]
for d in req.documents:
doc_parts = [f"document #{d.document_id} {d.source_type}"]
Expand Down
3 changes: 2 additions & 1 deletion ai/src/ai_server/model/messages/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class GenerateQuestionsRequest(BaseModel):

session_id: int
mode: InterviewMode
job_category: JobCategory
# 직군 다중 선택. 첫 항목이 대표 직군.
job_categories: list[JobCategory] = []
documents: list[DocumentContext] = []
initial_question_count: int = 1
max_questions: int = 10
Expand Down
3 changes: 2 additions & 1 deletion ai/tests/test_question_generation_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async def test_generate_forwards_formatted_recent_questions_to_chain():
generator = LlmQuestionGenerator(chain)

await generator.generate(
job_category="BACKEND",
job_categories=["BACKEND", "FRONTEND"],
mode="TECHNICAL",
max_questions=3,
context="ctx",
Expand All @@ -36,3 +36,4 @@ async def test_generate_forwards_formatted_recent_questions_to_chain():

chain_input = chain.ainvoke.call_args.args[0]
assert chain_input["recent_questions"] == "- 이전 질문"
assert chain_input["job_categories"] == "BACKEND, FRONTEND"
16 changes: 8 additions & 8 deletions ai/tests/test_questions_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ async def test_consumer_generates_questions_and_publishes_callback():
{
"sessionId": 99,
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"jobCategories": ["BACKEND"],
"documents": [
{
"documentId": 1,
Expand All @@ -97,7 +97,7 @@ async def test_consumer_generates_questions_and_publishes_callback():

generator.generate.assert_awaited_once()
call = generator.generate.await_args
assert call.kwargs["job_category"] == "BACKEND"
assert call.kwargs["job_categories"] == ["BACKEND"]
assert call.kwargs["mode"] == "TECHNICAL"
# maxQuestions is the session limit; initialQuestionCount controls this result.
assert call.kwargs["max_questions"] == 2
Expand Down Expand Up @@ -133,7 +133,7 @@ async def test_consumer_skips_when_message_id_already_seen():
{
"sessionId": 99,
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"jobCategories": ["BACKEND"],
"documents": [],
"maxQuestions": 3,
}
Expand Down Expand Up @@ -161,7 +161,7 @@ async def test_consumer_defaults_initial_question_count_to_one():
{
"sessionId": 99,
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"jobCategories": ["BACKEND"],
"documents": [],
"maxQuestions": 5,
}
Expand Down Expand Up @@ -189,7 +189,7 @@ async def test_consumer_clamps_initial_question_count_to_at_least_one():
{
"sessionId": 99,
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"jobCategories": ["BACKEND"],
"documents": [],
"initialQuestionCount": 0,
"maxQuestions": 5,
Expand Down Expand Up @@ -233,7 +233,7 @@ async def test_consumer_injects_initial_rag_chunks_when_available():
{
"sessionId": 99,
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"jobCategories": ["BACKEND"],
"documents": [
{
"documentId": 1,
Expand Down Expand Up @@ -292,7 +292,7 @@ async def test_single_document_skips_rag_plan_mode():
{
"sessionId": 1,
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"jobCategories": ["BACKEND"],
"documents": [
{
"documentId": 1,
Expand Down Expand Up @@ -336,7 +336,7 @@ async def test_consumer_falls_back_to_document_context_when_rag_fails():
{
"sessionId": 99,
"mode": "TECHNICAL",
"jobCategory": "BACKEND",
"jobCategories": ["BACKEND"],
"documents": [
{
"documentId": 1,
Expand Down
19 changes: 15 additions & 4 deletions backend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2515,9 +2515,13 @@
"type" : "string",
"enum" : [ "TECHNICAL", "PERSONALITY", "INTEGRATED" ]
},
"jobCategory" : {
"type" : "string",
"enum" : [ "FRONTEND", "BACKEND", "INFRA", "DBA" ]
"jobCategories" : {
"type" : "array",
"items" : {
"type" : "string",
"enum" : [ "FRONTEND", "BACKEND", "INFRA", "DBA" ]
},
"minItems" : 1
},
"maxQuestions" : {
"type" : "integer",
Expand Down Expand Up @@ -2549,7 +2553,7 @@
}
}
},
"required" : [ "jobCategory", "mode" ]
"required" : [ "jobCategories", "mode" ]
},
"SessionResponse" : {
"type" : "object",
Expand All @@ -2572,6 +2576,13 @@
"type" : "string",
"enum" : [ "FRONTEND", "BACKEND", "INFRA", "DBA" ]
},
"jobCategories" : {
"type" : "array",
"items" : {
"type" : "string",
"enum" : [ "FRONTEND", "BACKEND", "INFRA", "DBA" ]
}
},
"maxQuestions" : {
"type" : "integer",
"format" : "int32"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void onSessionCreated(SessionCreatedEvent event) {
GenerateQuestionsPayload payload = new GenerateQuestionsPayload(
event.sessionId(),
event.mode(),
event.jobCategory(),
event.jobCategories(),
documents,
generalCount,
event.maxQuestions(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
import com.stackup.stackup.session.application.event.SessionEndedEvent;
import com.stackup.stackup.session.domain.InterviewSession;
import com.stackup.stackup.session.domain.InterviewSessionRepository;
import com.stackup.stackup.session.domain.JobCategory;
import com.stackup.stackup.session.domain.SessionContext;
import com.stackup.stackup.session.domain.SessionContextRepository;
import com.stackup.stackup.user.domain.User;
import com.stackup.stackup.user.domain.UserRepository;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -49,7 +51,7 @@ public SessionResult create(Long userId, SessionCreateCommand command) {
title,
command.memo(),
command.mode(),
command.jobCategory(),
command.jobCategories(),
command.maxQuestions(),
command.maxDurationMinutes(),
command.generalQuestionCount(),
Expand All @@ -63,7 +65,7 @@ public SessionResult create(Long userId, SessionCreateCommand command) {
userId,
session.getId(),
session.getMode(),
session.getJobCategory(),
command.jobCategories(),
session.getMaxQuestions(),
session.getGeneralQuestionCount(),
linkedIds
Expand Down Expand Up @@ -146,14 +148,17 @@ public SessionResult updateMeta(Long userId, Long sessionId, String title, Strin
return SessionResult.of(session, contextDocumentIds(sessionId));
}

// 제목 미입력 시 모드+직군으로 규칙 기반 제목 생성 (예: "백엔드 기술 면접").
// 제목 미입력 시 모드+직군으로 규칙 기반 제목 생성 (예: "프론트엔드·백엔드 기술 면접").
// 히스토리에서 "면접 #id" 대신 의미 있는 제목을 보이게 한다.
private String resolveTitle(SessionCreateCommand command) {
String title = command.title();
if (title != null && !title.isBlank()) {
return title;
}
return command.jobCategory().koreanLabel() + " " + command.mode().koreanLabel();
String jobs = command.jobCategories().stream()
.map(JobCategory::koreanLabel)
.collect(Collectors.joining("·"));
return jobs + " " + command.mode().koreanLabel();
}

private User loadUser(Long userId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
public record GenerateQuestionsPayload(
Long sessionId,
SessionMode mode,
JobCategory jobCategory,
List<JobCategory> jobCategories,
List<DocumentContext> documents,
// Number of questions AI should generate for this initial callback.
Integer initialQuestionCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public record SessionCreateCommand(
String title,
String memo,
SessionMode mode,
JobCategory jobCategory,
List<JobCategory> jobCategories,
Integer maxQuestions,
Integer maxDurationMinutes,
Integer generalQuestionCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public record SessionResult(
String memo,
SessionMode mode,
JobCategory jobCategory,
List<JobCategory> jobCategories,
Integer maxQuestions,
Integer maxDurationMinutes,
Integer generalQuestionCount,
Expand All @@ -32,6 +33,7 @@ public static SessionResult of(InterviewSession session, List<Long> documentIds)
session.getMemo(),
session.getMode(),
session.getJobCategory(),
List.copyOf(session.getJobCategories()),
session.getMaxQuestions(),
session.getMaxDurationMinutes(),
session.getGeneralQuestionCount(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public record SessionCreatedEvent(
Long userId,
Long sessionId,
SessionMode mode,
JobCategory jobCategory,
List<JobCategory> jobCategories,
Integer maxQuestions,
Integer generalQuestionCount,
List<Long> contextDocumentIds
Expand Down
Loading