"""Bot KPI管理 API — 管理各Agent的KPI自评体系""" from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import Optional from datetime import datetime from app.database import get_db from app.auth_middleware import require_auth from app.models import KPIDefinition, KPIValue from app.risk_levels import risk_level router = APIRouter(prefix="/api/cma/bot-kpis", tags=["Bot KPI管理"], dependencies=[Depends(require_auth)], ) # 反向指标编码(值越低越好) REVERSE_BOT_INDICATORS = ['FB_RESPONSE_TIME'] def _calc_bot_kpi_score(current_value, target_value, is_reverse=False): """五档评分引擎(复用KPI体系)- 1~5分""" if current_value is None or target_value is None or target_value == 0: return None, "info" ratio = current_value / target_value if is_reverse: if ratio <= 0.5: return 5, "success" elif ratio <= 0.8: return 4, "success" elif ratio <= 1.0: return 3, "warning" elif ratio <= 1.2: return 2, "danger" else: return 1, "danger" else: if ratio >= 1.2: return 5, "success" elif ratio >= 1.0: return 4, "success" elif ratio >= 0.8: return 3, "warning" elif ratio >= 0.5: return 2, "danger" else: return 1, "danger" @router.get("") @risk_level("L1") def list_bot_kpis( source: str = Query("finance-bot", description="Bot标识"), period: Optional[str] = None, db: Session = Depends(get_db), ): """获取某Bot的所有KPI(含评分)""" kpis = db.query(KPIDefinition).filter( KPIDefinition.bot_source == source, KPIDefinition.status == "active", ).order_by(KPIDefinition.kpi_code).all() if not kpis: return { "source": source, "kpis": [], "groups": {}, "overall": None, } from app.api.kpis import REVERSE_INDICATORS as _ri result_kpis = [] for k in kpis: # 取最新实际值 val_query = db.query(KPIValue).filter( KPIValue.kpi_id == k.id, KPIValue.actual_value.isnot(None), ) if period: val_query = val_query.filter(KPIValue.period == period) latest_val = val_query.order_by(KPIValue.period.desc()).first() current_val = latest_val.actual_value if latest_val else None is_reverse = k.kpi_code in REVERSE_BOT_INDICATORS score, status = _calc_bot_kpi_score(current_val, k.target_value, is_reverse=is_reverse) result_kpis.append({ "id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name, "category": k.category or "core_output", "formula": k.formula, "target_value": k.target_value, "current_value": current_val, "unit": k.unit, "weight": 15, # 权重在PRD中定义,但未存表,前端使用 "score": score, "status": status, "period": latest_val.period if latest_val else None, "frequency": k.frequency or "monthly", }) # 按group分组: core_output / quality / user_feedback groups = { "core_output": {"label": "核心产出", "kpis": []}, "quality": {"label": "质量监控", "kpis": []}, "user_feedback": {"label": "用户反馈", "kpis": []}, } # category映射: FB编码前缀区分 for kp in result_kpis: code = kp["kpi_code"] if code.startswith("FB_ANALYSIS") or code.startswith("FB_ACCURACY") or code.startswith("FB_ISSUE") or code.startswith("FB_ACTION") or code.startswith("FB_RESPONSE"): groups["core_output"]["kpis"].append(kp) elif code.startswith("FB_DATA") or code.startswith("FB_CONSISTENCY") or code.startswith("FB_CITATION"): groups["quality"]["kpis"].append(kp) else: groups["user_feedback"]["kpis"].append(kp) # 综合得分 scored_kpis = [k for k in result_kpis if k["score"] is not None] if scored_kpis: overall = round(sum(k["score"] * k["weight"] for k in scored_kpis) / sum(k["weight"] for k in scored_kpis), 2) overall_status = "success" if overall >= 4 else ("warning" if overall >= 3 else "danger") else: overall = None overall_status = "info" return { "source": source, "kpis": result_kpis, "groups": groups, "overall": {"score": overall, "status": overall_status}, } @router.post("/{kpi_id}/value") @risk_level("L2") def update_bot_kpi_value( kpi_id: int, data: dict, db: Session = Depends(get_db), ): """更新Bot KPI当前值""" kpi = db.query(KPIDefinition).filter( KPIDefinition.id == kpi_id, KPIDefinition.bot_source.isnot(None), ).first() if not kpi: raise HTTPException(404, "Bot KPI不存在") actual_value = data.get("actual_value") if actual_value is None: raise HTTPException(422, "actual_value 不能为空") period = data.get("period", datetime.now().strftime("%Y-%m")) existing = db.query(KPIValue).filter( KPIValue.kpi_id == kpi_id, KPIValue.period == period, ).first() if existing: existing.actual_value = actual_value existing.source_type = "manual" else: val = KPIValue( kpi_id=kpi_id, entity_id=kpi.entity_id if kpi else None, # 账套隔离 P2 period=period, actual_value=actual_value, source_type="manual", data_status="pending", ) db.add(val) db.commit() return {"message": "更新成功", "kpi_id": kpi_id, "period": period, "actual_value": actual_value}