PMFlow/backend/app/services/ai_pmo/ai_pmo_decision/policy.py

157 lines
6.1 KiB
Python

"""Execution policy brain for AI PMO decision runtime."""
from __future__ import annotations
import os
from .shared import (
CanonicalIntent,
INTENT_QUERY,
INTENT_TASK_CREATE,
INTENT_SIGNAL_CREATE,
INTENT_SIGNAL_SOURCE_ONBOARD,
re,
)
class ExecutionPolicyBrain:
"""自动执行边界。"""
def __init__(self, *, policy_gate_rules: tuple[tuple[str, tuple[str, ...]], ...]) -> None:
self.policy_gate_rules = policy_gate_rules
def evaluate(
self,
*,
message_text: str,
world_model: dict[str, object],
intent: CanonicalIntent,
simulation: dict[str, object],
) -> dict[str, object]:
warn_threshold = self._env_float("CAPACITY_WARN_THRESHOLD", 1.0)
block_threshold = self._env_float("CAPACITY_BLOCK_THRESHOLD", 1.5)
if block_threshold < warn_threshold:
block_threshold = warn_threshold
projected_capacity_ratio = float(simulation.get("projected_capacity_peak_ratio") or 0.0)
capacity_warning: dict[str, object] | None = None
normalized = re.sub(r"\s+", "", str(message_text or "")).lower()
for gate_code, tokens in self.policy_gate_rules:
if any(token.lower().replace(" ", "") in normalized for token in tokens):
return {
"auto_execute": False,
"policy_gate_hit": True,
"policy_gate_code": gate_code,
"approval_requirement": {
"required": True,
"reason": gate_code,
"message": "命中硬策略边界,必须进入显式审批/人工确认。",
},
}
if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD and intent.project_id is None:
return {
"auto_execute": False,
"policy_gate_hit": True,
"policy_gate_code": "missing_project_scope",
"approval_requirement": {
"required": True,
"reason": "missing_project_scope",
"message": "信号源接入缺少项目归属,必须先确认项目。",
},
}
if intent.name in {INTENT_QUERY, INTENT_TASK_CREATE, INTENT_SIGNAL_CREATE}:
return {
"auto_execute": True,
"policy_gate_hit": False,
"policy_gate_code": None,
"approval_requirement": {"required": False},
}
budget = world_model.get("budget") if isinstance(world_model.get("budget"), dict) else {}
if bool(budget.get("hard_limit_enabled")) and float(budget.get("usage_ratio") or 0.0) >= 1.0:
return {
"auto_execute": False,
"policy_gate_hit": True,
"policy_gate_code": "budget_hard_limit",
"approval_requirement": {
"required": True,
"reason": "budget_hard_limit",
"message": "项目预算已触达硬限制,自动执行被拦截。",
},
}
if float(simulation.get("projected_budget_usage_ratio") or 0.0) >= 1.0:
return {
"auto_execute": False,
"policy_gate_hit": True,
"policy_gate_code": "simulation_budget_limit",
"approval_requirement": {
"required": True,
"reason": "simulation_budget_limit",
"message": "执行前仿真显示预算将触顶,自动执行被拦截。",
},
}
if projected_capacity_ratio >= block_threshold:
return {
"auto_execute": False,
"policy_gate_hit": True,
"policy_gate_code": "simulation_capacity_limit",
"approval_requirement": {
"required": True,
"reason": "simulation_capacity_limit",
"message": "执行前仿真显示产能严重超载,自动执行被拦截。",
},
}
if projected_capacity_ratio >= warn_threshold:
capacity_warning = {
"enabled": True,
"severity": "warning",
"peak_ratio": round(projected_capacity_ratio, 4),
"warn_threshold": round(warn_threshold, 4),
"block_threshold": round(block_threshold, 4),
}
if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD and int(simulation.get("missing_skill_count") or 0) > 0:
return {
"auto_execute": False,
"policy_gate_hit": True,
"policy_gate_code": "simulation_skill_gap",
"approval_requirement": {
"required": True,
"reason": "simulation_skill_gap",
"message": "执行前仿真显示当前项目存在技能缺口,信号源接入先进入待确认。",
},
}
if intent.name in {INTENT_SIGNAL_SOURCE_ONBOARD, INTENT_SIGNAL_CREATE} and str(simulation.get("dependency_pressure") or "") == "high":
return {
"auto_execute": False,
"policy_gate_hit": True,
"policy_gate_code": "simulation_dependency_pressure",
"approval_requirement": {
"required": True,
"reason": "simulation_dependency_pressure",
"message": "执行前仿真显示关键依赖压力过高,当前先进入待人工确认。",
},
}
return {
"auto_execute": True,
"policy_gate_hit": False,
"policy_gate_code": None,
"approval_requirement": {"required": False},
"capacity_warning": capacity_warning,
}
@staticmethod
def _env_float(name: str, default: float) -> float:
raw = str(os.getenv(name, "")).strip()
if not raw:
return float(default)
try:
return float(raw)
except (TypeError, ValueError):
return float(default)