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
15 changes: 10 additions & 5 deletions app/chat/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,16 @@
6. 답변은 한국어로, 간결하고 실행 가능한 안내 위주로 작성한다."""


def build_system_prompt(analysis_context: AnalysisContext, indicators: list[str]) -> str:
def build_system_prompt(analysis_context: AnalysisContext) -> str:
indicators = analysis_context.indicators
return SYSTEM_PROMPT_TEMPLATE.format(
risk_score=analysis_context.riskScore,
risk_grade=analysis_context.riskGrade.value,
phishing_type=analysis_context.phishingType or "미분류",
summary=analysis_context.summary,
indicators=", ".join(indicators) if indicators else "없음",
risk_grade=analysis_context.riskLevel.value,
phishing_type=analysis_context.category,
summary=analysis_context.explanation,
indicators=(
", ".join(f"{i.type}: {i.description}" for i in indicators)
if indicators
else "없음"
),
)
22 changes: 15 additions & 7 deletions app/chat/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,29 @@ class ChatRole(str, Enum):
ASSISTANT = "assistant"


class Indicator(BaseModel):
"""탐지 근거 하나. type/description 값 목록이 아직 확정되지 않아 자유 문자열로 수용"""
model_config = ConfigDict(extra="forbid")

type: str
description: str


class AnalysisContext(BaseModel):
"""Spring Boot가 /analyze 결과를 바탕으로 구성해 전달하는 분석 컨텍스트"""
model_config = ConfigDict(extra="forbid")

riskScore: int = Field(..., ge=0, le=100, description="최종 위험 점수 (0~100)")
riskGrade: RiskGrade = Field(..., description="위험 등급 (HIGH/MEDIUM/LOW)")
phishingType: str | None = Field(default=None, description="피싱 유형 (예: 기관 사칭형, 대출 사기형)")
summary: str = Field(..., description="분석 결과 요약 설명")
riskLevel: RiskGrade = Field(..., description="위험 등급 (HIGH/MEDIUM/LOW)")
category: str = Field(..., description="피싱 유형 분류 (예: FINANCIAL_INSTITUTION)")
explanation: str = Field(..., description="분석 결과 설명")
indicators: list[Indicator] = Field(default_factory=list, description="탐지 근거 목록")

@field_validator("summary")
@field_validator("explanation")
@classmethod
def summary_must_not_be_blank(cls, value: str) -> str:
def explanation_must_not_be_blank(cls, value: str) -> str:
if not value.strip():
raise ValueError("summary must not be blank")
raise ValueError("explanation must not be blank")
return value


Expand All @@ -45,7 +54,6 @@ class ChatRequest(BaseModel):
model_config = ConfigDict(extra="forbid")

analysisContext: AnalysisContext
indicators: list[str] = Field(default_factory=list, description="탐지 근거 목록")
messages: list[ChatMessage] = Field(..., min_length=1)


Expand Down
6 changes: 1 addition & 5 deletions app/chat/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,7 @@ async def get_response(self, request: ChatRequest) -> ChatResponse:
payload = {
"system_instruction": {
"parts": [
{
"text": build_system_prompt(
request.analysisContext, request.indicators
)
}
{"text": build_system_prompt(request.analysisContext)}
]
},
"contents": _build_contents(request.messages),
Expand Down
27 changes: 16 additions & 11 deletions tests/chat/test_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,34 @@
def test_build_system_prompt_includes_context_fields():
context = AnalysisContext(
riskScore=90,
riskGrade="HIGH",
phishingType="기관 사칭형",
summary="국민건강보험을 사칭한 스미싱 문자",
riskLevel="HIGH",
category="FINANCIAL_INSTITUTION",
explanation="국민건강보험을 사칭한 스미싱 문자",
indicators=[
{"type": "MALICIOUS_URL", "description": "악성 이력이 확인된 URL"},
{"type": "URGENCY_KEYWORD", "description": "즉시 확인 유도 문구"},
],
)

prompt = build_system_prompt(context, ["국민건강보험 언급", "즉시 확인 유도"])
prompt = build_system_prompt(context)

assert "90/100" in prompt
assert "HIGH" in prompt
assert "RiskGrade" not in prompt
assert "기관 사칭형" in prompt
assert "FINANCIAL_INSTITUTION" in prompt
assert "국민건강보험을 사칭한 스미싱 문자" in prompt
assert "국민건강보험 언급, 즉시 확인 유도" in prompt
assert "MALICIOUS_URL: 악성 이력이 확인된 URL" in prompt
assert "URGENCY_KEYWORD: 즉시 확인 유도 문구" in prompt


def test_build_system_prompt_handles_missing_phishing_type_and_indicators():
def test_build_system_prompt_handles_empty_indicators():
context = AnalysisContext(
riskScore=10,
riskGrade="LOW",
summary="일상적인 대화",
riskLevel="LOW",
category="ETC",
explanation="일상적인 대화",
)

prompt = build_system_prompt(context, [])
prompt = build_system_prompt(context)

assert "미분류" in prompt
assert "탐지 근거: 없음" in prompt
10 changes: 6 additions & 4 deletions tests/chat/test_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ def _payload(**overrides) -> dict:
payload = {
"analysisContext": {
"riskScore": 90,
"riskGrade": "HIGH",
"phishingType": "기관 사칭형",
"summary": "국민건강보험을 사칭한 스미싱 문자",
"riskLevel": "HIGH",
"category": "FINANCIAL_INSTITUTION",
"explanation": "국민건강보험을 사칭한 스미싱 문자",
"indicators": [
{"type": "URGENCY_KEYWORD", "description": "즉시 확인 유도"}
],
},
"indicators": ["즉시 확인 유도"],
"messages": [{"role": "user", "content": "이거 진짜인가요?"}],
}
payload.update(overrides)
Expand Down
37 changes: 21 additions & 16 deletions tests/chat/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
def _analysis_context(**overrides) -> AnalysisContext:
defaults = {
"riskScore": 90,
"riskGrade": "HIGH",
"phishingType": "기관 사칭형",
"summary": "국민건강보험을 사칭한 스미싱 문자",
"riskLevel": "HIGH",
"category": "FINANCIAL_INSTITUTION",
"explanation": "국민건강보험을 사칭한 스미싱 문자",
"indicators": [
{"type": "MALICIOUS_URL", "description": "악성 이력이 확인된 URL입니다."}
],
}
defaults.update(overrides)
return AnalysisContext(**defaults)
Expand All @@ -18,26 +21,26 @@ def _analysis_context(**overrides) -> AnalysisContext:
def test_chat_request_accepts_valid_payload():
request = ChatRequest(
analysisContext=_analysis_context(),
indicators=["국민건강보험 언급", "즉시 확인 유도"],
messages=[{"role": "user", "content": "이거 진짜인가요?"}],
)

assert request.analysisContext.riskScore == 90
assert request.messages[0].role.value == "user"


def test_chat_request_defaults_indicators_to_empty_list():
request = ChatRequest(
analysisContext=_analysis_context(),
messages=[{"role": "user", "content": "질문입니다"}],
def test_analysis_context_defaults_indicators_to_empty_list():
context = AnalysisContext(
riskScore=90,
riskLevel="HIGH",
category="FINANCIAL_INSTITUTION",
explanation="국민건강보험을 사칭한 스미싱 문자",
)

assert request.indicators == []
assert context.indicators == []
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def test_chat_request_rejects_empty_message_history():
with pytest.raises(ValidationError):
ChatRequest(analysisContext=_analysis_context(), indicators=[], messages=[])
ChatRequest(analysisContext=_analysis_context(), messages=[])


def test_chat_message_rejects_invalid_role():
Expand All @@ -50,14 +53,16 @@ def test_chat_message_rejects_blank_content():
ChatMessage(role="user", content=" ")


def test_analysis_context_rejects_blank_summary():
def test_analysis_context_rejects_blank_explanation():
with pytest.raises(ValidationError):
_analysis_context(summary=" ")
_analysis_context(explanation=" ")


def test_analysis_context_phishing_type_is_optional():
context = _analysis_context(phishingType=None)
assert context.phishingType is None
def test_analysis_context_indicator_requires_type_and_description():
with pytest.raises(ValidationError):
_analysis_context(indicators=[{"type": "MALICIOUS_URL"}])
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with pytest.raises(ValidationError):
_analysis_context(indicators=[{"description": "악성 이력이 확인된 URL입니다."}])


def test_chat_request_rejects_unknown_fields():
Expand Down
8 changes: 4 additions & 4 deletions tests/chat/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ def _request(messages=None) -> ChatRequest:
return ChatRequest(
analysisContext=AnalysisContext(
riskScore=90,
riskGrade="HIGH",
phishingType="기관 사칭형",
summary="국민건강보험을 사칭한 스미싱 문자",
riskLevel="HIGH",
category="FINANCIAL_INSTITUTION",
explanation="국민건강보험을 사칭한 스미싱 문자",
indicators=[{"type": "URGENCY_KEYWORD", "description": "즉시 확인 유도"}],
),
indicators=["즉시 확인 유도"],
messages=messages
or [ChatMessage(role=ChatRole.USER, content="이거 진짜인가요?")],
)
Expand Down
Loading