""" 铁律KPI看板API — CMA铁律执行效果数据采集 提供: 验证次数、通过率、违规数、按Bot分组的通过率 """ import os, json, sqlite3, logging from datetime import datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, Header from sqlalchemy.orm import Session from sqlalchemy import func from app.database import get_db from app.models import ActionPlan from app.risk_levels import risk_level logger = logging.getLogger("cma.iron_law") router = APIRouter(prefix="/api/cma/bot", tags=["铁律KPI看板"]) # ── BOT API Key 配置(复用bot_bridge的一致鉴权) ── _BOT_API_KEYS = {} def _load_bot_keys(): global _BOT_API_KEYS raw = os.getenv("CMA_BOT_API_KEYS", "") if not raw: _BOT_API_KEYS = { "cma-bot-finance-2026": {"role": "finance", "name": "财务BOT"}, "cma-bot-shop-2026": {"role": "business", "name": "店研学BOT"}, "cma-bot-admin-2026": {"role": "ceo", "name": "管理BOT"}, } else: try: _BOT_API_KEYS = json.loads(raw) except: _BOT_API_KEYS = {} _load_bot_keys() def verify_bot_key(x_bot_key: str = Header(None, alias="X-BOT-KEY")): if not x_bot_key or x_bot_key not in _BOT_API_KEYS: raise HTTPException(401, "无效的BOT API Key") bot_info = _BOT_API_KEYS[x_bot_key] return bot_info # ── 各Hermes Profile state.db 路径 ── HERMES_HOME = "/root/.hermes/profiles" def _list_profile_dbs(): """列出所有Hermes Profile的state.db路径""" dbs = [] if not os.path.isdir(HERMES_HOME): return dbs for name in os.listdir(HERMES_HOME): db_path = os.path.join(HERMES_HOME, name, "state.db") if os.path.isfile(db_path): dbs.append((name, db_path)) return sorted(dbs) def _query_state_db(db_path: str, since_days: int = 30): """从单个 state.db 查询验证相关消息""" cutoff = datetime.now() - timedelta(days=since_days) cutoff_ts = cutoff.timestamp() results = { "total_verify_msgs": 0, "pass_msgs": 0, "fail_msgs": 0, "violation_msgs": 0, } try: conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row c = conn.cursor() # 查询含验证/铁律关键词的消息(排除tool_call、校验自身和系统prompt) c.execute(""" SELECT content, role FROM messages WHERE timestamp >= ? AND (content LIKE '%verify%' OR content LIKE '%验证%' OR content LIKE '%铁律%' OR content LIKE '%validate%') AND content NOT LIKE '%verify_bot_key%' AND content NOT LIKE '%X-BOT-KEY%' AND content NOT LIKE '%system_prompt%' """, (cutoff_ts,)) rows = c.fetchall() results["total_verify_msgs"] = len(rows) for row in rows: content = row["content"] or "" role = row["role"] # 判断是否通过/成功/完成 pass_patterns = ["通过", "passed", "success", "✅", "完成", "completed", "验证通过"] fail_patterns = ["失败", "failed", "error", "❌", "违规", "未通过", "错误", "异常"] violation_patterns = ["违规", "violation", "拦截", "blocked", "违例"] # 助理角色消息:用于判断验证结果 if role == "assistant": has_pass = any(p in content for p in pass_patterns) has_fail = any(p in content for p in fail_patterns) if has_pass and not has_fail: results["pass_msgs"] += 1 elif has_fail and not has_pass: results["fail_msgs"] += 1 elif has_pass and has_fail: # 混合内容,默认算通过(因为通常有通过+补充说明) results["pass_msgs"] += 1 if any(p in content for p in violation_patterns): results["violation_msgs"] += 1 conn.close() except Exception as e: logger.warning(f"查询state.db失败 {db_path}: {e}") return results def _query_action_plan_verify(db: Session): """从CMA ActionPlan表查询验证数据""" total = db.query(func.count(ActionPlan.id)).scalar() or 0 verified = db.query(func.count(ActionPlan.id)).filter( ActionPlan.verify_result.isnot(None) ).scalar() or 0 passed = db.query(func.count(ActionPlan.id)).filter( ActionPlan.verify_result == "pass" ).scalar() or 0 failed = db.query(func.count(ActionPlan.id)).filter( ActionPlan.verify_result == "fail" ).scalar() or 0 with_rule = db.query(func.count(ActionPlan.id)).filter( ActionPlan.auto_verify_rule.isnot(None) ).scalar() or 0 return { "total_plans": total, "verified": verified, "passed": passed, "failed": failed, "with_auto_rule": with_rule, "pass_rate": round(passed / verified * 100, 1) if verified > 0 else 0, } # ═══════════════ 端点 ═══════════════ @router.get("/iron-law") @risk_level("L1") def get_iron_law_kpis( bot: dict = Depends(verify_bot_key), db: Session = Depends(get_db), since_days: int = 30, ): """ 铁律执行效果KPI看板 返回各Bot的验证统计、通过率、违规数,以及ActionPlan的验证覆盖率。 数据来源: - Hermes 各Profile state.db (消息含 verify/验证/铁律) - CMA action_plans 表 (verify_result, auto_verify_rule) """ # ── 1. 采集各Profile state.db ── profile_dbs = _list_profile_dbs() per_bot = {} total_verify = 0 total_pass = 0 total_fail = 0 total_violations = 0 for profile_name, db_path in profile_dbs: stats = _query_state_db(db_path, since_days=since_days) per_bot[profile_name] = { "verification_count": stats["total_verify_msgs"], "pass_count": stats["pass_msgs"], "fail_count": stats["fail_msgs"], "violation_count": stats["violation_msgs"], "pass_rate": round( stats["pass_msgs"] / stats["total_verify_msgs"] * 100, 1 ) if stats["total_verify_msgs"] > 0 else 0, } total_verify += stats["total_verify_msgs"] total_pass += stats["pass_msgs"] total_fail += stats["fail_msgs"] total_violations += stats["violation_msgs"] # ── 2. 采集ActionPlan验证数据 ── plan_stats = _query_action_plan_verify(db) return { "bot": bot, "timestamp": datetime.now().isoformat(), "period": f"past_{since_days}d", "summary": { "total_verifications": total_verify, "pass_rate": round(total_pass / total_verify * 100, 1) if total_verify > 0 else 0, "total_violations": total_violations, "plan_verify_count": plan_stats["verified"], "plan_pass_rate": plan_stats["pass_rate"], "plans_with_auto_rule": plan_stats["with_auto_rule"], "total_action_plans": plan_stats["total_plans"], }, "per_bot": per_bot, "action_plan_verification": { "total_plans": plan_stats["total_plans"], "verified": plan_stats["verified"], "passed": plan_stats["passed"], "failed": plan_stats["failed"], "pass_rate": plan_stats["pass_rate"], "with_auto_rule": plan_stats["with_auto_rule"], "coverage": round( plan_stats["verified"] / plan_stats["total_plans"] * 100, 1 ) if plan_stats["total_plans"] > 0 else 0, }, "data_sources": { "state_dbs": len(profile_dbs), "profiles_queried": [p[0] for p in profile_dbs], "action_plans_table": True, }, } @router.get("/iron-law/bots") @risk_level("L1") def get_bot_iron_law_ranking( bot: dict = Depends(verify_bot_key), db: Session = Depends(get_db), since_days: int = 30, ): """按Bot排名:验证通过率从高到低""" data = get_iron_law_kpis(bot=bot, db=db, since_days=since_days) ranking = sorted( data["per_bot"].items(), key=lambda x: x[1]["pass_rate"], reverse=True, ) ranked = [] for rank, (name, stats) in enumerate(ranking, 1): ranked.append({ "rank": rank, "bot_name": name, "pass_rate": stats["pass_rate"], "verification_count": stats["verification_count"], "violations": stats["violation_count"], "failures": stats["fail_count"], }) return { "bot": bot, "timestamp": datetime.now().isoformat(), "period": f"past_{since_days}d", "ranking": ranked, "total_bots": len(ranked), }