"""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.deps import get_entity_id 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)], ) # 反向指标编码(值越低越好)。 # 方向权威源 = app.api.kpis.REVERSE_INDICATORS(2026-09-15 M2 准入2 收口): # 本文件原 import 了权威表却未使用、另立字面量 ['FB_RESPONSE_TIME'],会让同一 kpi_code # 在「能力评分」(取权威表=反向) 与「Bot 自评」(取本表=正向) 方向打架 # (实测同一宇宙内 F_COST_RATIO / F_AR_DAYS / F_REBATE_RATE 三码即为此类)。 # 现改为派生:权威表 ∪ Bot 专有反向指标(零漂移)。 from app.api.kpis import REVERSE_INDICATORS as _AUTHORITATIVE_REVERSE_INDICATORS BOT_ONLY_REVERSE_INDICATORS = ['FB_RESPONSE_TIME'] REVERSE_BOT_INDICATORS = list(_AUTHORITATIVE_REVERSE_INDICATORS) + [ c for c in BOT_ONLY_REVERSE_INDICATORS if c not in _AUTHORITATIVE_REVERSE_INDICATORS] 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, } 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, # P0-① 三区标注: Bot KPI值最近回填时间/批次(UI BotSourceTag 消费) "value_updated_at": latest_val.calculated_at.strftime("%Y-%m-%d %H:%M") if latest_val and latest_val.calculated_at else None, "value_source_batch": latest_val.source_batch 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), entity_id: int = Depends(get_entity_id), ): """更新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不存在") # 多租户隔离 2026-09-07: 归属校验,禁止跨企业修改他企 Bot KPI if kpi.entity_id != entity_id: 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}