Files
cma-management/backend/app/api/bot_kpis.py
T
Hermes CI Fix 27b5b0da47 feat: 多租户隔离P2批1 — OKR域+kpi_values加entity_id列
- 模型: Objective/KR/ObjectiveKPI/KPIValue 加 entity_id
- DB: 4表加列; kpi_values 1610条按kpi_id回填(1269酣客/341博海)
- OKR域: 测试O删除重建; okr list/get/create + ontology trace/objectives 按token企业隔离(跨企业404)
- KPIValue写入: data/bot_bridge/bot_kpis 创建时带entity_id
- 验证: 酣客创建OKR博海不可见; 跨企业读404; pytest 451通过(expenses单跑37通过为既有排序flaky)
2026-08-23 17:53:37 +08:00

174 lines
5.7 KiB
Python

"""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
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("")
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")
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}