- 模型: 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)
174 lines
5.7 KiB
Python
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}
|