- 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.
184 lines
6.6 KiB
Python
184 lines
6.6 KiB
Python
from uuid import uuid4
|
|
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import inspect
|
|
|
|
from app.advisor_db import AgentSessionLocal, agent_engine
|
|
from app.main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
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 advisor_token() -> str:
|
|
return token_for("advisor_test", "advisor_test")
|
|
|
|
def compliance_token() -> str:
|
|
return token_for("compliance_test", "compliance_test")
|
|
|
|
def create_template_payload(title: str | None = None) -> dict:
|
|
suffix = uuid4().hex[:8]
|
|
return {
|
|
"scene": "loss_comfort",
|
|
"customer_type": "C3",
|
|
"title": title or f"亏损安抚模板 {suffix}",
|
|
"content": f"您好,近期市场波动较大,请结合风险承受能力理性看待。{suffix}",
|
|
"tags": ["亏损", "安抚", "市场波动"],
|
|
}
|
|
|
|
def create_template(headers: dict, payload: dict | None = None) -> dict:
|
|
response = client.post(
|
|
"/api/advisor-agent/script-templates",
|
|
json=payload or create_template_payload(),
|
|
headers=headers,
|
|
)
|
|
assert response.status_code == 200
|
|
return response.json()["data"]
|
|
|
|
def latest_use_log(use_id: str):
|
|
from app.model.entities_advisor import TemplateUseLog
|
|
|
|
with AgentSessionLocal() as session:
|
|
row = session.query(TemplateUseLog).filter(TemplateUseLog.use_id == use_id).one()
|
|
session.expunge(row)
|
|
return row
|
|
|
|
def load_template(template_id: int):
|
|
from app.model.entities_advisor import ScriptTemplate
|
|
|
|
with AgentSessionLocal() as session:
|
|
row = session.get(ScriptTemplate, template_id)
|
|
assert row is not None
|
|
session.expunge(row)
|
|
return row
|
|
|
|
def test_template_migration_creates_template_and_use_log_tables():
|
|
inspector = inspect(agent_engine)
|
|
|
|
assert "script_template" in inspector.get_table_names()
|
|
assert "template_use_log" in inspector.get_table_names()
|
|
|
|
template_columns = {column["name"] for column in inspector.get_columns("script_template")}
|
|
use_log_columns = {column["name"] for column in inspector.get_columns("template_use_log")}
|
|
|
|
assert {
|
|
"scene",
|
|
"customer_type",
|
|
"title",
|
|
"content",
|
|
"tags",
|
|
"embedding_id",
|
|
"is_approved",
|
|
"approved_by",
|
|
"approved_at",
|
|
"version",
|
|
"usage_count",
|
|
"is_active",
|
|
"created_by",
|
|
}.issubset(template_columns)
|
|
assert {
|
|
"use_id",
|
|
"trace_id",
|
|
"template_id",
|
|
"advisor_id",
|
|
"is_modified",
|
|
"original_content",
|
|
"modified_content",
|
|
"content_diff",
|
|
}.issubset(use_log_columns)
|
|
|
|
def test_advisor_cannot_find_or_use_unapproved_template():
|
|
compliance_headers = {"Authorization": f"Bearer {compliance_token()}"}
|
|
advisor_headers = {"Authorization": f"Bearer {advisor_token()}"}
|
|
template = create_template(compliance_headers)
|
|
|
|
list_response = client.get("/api/advisor-agent/script-templates", headers=advisor_headers)
|
|
search_response = client.get(
|
|
"/api/advisor-agent/script-templates/search",
|
|
params={"q": template["title"]},
|
|
headers=advisor_headers,
|
|
)
|
|
use_response = client.post(
|
|
f"/api/advisor-agent/script-templates/{template['id']}/use",
|
|
json={"is_modified": False},
|
|
headers={**advisor_headers, "X-Trace-Id": f"trace-template-denied-{uuid4().hex}"},
|
|
)
|
|
|
|
assert list_response.status_code == 200
|
|
assert all(item["id"] != template["id"] for item in list_response.json()["data"]["items"])
|
|
assert search_response.status_code == 200
|
|
assert search_response.json()["data"]["items"] == []
|
|
assert use_response.status_code == 400
|
|
assert use_response.json()["code"] == "40002"
|
|
|
|
def test_compliance_can_approve_template_and_advisor_use_writes_log_and_increments_count():
|
|
compliance_headers = {"Authorization": f"Bearer {compliance_token()}"}
|
|
advisor_headers = {"Authorization": f"Bearer {advisor_token()}"}
|
|
template = create_template(compliance_headers)
|
|
|
|
approve_response = client.put(
|
|
f"/api/advisor-agent/script-templates/{template['id']}",
|
|
json={"is_approved": True},
|
|
headers=compliance_headers,
|
|
)
|
|
assert approve_response.status_code == 200
|
|
assert approve_response.json()["data"]["is_approved"] is True
|
|
|
|
use_trace_id = f"trace-template-use-{uuid4().hex}"
|
|
modified_content = template["content"] + " 客户可自行决定是否继续了解。"
|
|
use_response = client.post(
|
|
f"/api/advisor-agent/script-templates/{template['id']}/use",
|
|
json={"is_modified": True, "modified_content": modified_content},
|
|
headers={**advisor_headers, "X-Trace-Id": use_trace_id},
|
|
)
|
|
|
|
assert use_response.status_code == 200
|
|
body = use_response.json()["data"]
|
|
log = latest_use_log(body["use_id"])
|
|
stored_template = load_template(template["id"])
|
|
|
|
assert body["template_id"] == template["id"]
|
|
assert body["is_modified"] is True
|
|
assert "-您好,近期市场波动较大" in body["diff"]
|
|
assert "+您好,近期市场波动较大" in body["diff"]
|
|
assert log.trace_id == use_trace_id
|
|
assert log.advisor_id == "STAFF-10086"
|
|
assert log.original_content == template["content"]
|
|
assert log.modified_content == modified_content
|
|
assert stored_template.usage_count == 1
|
|
|
|
def test_template_update_resets_approval_and_increments_version():
|
|
compliance_headers = {"Authorization": f"Bearer {compliance_token()}"}
|
|
advisor_headers = {"Authorization": f"Bearer {advisor_token()}"}
|
|
template = create_template(compliance_headers)
|
|
approved = client.put(
|
|
f"/api/advisor-agent/script-templates/{template['id']}",
|
|
json={"is_approved": True},
|
|
headers=compliance_headers,
|
|
).json()["data"]
|
|
assert approved["version"] == 1
|
|
|
|
update_response = client.put(
|
|
f"/api/advisor-agent/script-templates/{template['id']}",
|
|
json={"content": "您好,市场短期波动较大,请先阅读风险揭示材料。"},
|
|
headers=compliance_headers,
|
|
)
|
|
search_response = client.get(
|
|
"/api/advisor-agent/script-templates/search",
|
|
params={"q": approved["title"]},
|
|
headers=advisor_headers,
|
|
)
|
|
|
|
assert update_response.status_code == 200
|
|
updated = update_response.json()["data"]
|
|
assert updated["is_approved"] is False
|
|
assert updated["approved_by"] is None
|
|
assert updated["version"] == 2
|
|
assert search_response.json()["data"]["items"] == []
|