feat: 编辑目标——回车自动加%+权重合计校验

This commit is contained in:
Hermes CI Fix
2026-07-26 16:44:22 +08:00
parent abedf8cb8d
commit 5b6362a98a
3 changed files with 279 additions and 3 deletions
+258
View File
@@ -0,0 +1,258 @@
"""
铁律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
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")
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")
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),
}
+2 -1
View File
@@ -5,7 +5,7 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from dotenv import load_dotenv
from app.database import init_db
from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis
from app.api import auth, kpis, templates, maps, dashboard, data, alerts, ai_analysis, alert_rules, users, thresholds, notifications, permissions, action_plans, alignment, org, objectives, versions, budget, cost, predict, reports, security, knowledge, bot_bridge, bot_bridge_v2, lead, customer_dashboard, deviation_push, budget_generate, knowledge_articles, kpi_causality, data_quality, bi_reports, entities, bsc_layers, okr, okr_templates, subjects, driver_budget, bot_kpis, bot_iron_law
from app.utils.cache import clear_all as clear_cache, delete as delete_cache
from scripts.erp_sync import run_sync as run_erp_sync
from app.auth_middleware import require_auth
@@ -71,6 +71,7 @@ app.include_router(okr_templates.router)
app.include_router(subjects.router)
app.include_router(driver_budget.router)
app.include_router(bot_kpis.router)
app.include_router(bot_iron_law.router)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):