Files
group_xinghuo_jinrong/app/api/analyst.py
T
zyi b19a2415f9 feat: 数据分析 Agent 实现(API/服务/表结构元数据/文档/测试)
- 新增 app/api、app/service 数据分析 Agent 全套服务与接口
- schemas.py 重构为 schemas 包(analyst schema)
- 新增 SQL 防注入、guardrail、缓存、字典、LLM 等服务
- 新增 tests 测试套件与 scripts/dev、scripts/setup 脚本
- 补充需求规格、架构说明书、开发清单、表设计等文档
2026-09-09 18:04:45 +08:00

94 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""数据分析 Agent 路由(D-01~D-12 / N-03/07/08)。"""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from app.api.deps import get_auth_context
from app.model.schemas.analyst import (
AnalystResponse,
AssetCreateRequest,
ChatRequest,
)
from app.service.analyst_agent import AnalystAgent
from app.utils.auth import AuthContext, assert_analyst_access
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
@router.post("/chat", response_model=AnalystResponse)
def chat(
req: ChatRequest,
auth: AuthContext = Depends(get_auth_context),
agent: AnalystAgent = Depends(get_agent),
) -> AnalystResponse:
return agent.run(req.question, auth, req.session_id, req.trace_id)
@router.get("/dashboard")
def dashboard(
auth: AuthContext = Depends(get_auth_context),
agent: AnalystAgent = Depends(get_agent),
):
"""智能看数板(D-12)后端:按角色返回卡片。"""
domain = assert_analyst_access(auth)
role = next((r for r in auth.roles if r in ("analyst", "advisor", "risk_officer", "ops")), "analyst")
cards = {
"analyst": ["客户总数", "总持仓规模", "今日交易笔数与金额", "待处理预警数", "口径字典资产数"],
"advisor": ["名下客户数", "名下资产规模", "盈亏分布", "风险等级分布"],
"risk_officer": ["待处理预警数", "预警按类型分布", "近7天新增趋势"],
"ops": ["近30天申购金额", "近30天赎回金额", "各产品类型规模TOP"],
}.get(role, [])
metrics: dict = {}
if role == "analyst":
try:
res = agent.repo.execute_readonly(
"SELECT (SELECT COUNT(*) FROM core_customer) AS customers, "
"(SELECT COALESCE(SUM(market_value),0) FROM core_holding) AS holdings, "
"(SELECT COUNT(*) FROM jinrong_agent.risk_alert WHERE status='pending_review') AS pending"
)
if res["rows"]:
metrics = dict(zip(res["columns"], res["rows"][0]))
except Exception: # noqa: BLE001
pass
return {"role": role, "domain": domain, "cards": cards, "metrics": metrics}
@router.post("/assets")
def create_asset(
req: AssetCreateRequest,
auth: AuthContext = Depends(get_auth_context),
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)
return {"ok": True, "id": asset_id, "kind": req.kind}
@router.get("/ops/metrics")
def ops_metrics(
auth: AuthContext = Depends(get_auth_context),
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}