2026-09-09 18:04:45 +08:00
|
|
|
|
"""数据分析 Agent 路由(D-01~D-12 / N-03/07/08)。"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-09-11 15:18:56 +08:00
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
2026-09-09 18:04:45 +08:00
|
|
|
|
|
2026-09-09 21:02:11 +08:00
|
|
|
|
from app.api.analyst_auth_adapter import (
|
|
|
|
|
|
AnalystAuthContext,
|
|
|
|
|
|
AnalystAuthError,
|
|
|
|
|
|
analyst_auth_from_deps,
|
|
|
|
|
|
assert_analyst_query_access,
|
2026-09-09 18:04:45 +08:00
|
|
|
|
)
|
2026-09-09 21:09:03 +08:00
|
|
|
|
from app.api.deps import AuthContext, get_platform_auth_context
|
2026-09-11 15:18:56 +08:00
|
|
|
|
from app.model.analyst_schemas import AnalystResponse, AssetCreateRequest, ChatRequest, EscalateRequest, InterpretRequest
|
2026-09-09 18:04:45 +08:00
|
|
|
|
from app.service.analyst_agent import AnalystAgent
|
2026-09-09 21:02:11 +08:00
|
|
|
|
from app.utils.trace import current_trace
|
2026-09-09 18:04:45 +08:00
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/analyst", tags=["analyst"])
|
|
|
|
|
|
|
|
|
|
|
|
_agent: AnalystAgent | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_agent() -> AnalystAgent:
|
|
|
|
|
|
global _agent
|
|
|
|
|
|
if _agent is None:
|
|
|
|
|
|
_agent = AnalystAgent()
|
|
|
|
|
|
return _agent
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-09 21:09:03 +08:00
|
|
|
|
def _analyst_ctx(auth: AuthContext = Depends(get_platform_auth_context)) -> AnalystAuthContext:
|
2026-09-09 21:02:11 +08:00
|
|
|
|
return analyst_auth_from_deps(auth, trace_id=current_trace() or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-09 18:04:45 +08:00
|
|
|
|
@router.post("/chat", response_model=AnalystResponse)
|
|
|
|
|
|
def chat(
|
|
|
|
|
|
req: ChatRequest,
|
2026-09-09 21:02:11 +08:00
|
|
|
|
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
2026-09-09 18:04:45 +08:00
|
|
|
|
agent: AnalystAgent = Depends(get_agent),
|
|
|
|
|
|
) -> AnalystResponse:
|
2026-09-11 12:15:18 +08:00
|
|
|
|
return agent.run(
|
|
|
|
|
|
req.question,
|
|
|
|
|
|
auth,
|
|
|
|
|
|
req.session_id,
|
|
|
|
|
|
req.trace_id,
|
|
|
|
|
|
interpret=req.interpret,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/interpret", response_model=AnalystResponse)
|
|
|
|
|
|
def interpret(
|
|
|
|
|
|
req: InterpretRequest,
|
|
|
|
|
|
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
|
|
|
|
|
agent: AnalystAgent = Depends(get_agent),
|
|
|
|
|
|
) -> AnalystResponse:
|
|
|
|
|
|
"""按需解读:仅接受最近一次问数快照 + 原问题(看图说话)。"""
|
|
|
|
|
|
return agent.interpret(req, auth)
|
2026-09-09 18:04:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-09-11 15:18:56 +08:00
|
|
|
|
@router.get("/query/{trace_id}/sample", response_model=AnalystResponse)
|
|
|
|
|
|
def query_sample(
|
|
|
|
|
|
trace_id: str,
|
|
|
|
|
|
limit: int = Query(default=5, ge=1, le=20),
|
|
|
|
|
|
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
|
|
|
|
|
agent: AnalystAgent = Depends(get_agent),
|
|
|
|
|
|
) -> AnalystResponse:
|
|
|
|
|
|
"""聚合结果抽样明细(N-03)。"""
|
|
|
|
|
|
return agent.sample_by_trace(trace_id, auth, limit)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/escalate")
|
|
|
|
|
|
def escalate(
|
|
|
|
|
|
req: EscalateRequest,
|
|
|
|
|
|
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
|
|
|
|
|
agent: AnalystAgent = Depends(get_agent),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""问数失败/超时一键转人工(N-07)。"""
|
|
|
|
|
|
return agent.escalate(req.trace_id, req.question, req.reason, auth)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-09 18:04:45 +08:00
|
|
|
|
@router.get("/dashboard")
|
|
|
|
|
|
def dashboard(
|
2026-09-09 21:02:11 +08:00
|
|
|
|
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
2026-09-09 18:04:45 +08:00
|
|
|
|
agent: AnalystAgent = Depends(get_agent),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""智能看数板(D-12)后端:按角色返回卡片。"""
|
2026-09-09 21:02:11 +08:00
|
|
|
|
try:
|
|
|
|
|
|
domain = assert_analyst_query_access(auth)
|
|
|
|
|
|
except AnalystAuthError as exc:
|
|
|
|
|
|
raise HTTPException(status_code=403, detail=exc.message) from exc
|
|
|
|
|
|
role = next(
|
|
|
|
|
|
(r for r in auth.roles if r in ("analyst", "advisor", "risk_officer", "ops", "customer")),
|
|
|
|
|
|
"analyst",
|
|
|
|
|
|
)
|
2026-09-09 18:04:45 +08:00
|
|
|
|
cards = {
|
|
|
|
|
|
"analyst": ["客户总数", "总持仓规模", "今日交易笔数与金额", "待处理预警数", "口径字典资产数"],
|
|
|
|
|
|
"advisor": ["名下客户数", "名下资产规模", "盈亏分布", "风险等级分布"],
|
|
|
|
|
|
"risk_officer": ["待处理预警数", "预警按类型分布", "近7天新增趋势"],
|
2026-09-09 21:02:11 +08:00
|
|
|
|
"customer": ["我的持仓规模", "近30日交易笔数", "风险等级", "盈亏概览"],
|
2026-09-09 18:04:45 +08:00
|
|
|
|
"ops": ["近30天申购金额", "近30天赎回金额", "各产品类型规模TOP"],
|
|
|
|
|
|
}.get(role, [])
|
|
|
|
|
|
metrics: dict = {}
|
2026-09-09 21:32:59 +08:00
|
|
|
|
try:
|
|
|
|
|
|
if role == "analyst":
|
2026-09-09 18:04:45 +08:00
|
|
|
|
res = agent.repo.execute_readonly(
|
|
|
|
|
|
"SELECT (SELECT COUNT(*) FROM core_customer) AS customers, "
|
|
|
|
|
|
"(SELECT COALESCE(SUM(market_value),0) FROM core_holding) AS holdings, "
|
2026-09-09 21:32:59 +08:00
|
|
|
|
"(SELECT COUNT(*) FROM jinrong_agent.risk_alert WHERE status='pending_review') AS pending, "
|
|
|
|
|
|
"(SELECT COUNT(*) FROM jinrong_agent.analytics_metric_dict WHERE status='published') AS dict_count"
|
2026-09-09 18:04:45 +08:00
|
|
|
|
)
|
|
|
|
|
|
if res["rows"]:
|
|
|
|
|
|
metrics = dict(zip(res["columns"], res["rows"][0]))
|
2026-09-09 21:32:59 +08:00
|
|
|
|
elif role == "customer" and auth.customer_id:
|
|
|
|
|
|
cid = auth.customer_id
|
|
|
|
|
|
res = agent.repo.execute_readonly(
|
|
|
|
|
|
f"SELECT (SELECT COALESCE(SUM(market_value),0) FROM core_holding WHERE customer_id='{cid}') AS holdings, "
|
|
|
|
|
|
f"(SELECT COUNT(*) FROM core_trade WHERE customer_id='{cid}' AND trade_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)) AS trades_30d"
|
|
|
|
|
|
)
|
|
|
|
|
|
if res["rows"]:
|
|
|
|
|
|
metrics = dict(zip(res["columns"], res["rows"][0]))
|
|
|
|
|
|
elif role == "advisor" and auth.subject_id:
|
|
|
|
|
|
scope = agent.repo.resolve_advisor_scope(auth.subject_id)
|
|
|
|
|
|
if scope:
|
|
|
|
|
|
ids = ",".join(f"'{x}'" for x in scope)
|
|
|
|
|
|
res = agent.repo.execute_readonly(
|
|
|
|
|
|
f"SELECT COUNT(DISTINCT customer_id) AS clients, "
|
|
|
|
|
|
f"COALESCE(SUM(market_value),0) AS aum FROM core_holding WHERE customer_id IN ({ids})"
|
|
|
|
|
|
)
|
|
|
|
|
|
if res["rows"]:
|
|
|
|
|
|
metrics = dict(zip(res["columns"], res["rows"][0]))
|
|
|
|
|
|
else:
|
|
|
|
|
|
metrics = {"clients": 0, "aum": 0}
|
|
|
|
|
|
elif role == "risk_officer":
|
|
|
|
|
|
res = agent.repo.execute_readonly(
|
|
|
|
|
|
"SELECT COUNT(*) AS pending FROM jinrong_agent.risk_alert WHERE status='pending_review'"
|
|
|
|
|
|
)
|
|
|
|
|
|
if res["rows"]:
|
|
|
|
|
|
metrics = dict(zip(res["columns"], res["rows"][0]))
|
|
|
|
|
|
except Exception: # noqa: BLE001
|
|
|
|
|
|
pass
|
2026-09-09 18:04:45 +08:00
|
|
|
|
return {"role": role, "domain": domain, "cards": cards, "metrics": metrics}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/assets")
|
|
|
|
|
|
def create_asset(
|
|
|
|
|
|
req: AssetCreateRequest,
|
2026-09-09 21:02:11 +08:00
|
|
|
|
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
2026-09-09 18:04:45 +08:00
|
|
|
|
agent: AnalystAgent = Depends(get_agent),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""沉淀资产(D-11):仅分析师可写。"""
|
|
|
|
|
|
if "analyst" not in auth.roles:
|
|
|
|
|
|
raise HTTPException(status_code=403, detail="仅分析师可沉淀资产")
|
|
|
|
|
|
if req.kind not in ("dict", "few_shot", "template"):
|
|
|
|
|
|
raise HTTPException(status_code=400, detail="kind 必须是 dict/few_shot/template")
|
|
|
|
|
|
asset_id = agent.repo.insert_asset(req.kind, req.payload, auth.subject_id)
|
2026-09-10 15:22:07 +08:00
|
|
|
|
if req.kind == "template":
|
|
|
|
|
|
try:
|
|
|
|
|
|
agent.templates.reload()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-09-09 18:04:45 +08:00
|
|
|
|
return {"ok": True, "id": asset_id, "kind": req.kind}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/ops/metrics")
|
|
|
|
|
|
def ops_metrics(
|
2026-09-09 21:02:11 +08:00
|
|
|
|
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
2026-09-09 18:04:45 +08:00
|
|
|
|
agent: AnalystAgent = Depends(get_agent),
|
|
|
|
|
|
):
|
|
|
|
|
|
"""运营指标(N-08,P1 最小版)。"""
|
|
|
|
|
|
if "analyst" not in auth.roles:
|
|
|
|
|
|
raise HTTPException(status_code=403, detail="仅分析师可查看运营指标")
|
|
|
|
|
|
res = agent.repo.execute_readonly(
|
|
|
|
|
|
"SELECT COUNT(*) AS total, "
|
|
|
|
|
|
"SUM(exec_status='blocked') AS blocked "
|
|
|
|
|
|
"FROM jinrong_agent.analytics_query_log"
|
|
|
|
|
|
)
|
|
|
|
|
|
return res["rows"][0] if res["rows"] else {"total": 0, "blocked": 0}
|