Merge branch 'develop' of http://47.106.207.27:3000/AI260626/Mutual_Fund into develop_feature_customer
# Conflicts: # api/routers/advisor_agent.py
This commit is contained in:
+116
-11
@@ -2,11 +2,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Query, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import ValidationError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from agent.advisor_agent.auth import ensure_customer_access
|
||||
@@ -40,6 +40,7 @@ from config.deps import get_db
|
||||
from config.database import mysql, redis as redis_db
|
||||
from model.sys_user import SysUser
|
||||
from repositories.advisor_draft import AdvisorDraftRepo
|
||||
from repositories.customer_relation import CustomerRelationRepo
|
||||
from service.advisor_agent.context import (
|
||||
load_fund_analysis_context,
|
||||
load_customer_risk,
|
||||
@@ -92,6 +93,48 @@ def _advisor_runtime(request: Request):
|
||||
return getattr(getattr(app, "state", None), "advisor_agent_runtime", None)
|
||||
|
||||
|
||||
def _infer_chat_intent(query: str) -> str | None:
|
||||
"""从自然语言问题推断投顾意图;无法确定时保留通用问答。"""
|
||||
if any(word in query for word in ("调仓", "再平衡", "组合偏离")):
|
||||
return "rebalance"
|
||||
if any(word in query for word in ("沟通话术", "怎么和客户说", "解释给客户")):
|
||||
return "dialogue-script"
|
||||
if any(word in query for word in ("基金分析", "分析这只基金", "分析产品")):
|
||||
return "fund_analysis"
|
||||
if any(word in query for word in ("推荐", "产品建议", "买什么基金", "适合的基金")):
|
||||
return AGENT_INTENT_RECOMMEND
|
||||
return None
|
||||
|
||||
|
||||
async def _resolve_customer_from_query(db, *, advisor_id: int, query: str) -> tuple[int | None, str | None]:
|
||||
"""解析问题中的客户编号或姓名,并限制在当前投顾客户范围内。"""
|
||||
number_match = re.search(r"(?:客户|用户)\s*[#编号号:]?\s*(\d+)", query)
|
||||
relation_repo = CustomerRelationRepo(db)
|
||||
if number_match:
|
||||
customer_id = int(number_match.group(1))
|
||||
relation = await relation_repo.get_active_relation(
|
||||
customer_id=customer_id,
|
||||
advisor_id=advisor_id,
|
||||
)
|
||||
if relation is None:
|
||||
return None, "问题中的客户不在当前投顾的授权范围内"
|
||||
return customer_id, None
|
||||
|
||||
rows = await relation_repo.list_customer_rows(advisor_id=advisor_id, limit=100)
|
||||
matched = {
|
||||
int(account.id)
|
||||
for _relation, account, _profile in rows
|
||||
if account.real_name and account.real_name in query
|
||||
}
|
||||
if len(matched) == 1:
|
||||
return next(iter(matched)), None
|
||||
if len(matched) > 1:
|
||||
return None, "问题中的客户姓名无法唯一确定,请补充客户编号"
|
||||
if "客户" in query or "用户" in query:
|
||||
return None, "请在问题中补充客户编号或客户姓名"
|
||||
return None, None
|
||||
|
||||
|
||||
async def _recall_advisor_memories(
|
||||
request: Request, *, customer_id: int, query: str
|
||||
) -> list[dict]:
|
||||
@@ -148,19 +191,80 @@ async def _run_rebalance_background(
|
||||
@router.post("/chat/stream")
|
||||
async def chat_stream(
|
||||
request: Request,
|
||||
body: dict,
|
||||
body: AdvisorChatReq,
|
||||
user: SysUser = Depends(audited_advisor),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
trace_id = _trace_id(request)
|
||||
try:
|
||||
chat_request = AdvisorChatReq.model_validate(body)
|
||||
except ValidationError:
|
||||
payload = agent_failure(
|
||||
ERR_CODE_FORBIDDEN_CUSTOMER,
|
||||
"对话请求缺少有效客户范围或参数",
|
||||
trace_id=trace_id,
|
||||
chat_request = body
|
||||
customer_id = chat_request.customer_id
|
||||
inferred_intent = chat_request.intent or _infer_chat_intent(chat_request.query)
|
||||
|
||||
# 请求体只传问题时,从问题中解析客户;解析结果仍必须经过投顾关系授权校验。
|
||||
if customer_id is None:
|
||||
customer_id, resolve_error = await _resolve_customer_from_query(
|
||||
db,
|
||||
advisor_id=user.id,
|
||||
query=chat_request.query,
|
||||
)
|
||||
if resolve_error:
|
||||
payload = agent_failure(
|
||||
ERR_CODE_FORBIDDEN_CUSTOMER,
|
||||
resolve_error,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
async def resolve_error_events():
|
||||
yield f"data: {json.dumps({'type': SSE_EVENT_TYPE_ERROR, **payload}, ensure_ascii=False)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
resolve_error_events(),
|
||||
media_type="text/event-stream",
|
||||
headers={"X-Trace-Id": trace_id},
|
||||
)
|
||||
else:
|
||||
payload = None
|
||||
|
||||
# 不带客户编号时只提供通用基金问答,不读取客户画像,也不生成个性化草稿。
|
||||
if customer_id is None:
|
||||
if inferred_intent in {
|
||||
AGENT_INTENT_RECOMMEND,
|
||||
"rebalance",
|
||||
"fund_analysis",
|
||||
"dialogue-script",
|
||||
}:
|
||||
if payload is None:
|
||||
payload = agent_failure(
|
||||
ERR_CODE_FORBIDDEN_CUSTOMER,
|
||||
"个性化投顾分析需要在问题中明确客户编号或姓名",
|
||||
trace_id=trace_id,
|
||||
)
|
||||
else:
|
||||
runtime = _advisor_runtime(request)
|
||||
llm_client = getattr(runtime, "llm_client", None)
|
||||
if llm_client is None:
|
||||
answer = "已收到问题。当前未配置通用投顾模型,请选择客户后使用个性化分析,或联系管理员配置 Agent 服务。"
|
||||
else:
|
||||
answer = await generate_text(
|
||||
llm_client,
|
||||
system_prompt="你是基金投顾助手,只回答通用基金知识和产品分析问题,不读取或推断任何客户信息,不承诺收益,不代客交易。",
|
||||
user_prompt=chat_request.query,
|
||||
fallback=lambda: "当前模型暂时不可用,请稍后重试。",
|
||||
timeout=5.0,
|
||||
)
|
||||
|
||||
async def events():
|
||||
for event in (
|
||||
{"type": SSE_EVENT_TYPE_META, "intent": "general_question"},
|
||||
{"type": SSE_EVENT_TYPE_TEXT, "content": answer},
|
||||
{"type": SSE_EVENT_TYPE_DONE},
|
||||
):
|
||||
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
events(),
|
||||
media_type="text/event-stream",
|
||||
headers={"X-Trace-Id": trace_id},
|
||||
)
|
||||
else:
|
||||
customer_id = chat_request.customer_id
|
||||
resolved_intent = recognize_advisor_intent(
|
||||
@@ -170,11 +274,12 @@ async def chat_stream(
|
||||
relation = await ensure_customer_access(
|
||||
db, advisor_id=user.id, customer_id=int(customer_id)
|
||||
)
|
||||
if inferred_intent == AGENT_INTENT_RECOMMEND:
|
||||
if resolved_intent == AGENT_INTENT_RECOMMEND:
|
||||
memories = await _recall_advisor_memories(
|
||||
request,
|
||||
customer_id=int(customer_id),
|
||||
query=chat_request.query or "",
|
||||
query=chat_request.query,
|
||||
)
|
||||
runtime = _advisor_runtime(request)
|
||||
context = await load_recommendation_context(
|
||||
@@ -296,7 +401,7 @@ async def list_drafts(
|
||||
customer_id: int | None = Query(default=None),
|
||||
status: Literal[DRAFT_STATUS_DRAFT, DRAFT_STATUS_DISCARDED] | None = Query(default=None),
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
page_size: int = Query(default=10, ge=1, le=100),
|
||||
user: SysUser = Depends(audited_advisor),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
|
||||
@@ -57,6 +57,7 @@ from tool.llm import llm
|
||||
from utils.exceptions import ForbiddenError, NotFoundError, ParamError
|
||||
from utils.request_id import get_request_id, new_request_id
|
||||
from utils.response import success
|
||||
from utils.pagination import normalize_pagination
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
@@ -474,10 +475,11 @@ async def list_query_history(
|
||||
):
|
||||
"""分页读取当前员工自己的查询历史。"""
|
||||
ensure_query_employee(user)
|
||||
page, page_size, offset = normalize_pagination(page, page_size)
|
||||
rows = await Nl2SqlPermissionRepo(db).list_query_history(
|
||||
user.id,
|
||||
limit=page_size,
|
||||
offset=(page - 1) * page_size,
|
||||
offset=offset,
|
||||
status=status,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import PlainTextResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -275,7 +275,7 @@ async def run_admin_job(
|
||||
@router.get("/nl2sql/admin/jobs/history")
|
||||
async def admin_job_history(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
page_size: int = Query(10, ge=1, le=10),
|
||||
status: str | None = None,
|
||||
user: SysUser = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
"""风控处置路由:预警列表 + 放行/拦截/冻结(仅风控专员)。"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.deps import require_risk_officer
|
||||
@@ -14,11 +14,12 @@ router = APIRouter(prefix="/risk", tags=["风控"])
|
||||
@router.get("/alert/list", summary="预警列表")
|
||||
async def list_alerts(
|
||||
status: str | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=10),
|
||||
user: SysUser = Depends(require_risk_officer),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
alerts = await risk_handle.list_alerts(db, status)
|
||||
return success([a.model_dump(mode="json") for a in alerts])
|
||||
return success(await risk_handle.list_alerts(db, status, page=page, page_size=page_size))
|
||||
|
||||
|
||||
@router.post("/alert/{alert_id}/release", summary="放行")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""业务工单路由:列表 / 详情 / 认领 / 提交审核 / 复核(仅风控专员)。"""
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.deps import require_risk_officer
|
||||
@@ -15,11 +15,16 @@ router = APIRouter(prefix="/work-order", tags=["工单"])
|
||||
@router.get("/list", summary="工单列表")
|
||||
async def list_work_orders(
|
||||
status: str | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(10, ge=1, le=10),
|
||||
user: SysUser = Depends(require_risk_officer),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
orders = await work_order_service.list_work_orders(db, status)
|
||||
return success([o.model_dump(mode="json") for o in orders])
|
||||
return success(
|
||||
await work_order_service.list_work_orders(
|
||||
db, status, page=page, page_size=page_size
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{work_order_id}", summary="工单详情")
|
||||
|
||||
Reference in New Issue
Block a user