dieya_risk_prediction_backend/tests/test_conversations.py
kx_guo 1d58bb8913 按需求完善供应商频次分析
- 统一供应商异常统计为近3年数据口径
- 以近3年异常记录数作为合作频次代理值并明确提示
- 实现 A/B 缺陷占比与 S/A/B/C 层级判定
- 按供应商层级输出正常合作、加强巡检、驻厂及备选供应商等管理建议
- 完善风险卡片和供应商卡片的频次、层级、建议展示
- 补充供应商画像接口、测试和历史会话卡片兼容处理
2026-09-07 13:53:29 +08:00

317 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import UTC, datetime
import pytest
from app.models.entities import Conversation, ConversationMessage
from app.schemas.risk_prediction import SupplierFrequency
from app.services.conversations import ConversationService
def test_conversation_message_data_reads_persisted_risk_result() -> None:
message = ConversationMessage(
id=10,
conversation_id=3,
role="assistant",
content="已完成风险预估。",
structured_data={
"riskResult": {
"subjectId": "软管新品",
"score": 82,
"level": "low",
"modelVersion": "baseline-safety-v2",
"factors": [],
}
},
created_at=datetime.now(UTC),
)
result = ConversationService._message(message)
assert result.role == "assistant"
assert result.risk_result is not None
assert result.risk_result.score == 82
assert result.risk_result.level.value == "low"
def test_conversation_message_data_reads_persisted_semantic_and_response_card() -> None:
message = ConversationMessage(
id=11,
conversation_id=3,
role="assistant",
content="已完成分析。",
structured_data={
"semantic": {"intent": "solution", "category": "洗发水"},
"responseCard": {
"type": "solution",
"title": "AI 案例推理生成",
"similarCases": [{"case_id": 101, "similarity": 0.82}],
"suggestions": ["完成包材相容性测试"],
},
},
created_at=datetime.now(UTC),
)
result = ConversationService._message(message)
assert result.semantic == {"intent": "solution", "category": "洗发水"}
assert result.response_card is not None
assert result.response_card["similarCases"][0]["case_id"] == 101
assert result.response_card["suggestions"] == ["完成包材相容性测试"]
def test_conversation_data_includes_all_messages() -> None:
conversation = Conversation(
id=3,
user_id=1,
title="软管新品评估",
created_at=datetime.now(UTC),
updated_at=datetime.now(UTC),
messages=[
ConversationMessage(
id=1,
conversation_id=3,
role="user",
content="评估软管新品",
created_at=datetime.now(UTC),
),
ConversationMessage(
id=2,
conversation_id=3,
role="assistant",
content="已完成分析",
created_at=datetime.now(UTC),
),
],
)
result = ConversationService._data(conversation)
assert result.message_count == 2
assert [message.role for message in result.messages] == ["user", "assistant"]
def test_only_explicit_risk_requests_generate_risk_cards() -> None:
assert ConversationService._is_risk_request(
"你好",
{"intent": "greet", "risk_requested": False},
) is False
assert ConversationService._is_risk_request(
"请只回复:AI 连通成功。",
{"intent": "general", "risk_requested": False},
) is False
assert ConversationService._is_risk_request(
"请评估这个产品的风险",
{"intent": "assessment", "risk_requested": True},
) is True
def test_legacy_general_semantics_hide_old_risk_card() -> None:
messages = [
ConversationMessage(
id=1,
conversation_id=1,
role="user",
content="你好",
created_at=datetime.now(UTC),
),
ConversationMessage(
id=2,
conversation_id=1,
role="assistant",
content="你好,我可以帮你做什么?",
structured_data={
"semantic": {"intent": "general"},
"riskResult": {
"subjectId": "误判",
"score": 61,
"level": "moderately_low",
"modelVersion": "baseline-safety-v2",
"factors": [],
},
},
created_at=datetime.now(UTC),
),
]
assert ConversationService._risk_result_for_message(messages, 1) is None
assert ConversationService._is_risk_request(
"我要做一款脱毛膏新品,100g,PE软管包装,加工厂准备使用瑞沣",
{"intent": "general", "risk_requested": False},
) is True
def test_product_assessment_semantics_override_nlu_miss() -> None:
normalized = ConversationService._normalize_semantics(
"我要做一款脱毛膏新品,100g,PE软管包装,加工厂准备使用瑞沣",
{"intent": "general", "risk_requested": False, "category": "脱毛膏"},
)
assert normalized["intent"] == "assessment"
assert normalized["risk_requested"] is True
def test_short_known_supplier_follow_up_is_routed_to_supplier_card() -> None:
assert ConversationService._infer_supplier_follow_up(
"佳亿",
["佳亿", "祥印", "瑞沣"],
) == "佳亿"
assert ConversationService._infer_supplier_follow_up(
"这个供应商",
["佳亿", "祥印"],
) is None
def test_unknown_standalone_name_is_treated_as_new_supplier() -> None:
assert ConversationService._infer_unknown_supplier_follow_up("德盛") == "德盛"
assert ConversationService._infer_unknown_supplier_follow_up("新供应商A") is None
assert ConversationService._infer_unknown_supplier_follow_up("测试供应商") is None
assert ConversationService._infer_unknown_supplier_follow_up("新供应商B") is None
assert ConversationService._infer_unknown_supplier_follow_up("洗发水") is None
assert ConversationService._infer_unknown_supplier_follow_up("帮我查德盛") is None
assert ConversationService._response_type("测试供应商", {"intent": "supplier"}) == "general"
assert ConversationService._response_type("新供应商A", {"intent": "supplier"}) == "general"
@pytest.mark.parametrize(
("count", "a_count", "b_count", "expected"),
[
(0, 0, 0, "新供应商"),
(12, 0, 1, "S级 · 战略"),
(40, 0, 4, "A级 · 优质"),
(70, 0, 10, "B级 · 关注"),
(40, 0, 16, "B级 · 关注"),
(10, 1, 0, "C级 · 高风险"),
(121, 0, 0, "C级 · 高风险"),
],
)
def test_supplier_tier_uses_prd_frequency_and_defect_rules(
count: int,
a_count: int,
b_count: int,
expected: str,
) -> None:
assert ConversationService._supplier_tier(
count,
a_count=a_count,
b_count=b_count,
) == expected
def test_supplier_management_suggestions_follow_prd_actions() -> None:
assert ConversationService._supplier_management_suggestions("B级 · 关注") == [
"加强巡检",
"新品打样加严",
]
assert ConversationService._supplier_management_suggestions("新供应商") == [
"无历史数据",
"首单全检",
"安排驻厂巡检",
]
@pytest.mark.asyncio
async def test_supplier_card_uses_prd_proxy_frequency_and_tier_advice(
monkeypatch: pytest.MonkeyPatch,
) -> None:
service = object.__new__(ConversationService)
async def history_stats(*_args, **_kwargs):
return 70, 0, 10
async def top_anomaly_types(*_args, **_kwargs):
return [{"name": "泵头异常", "count": 12}]
monkeypatch.setattr(service, "_supplier_history_stats", history_stats)
monkeypatch.setattr(service, "_top_anomaly_types", top_anomaly_types)
result = await service._supplier_card(
"祥印供应商风险怎么样",
{"supplier": "祥印"},
)
assert result["anomalyCount"] == 70
assert result["cooperationCount"] == 70
assert result["frequencyCoefficient"] == 1.21
assert result["tier"] == "B级 · 关注"
assert result["suggestions"] == ["加强巡检", "新品打样加严"]
assert result["frequencyBasis"] == "近3年异常记录数"
assert result["dataAvailable"] is True
def test_new_supplier_frequency_serializes_zero_proxy_count() -> None:
result = SupplierFrequency(
supplier_name="新供应商",
cooperation_count=0,
anomaly_count=0,
tier="新供应商",
suggestion="无历史数据;首单全检;安排驻厂巡检",
note="合作次数为近3年异常记录数代理值,不代表真实采购订单合作次数。",
).model_dump(by_alias=True)
assert result["cooperationCount"] == 0
assert result["anomalyCount"] == 0
assert result["frequencyBasis"] == "近3年异常记录数"
def test_unspecified_supplier_returns_displayable_no_data_frequency_card() -> None:
result = SupplierFrequency(
note="未指定供应商,暂无供应商频次分析。",
frequency_basis="未提供供应商",
).model_dump(by_alias=True)
assert result["dataAvailable"] is False
assert result["alpha"] is None
assert result["note"] == "未指定供应商,暂无供应商频次分析。"
def test_similar_cases_are_typed_before_risk_result_serialization() -> None:
from app.schemas.risk_prediction import RiskAssessmentResult, SimilarCase
result = RiskAssessmentResult(
subject_id="test",
score=52,
level="medium",
model_version="test",
factors=[],
similar_cases=[SimilarCase(case_id=1, similarity=0.75)],
)
payload = result.model_dump(mode="json")
assert payload["similar_cases"][0]["case_id"] == 1
@pytest.mark.asyncio
async def test_any_product_without_history_uses_solution_card(monkeypatch: pytest.MonkeyPatch) -> None:
service = object.__new__(ConversationService)
async def no_history(_: str) -> int:
return 0
monkeypatch.setattr(service, "_count_category_anomalies", no_history)
response_type = ConversationService._response_type(
"评估防晒霜新品,50g,软管包装",
{"intent": "assessment", "risk_requested": True, "category": "防晒霜"},
)
result = await service._use_solution_card_without_history(
response_type,
"评估防晒霜新品,50g,软管包装",
{"intent": "assessment", "risk_requested": True, "category": "防晒霜"},
)
assert response_type == "risk_assessment"
assert result == "solution"
@pytest.mark.asyncio
async def test_unknown_product_without_category_uses_solution_card() -> None:
service = object.__new__(ConversationService)
result = await service._use_solution_card_without_history(
"risk_assessment",
"评估这个新产品的风险",
{"intent": "assessment", "risk_requested": True},
)
assert result == "solution"