Files
group_xinghuo_jinrong/tests/test_wave2_profile_slots.py
T
zhanghongyu_0626 b841f68295 feat(visitor): Implement visitor chat functionality and enhance customer service interactions
- Added a new visitor chat API endpoint (`/api/chat/visitor`) to allow unauthenticated users to engage in conversations without requiring customer data.
- Introduced a visitor context dependency to manage visitor interactions seamlessly.
- Enhanced the chat API to support explicit session termination and improved response handling for customer service interactions.
- Updated the database configuration to include Redis client support for caching visitor data.
- Added a new customer note repository to persist user notes independently of the L1 profile slots.

This update significantly improves the customer service experience by enabling visitor interactions and ensuring efficient data handling for both registered and unregistered users.
2026-09-09 18:32:00 +08:00

204 lines
6.6 KiB
Python

"""Wave 2:画像槽位定义与归一化纯函数测试(不依赖 DB/LLM)。"""
from __future__ import annotations
import pytest
from app.config.profile_slots import (
HIGH_SENSITIVITY_PATHS,
SLOT_PATHS,
SLOTS,
build_extraction_prompt,
get_slot,
normalize_value,
)
def test_slots_integrity():
"""12 槽位、path 唯一、字段合法、与 style_tags 路径对齐。"""
assert len(SLOTS) == 12
assert len(SLOT_PATHS) == len(set(SLOT_PATHS)) == 12
valid_merge = {"latest", "set_union"}
valid_fmt = {"band", "enum", "list", "text"}
valid_src = {"chat", "chat_or_questionnaire", "chat_or_consult_or_trade"}
valid_sens = {"high", "medium", "low"}
for s in SLOTS:
assert s["merge_mode"] in valid_merge
assert s["value_format"] in valid_fmt
assert s["allowed_source"] in valid_src
assert s["sensitivity"] in valid_sens
assert s["name"] and s["examples"]
# style_tags 顶层路径必须对齐 06 文档 §4.1
top = s["path"].split(".")[0]
assert top in {"basic", "financial", "investment", "lifecycle"} or s["path"] == "threshold_pref_summary"
# 高敏槽位恰好 3 个(年龄/年收入/月可投)
assert set(HIGH_SENSITIVITY_PATHS) == {
"basic.age_band",
"financial.income_band",
"financial.monthly_investable",
}
# 集合累计槽位恰好 2 个(偏好/排除)
set_union_paths = {s["path"] for s in SLOTS if s["merge_mode"] == "set_union"}
assert set_union_paths == {
"investment.product_preferences",
"investment.excluded_products",
}
def test_get_slot():
assert get_slot("basic.city")["name"] == "所在城市"
assert get_slot("not.a.slot") is None
def test_extraction_prompt_covers_all_slots():
prompt = build_extraction_prompt()
for s in SLOTS:
assert s["path"] in prompt
assert s["name"] in prompt
# 高敏保护规则必须出现在 prompt 中
assert "高敏感" in prompt
assert "禁止" in prompt
@pytest.mark.parametrize(
"raw,expected",
[
("我今年28", "25-30"),
("30岁", "25-30"), # 年龄档含上界
("我35了", "31-35"),
("今年40", "36-40"),
("我爸72", "61+"),
("刚工作22", "18-24"),
],
)
def test_normalize_age_band(raw, expected):
assert normalize_value("basic.age_band", raw) == expected
@pytest.mark.parametrize("raw", ["很年轻", "年龄不大", ""])
def test_normalize_age_invalid(raw):
assert normalize_value("basic.age_band", raw) is None
@pytest.mark.parametrize(
"raw,expected",
[
("年薪30万", "20-50万"),
("月入2万", "20-50万"), # 2万*12=24万
("一年到手十几万", "10-20万"), # 中文数字"十几万"取下界 10 万
("月薪8000", "10万以下"), # 8000元*12=9.6万
("月薪一万", "10-20万"), # 中文数字 1万*12=12万
("年收入80万", "50-100万"),
],
)
def test_normalize_income_band(raw, expected):
assert normalize_value("financial.income_band", raw) == expected
@pytest.mark.parametrize(
"raw,expected",
[
("每月能拿3000定投", "3000-5000"),
("月可投4999", "3000-5000"),
("月可投5000左右", "5000-10000"),
("每月1万", "10000以上"),
("每个月2000块", "1000-3000"),
("月投800", "1000以下"),
("每月三千定投", "3000-5000"),
],
)
def test_normalize_monthly_investable(raw, expected):
assert normalize_value("financial.monthly_investable", raw) == expected
def test_normalize_monthly_no_number():
assert normalize_value("financial.monthly_investable", "一个月剩不下钱投") is None
@pytest.mark.parametrize(
"raw,expected",
[
("我本科毕业", "本科"),
("研究生学历", "硕士及以上"),
("大专", "大专"),
("博士在读", "硕士及以上"),
("高中毕业", "高中及以下"),
],
)
def test_normalize_education(raw, expected):
assert normalize_value("basic.education", raw) == expected
def test_normalize_education_unknown():
assert normalize_value("basic.education", "我学历还行") is None
@pytest.mark.parametrize(
"raw,expected",
[
("半年后要用钱", "1年以内"),
("这笔钱三年不用", "1-3年"),
("两三年吧", "1-3年"),
("计划持有三年以上", "3-5年"),
("长期放着养老", "5年以上"),
],
)
def test_normalize_horizon(raw, expected):
assert normalize_value("investment.horizon", raw) == expected
@pytest.mark.parametrize(
"raw,expected",
[
("刚工作单身", "单身奋斗期"),
("刚结婚还没孩子", "家庭形成期"),
("孩子上小学", "家庭成长期"),
("孩子都上大学了", "家庭成熟期"),
("退休了", "退休养老期"),
],
)
def test_normalize_stage(raw, expected):
assert normalize_value("lifecycle.stage", raw) == expected
def test_normalize_product_preferences():
assert normalize_value("investment.product_preferences", "我喜欢指数基金和货币基金") == [
"指数基金",
"货币基金",
]
def test_normalize_excluded_products():
assert normalize_value("investment.excluded_products", "私募不碰,期货也不做") == [
"私募基金",
"期货",
]
def test_normalize_list_alias():
assert normalize_value("investment.product_preferences", "想买点债基") == ["债券基金"]
def test_normalize_list_non_vocab_dropped():
"""非词表自由值(如个股名)不进画像。"""
assert normalize_value("investment.product_preferences", "我看好白酒股") is None
def test_normalize_text_slots():
# basic.city Wave 3 起归一为干净城市名(剥前后缀)
assert normalize_value("basic.city", "我在上海") == "上海"
assert normalize_value("basic.city", "老家成都的") == "成都"
assert normalize_value("basic.city", "我搬去杭州了") == "杭州"
assert normalize_value("basic.city", "在深圳上班") == "深圳"
assert normalize_value("basic.city", "浙江省杭州市") == "杭州"
assert normalize_value("basic.city", "我在上海出差顺便开会") is None # 清洗后超 7 字,非城市名
assert normalize_value("investment.goal", "存钱买房") == "存钱买房"
assert normalize_value("threshold_pref_summary", "亏10%就提醒我") == "亏10%就提醒我"
assert normalize_value("basic.city", "") is None
def test_normalize_unknown_slot():
assert normalize_value("not.exists", "随便") is None