Files
mini-openclaw-v2/backend/services/prescreen_policy.py
T

131 lines
6.3 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.
"""T07-a 预审结果约束:把核查结果收敛成受约束的建议。
依据:docs/v2/01-requirements.md §5、03-detailed-design.md §5;
BR-05/06/07/12、E-02/06/07/08、AC-05/06/07/09/12/45。
本模块只做**确定性结论校验**,不调用任何模型、不算概率。四条不可让渡的规则:
1. **硬性阻断不可被覆盖**(BR-06):已确认的 hard 规则命中即 `stop`,模型的高分
或"建议推进"都不能翻盘。同时**保留模型原始结构化提案**供审计,并指出规则版本。
2. **未知不等于风险,也不等于通过**(BR-07):关键核查不可完成时给
`insufficient_information` 并列补件,**不得**据此评判企业高风险。
3. **单题不能代表综合意见**(BR-05):专项判断只进 findings 与被引用,不能直接
写入 recommendation。
4. **未校准就没有概率**(BR-12):`probability` 恒为 None + `not_calibrated`,
任何模型自报置信度都不改写它。
"""
from __future__ import annotations
RECOMMENDATIONS = ("advance", "supplement", "stop", "insufficient_information")
NOT_CALIBRATED = "not_calibrated"
NO_PROBABILITY_REASON = "尚未完成真实历史校准;不展示审批通过概率"
# 判定优先级(从上到下,先命中先返回)。顺序本身是业务口径,改动等于改结论。
PRECEDENCE = ("hard_block", "critical_incomplete", "open_findings", "clean")
INCOMPLETE_RESULTS = ("unknown", "error", "not_applicable")
def _codes(findings) -> set[str]:
return {str(item.get("code")) for item in findings or () if item.get("code")}
def hard_blocks(findings, hard_rule_codes) -> list[dict]:
"""命中的硬性阻断项。`hard_rule_codes` 来自冻结方案里 effect=hard 且 on_match=block 的规则。"""
wanted = set(hard_rule_codes or ())
return [dict(item) for item in findings or () if item.get("code") in wanted and item.get("result") == "issue"]
def critical_incomplete(findings, critical_check_ids) -> list[str]:
"""关键核查项中未完成的那些编码(未知/执行失败/不适用)。"""
critical = set(critical_check_ids or ())
return sorted({
str(item.get("code")) for item in findings or ()
if item.get("code") in critical and item.get("result") in INCOMPLETE_RESULTS
})
def open_findings(findings) -> list[dict]:
"""需要业务处理但未阻断的问题(issue / unknown)。"""
return [dict(item) for item in findings or ()
if item.get("result") in ("issue", "unknown")]
def probability() -> dict:
"""P0 恒为空。**不接受任何入参**——不给"顺手传个模型分数"留口子。"""
return {"value": None, "reason": NOT_CALIBRATED, "message": NO_PROBABILITY_REASON}
def decide(findings, missing_inputs=(), degradations=(), judgments=(),
critical_check_ids=(), hard_rule_codes=(), rule_versions=()) -> dict:
"""收敛结论。返回 recommendation 与支撑它的理由、降级与审计信息。"""
blocks = hard_blocks(findings, hard_rule_codes)
incomplete = critical_incomplete(findings, critical_check_ids)
open_items = open_findings(findings)
degradations = [dict(item) for item in degradations or ()]
missing = sorted({str(item) for item in missing_inputs or ()})
if blocks:
# 命中硬阻断:即便专项模型给出正面单题判断,也不改变结论(BR-05 / BR-06)
stage = "hard_block"
recommendation = "stop"
reason = "存在已确认的硬性阻断,暂不建议推进;" + "、".join(
f'{item.get("code")}@{item.get("basis_version", "未标注版本")}' for item in blocks)
elif incomplete:
stage = "critical_incomplete"
recommendation = "insufficient_information"
reason = ("关键核查未能完成,无法形成结论,请补充材料后重新发起;"
"不得据此判定企业高风险。未完成项:" + "、".join(incomplete))
missing = sorted(set(missing) | set(incomplete))
elif degradations or missing or any(item.get("result") == "error" for item in findings or ()):
stage = "critical_incomplete"
recommendation = "insufficient_information"
reason = "核查存在未完成步骤或缺失材料,请补充并重新核查;不得按已通过处理"
elif open_items:
stage = "open_findings"
recommendation = "supplement"
reason = f"存在 {len(open_items)} 项待处理事项,建议补充材料或说明后重新核查"
else:
stage = "clean"
recommendation = "advance"
reason = "已完成方案内的确定性核查,未发现阻断项;正式审批仍由业务流程决定"
return {
"recommendation": recommendation,
"recommendation_reason": reason,
"stage": stage,
"findings": [dict(item) for item in findings or ()],
"open_findings": open_items,
"hard_blocks": blocks,
"critical_incomplete": incomplete,
"missing_inputs": missing,
"degradations": degradations,
# 专项判断只作为依据入档,**不参与** recommendation 的判定
"judgments": [dict(item) for item in judgments or ()],
"judgment_can_override": False,
"rule_versions": [dict(item) for item in rule_versions or ()],
"probability": probability(),
# AC-45:stop 只是"暂不建议推进",系统不得自动关闭项目或执行正式审批
"formal_approval_touched": False,
}
def assert_no_probability_leak(result: dict) -> None:
"""任何路径都不得产出未校准概率(BR-12 / AC-12)。"""
value = (result.get("probability") or {}).get("value")
if value is not None:
raise ValueError("未校准的数据集不得产出审批通过概率")
for item in result.get("judgments", ()):
for metric in item.get("native_metrics", ()) or ():
if str(metric.get("name", "")).lower() in ("probability", "approval_probability"):
raise ValueError("专项模型的原生指标不得被当作审批概率")
def budget_exhausted(reason: str = "RUN_BUDGET_EXCEEDED") -> dict:
"""预算耗尽(E-08):停止并列出未完成项,不伪造完成。"""
return {"code": reason, "message": "本次核查已达到执行上限,请复核未完成项",
"retryable": False}