Files
mini-openclaw/backend/services/skill_executor.py
T

86 lines
4.8 KiB
Python
Raw Normal View History

"""Allowlisted deterministic business executors, never dynamic imports/eval."""
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
EXECUTORS = {"quality", "ratio", "overdue", "compare", "required", "duplicate", "model"}
def number(value):
if value is None or isinstance(value, bool):
raise ValueError("缺少有效数值")
try:
result = Decimal(str(value))
except (InvalidOperation, ValueError, TypeError):
raise ValueError("金额必须是十进制数") from None
if not result.is_finite() or abs(result) > Decimal("1e18"):
raise ValueError("数值超出支持范围")
return result
def ratio(numerator, denominator):
n, d = number(numerator), number(denominator)
if d <= 0:
raise ValueError("分母必须大于零")
return str((n / d).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP))
def execute_skill(skill, values, sources):
engine = skill.get("engine")
if engine not in EXECUTORS or engine == "model":
raise ValueError("不支持的确定性执行器")
checks, outputs = [], {}
fields = skill.get("inputs", [])
def check(code, title, result, refs, action="请核实材料与口径"):
checks.append({"code": code, "title": title, "result": result, "fields": refs, "suggested_action": action,
"evidence_ids": [r["id"] for f in refs for r in sources.get(f, [])],
"basis_ids": [skill["id"] + "@" + skill["version"]], "severity": "attention"})
if engine in ("quality", "required"):
for field in (list(values) if "*" in fields else fields):
check(skill.get("code", "missing." + field), skill.get("title", field + "完整性"),
"unknown" if values.get(field) is None else "pass", [field], "补充对应材料和来源依据")
elif engine == "compare":
a, b = skill["left"], skill["right"]
result = "unknown" if values.get(a) is None or values.get(b) is None else ("pass" if number(values[a]) == number(values[b]) else "issue")
refs = sources.get(a, []) + sources.get(b, [])
if not sources.get(a) or not sources.get(b) or any(not r.get(k) for r in refs for k in ("currency", "unit", "scope", "period")) or any(len({r.get(k) for r in refs}) != 1 for k in ("currency", "unit", "scope", "period")):
result = "unknown"
check(skill["code"], skill["name"], result, [a, b])
elif engine == "duplicate":
value = values.get(skill["field"])
check(skill["code"], skill["name"], "unknown" if value is None else ("issue" if number(value) > 0 else "pass"), [skill["field"]], "核对设备编号并提交去重清单")
elif engine == "ratio":
a, b = skill["numerator"], skill["denominator"]
refs = sources.get(a, []) + sources.get(b, [])
if not sources.get(a) or not sources.get(b) or any(not r.get(k) for r in refs for k in ("currency", "unit", "scope")):
raise ValueError("缺少币种、单位或口径依据,不能计算")
for dimension in ("currency", "unit", "scope"):
if len({r.get(dimension) for r in refs if r.get(dimension)}) > 1:
raise ValueError(dimension + "不一致,不能交叉计算")
if a.startswith("financial.") and b.startswith("financial.") and len({r.get("period") for r in refs}) > 1:
raise ValueError("期间不一致,不能交叉计算")
outputs[skill["output"]] = ratio(values.get(a), values.get(b))
else:
for field in fields:
check("input." + field, field + "输入检查", "unknown" if values.get(field) is None else "pass", [field])
return outputs, checks
def execute_rules(rule, values, sources):
checks = []
for index, condition in enumerate(rule.get("conditions", [])):
field = condition["field"]
applicable = condition.get("when", {})
if any(values.get(k) != v for k, v in applicable.items()):
result = "not_applicable"
else:
try:
value, threshold = number(values.get(field)), number(condition["threshold"])
hit = {"gt": value > threshold, "lt": value < threshold, "eq": value == threshold,
"gte": value >= threshold, "lte": value <= threshold}[condition["operator"]]
result = "issue" if hit else "pass"
except ValueError:
result = "unknown"
checks.append({"code": condition.get("code", rule["id"] + "." + str(index)), "title": condition.get("title", field),
"result": result, "fields": [field], "value": values.get(field), "evidence_ids": [r["id"] for r in sources.get(field, [])],
"basis_ids": [rule["id"] + "@" + rule["version"]], "suggested_action": condition.get("suggestion", "请核实"), "severity": "attention"})
return checks