feat:新增投顾agent和nl2sqlagent

This commit is contained in:
2026-09-13 16:19:24 +08:00
parent c80c6acac0
commit 163192bf55
122 changed files with 7488 additions and 362 deletions
+46 -11
View File
@@ -29,6 +29,7 @@ from repositories.advisor_report import AdvisorReportRepo
from repositories.product import ProductRepo
from repositories.risk_assessment import CustomerProfileRepo
from repositories.sensitive_word import SensitiveWordRepo
from repositories.sys_message import SysMessageRepo
from schemas.advisor import DraftSaveReq, RebalanceRunReq, TalkScriptReq
from service.advisor.agent_client import get_agent_client
from service.advisor.permissions import ensure_customer_owned, require_owned_relation
@@ -60,20 +61,30 @@ def _build_report_from_detail(detail: dict, advisor_id: int) -> AdvisorReport:
)
def _extract_buy_codes(suggestions: Any) -> list[str]:
"""从建议清单提取「低配申购」产品代码(发送终审适当性校验对象)。
def _extract_buy_codes(detail: Any) -> list[str]:
"""从 Agent 草稿详情提取发送终审需要校验的产品代码。
假设:结构为 dict,申购侧键见 _BUY_KEYS;项为 {product_code} 或 {code}/{fund_code}。
解析失败返回空列表(视为无结构化产品建议,适当性空过,不误拦)。
兼容旧的顶层 ``suggestions``,以及 Agent 当前的 ``structured_data``:调仓只取
``buy``,推荐取 ``items``。解析失败返回空列表,交由其它终审规则继续处理。
"""
if not isinstance(suggestions, dict):
if not isinstance(detail, dict):
return []
structured_data = detail.get("structured_data")
data = structured_data if isinstance(structured_data, dict) else detail
suggestions = data.get("suggestions")
if isinstance(suggestions, dict):
data = suggestions
items: list | None = None
for key in _BUY_KEYS:
value = suggestions.get(key)
value = data.get(key)
if isinstance(value, list):
items = value
break
if items is None and isinstance(data.get("items"), list):
items = data["items"]
codes: list[str] = []
for it in items or []:
if isinstance(it, dict):
@@ -104,7 +115,7 @@ async def _resolve_product_risks(
result = await get_agent_client().draft_detail(
draft_id, auth_header=auth_header, trace_id=trace_id
)
codes = _extract_buy_codes((result["data"] or {}).get("suggestions"))
codes = _extract_buy_codes(result["data"] or {})
product_repo = ProductRepo(db)
risks: list[str | None] = []
for code in codes:
@@ -214,7 +225,11 @@ async def save_draft(
# 1) 先取草稿确认归属(避免对无权限草稿执行写操作),并拿到 intent/customer_id
detail = await get_draft(db, user, auth_header=auth_header, trace_id=trace_id, draft_id=draft_id)
# 2) 调 Agent 保存(Agent 重新适当性校验,违规 40020;缺免责仅告警不阻断)
payload = {"title": req.title, "content": req.content, "suggestions": req.suggestions}
payload = {
"title": req.title,
"content": req.content,
"structured_data": req.suggestions,
}
result = await get_agent_client().draft_save(
draft_id, payload, auth_header=auth_header, trace_id=trace_id
)
@@ -262,6 +277,9 @@ async def send_draft(
elif report.advisor_id != user.id:
raise ForbiddenError("无权操作该客户数据")
if report.send_status == REPORT_SEND_STATUS_DISCARDED:
raise ParamError("已废弃的报告不可发送")
# 幂等:已发送直接返回(重复点击不重复发站内信)
if report.send_status == REPORT_SEND_STATUS_SENT:
return {**_sent_payload(report), "duplicated": True}
@@ -283,6 +301,19 @@ async def send_draft(
sensitive_words=sensitive_words,
)
# 提交结果不确定后重试时,先复用已经写入的站内信,避免重复触达客户。
existing_message = await SysMessageRepo(db).get_by_biz_id(
report.report_id, user_id=report.customer_id
)
if existing_message is not None:
report.send_status = REPORT_SEND_STATUS_SENT
report.send_time = report.send_time or existing_message.create_time
report.send_by = report.send_by or user.id
report.msg_id = existing_message.id
db.add(report)
await db.commit()
return {**_sent_payload(report), "duplicated": True}
# 写站内信 + 报告置 sent,同事务(失败可重试,避免假送达)
msg_type = INTENT_TO_MSG_TYPE.get(report.intent, MSG_TYPE_RECOMMEND)
message = SysMessage(
@@ -297,9 +328,13 @@ async def send_draft(
report.send_by = user.id
db.add(message)
db.add(report) # 已跟踪对象时无副作用,新对象时入 session
await db.flush() # 生成 message.id,供回填 msg_id
report.msg_id = message.id
await db.commit()
try:
await db.flush() # 生成 message.id,供回填 msg_id
report.msg_id = message.id
await db.commit()
except Exception:
await db.rollback()
raise
return _sent_payload(report)