- Added new modules for advisor compliance, KYC sessions, and script templates, enhancing the advisor agent's capabilities. - Implemented a comprehensive API structure under the `/api/advisor-agent` prefix, ensuring clear organization and access to new features. - Established database models and repositories for compliance rules and KYC sessions, facilitating robust data management. - Integrated exception handling and response models to improve error management and user feedback. - Updated settings to include new configurations for compliance and KYC features, ensuring flexibility and adaptability. This update significantly expands the advisor agent's functionality, providing essential tools for compliance and customer interaction while maintaining a structured API design.
131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
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"
|