- Added new endpoints to the analyst API for managing assets, including `GET /assets` to list assets and `POST /assets/{kind}/{asset_id}/publish` to publish assets.
- Introduced `DictAmbiguityCheckRequest` schema for checking metric ambiguities, enhancing the analyst's ability to clarify definitions and aliases.
- Implemented `detect_dict_ambiguity` function to analyze potential ambiguities in metrics, providing structured feedback for users.
- Updated `AnalystAgent` to support the new asset management functionalities and ambiguity detection logic, improving overall user experience.
- Enhanced existing schemas and services to accommodate new features, ensuring robust data handling and validation.
This update significantly improves the analyst API's capabilities, allowing for better asset management and clarity in metric definitions.
271 lines
10 KiB
Python
271 lines
10 KiB
Python
"""数据分析 Agent 路由(D-01~D-12 / N-03/07/08)。"""
|
||
from __future__ import annotations
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
|
||
from app.api.analyst_auth_adapter import (
|
||
AnalystAuthContext,
|
||
AnalystAuthError,
|
||
analyst_auth_from_deps,
|
||
assert_analyst_query_access,
|
||
)
|
||
from app.api.deps import AuthContext, get_platform_auth_context
|
||
from app.model.analyst_schemas import (
|
||
AnalystResponse,
|
||
AssetCreateRequest,
|
||
AnalyzeRequest,
|
||
ChatRequest,
|
||
DictAmbiguityCheckRequest,
|
||
EscalateRequest,
|
||
InterpretRequest,
|
||
)
|
||
from app.service.analyst_agent import AnalystAgent
|
||
from app.service.metric_ambiguity import detect_dict_ambiguity
|
||
from app.utils.trace import current_trace
|
||
|
||
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
|
||
|
||
|
||
def _analyst_ctx(auth: AuthContext = Depends(get_platform_auth_context)) -> AnalystAuthContext:
|
||
return analyst_auth_from_deps(auth, trace_id=current_trace() or "")
|
||
|
||
|
||
@router.post("/chat", response_model=AnalystResponse)
|
||
def chat(
|
||
req: ChatRequest,
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
agent: AnalystAgent = Depends(get_agent),
|
||
) -> AnalystResponse:
|
||
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)
|
||
|
||
|
||
@router.post("/analyze", response_model=AnalystResponse)
|
||
def analyze(
|
||
req: AnalyzeRequest,
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
agent: AnalystAgent = Depends(get_agent),
|
||
) -> AnalystResponse:
|
||
"""分析助手:快照 + 提示词 → 文字解读与/或 ChartSpec(不重新查库)。"""
|
||
return agent.analyze(req, auth)
|
||
|
||
|
||
@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)
|
||
|
||
|
||
@router.get("/template-prompts")
|
||
def template_prompts(
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
agent: AnalystAgent = Depends(get_agent),
|
||
):
|
||
"""已发布模板收录问句(问数页推荐/标星)。"""
|
||
try:
|
||
assert_analyst_query_access(auth)
|
||
except AnalystAuthError as exc:
|
||
raise HTTPException(status_code=403, detail=exc.message) from exc
|
||
prompts = agent.templates.list_published_prompts(auth.roles)
|
||
return {"prompts": prompts}
|
||
|
||
|
||
@router.get("/dashboard")
|
||
def dashboard(
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
agent: AnalystAgent = Depends(get_agent),
|
||
):
|
||
"""智能看数板(D-12)后端:按角色返回卡片。"""
|
||
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",
|
||
)
|
||
cards = {
|
||
"analyst": ["客户总数", "总持仓规模", "今日交易笔数与金额", "待处理预警数", "口径字典资产数"],
|
||
"advisor": ["名下客户数", "名下资产规模", "盈亏分布", "风险等级分布"],
|
||
"risk_officer": ["待处理预警数", "预警按类型分布", "近7天新增趋势"],
|
||
"customer": ["我的持仓规模", "近30日交易笔数", "风险等级", "盈亏概览"],
|
||
"ops": ["近30天申购金额", "近30天赎回金额", "各产品类型规模TOP"],
|
||
}.get(role, [])
|
||
metrics: dict = {}
|
||
try:
|
||
if role == "analyst":
|
||
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, "
|
||
"(SELECT COUNT(*) FROM jinrong_agent.analytics_metric_dict WHERE status='published') AS dict_count"
|
||
)
|
||
if res["rows"]:
|
||
metrics = dict(zip(res["columns"], res["rows"][0]))
|
||
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
|
||
return {"role": role, "domain": domain, "cards": cards, "metrics": metrics}
|
||
|
||
|
||
@router.post("/assets")
|
||
def create_asset(
|
||
req: AssetCreateRequest,
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
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")
|
||
payload = dict(req.payload)
|
||
if req.publish:
|
||
payload["status"] = "published"
|
||
asset_id = agent.repo.insert_asset(req.kind, payload, auth.subject_id)
|
||
if req.publish:
|
||
try:
|
||
agent.reload_published_assets()
|
||
except Exception:
|
||
pass
|
||
return {"ok": True, "id": asset_id, "kind": req.kind, "published": req.publish}
|
||
|
||
|
||
@router.get("/assets")
|
||
def list_assets(
|
||
kind: str | None = Query(default=None, description="dict / few_shot / template"),
|
||
limit: int = Query(default=50, ge=1, le=100),
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
agent: AnalystAgent = Depends(get_agent),
|
||
):
|
||
"""我的沉淀资产列表(D-11)。"""
|
||
if "analyst" not in auth.roles:
|
||
raise HTTPException(status_code=403, detail="仅分析师可查看沉淀资产")
|
||
if kind and kind not in ("dict", "few_shot", "template"):
|
||
raise HTTPException(status_code=400, detail="kind 必须是 dict/few_shot/template")
|
||
items = agent.repo.list_assets(created_by=auth.subject_id, kind=kind, limit=limit)
|
||
return {"items": items}
|
||
|
||
|
||
@router.post("/assets/{kind}/{asset_id}/publish")
|
||
def publish_asset(
|
||
kind: str,
|
||
asset_id: int,
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
agent: AnalystAgent = Depends(get_agent),
|
||
):
|
||
"""draft → published 并热加载(网页发布不重启进程)。"""
|
||
if "analyst" not in auth.roles:
|
||
raise HTTPException(status_code=403, detail="仅分析师可发布资产")
|
||
if kind not in ("dict", "few_shot", "template"):
|
||
raise HTTPException(status_code=400, detail="kind 必须是 dict/few_shot/template")
|
||
ok = agent.repo.publish_asset(kind, asset_id, auth.subject_id)
|
||
if not ok:
|
||
raise HTTPException(status_code=404, detail="资产不存在或已发布")
|
||
try:
|
||
agent.reload_published_assets()
|
||
except Exception:
|
||
pass
|
||
return {"ok": True, "kind": kind, "id": asset_id}
|
||
|
||
|
||
@router.post("/dict/ambiguity-check")
|
||
def dict_ambiguity_check(
|
||
req: DictAmbiguityCheckRequest,
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
agent: AnalystAgent = Depends(get_agent),
|
||
):
|
||
"""沉淀口径前:扫描歧义 + 可选 LLM 建议(D-11)。"""
|
||
if "analyst" not in auth.roles:
|
||
raise HTTPException(status_code=403, detail="仅分析师可检测口径歧义")
|
||
result = detect_dict_ambiguity(
|
||
metric_key=req.metric_key,
|
||
metric_name=req.metric_name,
|
||
definition=req.definition,
|
||
aliases=req.aliases,
|
||
repo=agent.repo,
|
||
base_registry=agent.registry,
|
||
llm=agent.llm,
|
||
)
|
||
return result
|
||
|
||
|
||
@router.get("/ops/metrics")
|
||
def ops_metrics(
|
||
auth: AnalystAuthContext = Depends(_analyst_ctx),
|
||
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}
|