356 lines
16 KiB
Python
356 lines
16 KiB
Python
"""AI PMO decision brains (strategic/tactical/simulation/policy)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
from .ai_pmo_intent_routing import (
|
|
INTENT_FOLLOWUP_CONFIRM,
|
|
INTENT_FOLLOWUP_CONTINUE,
|
|
INTENT_PROJECT_CREATE,
|
|
INTENT_QUERY,
|
|
INTENT_SIGNAL_CREATE,
|
|
INTENT_SIGNAL_SOURCE_ONBOARD,
|
|
ROUTE_PROJECT_SIGNAL_CANCEL,
|
|
ROUTE_PROJECT_SIGNAL_COMMIT,
|
|
ROUTE_PROJECT_SIGNAL_DISCOVER,
|
|
ROUTE_PROJECT_SIGNAL_REFRESH,
|
|
CanonicalIntent,
|
|
)
|
|
|
|
|
|
def _normalize_workload_ratio(value: Any) -> float:
|
|
try:
|
|
ratio = float(value or 0.0)
|
|
except (TypeError, ValueError):
|
|
return 0.0
|
|
if ratio <= 0:
|
|
return 0.0
|
|
return ratio / 100.0 if ratio > 1.0 else ratio
|
|
|
|
|
|
class StrategicBrain:
|
|
"""组合级判断。"""
|
|
|
|
@staticmethod
|
|
def evaluate(*, intent: CanonicalIntent, world_model: dict[str, Any], message_text: str) -> dict[str, Any]:
|
|
project = world_model.get("project") if isinstance(world_model.get("project"), dict) else None
|
|
budget = world_model.get("budget") if isinstance(world_model.get("budget"), dict) else {}
|
|
capacity = world_model.get("capacity") if isinstance(world_model.get("capacity"), list) else []
|
|
risk_count = int((world_model.get("risk_summary") or {}).get("open_risk_count") or 0)
|
|
active_projects = int((world_model.get("portfolio") or {}).get("active_project_count") or 0)
|
|
|
|
budget_ratio = float(budget.get("usage_ratio") or 0.0)
|
|
overloaded_people = [row for row in capacity if _normalize_workload_ratio(row.get("workload_ratio")) >= 0.85]
|
|
risk_score = min(1.0, 0.18 + risk_count * 0.08 + budget_ratio * 0.25 + len(overloaded_people) * 0.1)
|
|
if intent.name == INTENT_PROJECT_CREATE:
|
|
risk_score = max(risk_score, 0.62)
|
|
if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD:
|
|
risk_score = max(risk_score, 0.58)
|
|
|
|
return {
|
|
"decision_level": "project" if project else "portfolio",
|
|
"decision_object_type": "project" if project else "portfolio",
|
|
"decision_object_id": _safe_int((project or {}).get("id")),
|
|
"risk_score": round(risk_score, 3),
|
|
"budget_impact": {
|
|
"budget_id": budget.get("budget_id"),
|
|
"usage_ratio": budget_ratio,
|
|
"hard_limit_enabled": bool(budget.get("hard_limit_enabled")),
|
|
},
|
|
"resource_impact": {
|
|
"overloaded_user_ids": [row.get("user_id") for row in overloaded_people],
|
|
"active_project_count": active_projects,
|
|
},
|
|
"rationale": (
|
|
f"intent={intent.name}; "
|
|
f"project_scope={'project' if project else 'portfolio'}; "
|
|
f"budget_ratio={budget_ratio:.2f}; "
|
|
f"open_risks={risk_count}; "
|
|
f"overloaded={len(overloaded_people)}"
|
|
),
|
|
"message_excerpt": message_text[:240],
|
|
}
|
|
|
|
|
|
class TacticalBrain:
|
|
"""周/日执行计划。"""
|
|
|
|
@staticmethod
|
|
def plan(*, intent: CanonicalIntent, world_model: dict[str, Any], decision: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
project = world_model.get("project") if isinstance(world_model.get("project"), dict) else None
|
|
project_ref = f"项目#{project['id']}" if project and project.get("id") is not None else "项目群"
|
|
strategic_plan = {
|
|
"priority": "high" if float(decision.get("risk_score") or 0.0) >= 0.6 else "medium",
|
|
"objectives": [
|
|
f"围绕 {project_ref} 控制风险与节奏",
|
|
"优先保证关键路径和资源边界不被破坏",
|
|
],
|
|
"signals": [
|
|
f"budget_usage_ratio={float((decision.get('budget_impact') or {}).get('usage_ratio') or 0.0):.2f}",
|
|
f"overloaded_users={len((decision.get('resource_impact') or {}).get('overloaded_user_ids') or [])}",
|
|
],
|
|
}
|
|
execution_actions = []
|
|
workflow_route = str((intent.metadata or {}).get("workflow_route") or "").strip().lower()
|
|
if intent.name == INTENT_QUERY:
|
|
execution_actions.append({"operation_code": "query.answer", "mode": "read_only"})
|
|
elif intent.name == INTENT_PROJECT_CREATE:
|
|
execution_actions.append({"operation_code": "project.create", "mode": "write"})
|
|
elif intent.name == INTENT_SIGNAL_SOURCE_ONBOARD:
|
|
execution_actions.append({"operation_code": "signal.enter", "mode": "write"})
|
|
execution_actions.append({"operation_code": "signal_source.onboard", "mode": "write"})
|
|
elif intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
|
|
execution_actions.append({"operation_code": "work_item.resume", "mode": "write"})
|
|
elif intent.name == INTENT_SIGNAL_CREATE:
|
|
if workflow_route == ROUTE_PROJECT_SIGNAL_DISCOVER:
|
|
execution_actions.append({"operation_code": ROUTE_PROJECT_SIGNAL_DISCOVER, "mode": "write"})
|
|
elif workflow_route == ROUTE_PROJECT_SIGNAL_COMMIT:
|
|
execution_actions.append({"operation_code": ROUTE_PROJECT_SIGNAL_COMMIT, "mode": "write"})
|
|
elif workflow_route == ROUTE_PROJECT_SIGNAL_REFRESH:
|
|
execution_actions.append({"operation_code": ROUTE_PROJECT_SIGNAL_REFRESH, "mode": "write"})
|
|
elif workflow_route == ROUTE_PROJECT_SIGNAL_CANCEL:
|
|
execution_actions.append({"operation_code": ROUTE_PROJECT_SIGNAL_CANCEL, "mode": "write"})
|
|
else:
|
|
execution_actions.append({"operation_code": "signal.enter", "mode": "write"})
|
|
|
|
execution_plan = {
|
|
"intent": intent.name,
|
|
"project_id": intent.project_id,
|
|
"actions": execution_actions,
|
|
"summary": f"执行 {intent.name},共 {len(execution_actions)} 个 typed actions",
|
|
}
|
|
return strategic_plan, execution_plan
|
|
|
|
|
|
class SimulationEngine:
|
|
"""执行前仿真。"""
|
|
|
|
BUDGET_COST_BY_OPERATION = {
|
|
"query.answer": 1.0,
|
|
"project.create": 120.0,
|
|
"signal.enter": 8.0,
|
|
"signal_source.onboard": 40.0,
|
|
ROUTE_PROJECT_SIGNAL_DISCOVER: 3.0,
|
|
ROUTE_PROJECT_SIGNAL_COMMIT: 8.0,
|
|
ROUTE_PROJECT_SIGNAL_REFRESH: 3.0,
|
|
ROUTE_PROJECT_SIGNAL_CANCEL: 0.2,
|
|
}
|
|
HOURS_BY_OPERATION = {
|
|
"query.answer": 0.2,
|
|
"project.create": 12.0,
|
|
"signal.enter": 1.5,
|
|
"signal_source.onboard": 6.0,
|
|
ROUTE_PROJECT_SIGNAL_DISCOVER: 0.8,
|
|
ROUTE_PROJECT_SIGNAL_COMMIT: 1.5,
|
|
ROUTE_PROJECT_SIGNAL_REFRESH: 0.8,
|
|
ROUTE_PROJECT_SIGNAL_CANCEL: 0.1,
|
|
}
|
|
|
|
@classmethod
|
|
def simulate(cls, *, intent: CanonicalIntent, world_model: dict[str, Any], execution_plan: dict[str, Any]) -> dict[str, Any]:
|
|
actions = execution_plan.get("actions") if isinstance(execution_plan.get("actions"), list) else []
|
|
operation_codes = [str(item.get("operation_code") or "").strip() for item in actions if isinstance(item, dict)]
|
|
estimated_budget_delta = sum(cls.BUDGET_COST_BY_OPERATION.get(code, 0.0) for code in operation_codes)
|
|
estimated_capacity_delta = sum(cls.HOURS_BY_OPERATION.get(code, 0.0) for code in operation_codes)
|
|
|
|
budget = world_model.get("budget") if isinstance(world_model.get("budget"), dict) else {}
|
|
budget_amount = float(budget.get("budget_amount_usd") or 0.0)
|
|
current_ratio = float(budget.get("usage_ratio") or 0.0)
|
|
projected_budget_ratio = current_ratio + (estimated_budget_delta / budget_amount if budget_amount > 0 else 0.0)
|
|
|
|
capacity = world_model.get("capacity") if isinstance(world_model.get("capacity"), list) else []
|
|
current_peak_ratio = max([_normalize_workload_ratio(item.get("workload_ratio")) for item in capacity], default=0.0)
|
|
project_member_count = max(1, int(world_model.get("member_count") or len(capacity) or 1))
|
|
projected_capacity_peak_ratio = current_peak_ratio + (estimated_capacity_delta / (40.0 * project_member_count))
|
|
|
|
task_summary = world_model.get("task_summary") if isinstance(world_model.get("task_summary"), dict) else {}
|
|
blocked_count = int((task_summary.get("by_status") or {}).get("blocked") or 0)
|
|
overdue_count = int(task_summary.get("overdue_count") or 0)
|
|
missing_skill_tag_ids = task_summary.get("uncovered_skill_tag_ids") if isinstance(task_summary.get("uncovered_skill_tag_ids"), list) else []
|
|
|
|
charter = world_model.get("charter") if isinstance(world_model.get("charter"), list) else []
|
|
dependency_pressure = "high" if blocked_count > 0 or overdue_count > 0 else ("medium" if not charter else "low")
|
|
projected_risk_score = min(
|
|
1.0,
|
|
0.12
|
|
+ projected_budget_ratio * 0.24
|
|
+ projected_capacity_peak_ratio * 0.28
|
|
+ blocked_count * 0.08
|
|
+ overdue_count * 0.05
|
|
+ len(missing_skill_tag_ids) * 0.07,
|
|
)
|
|
|
|
return {
|
|
"intent": intent.name,
|
|
"operation_codes": operation_codes,
|
|
"estimated_budget_delta_usd": round(estimated_budget_delta, 2),
|
|
"projected_budget_usage_ratio": round(projected_budget_ratio, 4),
|
|
"estimated_capacity_delta_hours": round(estimated_capacity_delta, 2),
|
|
"projected_capacity_peak_ratio": round(projected_capacity_peak_ratio, 4),
|
|
"blocked_task_count": blocked_count,
|
|
"overdue_task_count": overdue_count,
|
|
"dependency_pressure": dependency_pressure,
|
|
"missing_skill_tag_ids": missing_skill_tag_ids[:20],
|
|
"missing_skill_count": len(missing_skill_tag_ids),
|
|
"projected_risk_score": round(projected_risk_score, 4),
|
|
}
|
|
|
|
|
|
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, Any],
|
|
intent: CanonicalIntent,
|
|
simulation: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
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_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 float(simulation.get("projected_capacity_peak_ratio") or 0.0) >= 1.0:
|
|
return {
|
|
"auto_execute": False,
|
|
"policy_gate_hit": True,
|
|
"policy_gate_code": "simulation_capacity_limit",
|
|
"approval_requirement": {
|
|
"required": True,
|
|
"reason": "simulation_capacity_limit",
|
|
"message": "执行前仿真显示产能会超载,自动执行被拦截。",
|
|
},
|
|
}
|
|
|
|
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},
|
|
}
|
|
|
|
|
|
class DecisionEngine:
|
|
"""汇总三脑输出。"""
|
|
|
|
def __init__(self, *, policy_gate_rules: tuple[tuple[str, tuple[str, ...]], ...]) -> None:
|
|
self.strategic_brain = StrategicBrain()
|
|
self.tactical_brain = TacticalBrain()
|
|
self.simulation_engine = SimulationEngine()
|
|
self.execution_policy_brain = ExecutionPolicyBrain(policy_gate_rules=policy_gate_rules)
|
|
|
|
def decide(self, *, intent: CanonicalIntent, world_model: dict[str, Any], message_text: str) -> dict[str, Any]:
|
|
strategic = self.strategic_brain.evaluate(intent=intent, world_model=world_model, message_text=message_text)
|
|
strategic_plan, execution_plan = self.tactical_brain.plan(intent=intent, world_model=world_model, decision=strategic)
|
|
simulation = self.simulation_engine.simulate(intent=intent, world_model=world_model, execution_plan=execution_plan)
|
|
strategic_plan["simulation"] = simulation
|
|
execution_plan["simulation"] = simulation
|
|
policy = self.execution_policy_brain.evaluate(
|
|
message_text=message_text,
|
|
world_model=world_model,
|
|
intent=intent,
|
|
simulation=simulation,
|
|
)
|
|
strategic["risk_score"] = max(float(strategic.get("risk_score") or 0.0), float(simulation.get("projected_risk_score") or 0.0))
|
|
return {
|
|
"strategic": strategic,
|
|
"strategic_plan": strategic_plan,
|
|
"execution_plan": execution_plan,
|
|
"simulation": simulation,
|
|
"policy": policy,
|
|
}
|
|
|
|
|
|
def _safe_int(value: Any) -> int | None:
|
|
try:
|
|
if value is None or value == "":
|
|
return None
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return None
|