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

128 lines
5.4 KiB
Python

"""Simulation engine for AI PMO decision runtime."""
from __future__ import annotations
from .shared import (
CanonicalIntent,
ROUTE_PROJECT_SIGNAL_CANCEL,
ROUTE_PROJECT_SIGNAL_COMMIT,
ROUTE_PROJECT_SIGNAL_DISCOVER,
ROUTE_PROJECT_SIGNAL_REFRESH,
Any,
normalize_workload_ratio,
)
class SimulationEngine:
"""执行前仿真。"""
BUDGET_COST_BY_OPERATION = {
"query.answer": 1.0,
"project.create": 120.0,
"project.update": 12.0,
"project.archive": 6.0,
"project.reopen": 6.0,
"project.delete": 8.0,
"task.create": 14.0,
"task.update": 4.0,
"task.assign": 3.0,
"task.prioritize": 2.0,
"task.status_transition": 2.0,
"task.delete": 3.0,
"personnel.role_update": 4.0,
"personnel.workload_update": 2.0,
"personnel.availability_update": 2.0,
"knowledge.doc_upsert": 12.0,
"knowledge.chunk_upsert": 6.0,
"knowledge.link_upsert": 4.0,
"knowledge.reindex": 20.0,
"notification.create": 1.0,
"notification.ack": 0.3,
"notification.close": 0.5,
"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,
"project.update": 1.2,
"project.archive": 0.6,
"project.reopen": 0.6,
"project.delete": 0.8,
"task.create": 2.5,
"task.update": 0.8,
"task.assign": 0.5,
"task.prioritize": 0.4,
"task.status_transition": 0.4,
"task.delete": 0.5,
"personnel.role_update": 0.5,
"personnel.workload_update": 0.3,
"personnel.availability_update": 0.3,
"knowledge.doc_upsert": 1.8,
"knowledge.chunk_upsert": 0.8,
"knowledge.link_upsert": 0.5,
"knowledge.reindex": 3.0,
"notification.create": 0.2,
"notification.ack": 0.1,
"notification.close": 0.1,
"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),
}