PMFlow/scripts/openclaw_routing_lint.py

177 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""OpenClaw 路由规则静态校验(PMFlow 三引擎版)。"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any, Optional
VALID_PROVIDERS = {"glm", "openai", "deepseek", "ollama"}
VALID_TIERS = {"low", "high", "safety"}
LOW_REQUIRED = {
"feishu_level_classification",
"feishu_signal_extraction",
"rag_intent_classification",
"classify_intent",
"feishu_humanized_reply",
}
HIGH_REQUIRED = {
"adaptive_rag_answer",
"feishu_project_prefill",
"feishu_task_orchestration",
"generate_task_breakdown",
"generate_charter_from_signal",
"decompose_tasks_from_charter",
"recommend_assignees_for_task",
"evaluate_model_requirement",
"calculate_alignment_score",
"detect_cognitive_conflicts",
"rag_db_project_semantic_retrieval",
"workload_optimize_prompt",
"analyze_cost_roi",
}
def _rule_matches_function(rule: dict[str, Any], function_name: str) -> bool:
if bool(rule.get("match_all", False)):
return True
fn = str(function_name or "").strip().lower()
if not fn:
return False
exact = str(rule.get("function_exact") or "").strip().lower()
if exact and fn == exact:
return True
prefix = str(rule.get("function_prefix") or "").strip().lower()
if prefix in {"*", "all"}:
return True
if prefix and fn.startswith(prefix):
return True
match_any = rule.get("match_any")
if isinstance(match_any, list):
for hint in match_any:
token = str(hint or "").strip().lower()
if token and token in fn:
return True
return False
def _resolve_primary_tier(function_name: str, rules: list[dict[str, Any]]) -> Optional[str]:
scored: list[tuple[int, int, str]] = []
for idx, row in enumerate(rules):
if not bool(row.get("enabled", True)):
continue
tier = str(row.get("tier") or "").strip().lower()
if tier not in {"low", "high"}:
continue
if not _rule_matches_function(row, function_name):
continue
scored.append((int(row.get("priority") or 0), -idx, tier))
if not scored:
return None
scored.sort(reverse=True)
return scored[0][2]
def _lint(path: Path) -> tuple[bool, dict[str, Any]]:
if not path.exists():
return False, {"error": f"rules file not found: {path}"}
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except Exception as exc:
return False, {"error": f"invalid json: {exc}"}
if not isinstance(payload, dict):
return False, {"error": "root must be object"}
rules_raw = payload.get("rules")
if not isinstance(rules_raw, list):
return False, {"error": "rules must be list"}
rules: list[dict[str, Any]] = []
rule_names: list[str] = []
problems: list[str] = []
for idx, row in enumerate(rules_raw, start=1):
if not isinstance(row, dict):
problems.append(f"rule#{idx}: must be object")
continue
name = str(row.get("name") or "").strip()
if not name:
problems.append(f"rule#{idx}: missing name")
continue
tier = str(row.get("tier") or "").strip().lower()
if tier and tier not in VALID_TIERS:
problems.append(f"rule#{idx}:{name} unsupported tier={tier}")
provider = str(row.get("provider") or "").strip().lower()
if provider and provider not in VALID_PROVIDERS:
problems.append(f"rule#{idx}:{name} unsupported provider={provider}")
if row.get("match_any") is not None and not isinstance(row.get("match_any"), list):
problems.append(f"rule#{idx}:{name} match_any must be list")
rules.append(row)
rule_names.append(name)
for fn in sorted(LOW_REQUIRED):
actual_tier = _resolve_primary_tier(fn, rules)
if actual_tier != "low":
problems.append(f"function {fn} expected tier=low, actual={actual_tier}")
for fn in sorted(HIGH_REQUIRED):
actual_tier = _resolve_primary_tier(fn, rules)
if actual_tier != "high":
problems.append(f"function {fn} expected tier=high, actual={actual_tier}")
safety_rules = [
row
for row in rules
if bool(row.get("enabled", True)) and str(row.get("tier") or "").strip().lower() == "safety"
]
if not safety_rules:
problems.append("missing safety tier rule")
else:
for fn in sorted(LOW_REQUIRED | HIGH_REQUIRED):
if not any(_rule_matches_function(row, fn) for row in safety_rules):
problems.append(f"function {fn} not covered by safety tier rule")
break
model_hints = {
"low": "zai/glm-4-flash",
"high": "minimax/MiniMax-M2.5",
"safety": "deepseek/deepseek-chat",
}
for tier, expected_model in model_hints.items():
tier_rows = [
row for row in rules
if bool(row.get("enabled", True)) and str(row.get("tier") or "").strip().lower() == tier
]
if tier_rows and not any(str(row.get("model") or "").strip() == expected_model for row in tier_rows):
problems.append(f"tier={tier} missing expected model hint: {expected_model}")
return len(problems) == 0, {
"path": str(path),
"version": payload.get("version"),
"rule_count": len(rules),
"rule_names": sorted(rule_names),
"required_low_count": len(LOW_REQUIRED),
"required_high_count": len(HIGH_REQUIRED),
"problems": problems,
}
def main() -> None:
parser = argparse.ArgumentParser(description="OpenClaw routing lint")
parser.add_argument(
"--input",
default=".openclaw/pmflow-routing-rules.json",
help="Rules file path",
)
args = parser.parse_args()
ok, report = _lint(Path(args.input))
print(json.dumps(report, ensure_ascii=False, indent=2))
raise SystemExit(0 if ok else 2)
if __name__ == "__main__":
main()