feat: API业务动作层聚合接口2个(Agent化生产链路·行动1)
建议A: POST /bot/kpi-value-with-check — 写KPI值+自动跑该KPI预警检查 复用alert_rules检查函数(static/dynamic/trend), 非全量check-all 建议B: POST /bot/kpis/create-with-links — 创建KPI+关联地图+批量因果链 复用kpis治理校验/apply_calc_type_inference, 入参可选退化纯创建 - X-BOT-KEY鉴权(bot层, Agent免登录) - 多租户: entity校验(跨企业404/创建强制token企业) - 端到端: A写值15.5无预警命中✓ B创建KPI430+map49+因果链430→414✓ - pytest 486 passed, 测试数据已清理
This commit is contained in:
@@ -17,6 +17,8 @@ from app.models import (
|
||||
)
|
||||
from app.models.budget_plan import BudgetPlan
|
||||
from app.models.cost_model import StandardCost, ActualCost, AbcActivity, AbcAllocation
|
||||
from app.models import KPICausality
|
||||
import json
|
||||
|
||||
logger = logging.getLogger("cma.bot_bridge")
|
||||
|
||||
@@ -601,3 +603,134 @@ def bot_nlp(
|
||||
}
|
||||
resolved = m.get(intent, intent)
|
||||
return bot_query(q=resolved, bot=bot, db=db)
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 聚合接口(Agent化生产链路 · 行动1, 2026-08-25)
|
||||
# 建议A: KPI值更新联动预警检查 | 建议B: KPI创建联动关联
|
||||
# ════════════════════════════════════════════════════════════
|
||||
|
||||
@router.post("/kpi-value-with-check")
|
||||
def bot_kpi_value_with_check(data: dict, db: Session = Depends(get_db), bot: dict = Depends(verify_bot_key)):
|
||||
"""聚合A: 写KPI值 + 自动跑该KPI预警检查(Agent一次调用,免自拼check-all)
|
||||
body: {kpi_id, actual_value, period?, entity_id?, run_check?}"""
|
||||
kpi_id = data.get("kpi_id")
|
||||
actual_value = data.get("actual_value")
|
||||
period = data.get("period")
|
||||
entity_id = int(data.get("entity_id") or 1)
|
||||
run_check = bool(data.get("run_check", True))
|
||||
if not kpi_id or actual_value is None:
|
||||
raise HTTPException(400, "kpi_id 和 actual_value 必填")
|
||||
# 校验 KPI 归属(多租户)
|
||||
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||
if not kpi or kpi.entity_id != entity_id:
|
||||
raise HTTPException(404, "KPI不存在")
|
||||
period = period or datetime.now().strftime("%Y-%m")
|
||||
|
||||
# ① 写值(upsert)
|
||||
existing = db.query(KPIValue).filter(
|
||||
KPIValue.kpi_id == kpi_id, KPIValue.period == period).first()
|
||||
if existing:
|
||||
existing.actual_value = float(actual_value)
|
||||
existing.source_type = "bot"
|
||||
kv = existing
|
||||
else:
|
||||
kv = KPIValue(kpi_id=kpi_id, entity_id=entity_id, period=period,
|
||||
actual_value=float(actual_value), source_type="bot", data_status="verified")
|
||||
db.add(kv)
|
||||
db.commit()
|
||||
db.refresh(kv)
|
||||
|
||||
# ② 跑该KPI关联的预警规则(复用 alert_rules 检查函数,非全量)
|
||||
alerts = []
|
||||
if run_check:
|
||||
from app.api.alert_rules import AlertRule, _check_static, _check_dynamic, _check_trend
|
||||
rules = db.query(AlertRule).filter(
|
||||
AlertRule.kpi_id == kpi_id, AlertRule.entity_id == entity_id,
|
||||
AlertRule.enabled == 1).all()
|
||||
for rule in rules:
|
||||
try:
|
||||
params = json.loads(rule.params) if isinstance(rule.params, str) else (rule.params or {})
|
||||
value = float(actual_value)
|
||||
if rule.rule_type == "static":
|
||||
level, msg = _check_static(value, params, kpi)
|
||||
elif rule.rule_type == "dynamic":
|
||||
level, msg = _check_dynamic(kpi_id, value, params, db)
|
||||
elif rule.rule_type == "trend_up":
|
||||
level, msg = _check_trend(kpi_id, value, "up", params, db)
|
||||
elif rule.rule_type == "trend_down":
|
||||
level, msg = _check_trend(kpi_id, value, "down", params, db)
|
||||
else:
|
||||
continue
|
||||
if level and level != "green":
|
||||
dup = db.query(KPIAlert).filter(
|
||||
KPIAlert.kpi_id == kpi_id, KPIAlert.kpi_value_id == kv.id,
|
||||
KPIAlert.alert_level == level, KPIAlert.status == "pending").first()
|
||||
if not dup:
|
||||
db.add(KPIAlert(kpi_id=kpi_id, kpi_value_id=kv.id, alert_level=level,
|
||||
alert_message=msg, status="pending", alert_type="bot"))
|
||||
alerts.append({"rule_id": rule.id, "rule_type": rule.rule_type,
|
||||
"level": level, "message": msg})
|
||||
except Exception as e:
|
||||
logger.warning(f"聚合检查失败 rule={rule.id}: {e}")
|
||||
db.commit()
|
||||
return {"kpi_id": kpi_id, "kpi_code": kpi.kpi_code, "value": float(actual_value),
|
||||
"period": period, "alerts": alerts, "status": "ok"}
|
||||
|
||||
|
||||
@router.post("/kpis/create-with-links")
|
||||
def bot_kpi_create_with_links(data: dict, db: Session = Depends(get_db), bot: dict = Depends(verify_bot_key)):
|
||||
"""聚合B: 创建KPI + 关联战略地图 + 批量因果链(Agent建KPI标准动作)
|
||||
body: {kpi_code, kpi_name, dimension, entity_id?, target_value?, unit?, link_map_id?, link_causality?}"""
|
||||
entity_id = int(data.get("entity_id") or 1)
|
||||
from app.api.kpis import _validate_kpi_data, apply_calc_type_inference
|
||||
kpi_data = {k: v for k, v in data.items() if k not in ("entity_id", "link_map_id", "link_causality")}
|
||||
|
||||
# ① 创建KPI(编码唯一 + 治理校验 + 强制企业)
|
||||
code = kpi_data.get("kpi_code", "")
|
||||
if not code:
|
||||
raise HTTPException(400, "kpi_code 必填")
|
||||
if db.query(KPIDefinition).filter(
|
||||
KPIDefinition.kpi_code == code, KPIDefinition.entity_id == entity_id).first():
|
||||
raise HTTPException(400, f"KPI编码 {code} 已存在")
|
||||
errs = _validate_kpi_data(kpi_data, db=db, is_update=False)
|
||||
if errs:
|
||||
raise HTTPException(422, detail={"message": "数据校验不通过", "errors": errs})
|
||||
kpi_data["entity_id"] = entity_id
|
||||
kpi_data = apply_calc_type_inference(kpi_data)
|
||||
kpi = KPIDefinition(**kpi_data)
|
||||
db.add(kpi)
|
||||
db.commit()
|
||||
db.refresh(kpi)
|
||||
|
||||
# ② 关联战略地图
|
||||
link_map_id = data.get("link_map_id")
|
||||
if link_map_id:
|
||||
m = db.query(StrategicMap).filter(
|
||||
StrategicMap.id == link_map_id, StrategicMap.entity_id == entity_id).first()
|
||||
if m:
|
||||
kpi.map_id = link_map_id
|
||||
db.commit()
|
||||
|
||||
# ③ 批量因果链(源=新KPI → 目标列表)
|
||||
links = []
|
||||
for c in data.get("link_causality") or []:
|
||||
tgt = c.get("target_kpi_id")
|
||||
if not tgt or int(tgt) == kpi.id:
|
||||
continue
|
||||
tgt_kpi = db.query(KPIDefinition).filter(
|
||||
KPIDefinition.id == int(tgt), KPIDefinition.entity_id == entity_id).first()
|
||||
if not tgt_kpi:
|
||||
continue
|
||||
if db.query(KPICausality).filter(
|
||||
KPICausality.source_kpi_id == kpi.id,
|
||||
KPICausality.target_kpi_id == int(tgt)).first():
|
||||
continue
|
||||
db.add(KPICausality(source_kpi_id=kpi.id, target_kpi_id=int(tgt),
|
||||
strength=c.get("strength", 0.5), lag_months=c.get("lag_months", 1),
|
||||
direction=c.get("direction", "positive")))
|
||||
links.append({"source": kpi.kpi_code, "target": tgt_kpi.kpi_code,
|
||||
"strength": c.get("strength", 0.5)})
|
||||
db.commit()
|
||||
return {"kpi_id": kpi.id, "kpi_code": kpi.kpi_code, "map_id": link_map_id,
|
||||
"causality_links": links, "status": "ok"}
|
||||
|
||||
Reference in New Issue
Block a user