Files
group_xinghuo_jinrong/tests/test_sprint2_template_hybrid_search.py
T

131 lines
4.6 KiB
Python
Raw Normal View History

from types import SimpleNamespace
from uuid import uuid4
from fastapi.testclient import TestClient
import app.api.advisor_script_templates as templates_api
from app.advisor_db import AgentSessionLocal
from app.main import app
from app.model.entities_advisor import ScriptTemplate
from app.model.advisor_schemas import AuthContext
from app.repository.script_template_repository import ScriptTemplateRepository
from app.service.script_template_service import ScriptTemplateService
from app.service.script_template_vector_service import TemplateVectorError
client = TestClient(app)
def advisor_auth() -> AuthContext:
return AuthContext(
user_id="advisor_test",
display_name="Advisor Test",
roles=["advisor"],
permissions=["template:read"],
trace_id=f"trace-hybrid-{uuid4().hex}",
advisor_id="ADV-TEST-001",
)
def token_for(username: str, password: str) -> str:
response = client.post(
"/api/v1/auth/login",
json={"username": username, "password": password},
)
assert response.status_code == 200
return response.json()["data"]["access_token"]
def create_approved_template(
*,
title: str,
content: str,
usage_count: int = 0,
scene: str = "loss_comfort",
created_by: str | None = None,
) -> ScriptTemplate:
actor = created_by or f"test:hybrid_search:{uuid4().hex}"
with AgentSessionLocal() as session:
template = ScriptTemplate(
scene=scene,
customer_type="C3",
title=title,
content=content,
tags=["安抚", "市场波动"],
embedding_id=f"tpl_test_{uuid4().hex[:8]}",
is_approved=True,
approved_by="compliance_test",
is_active=True,
version=1,
usage_count=usage_count,
created_by=actor,
updated_by=actor,
)
session.add(template)
session.commit()
session.refresh(template)
session.expunge(template)
return template
class FakeHybridVectorService:
def __init__(self, hits: list[SimpleNamespace]) -> None:
self.hits = hits
self.calls: list[dict] = []
def search_templates(self, *, query: str, scene: str | None, top_k: int):
self.calls.append({"query": query, "scene": scene, "top_k": top_k})
return self.hits
class FailingVectorService:
def search_templates(self, *, query: str, scene: str | None, top_k: int):
raise TemplateVectorError("milvus unavailable")
def test_template_search_merges_keyword_and_vector_hits_with_score_order():
keyword = create_approved_template(
title="客户情绪安抚话术",
content="您好,短期波动不代表长期趋势,请先看风险揭示材料。",
usage_count=1,
)
semantic = create_approved_template(
title="市场波动沟通话术",
content="您好,近期净值回撤来自市场波动,可以先复盘持仓结构。",
usage_count=0,
)
vector_service = FakeHybridVectorService(
[
SimpleNamespace(template_id=semantic.id, score=0.95),
SimpleNamespace(template_id=keyword.id, score=0.80),
]
)
service = ScriptTemplateService(repository=ScriptTemplateRepository(), vector_service=vector_service)
result = service.search_templates(auth=advisor_auth(), q="客户情绪安抚", top_k=3)
ids = [item.id for item in result.items]
assert ids[0] == semantic.id
assert keyword.id in ids
assert result.items[0].match_type == "semantic"
assert all(item.score <= 1.0 for item in result.items)
assert vector_service.calls == [{"query": "客户情绪安抚", "scene": None, "top_k": 3}]
def test_template_search_api_falls_back_to_keyword_when_vector_unavailable(monkeypatch):
suffix = uuid4().hex[:8]
query = f"跌幅沟通关键词{suffix}"
template = create_approved_template(
title=f"{query}模板",
content="您好,产品净值短期有波动,请结合自身风险承受能力理性看待。",
)
monkeypatch.setattr(
templates_api,
"template_service",
ScriptTemplateService(repository=ScriptTemplateRepository(), vector_service=FailingVectorService()),
)
token = token_for("advisor_test", "advisor_test")
response = client.get(
"/api/advisor-agent/script-templates/search",
params={"q": query},
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 200
items = response.json()["data"]["items"]
assert items[0]["id"] == template.id
assert items[0]["match_type"] == "keyword"