新功能:个性化推荐算法
This commit is contained in:
178
server/tests/test_content_repository.py
Normal file
178
server/tests/test_content_repository.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.db.models.content import Content
|
||||
from app.db.models.content_profile import ContentProfile
|
||||
from app.db.models.content_risk_flag import ContentRiskFlag
|
||||
from app.features.personalized_reco.content_repository.sqlalchemy_repo import SqlAlchemyContentRepository
|
||||
|
||||
|
||||
def _ctx_json() -> dict:
|
||||
return {"family": 0.5, "work": 0.5, "relationship": 0.5, "friends": 0.5, "health": 0.5}
|
||||
|
||||
|
||||
def _need_json() -> dict:
|
||||
return {
|
||||
"emotional_support": 0.5,
|
||||
"parenting_pressure": 0.5,
|
||||
"self_worth": 0.5,
|
||||
"anxiety_relief": 0.5,
|
||||
"rest_balance": 0.5,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_contents_by_ids_locale_no_fallback(db_session, query_counter):
|
||||
# content 1: 只有英文
|
||||
c1 = Content(text_en="hello", text_tc=None, author_id="a1", template_id="t1")
|
||||
db_session.add(c1)
|
||||
await db_session.flush()
|
||||
db_session.add(
|
||||
ContentProfile(
|
||||
content_id=c1.content_id,
|
||||
stage="general",
|
||||
emotion_score=None,
|
||||
context_suitability_json=_ctx_json(),
|
||||
need_suitability_json=_need_json(),
|
||||
personalization_power=10,
|
||||
review_confidence=None,
|
||||
is_safe_pool=False,
|
||||
)
|
||||
)
|
||||
db_session.add(ContentRiskFlag(content_id=c1.content_id, flag="block_stage_unknown"))
|
||||
|
||||
# content 2: 只有繁中
|
||||
c2 = Content(text_en=None, text_tc="繁體中文", author_id="a2", template_id="t2")
|
||||
db_session.add(c2)
|
||||
await db_session.flush()
|
||||
db_session.add(
|
||||
ContentProfile(
|
||||
content_id=c2.content_id,
|
||||
stage="general",
|
||||
emotion_score=None,
|
||||
context_suitability_json=_ctx_json(),
|
||||
need_suitability_json=_need_json(),
|
||||
personalization_power=0,
|
||||
review_confidence=0.9,
|
||||
is_safe_pool=True,
|
||||
)
|
||||
)
|
||||
db_session.add(ContentRiskFlag(content_id=c2.content_id, flag="block_health_sensitive"))
|
||||
|
||||
await db_session.flush()
|
||||
|
||||
repo = SqlAlchemyContentRepository(db_session)
|
||||
start = query_counter()
|
||||
|
||||
# en:只能拿到有 text_en 的内容(不允许回退到 text_tc)
|
||||
en_items = await repo.fetch_contents_by_ids(content_ids=[c2.content_id, c1.content_id], locale="en")
|
||||
assert [x.content_id for x in en_items] == [c1.content_id]
|
||||
assert en_items[0].text == "hello"
|
||||
assert en_items[0].personalization_power == 1.0
|
||||
assert en_items[0].review_confidence == 0.7 # NULL -> 0.7
|
||||
assert "unsafe_for_stage_unknown" in en_items[0].risk_flags
|
||||
assert "block_stage_unknown" not in en_items[0].risk_flags
|
||||
|
||||
# tc:只能拿到有 text_tc 的内容(不允许回退到 text_en)
|
||||
tc_items = await repo.fetch_contents_by_ids(content_ids=[c1.content_id, c2.content_id], locale="tc")
|
||||
assert [x.content_id for x in tc_items] == [c2.content_id]
|
||||
assert tc_items[0].text == "繁體中文"
|
||||
assert tc_items[0].review_confidence == 0.9
|
||||
assert "block_health_medical" in tc_items[0].risk_flags
|
||||
assert "block_health_sensitive" not in tc_items[0].risk_flags
|
||||
|
||||
# 两次调用各自 2 次查询(主体+画像一次,flags 一次),总计应为常数级
|
||||
end = query_counter()
|
||||
assert (end - start) <= 4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_candidates_fallback_and_locale_filter(db_session, query_counter):
|
||||
# 构造 4 条英文内容:power 0/5/10,安全池标记不同
|
||||
contents = []
|
||||
for i, (power, safe) in enumerate([(0, True), (5, False), (10, False), (0, False)], start=1):
|
||||
c = Content(text_en=f"en_{i}", text_tc=None, author_id=f"a{i}", template_id=f"t{i}")
|
||||
db_session.add(c)
|
||||
await db_session.flush()
|
||||
db_session.add(
|
||||
ContentProfile(
|
||||
content_id=c.content_id,
|
||||
stage="general",
|
||||
emotion_score=None,
|
||||
context_suitability_json=_ctx_json(),
|
||||
need_suitability_json=_need_json(),
|
||||
personalization_power=power,
|
||||
review_confidence=None,
|
||||
is_safe_pool=safe,
|
||||
# 让测试数据在候选排序中排到最前,避免依赖“库为空”
|
||||
updated_at=datetime(2099, 1, 1, 0, 0, 0),
|
||||
)
|
||||
)
|
||||
contents.append(c)
|
||||
await db_session.flush()
|
||||
inserted_ids = {int(c.content_id) for c in contents}
|
||||
|
||||
repo = SqlAlchemyContentRepository(db_session)
|
||||
|
||||
class _MinimalUser:
|
||||
# need/context/emotion_score 全缺失 -> effective_fallback 至少 L1
|
||||
need = {}
|
||||
context = {}
|
||||
emotion_score = None
|
||||
stage = {"unknown": 1}
|
||||
|
||||
start = query_counter()
|
||||
|
||||
# 入参 L0,但因为缺失字段,effective_fallback=1 -> power<=5(排除 power=10)
|
||||
items_l0 = await repo.fetch_candidates(
|
||||
scene="feed",
|
||||
user_profile=_MinimalUser(),
|
||||
fallback_level=0,
|
||||
limit=3,
|
||||
locale="en-US",
|
||||
)
|
||||
powers = [x.personalization_power for x in items_l0]
|
||||
assert 1.0 not in powers
|
||||
assert all(x.text.startswith("en_") for x in items_l0)
|
||||
|
||||
# L2:强制 power=0 且 stage=general(这里都 general),只剩 power=0 的两条
|
||||
items_l2 = await repo.fetch_candidates(
|
||||
scene="feed",
|
||||
user_profile=_MinimalUser(),
|
||||
fallback_level=2,
|
||||
limit=2,
|
||||
locale="en",
|
||||
)
|
||||
assert all(x.personalization_power == 0.0 for x in items_l2)
|
||||
assert all(x.text.startswith("en_") for x in items_l2)
|
||||
|
||||
# L3:只安全池(is_safe_pool=true)且 power=0
|
||||
items_l3 = await repo.fetch_candidates(
|
||||
scene="feed",
|
||||
user_profile=_MinimalUser(),
|
||||
fallback_level=3,
|
||||
limit=1,
|
||||
locale="en",
|
||||
)
|
||||
assert len(items_l3) == 1
|
||||
assert items_l3[0].text.startswith("en_")
|
||||
assert items_l3[0].personalization_power == 0.0
|
||||
|
||||
# locale 过滤:tc 请求下这些内容都没有 text_tc -> 返回空
|
||||
items_tc = await repo.fetch_candidates(
|
||||
scene="feed",
|
||||
user_profile=_MinimalUser(),
|
||||
fallback_level=0,
|
||||
limit=10,
|
||||
locale="tc",
|
||||
)
|
||||
# 不要求库为空:只断言“不会把本次插入的 en-only 测试数据返回出来”
|
||||
assert not any(x.content_id in inserted_ids for x in items_tc)
|
||||
|
||||
end = query_counter()
|
||||
# 期望为常数级(每次 fetch_candidates:1 次取 ids + 2 次补全),这里 4 次调用 -> <= 12
|
||||
assert (end - start) <= 12
|
||||
|
||||
Reference in New Issue
Block a user