PMFlow/backend/app/services/ai_pmo_kernel_service.py

2301 lines
99 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.

"""AI PMO 决策中枢内核服务。"""
from __future__ import annotations
import hashlib
import json
import re
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Optional
from sqlalchemy import and_, func, or_, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from ..core.logging import get_logger
from ..models import (
AIProcessingLog,
Budget,
DecisionRecord,
IngestEvent,
Notification,
PlanBundle,
PortfolioSnapshot,
Project,
ProjectDomain,
ProjectSignalLink,
ReplyReceipt,
Signal,
Task,
TruthNode,
User,
UserProject,
UserSkillBinding,
UserWorkload,
WorkItem,
)
from .adaptive_rag_service import AdaptiveRAGService
from .ai_pmo_cadence_service import AIPMOCadenceService
from .ai_steward import AIStewardService
from .ai_pmo_review_service import AIPMOReviewService
from .notification_service import NotificationService
from .openclaw_commander_service import OpenClawCommanderService
from .ai_pmo_domain_workflow import SignalSourceDomainWorkflow
from .signal_source_orchestration_service import SignalSourceOrchestrationService
logger = get_logger(__name__)
INTENT_QUERY = "query.read_only"
INTENT_PROJECT_CREATE = "project.create"
INTENT_SIGNAL_CREATE = "signal.create"
INTENT_SIGNAL_SOURCE_ONBOARD = "signal_source.onboard"
INTENT_FOLLOWUP_CONFIRM = "followup.confirm"
INTENT_FOLLOWUP_CONTINUE = "followup.continue"
INTENT_IGNORE = "ignore"
FOLLOWUP_TARGET_INTENTS = {
INTENT_PROJECT_CREATE,
INTENT_SIGNAL_CREATE,
INTENT_SIGNAL_SOURCE_ONBOARD,
}
STATE_RECEIVED = "received"
STATE_MODELED = "modeled"
STATE_DECIDED = "decided"
STATE_PLANNED = "planned"
STATE_EXECUTING = "executing"
STATE_WAITING_MANUAL = "waiting_manual"
STATE_SUCCEEDED = "succeeded"
STATE_FAILED = "failed"
STATE_IGNORED = "ignored"
RECEIPT_PHASE_ACTION = "action_applied"
RECEIPT_PHASE_PENDING = "pending_manual"
RECEIPT_PHASE_ERROR = "error"
QUESTION_HINTS = ("?", "?", "怎么", "为何", "为什么", "什么", "多少", "是否", "帮我看", "状态")
SMALLTALK_EXACT = {
"收到",
"收到啦",
"好的",
"ok",
"okk",
"yes",
"no",
"谢谢",
"thanks",
"thx",
"在吗",
"在不在",
}
TRANSPORT_MODE_CORRECTION_HINTS = ("不是", "不再", "改为", "改成", "替换为", "取代", "现在都", "现在是")
FOLLOWUP_CONFIRM_HINTS = ("确认归属", "确认项目", "确认到项目", "确认关联", "确认这条")
FOLLOWUP_CONTINUE_HINTS = ("继续处理", "继续执行", "接着处理", "往下做", "继续推进")
FOLLOWUP_WORK_ITEM_ID_RE = re.compile(r"(?:work[_ -]?item|工作项|队列项)\s*(?:id|ID)?\s*[:#:]?\s*(\d+)", re.IGNORECASE)
SIGNAL_CREATE_HINTS = ("新增一个信号", "新增一条信号", "增加一个信号", "录入一个信号", "关注", "跟踪")
PROJECT_CREATE_HINTS = ("创建项目", "新建项目", "建立项目", "做个项目", "立项", "开个项目")
POLICY_GATE_RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
("delete_or_archive", ("删除项目", "删除信号", "归档项目", "归档", "archive", "清空项目")),
("budget_overrun", ("超预算", "突破预算", "额外预算", "加预算")),
("cross_project_staffing", ("跨项目抢人", "抽调", "抢占关键人力", "挪人")),
("charter_rewrite", ("改写charter", "success criteria", "成功标准改", "范围重大变更")),
("critical_path_change", ("关键路径", "里程碑改动", "影响多个项目", "跨项目依赖调整")),
)
def _utc_now_naive() -> datetime:
return datetime.now(timezone.utc).replace(tzinfo=None)
def _safe_int(value: Any) -> Optional[int]:
try:
if value is None or value == "":
return None
return int(value)
except (TypeError, ValueError):
return None
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
@dataclass(slots=True)
class CanonicalIntent:
name: str
confidence: float
project_id: Optional[int]
signal_title: str
signal_summary: str
metadata: dict[str, Any]
class IntentResolver:
"""把输入收口为 canonical intent。"""
@staticmethod
def resolve(*, text: str, project_id: Optional[int]) -> CanonicalIntent:
normalized = re.sub(r"\s+", "", str(text or "")).lower()
pretty = re.sub(r"\s+", " ", str(text or "")).strip()
if not pretty:
return CanonicalIntent(INTENT_IGNORE, 1.0, project_id, "", "", {"reason": "empty"})
if normalized in SMALLTALK_EXACT:
return CanonicalIntent(INTENT_IGNORE, 0.95, project_id, "", "", {"reason": "smalltalk"})
if any(token in pretty for token in FOLLOWUP_CONFIRM_HINTS):
return CanonicalIntent(INTENT_FOLLOWUP_CONFIRM, 0.82, project_id, "", "", {})
if any(token in pretty for token in FOLLOWUP_CONTINUE_HINTS):
return CanonicalIntent(INTENT_FOLLOWUP_CONTINUE, 0.82, project_id, "", "", {})
if SignalSourceOrchestrationService.looks_like_signal_source_request(pretty):
return CanonicalIntent(
INTENT_SIGNAL_SOURCE_ONBOARD,
0.92,
project_id,
IntentResolver._build_signal_title(pretty, default="新增信号源接入请求"),
pretty[:300],
{"source_request": True},
)
if any(token in pretty for token in PROJECT_CREATE_HINTS) or OpenClawCommanderService._extract_project_create_title(pretty):
return CanonicalIntent(
INTENT_PROJECT_CREATE,
0.9,
project_id,
IntentResolver._build_signal_title(pretty, default="新项目创建请求"),
pretty[:300],
{},
)
if any(token in pretty for token in QUESTION_HINTS):
return CanonicalIntent(
INTENT_QUERY,
0.88,
project_id,
"",
pretty[:300],
{},
)
if any(token in pretty for token in SIGNAL_CREATE_HINTS) or any(token in pretty for token in ("新增", "增加", "录入", "跟踪", "关注")):
return CanonicalIntent(
INTENT_SIGNAL_CREATE,
0.78,
project_id,
IntentResolver._build_signal_title(pretty, default="新信号"),
pretty[:300],
{},
)
return CanonicalIntent(
INTENT_SIGNAL_CREATE,
0.65,
project_id,
IntentResolver._build_signal_title(pretty, default="新信号"),
pretty[:300],
{"fallback": True},
)
@staticmethod
def _build_signal_title(text: str, *, default: str) -> str:
cleaned = re.sub(r"\s+", " ", str(text or "")).strip(" ,,::")
if not cleaned:
return default
for token in ("。", ";", ";", "!", "!", "?", "?"):
if token in cleaned:
cleaned = cleaned.split(token, 1)[0].strip()
break
return cleaned[:120] or default
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 = []
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:
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,
}
HOURS_BY_OPERATION = {
"query.answer": 0.2,
"project.create": 12.0,
"signal.enter": 1.5,
"signal_source.onboard": 6.0,
}
@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:
"""自动执行边界。"""
@staticmethod
def evaluate(
*,
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 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": "信号源接入缺少项目归属,必须先确认项目。",
},
}
# 轻量级事实写入/只读查询不应被项目当前依赖或产能压力阻塞。
# 否则“AI 雷达”会在最需要先收集事实时失去自动化能力。
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) -> None:
self.strategic_brain = StrategicBrain()
self.tactical_brain = TacticalBrain()
self.simulation_engine = SimulationEngine()
self.execution_policy_brain = ExecutionPolicyBrain()
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,
}
class ReplyRenderer:
"""唯一回执出口。"""
@staticmethod
def render(
*,
work_state: str,
intent: CanonicalIntent,
decision_record: dict[str, Any],
execution_result: dict[str, Any],
) -> dict[str, Any]:
if work_state == STATE_WAITING_MANUAL:
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
followup_text = str(execution_result.get("followup_reply") or "").strip()
if followup_text:
return {
"receipt_phase": RECEIPT_PHASE_PENDING,
"template_key": "pending_manual",
"payload": {"intent": intent.name, "reason": followup_text},
"text": followup_text,
}
gate_message = str(((decision_record.get("approval_requirement") or {}).get("message")) or "").strip()
if not gate_message:
gate_message = "已进入待人工确认队列。"
return {
"receipt_phase": RECEIPT_PHASE_PENDING,
"template_key": "pending_manual",
"payload": {"intent": intent.name, "reason": gate_message},
"text": gate_message,
}
if work_state == STATE_FAILED:
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
followup_text = str(execution_result.get("followup_reply") or "").strip()
if followup_text:
return {
"receipt_phase": RECEIPT_PHASE_ERROR,
"template_key": "safe_error",
"payload": {"intent": intent.name, "reason": followup_text},
"text": followup_text,
}
reason = str(execution_result.get("error_message") or "系统处理中出现异常,本次未执行。").strip()
return {
"receipt_phase": RECEIPT_PHASE_ERROR,
"template_key": "safe_error",
"payload": {"intent": intent.name, "reason": reason},
"text": reason,
}
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
followup_text = str(execution_result.get("followup_reply") or "已继续处理。").strip()
return {
"receipt_phase": RECEIPT_PHASE_ACTION,
"template_key": "action_succeeded",
"payload": {
"intent": intent.name,
"target_work_item_id": _safe_int(execution_result.get("target_work_item_id")),
"target_state": execution_result.get("target_state"),
},
"text": followup_text,
}
if intent.name == INTENT_QUERY:
answer = str(execution_result.get("answer") or "当前暂无可用回答。").strip()
return {
"receipt_phase": RECEIPT_PHASE_ACTION,
"template_key": "action_succeeded",
"payload": {"intent": intent.name, "answer": answer},
"text": answer,
}
if intent.name == INTENT_PROJECT_CREATE:
project = execution_result.get("project") if isinstance(execution_result.get("project"), dict) else {}
project_id = _safe_int(project.get("id"))
project_title = str(project.get("title") or project.get("name") or "新项目").strip()
return {
"receipt_phase": RECEIPT_PHASE_ACTION,
"template_key": "action_succeeded",
"payload": {"intent": intent.name, "project_id": project_id, "project_title": project_title},
"text": f"已立项:项目#{project_id}《{project_title}》已创建完成。" if project_id else f"已立项:《{project_title}》已创建完成。",
}
if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD:
signal_id = _safe_int(execution_result.get("signal_id"))
workflow = execution_result.get("signal_source_workflow") if isinstance(execution_result.get("signal_source_workflow"), dict) else {}
status = str(workflow.get("status") or (execution_result.get("signal_source_orchestration") or {}).get("status") or "").strip()
if status == "pending_manual":
return {
"receipt_phase": RECEIPT_PHASE_PENDING,
"template_key": "pending_manual",
"payload": {"intent": intent.name, "signal_id": signal_id, "status": status},
"text": f"已记录信号源接入请求,当前为待人工确认。信号#{signal_id} 已入库。" if signal_id else "已记录信号源接入请求,当前为待人工确认。",
}
return {
"receipt_phase": RECEIPT_PHASE_ACTION,
"template_key": "action_succeeded",
"payload": {"intent": intent.name, "signal_id": signal_id, "status": status},
"text": f"已接收入库并启动信号源接入流程。信号#{signal_id},状态:{status or 'running'}。",
}
signal_id = _safe_int(execution_result.get("signal_id"))
project_id = _safe_int(execution_result.get("project_id"))
if signal_id is not None and project_id is not None:
text = f"已入库:已为项目#{project_id}创建信号#{signal_id}。"
elif signal_id is not None:
text = f"已入库:已创建信号#{signal_id}。"
else:
text = "已收到并入库。"
return {
"receipt_phase": RECEIPT_PHASE_ACTION,
"template_key": "action_succeeded",
"payload": {"intent": intent.name, "signal_id": signal_id, "project_id": project_id},
"text": text,
}
class TypedExecutor:
"""执行层只做 typed action 落地。"""
def __init__(self, db: AsyncSession, tenant_id: int) -> None:
self.db = db
self.tenant_id = int(tenant_id)
self.signal_source_workflow = SignalSourceDomainWorkflow(db)
async def execute(
self,
*,
intent: CanonicalIntent,
message_text: str,
sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
) -> dict[str, Any]:
if intent.name == INTENT_QUERY:
rag_service = AdaptiveRAGService(self.db, tenant_id=self.tenant_id)
rag_result = await rag_service.answer_question(
question=message_text,
asked_by=operator_user_id,
asked_via=source_system,
project_id=intent.project_id,
source_chat_id=source_message_id,
conversation_key=source_message_id,
action_mode="read",
sync_requirement=False,
)
return {
"status": "succeeded",
"answer": str(rag_result.get("answer") or "").strip(),
"rag_result": rag_result,
"project_id": intent.project_id,
}
if intent.name == INTENT_PROJECT_CREATE:
if operator_user_id is None:
return {
"status": "failed_safe",
"error_message": "未找到可用项目创建人,当前无法自动创建项目。",
}
steward = AIStewardService(self.db)
payload = await steward.create_project_from_natural_language(
input_text=message_text,
creator_id=int(operator_user_id),
tenant_id=self.tenant_id,
)
project = payload.get("project") if isinstance(payload.get("project"), dict) else {}
signal = payload.get("signal") if isinstance(payload.get("signal"), dict) else {}
return {
"status": "succeeded",
"project": project,
"signal": signal,
"project_id": _safe_int(project.get("id")),
"signal_id": _safe_int(signal.get("id")),
"tasks": payload.get("tasks") if isinstance(payload.get("tasks"), list) else [],
"raw_payload": payload,
}
if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD:
signal = await self._create_signal(
title=intent.signal_title or "新增信号源接入请求",
summary=intent.signal_summary or message_text,
content=message_text,
project_id=intent.project_id,
source="PMFlow",
signal_type="新需求",
recommended_action="enter_only",
)
domain_workflow = await self.signal_source_workflow.run(
tenant_id=self.tenant_id,
project_id=int(intent.project_id or 0),
signal_id=int(signal.id),
initiating_text=message_text,
requested_via=source_system,
operator_user_id=operator_user_id,
)
orchestration = domain_workflow.get("raw_result") if isinstance(domain_workflow.get("raw_result"), dict) else {}
return {
"status": "succeeded" if str(domain_workflow.get("status") or "").lower() == "completed" else (
"pending_manual" if str(domain_workflow.get("status") or "").lower() == "pending_manual" else "failed_safe"
),
"signal_id": int(signal.id),
"project_id": intent.project_id,
"signal_source_workflow": domain_workflow,
"signal_source_orchestration": orchestration,
"auto_tasks": self._extract_auto_tasks(orchestration.get("openclaw")),
}
if intent.name == INTENT_SIGNAL_CREATE:
signal = await self._create_signal(
title=intent.signal_title or "新信号",
summary=intent.signal_summary or message_text,
content=message_text,
project_id=intent.project_id,
source="PMFlow",
signal_type="新需求",
recommended_action="enter_only",
)
return {
"status": "succeeded",
"signal_id": int(signal.id),
"project_id": intent.project_id,
"auto_tasks": [],
}
return {
"status": "failed_safe",
"error_message": "当前意图尚未迁移到 AI PMO kernel,已保留给兼容链路处理。",
}
async def _create_signal(
self,
*,
title: str,
summary: str,
content: str,
project_id: Optional[int],
source: str,
signal_type: str,
recommended_action: str,
) -> Signal:
signal = Signal(
tenant_id=self.tenant_id,
source=source,
title=(title or "新信号")[:120],
summary=(summary or content or "")[:300],
content=(content or summary or "")[:2000],
signal_type=signal_type,
reasoning=json.dumps(["由 AI PMO kernel 统一入库。"], ensure_ascii=False),
confidence=0.78,
recommended_action=recommended_action,
matched_project_id=int(project_id) if project_id is not None else None,
impact_score=0.7,
priority="medium",
processed=False,
tasks_generated=json.dumps([], ensure_ascii=False),
status="待处理",
)
self.db.add(signal)
await self.db.flush()
if project_id is not None:
exists = (
await self.db.execute(
select(ProjectSignalLink.id).where(
and_(
ProjectSignalLink.tenant_id == self.tenant_id,
ProjectSignalLink.project_id == int(project_id),
ProjectSignalLink.signal_id == int(signal.id),
)
).limit(1)
)
).scalar_one_or_none()
if exists is None:
self.db.add(
ProjectSignalLink(
tenant_id=self.tenant_id,
project_id=int(project_id),
signal_id=int(signal.id),
link_type="related",
relevance_score=1.0,
)
)
await self.db.flush()
return signal
@staticmethod
def _extract_auto_tasks(openclaw_payload: Any) -> list[dict[str, Any]]:
if not isinstance(openclaw_payload, dict):
return []
result = openclaw_payload.get("result") if isinstance(openclaw_payload.get("result"), dict) else openclaw_payload
execution = result.get("execution") if isinstance(result, dict) and isinstance(result.get("execution"), dict) else {}
rows = execution.get("results") if isinstance(execution.get("results"), list) else []
items: list[dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
operation_code = str(row.get("operation_code") or "").strip()
if operation_code != "task.create":
continue
payload = row.get("payload") if isinstance(row.get("payload"), dict) else {}
items.append(
{
"operation_code": operation_code,
"status": str(row.get("status") or "").strip(),
"entity_id": row.get("entity_id"),
"title": payload.get("title"),
"task_id": payload.get("task_id"),
}
)
return items
class AIPMOKernelService:
"""AI PMO kernel,作为新的统一事实链。"""
def __init__(self, db: AsyncSession, tenant_id: int = 1) -> None:
self.db = db
self.tenant_id = int(tenant_id)
self.intent_resolver = IntentResolver()
self.decision_engine = DecisionEngine()
self.reply_renderer = ReplyRenderer()
self.executor = TypedExecutor(db, tenant_id=self.tenant_id)
self.review_service = AIPMOReviewService(db, tenant_id=self.tenant_id)
self.cadence_service = AIPMOCadenceService(db, tenant_id=self.tenant_id)
async def process_feishu_message(
self,
*,
group_chat_id: str,
message_content: str,
sender: str,
source_message_id: Optional[str],
source_sender_open_id: Optional[str] = None,
captured_at: Optional[datetime] = None,
chat_type: Optional[str] = None,
) -> dict[str, Any]:
return await self.process_ingress(
source_system="feishu",
source_message_id=source_message_id,
message_text=message_content,
sender=sender,
channel_ref=group_chat_id,
source_sender_open_id=source_sender_open_id,
captured_at=captured_at,
chat_type=chat_type,
)
async def process_ingress(
self,
*,
source_system: str,
source_message_id: Optional[str],
message_text: str,
sender: str,
channel_ref: Optional[str] = None,
source_sender_open_id: Optional[str] = None,
captured_at: Optional[datetime] = None,
chat_type: Optional[str] = None,
project_hint_id: Optional[int] = None,
) -> dict[str, Any]:
started = time.perf_counter()
clean_text = self._normalize_text(message_text)
if not clean_text:
return {"handled": True, "status": "ignored", "classification": "empty", "direct_reply_required": False}
transport_mode_reply = self._build_transport_mode_reply(clean_text)
if transport_mode_reply:
return {
"handled": True,
"status": "answered",
"classification": "question",
"linked_project_ids": [int(project_hint_id)] if project_hint_id is not None else [],
"signal_id": None,
"created_project_id": None,
"auto_tasks": [],
"ai_trace_id": None,
"results": [],
"intent": INTENT_QUERY,
"direct_reply_required": True,
"direct_reply_text": transport_mode_reply,
"reply_template_key": "transport_mode_ack",
"work_item_id": None,
"decision_level": None,
"review_summary": None,
}
synthetic_message_id = self._build_source_message_id(
source_system=source_system,
source_message_id=source_message_id,
channel_ref=channel_ref,
sender=sender,
message_text=clean_text,
)
ingest_event, created = await self._claim_ingress_event(
source_system=source_system,
source_message_id=synthetic_message_id,
payload={
"sender": sender,
"source_sender_open_id": source_sender_open_id,
"channel_ref": channel_ref,
"chat_type": chat_type,
"captured_at": captured_at.isoformat() if isinstance(captured_at, datetime) else None,
"message_text": clean_text,
},
)
if not created:
return {
"handled": True,
"status": "ignored",
"reason": "duplicate_filtered",
"classification": "duplicate",
"direct_reply_required": False,
"source_message_id": synthetic_message_id,
}
operator = await self._resolve_operator_user(sender=sender, sender_open_id=source_sender_open_id)
work_item = await self._create_work_item(
ingest_event=ingest_event,
source_system=source_system,
source_message_id=synthetic_message_id,
owner_user_id=_safe_int(getattr(operator, "id", None)),
)
try:
world_model = await self._build_world_model(
message_text=clean_text,
sender=sender,
project_hint_id=project_hint_id,
)
work_item.state = STATE_MODELED
work_item.context_snapshot = world_model
intent = self.intent_resolver.resolve(
text=clean_text,
project_id=_safe_int(world_model.get("resolved_project_id")),
)
if intent.name == INTENT_IGNORE:
work_item.state = STATE_IGNORED
work_item.intent = intent.name
ingest_event.status = "done"
ingest_event.processed_at = _utc_now_naive()
await self._write_processing_log(
source_message_id=synthetic_message_id,
work_item=work_item,
message_text=clean_text,
route="ai_pmo.ignore",
operation_code=None,
operation_result={"reason": intent.metadata.get("reason")},
processing_status="success",
duration_ms=int((time.perf_counter() - started) * 1000),
)
await self.db.commit()
return {
"handled": True,
"status": "ignored",
"classification": "ignored",
"direct_reply_required": False,
"work_item_id": int(work_item.id),
"ai_trace_id": f"ai-pmo-{int(work_item.id)}",
}
work_item.intent = intent.name
work_item.project_id = intent.project_id
decision_payload = self.decision_engine.decide(
intent=intent,
world_model=world_model,
message_text=clean_text,
)
work_item.state = STATE_DECIDED
work_item.decision_level = str((decision_payload["strategic"] or {}).get("decision_level") or "portfolio")
decision_row = await self._create_decision_record(
work_item=work_item,
world_model=world_model,
decision_payload=decision_payload,
)
await self._create_plan_bundle(
work_item_id=int(work_item.id),
plan_type="strategic",
summary="Strategic Brain 产出的项目群/项目策略。",
payload=decision_payload["strategic_plan"],
)
await self._create_plan_bundle(
work_item_id=int(work_item.id),
plan_type="execution",
summary="Execution Plan typed action bundle。",
payload=decision_payload["execution_plan"],
)
policy = decision_payload["policy"]
if not bool(policy.get("auto_execute")):
work_item.state = STATE_WAITING_MANUAL
work_item.action_bundle = decision_payload["execution_plan"]
work_item.execution_result = {
"status": "waiting_manual",
"reason": ((policy.get("approval_requirement") or {}).get("message") or "").strip(),
}
manual_issues = ["waiting_manual"]
policy_gate_code = str(policy.get("policy_gate_code") or "").strip()
if policy_gate_code:
manual_issues.append(policy_gate_code)
work_item.review_summary = await self.review_service.review_work_item(
work_item=work_item,
intent_name=intent.name,
execution_result=work_item.execution_result,
world_model=world_model,
verification={"verified": False, "issues": manual_issues},
simulation=decision_payload["simulation"],
)
render = self.reply_renderer.render(
work_state=STATE_WAITING_MANUAL,
intent=intent,
decision_record={
"approval_requirement": policy.get("approval_requirement"),
},
execution_result={},
)
work_item.reply_template_key = str(render["template_key"])
await self._project_waiting_manual(
work_item=work_item,
message_text=clean_text,
render=render,
intent=intent,
)
await self._maybe_emit_risk_cadence_snapshot(
project_id=_safe_int(work_item.project_id),
review_summary=work_item.review_summary,
)
direct_reply_required = await self._claim_reply_receipt(
work_item_id=int(work_item.id),
source_system=source_system,
source_message_id=synthetic_message_id,
receipt_phase=str(render["receipt_phase"]),
template_key=str(render["template_key"]),
payload=render["payload"],
)
ingest_event.status = "done"
ingest_event.processed_at = _utc_now_naive()
await self._write_processing_log(
source_message_id=synthetic_message_id,
work_item=work_item,
message_text=clean_text,
route="ai_pmo.waiting_manual",
operation_code=None,
operation_result=work_item.execution_result,
processing_status="success",
duration_ms=int((time.perf_counter() - started) * 1000),
)
await self.db.commit()
return self._build_capture_result(
work_item=work_item,
intent=intent,
execution_result=work_item.execution_result or {},
render=render,
direct_reply_required=direct_reply_required,
status="captured",
classification="command",
)
work_item.state = STATE_PLANNED
work_item.action_bundle = decision_payload["execution_plan"]
work_item.state = STATE_EXECUTING
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
execution_result, verification = await self._execute_followup_intent(
work_item=work_item,
intent=intent,
message_text=clean_text,
sender=sender,
operator_user_id=_safe_int(getattr(operator, "id", None)),
source_system=source_system,
source_message_id=synthetic_message_id,
channel_ref=channel_ref,
source_sender_open_id=source_sender_open_id,
world_model=world_model,
)
else:
execution_result = await self.executor.execute(
intent=intent,
message_text=clean_text,
sender=sender,
operator_user_id=_safe_int(getattr(operator, "id", None)),
source_system=source_system,
source_message_id=synthetic_message_id,
)
verification = await self._verify_execution(intent=intent, execution_result=execution_result, world_model=world_model)
work_item.execution_result = execution_result
if _safe_int(execution_result.get("project_id")) is not None:
work_item.project_id = _safe_int(execution_result.get("project_id"))
if _safe_int(execution_result.get("signal_id")) is not None:
work_item.signal_id = _safe_int(execution_result.get("signal_id"))
work_item.review_summary = await self.review_service.review_work_item(
work_item=work_item,
intent_name=intent.name,
execution_result=execution_result,
world_model=world_model,
verification=verification,
simulation=decision_payload["simulation"],
)
await self._maybe_emit_risk_cadence_snapshot(
project_id=_safe_int(work_item.project_id),
review_summary=work_item.review_summary,
)
decision_row.execution_result = execution_result
issues = verification.get("issues") if isinstance(verification.get("issues"), list) else []
if verification["verified"]:
work_item.state = STATE_SUCCEEDED
await self._project_success(intent=intent, execution_result=execution_result)
elif any(
issue in {"waiting_manual", "followup_ambiguous", "signal_source_pending_manual"}
for issue in issues
) or str(execution_result.get("status") or "").strip().lower() == "pending_manual":
work_item.state = STATE_WAITING_MANUAL
else:
work_item.state = STATE_FAILED
decision_row.final_outcome = work_item.state
render = self.reply_renderer.render(
work_state=work_item.state,
intent=intent,
decision_record={
"approval_requirement": decision_payload["policy"].get("approval_requirement"),
},
execution_result=execution_result,
)
work_item.reply_template_key = str(render["template_key"])
direct_reply_required = await self._claim_reply_receipt(
work_item_id=int(work_item.id),
source_system=source_system,
source_message_id=synthetic_message_id,
receipt_phase=str(render["receipt_phase"]),
template_key=str(render["template_key"]),
payload=render["payload"],
)
ingest_event.status = "done"
ingest_event.processed_at = _utc_now_naive()
await self._write_processing_log(
source_message_id=synthetic_message_id,
work_item=work_item,
message_text=clean_text,
route=f"ai_pmo.{intent.name}",
operation_code=self._primary_operation_code(intent.name),
operation_result=execution_result,
processing_status="success" if work_item.state in {STATE_SUCCEEDED, STATE_WAITING_MANUAL} else "failed",
duration_ms=int((time.perf_counter() - started) * 1000),
)
await self.db.commit()
return self._build_capture_result(
work_item=work_item,
intent=intent,
execution_result=execution_result,
render=render,
direct_reply_required=direct_reply_required,
status="answered" if intent.name in {INTENT_QUERY, INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE} and work_item.state == STATE_SUCCEEDED else "captured",
classification=(
"question"
if intent.name == INTENT_QUERY
else ("signal" if intent.name == INTENT_SIGNAL_CREATE else "command")
),
)
except Exception as exc:
await self.db.rollback()
logger.warning(
"ai_pmo_kernel_failed",
tenant_id=self.tenant_id,
source_system=source_system,
source_message_id=synthetic_message_id,
error=str(exc),
)
return {"handled": False, "reason": "kernel_exception", "error": str(exc)}
async def _claim_ingress_event(
self,
*,
source_system: str,
source_message_id: str,
payload: dict[str, Any],
) -> tuple[IngestEvent, bool]:
channel = f"{source_system}_message"
existing = (
await self.db.execute(
select(IngestEvent).where(
and_(
IngestEvent.tenant_id == self.tenant_id,
IngestEvent.channel == channel,
IngestEvent.event_key == source_message_id,
)
).limit(1)
)
).scalar_one_or_none()
if existing is not None:
return existing, False
row = IngestEvent(
tenant_id=self.tenant_id,
channel=channel,
event_key=source_message_id,
status="processing",
payload=payload,
retry_count=0,
error_message=None,
processed_at=None,
)
self.db.add(row)
try:
await self.db.flush()
except IntegrityError:
await self.db.rollback()
existing = (
await self.db.execute(
select(IngestEvent).where(
and_(
IngestEvent.tenant_id == self.tenant_id,
IngestEvent.channel == channel,
IngestEvent.event_key == source_message_id,
)
).limit(1)
)
).scalar_one()
return existing, False
return row, True
async def _create_work_item(
self,
*,
ingest_event: IngestEvent,
source_system: str,
source_message_id: str,
owner_user_id: Optional[int],
) -> WorkItem:
row = WorkItem(
tenant_id=self.tenant_id,
ingest_event_id=int(ingest_event.id),
source_system=source_system,
source_message_id=source_message_id,
state=STATE_RECEIVED,
owner_user_id=owner_user_id,
dedupe_key=f"{source_system}:{source_message_id}",
)
self.db.add(row)
await self.db.flush()
return row
async def _build_world_model(
self,
*,
message_text: str,
sender: str,
project_hint_id: Optional[int],
) -> dict[str, Any]:
project = await self._resolve_project(message_text=message_text, project_hint_id=project_hint_id)
project_id = _safe_int(getattr(project, "id", None))
portfolio_summary = await self._build_portfolio_summary(project_id=project_id)
budget = await self._build_budget_summary(project_id=project_id)
capacity = await self._build_capacity_summary(project_id=project_id)
skill_graph = await self._build_skill_graph(project_id=project_id)
charter = await self._build_charter_snapshot(project_id=project_id)
task_summary = await self._build_task_summary(project_id=project_id)
risk_summary = await self._build_risk_summary(project_id=project_id)
domain = await self._build_project_domain_snapshot(project_id=project_id)
dependencies = self._build_dependency_summary(charter=charter, task_summary=task_summary)
recent_signals = await self._build_recent_signal_summary(project_id=project_id)
world_model = {
"sender": sender,
"resolved_project_id": project_id,
"project": self._serialize_project(project) if project is not None else None,
"project_domain": domain,
"portfolio": portfolio_summary,
"budget": budget,
"capacity": capacity,
"member_count": len(capacity),
"skill_graph": skill_graph,
"charter": charter,
"task_summary": task_summary,
"risk_summary": risk_summary,
"dependencies": dependencies,
"recent_signals": recent_signals,
"decision_context_snapshot": {
"captured_at": _utc_now_naive().isoformat(),
"project_id": project_id,
"budget": budget,
"capacity": capacity,
"skill_graph_size": len(skill_graph),
"dependency_pressure": dependencies.get("pressure"),
},
}
self.db.add(
PortfolioSnapshot(
tenant_id=self.tenant_id,
scope_type="tenant",
scope_ref=f"tenant:{self.tenant_id}",
summary=portfolio_summary,
payload=world_model,
)
)
await self.db.flush()
return world_model
async def _resolve_project(self, *, message_text: str, project_hint_id: Optional[int]) -> Optional[Project]:
if project_hint_id is not None:
return (
await self.db.execute(
select(Project).where(
and_(
Project.tenant_id == self.tenant_id,
Project.id == int(project_hint_id),
)
).limit(1)
)
).scalar_one_or_none()
id_match = re.search(r"(?:项目(?:id|ID)?|project(?:id)?)\s*[:#:]?\s*(\d+)", str(message_text or ""))
if id_match:
project_id = int(id_match.group(1))
return (
await self.db.execute(
select(Project).where(
and_(
Project.tenant_id == self.tenant_id,
Project.id == project_id,
)
).limit(1)
)
).scalar_one_or_none()
projects = (
await self.db.execute(
select(Project).where(Project.tenant_id == self.tenant_id).order_by(Project.updated_at.desc(), Project.id.desc()).limit(50)
)
).scalars().all()
normalized_message = re.sub(r"\s+", "", str(message_text or "")).lower()
for project in projects:
for candidate in (project.title, project.name):
normalized_candidate = re.sub(r"\s+", "", str(candidate or "")).lower()
if normalized_candidate and normalized_candidate in normalized_message:
return project
return None
async def _build_portfolio_summary(self, *, project_id: Optional[int]) -> dict[str, Any]:
active_project_count = (
await self.db.execute(
select(func.count(Project.id)).where(
and_(
Project.tenant_id == self.tenant_id,
Project.status.in_(["active", "planning", "todo", "doing"]),
)
)
)
).scalar_one()
return {
"active_project_count": int(active_project_count or 0),
"focus_project_id": project_id,
}
async def _build_budget_summary(self, *, project_id: Optional[int]) -> dict[str, Any]:
stmt = select(Budget).where(Budget.tenant_id == self.tenant_id)
if project_id is not None:
stmt = stmt.where(or_(Budget.project_id == int(project_id), Budget.project_id.is_(None)))
row = (await self.db.execute(stmt.order_by(Budget.project_id.desc().nulls_last(), Budget.id.desc()).limit(1))).scalars().first()
if row is None:
return {"budget_id": None, "usage_ratio": 0.0, "hard_limit_enabled": False}
amount = float(row.budget_amount_usd or 0.0)
usage = float(row.current_usage_usd or 0.0)
ratio = (usage / amount) if amount > 0 else 0.0
return {
"budget_id": int(row.id),
"budget_type": str(row.budget_type or ""),
"period": str(row.period or ""),
"budget_amount_usd": amount,
"current_usage_usd": usage,
"usage_ratio": round(ratio, 4),
"hard_limit_enabled": bool(row.hard_limit_enabled),
}
async def _build_capacity_summary(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
user_ids: list[int] = []
if project_id is not None:
rows = (
await self.db.execute(
select(UserProject.user_id).where(
and_(
UserProject.tenant_id == self.tenant_id,
UserProject.project_id == int(project_id),
)
)
)
).scalars().all()
user_ids = [int(uid) for uid in rows]
if not user_ids and project_id is not None:
owner_id = (
await self.db.execute(
select(Project.owner_id).where(
and_(
Project.tenant_id == self.tenant_id,
Project.id == int(project_id),
)
)
)
).scalar_one_or_none()
if owner_id is not None:
user_ids = [int(owner_id)]
if not user_ids:
return []
rows = (
await self.db.execute(
select(UserWorkload).where(
and_(
UserWorkload.tenant_id == self.tenant_id,
UserWorkload.user_id.in_(user_ids),
)
)
)
).scalars().all()
return [
{
"user_id": int(row.user_id),
"current_assigned_hours": float(row.current_assigned_hours or 0.0),
"max_available_hours_per_week": float(row.max_available_hours_per_week or 0.0),
"workload_ratio": float(row.workload_ratio or 0.0),
}
for row in rows
]
async def _build_skill_graph(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
user_ids: list[int] = []
if project_id is not None:
rows = (
await self.db.execute(
select(UserProject.user_id).where(
and_(
UserProject.tenant_id == self.tenant_id,
UserProject.project_id == int(project_id),
)
)
)
).scalars().all()
user_ids = [int(uid) for uid in rows]
if not user_ids:
return []
rows = (
await self.db.execute(
select(UserSkillBinding).where(
and_(
UserSkillBinding.tenant_id == self.tenant_id,
UserSkillBinding.user_id.in_(user_ids),
)
).limit(50)
)
).scalars().all()
return [
{
"user_id": int(row.user_id),
"skill_tag_id": int(row.skill_tag_id),
"proficiency_level": str(row.proficiency_level or ""),
"quality_score": float(row.quality_score or 0.0),
"efficiency_score": float(row.efficiency_score or 0.0),
}
for row in rows
]
async def _build_project_domain_snapshot(self, *, project_id: Optional[int]) -> Optional[dict[str, Any]]:
if project_id is None:
return None
domain_row = (
await self.db.execute(
select(ProjectDomain)
.join(Project, Project.domain_id == ProjectDomain.id)
.where(
and_(
Project.tenant_id == self.tenant_id,
Project.id == int(project_id),
ProjectDomain.tenant_id == self.tenant_id,
)
)
.limit(1)
)
).scalar_one_or_none()
if domain_row is None:
return None
return {
"id": int(domain_row.id),
"name": str(domain_row.name or ""),
"slug": str(domain_row.slug or ""),
"description": str(domain_row.description or ""),
"owner_id": domain_row.owner_id,
}
async def _build_charter_snapshot(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
if project_id is None:
return []
rows = (
await self.db.execute(
select(TruthNode).where(
and_(
TruthNode.tenant_id == self.tenant_id,
TruthNode.project_id == int(project_id),
TruthNode.node_type == "charter",
)
).order_by(TruthNode.updated_at.desc(), TruthNode.id.desc()).limit(3)
)
).scalars().all()
return [
{
"id": int(row.id),
"title": str(row.title or ""),
"status": str(row.status or ""),
"content": row.content if isinstance(row.content, dict) else {},
}
for row in rows
]
async def _build_task_summary(self, *, project_id: Optional[int]) -> dict[str, Any]:
if project_id is None:
return {"total": 0, "by_status": {}, "overdue_count": 0, "uncovered_skill_tag_ids": [], "blocked_tasks": []}
tasks = (
await self.db.execute(
select(Task).where(and_(Task.tenant_id == self.tenant_id, Task.project_id == int(project_id)))
)
).scalars().all()
by_status: dict[str, int] = {}
required_skill_tag_ids: set[int] = set()
blocked_tasks: list[dict[str, Any]] = []
overdue_count = 0
now = _utc_now_naive()
for row in tasks:
status = str(row.status or "unknown")
by_status[status] = by_status.get(status, 0) + 1
for skill_id in row.required_skill_tag_ids or []:
if skill_id is not None:
required_skill_tag_ids.add(int(skill_id))
if status == "blocked":
blocked_tasks.append({"id": int(row.id), "title": str(row.title or "")[:120]})
if row.due_date and str(row.status or "").lower() not in {"done", "completed", "closed"}:
due_date = row.due_date.replace(tzinfo=None) if row.due_date.tzinfo else row.due_date
if due_date < now:
overdue_count += 1
skill_graph = await self._build_skill_graph(project_id=project_id)
available_skill_ids = {int(item["skill_tag_id"]) for item in skill_graph if item.get("skill_tag_id") is not None}
uncovered_skill_ids = sorted(skill_id for skill_id in required_skill_tag_ids if skill_id not in available_skill_ids)
return {
"total": sum(by_status.values()),
"by_status": by_status,
"overdue_count": overdue_count,
"required_skill_tag_ids": sorted(required_skill_tag_ids),
"uncovered_skill_tag_ids": uncovered_skill_ids,
"blocked_tasks": blocked_tasks[:8],
}
async def _build_recent_signal_summary(self, *, project_id: Optional[int]) -> list[dict[str, Any]]:
if project_id is None:
return []
rows = (
await self.db.execute(
select(Signal).where(
and_(
Signal.tenant_id == self.tenant_id,
or_(
Signal.matched_project_id == int(project_id),
Signal.id.in_(
select(ProjectSignalLink.signal_id).where(
and_(
ProjectSignalLink.tenant_id == self.tenant_id,
ProjectSignalLink.project_id == int(project_id),
)
)
),
),
)
).order_by(Signal.updated_at.desc(), Signal.id.desc()).limit(5)
)
).scalars().all()
return [
{
"id": int(row.id),
"title": str(row.title or ""),
"status": str(row.status or ""),
"signal_type": str(row.signal_type or ""),
}
for row in rows
]
async def _build_risk_summary(self, *, project_id: Optional[int]) -> dict[str, Any]:
stmt = select(func.count(Notification.id)).where(
and_(
Notification.tenant_id == self.tenant_id,
Notification.is_read.is_(False),
Notification.kind.in_(["risk", "alert", "action_required"]),
)
)
if project_id is not None:
stmt = stmt.where(or_(Notification.entity_id == int(project_id), Notification.payload.is_(None), Notification.entity_type == "project"))
open_risk_count = (await self.db.execute(stmt)).scalar_one()
return {"open_risk_count": int(open_risk_count or 0)}
@staticmethod
def _build_dependency_summary(*, charter: list[dict[str, Any]], task_summary: dict[str, Any]) -> dict[str, Any]:
blocked_tasks = task_summary.get("blocked_tasks") if isinstance(task_summary.get("blocked_tasks"), list) else []
overdue_count = int(task_summary.get("overdue_count") or 0)
pressure = "high" if blocked_tasks or overdue_count > 0 else ("medium" if not charter else "low")
return {
"pressure": pressure,
"blocked_tasks": blocked_tasks[:5],
"overdue_count": overdue_count,
"charter_ready": bool(charter),
}
async def _create_decision_record(
self,
*,
work_item: WorkItem,
world_model: dict[str, Any],
decision_payload: dict[str, Any],
) -> DecisionRecord:
strategic = decision_payload["strategic"]
policy = decision_payload["policy"]
row = DecisionRecord(
tenant_id=self.tenant_id,
work_item_id=int(work_item.id),
decision_object_type=str(strategic.get("decision_object_type") or "portfolio"),
decision_object_id=_safe_int(strategic.get("decision_object_id")),
decision_level=str(strategic.get("decision_level") or "portfolio"),
context_snapshot=world_model,
rationale=str(strategic.get("rationale") or ""),
confidence=0.82,
risk_score=float(strategic.get("risk_score") or 0.0),
budget_impact=strategic.get("budget_impact"),
resource_impact=strategic.get("resource_impact"),
simulation_result=decision_payload.get("simulation"),
policy_gate_hit=bool(policy.get("policy_gate_hit")),
policy_gate_code=(str(policy.get("policy_gate_code") or "")[:80] or None),
action_bundle=decision_payload["execution_plan"],
approval_requirement=policy.get("approval_requirement"),
final_outcome=STATE_DECIDED,
)
self.db.add(row)
await self.db.flush()
return row
async def _create_plan_bundle(
self,
*,
work_item_id: int,
plan_type: str,
summary: str,
payload: dict[str, Any],
) -> PlanBundle:
row = PlanBundle(
tenant_id=self.tenant_id,
work_item_id=int(work_item_id),
plan_type=plan_type[:20],
summary=summary[:2000],
payload=payload,
)
self.db.add(row)
await self.db.flush()
return row
async def _claim_reply_receipt(
self,
*,
work_item_id: int,
source_system: str,
source_message_id: str,
receipt_phase: str,
template_key: str,
payload: dict[str, Any],
) -> bool:
row = ReplyReceipt(
tenant_id=self.tenant_id,
work_item_id=int(work_item_id),
source_system=source_system[:32],
source_message_id=source_message_id[:128],
receipt_phase=receipt_phase[:32],
template_key=template_key[:64],
payload=payload,
status="ready",
)
self.db.add(row)
try:
await self.db.flush()
return True
except IntegrityError:
await self.db.rollback()
existing_ingest = (
await self.db.execute(
select(IngestEvent).where(
and_(
IngestEvent.tenant_id == self.tenant_id,
IngestEvent.channel == f"{source_system}_message",
IngestEvent.event_key == source_message_id,
)
).limit(1)
)
).scalar_one()
existing_work_item = (
await self.db.execute(
select(WorkItem).where(
and_(
WorkItem.tenant_id == self.tenant_id,
WorkItem.ingest_event_id == int(existing_ingest.id),
)
).limit(1)
)
).scalar_one_or_none()
if existing_work_item is not None:
self.db.add(existing_work_item)
return False
async def _project_waiting_manual(
self,
*,
work_item: WorkItem,
message_text: str,
render: dict[str, Any],
intent: CanonicalIntent,
) -> None:
await NotificationService(self.db).create_notifications(
tenant_id=self.tenant_id,
user_ids=await NotificationService(self.db)._resolve_recipients(tenant_id=self.tenant_id),
title="AI PMO 待人工确认",
content=str(render.get("text") or message_text)[:4000],
category="decision",
source="ai_pmo",
entity_type="work_item",
entity_id=int(work_item.id),
target_url=f"/inbox?workItemId={int(work_item.id)}",
kind="action_required",
priority="high",
status="pending_manual",
recommended_action="view_detail",
dedupe_key=f"ai-pmo-waiting:{int(work_item.id)}",
payload={
"work_item_id": int(work_item.id),
"intent": intent.name,
"project_id": intent.project_id,
"message_text": message_text[:500],
},
commit=False,
)
async def _project_success(self, *, intent: CanonicalIntent, execution_result: dict[str, Any]) -> None:
if intent.name == INTENT_SIGNAL_CREATE and _safe_int(execution_result.get("signal_id")) is not None:
await NotificationService(self.db).notify_signal_created(
tenant_id=self.tenant_id,
signal_id=int(execution_result["signal_id"]),
signal_content=str(execution_result.get("summary") or execution_result.get("signal_id") or ""),
source="ai_pmo",
target_url=(
f"/projects/{int(execution_result['project_id'])}/board"
if _safe_int(execution_result.get("project_id")) is not None
else None
),
)
async def _execute_followup_intent(
self,
*,
work_item: WorkItem,
intent: CanonicalIntent,
message_text: str,
sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
channel_ref: Optional[str],
source_sender_open_id: Optional[str],
world_model: dict[str, Any],
) -> tuple[dict[str, Any], dict[str, Any]]:
explicit_target_id = self._extract_followup_work_item_id(message_text)
candidates = await self._load_recent_followup_candidates(
current_work_item_id=int(work_item.id),
source_system=source_system,
channel_ref=channel_ref,
sender=sender,
source_sender_open_id=source_sender_open_id,
explicit_target_id=explicit_target_id,
)
actionable = [
(target, ingest)
for target, ingest in candidates
if str(target.state or "") == STATE_WAITING_MANUAL
and str(target.intent or "") in FOLLOWUP_TARGET_INTENTS
]
if explicit_target_id is not None and not actionable:
matched = next((target for target, _ in candidates if int(target.id) == int(explicit_target_id)), None)
if matched is not None:
reply_text = f"工作项#{int(matched.id)} 当前状态为 {matched.state},无需继续处理。"
return {
"status": "succeeded",
"followup_reply": reply_text,
"target_work_item_id": int(matched.id),
"target_state": matched.state,
"target_intent": matched.intent,
"project_id": _safe_int(matched.project_id),
"signal_id": _safe_int(matched.signal_id),
}, {"verified": True, "issues": []}
if not actionable:
reply_text = "当前群最近没有可继续处理的待确认工作项。"
return {
"status": "pending_manual",
"followup_reply": reply_text,
"candidate_work_item_ids": [],
}, {"verified": False, "issues": ["waiting_manual"]}
if len(actionable) > 1:
reply_text = self._build_followup_candidate_clarification([target for target, _ in actionable])
return {
"status": "pending_manual",
"followup_reply": reply_text,
"candidate_work_item_ids": [int(target.id) for target, _ in actionable[:5]],
}, {"verified": False, "issues": ["waiting_manual", "followup_ambiguous"]}
target_work_item, target_ingest = actionable[0]
return await self._resume_followup_target(
target_work_item=target_work_item,
target_ingest=target_ingest,
followup_intent=intent,
followup_sender=sender,
operator_user_id=operator_user_id,
source_system=source_system,
source_message_id=source_message_id,
world_model=world_model,
)
async def _load_recent_followup_candidates(
self,
*,
current_work_item_id: int,
source_system: str,
channel_ref: Optional[str],
sender: str,
source_sender_open_id: Optional[str],
explicit_target_id: Optional[int],
limit: int = 12,
) -> list[tuple[WorkItem, IngestEvent]]:
rows = (
await self.db.execute(
select(WorkItem, IngestEvent)
.join(IngestEvent, IngestEvent.id == WorkItem.ingest_event_id)
.where(
and_(
WorkItem.tenant_id == self.tenant_id,
WorkItem.id != int(current_work_item_id),
)
)
.order_by(WorkItem.id.desc())
.limit(max(limit * 3, 18))
)
).all()
selected: list[tuple[WorkItem, IngestEvent]] = []
normalized_channel = str(channel_ref or "").strip()
normalized_sender = str(sender or "").strip()
normalized_open_id = str(source_sender_open_id or "").strip()
for target_work_item, ingest in rows:
if explicit_target_id is not None and int(target_work_item.id) == int(explicit_target_id):
selected.append((target_work_item, ingest))
continue
payload = ingest.payload if isinstance(ingest.payload, dict) else {}
same_channel = normalized_channel and str(payload.get("channel_ref") or "").strip() == normalized_channel
same_open_id = normalized_open_id and str(payload.get("source_sender_open_id") or "").strip() == normalized_open_id
same_sender = normalized_sender and str(payload.get("sender") or "").strip() == normalized_sender
if not (same_channel or same_open_id or same_sender):
continue
selected.append((target_work_item, ingest))
if len(selected) >= limit:
break
return selected
async def _resume_followup_target(
self,
*,
target_work_item: WorkItem,
target_ingest: IngestEvent,
followup_intent: CanonicalIntent,
followup_sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
world_model: dict[str, Any],
) -> tuple[dict[str, Any], dict[str, Any]]:
payload = target_ingest.payload if isinstance(target_ingest.payload, dict) else {}
original_message = str(payload.get("message_text") or "").strip()
override_project_id = _safe_int(world_model.get("resolved_project_id"))
target_project_id = _safe_int(target_work_item.project_id) or override_project_id
rebuilt_intent = self.intent_resolver.resolve(text=original_message, project_id=target_project_id)
target_intent_name = str(target_work_item.intent or rebuilt_intent.name or "").strip()
target_intent = CanonicalIntent(
name=target_intent_name or rebuilt_intent.name,
confidence=float(rebuilt_intent.confidence or 0.88),
project_id=target_project_id,
signal_title=rebuilt_intent.signal_title,
signal_summary=rebuilt_intent.signal_summary,
metadata=dict(rebuilt_intent.metadata),
)
if target_intent.name not in FOLLOWUP_TARGET_INTENTS:
reply_text = f"工作项#{int(target_work_item.id)} 当前类型为 {target_intent.name or 'unknown'},暂不支持继续处理。"
return {
"status": "failed_safe",
"followup_reply": reply_text,
"target_work_item_id": int(target_work_item.id),
"target_state": target_work_item.state,
"target_intent": target_intent.name,
}, {"verified": False, "issues": ["unsupported_intent"]}
if target_intent.name in {INTENT_SIGNAL_CREATE, INTENT_SIGNAL_SOURCE_ONBOARD} and target_project_id is None:
reply_text = "未执行:目标工作项仍缺少项目归属,请补充项目 ID 后再继续处理。"
return {
"status": "pending_manual",
"followup_reply": reply_text,
"target_work_item_id": int(target_work_item.id),
"target_state": STATE_WAITING_MANUAL,
"target_intent": target_intent.name,
}, {"verified": False, "issues": ["waiting_manual"]}
target_work_item.project_id = target_project_id
target_work_item.state = STATE_EXECUTING
target_execution_result = await self.executor.execute(
intent=target_intent,
message_text=original_message,
sender=followup_sender,
operator_user_id=operator_user_id,
source_system=source_system,
source_message_id=f"{source_message_id}:resume:{int(target_work_item.id)}",
)
target_work_item.execution_result = target_execution_result
if _safe_int(target_execution_result.get("signal_id")) is not None:
target_work_item.signal_id = _safe_int(target_execution_result.get("signal_id"))
target_verification = await self._verify_execution(
intent=target_intent,
execution_result=target_execution_result,
world_model=target_work_item.context_snapshot if isinstance(target_work_item.context_snapshot, dict) else world_model,
)
target_work_item.review_summary = await self.review_service.review_work_item(
work_item=target_work_item,
intent_name=target_intent.name,
execution_result=target_execution_result,
world_model=target_work_item.context_snapshot if isinstance(target_work_item.context_snapshot, dict) else world_model,
verification=target_verification,
simulation={
"followup_resumed": True,
"target_work_item_id": int(target_work_item.id),
"followup_intent": followup_intent.name,
},
)
await self._maybe_emit_risk_cadence_snapshot(
project_id=_safe_int(target_work_item.project_id),
review_summary=target_work_item.review_summary,
)
if target_verification.get("verified"):
target_work_item.state = STATE_SUCCEEDED
await self._project_success(intent=target_intent, execution_result=target_execution_result)
else:
target_issues = target_verification.get("issues") if isinstance(target_verification.get("issues"), list) else []
if any(issue in {"pending_manual", "signal_source_pending_manual"} for issue in target_issues):
target_work_item.state = STATE_WAITING_MANUAL
else:
target_work_item.state = STATE_FAILED
target_decision = (
await self.db.execute(
select(DecisionRecord)
.where(
and_(
DecisionRecord.tenant_id == self.tenant_id,
DecisionRecord.work_item_id == int(target_work_item.id),
)
)
.order_by(DecisionRecord.id.desc())
.limit(1)
)
).scalar_one_or_none()
if target_decision is not None:
target_decision.execution_result = target_execution_result
target_decision.final_outcome = target_work_item.state
target_render = self.reply_renderer.render(
work_state=target_work_item.state,
intent=target_intent,
decision_record={},
execution_result=target_execution_result,
)
reply_text = str(target_render.get("text") or "").strip()
if target_work_item.state == STATE_SUCCEEDED:
reply_text = f"已继续处理:{reply_text}"
elif not reply_text:
reply_text = "未执行:继续处理后仍未形成可验证终态。"
project_id = _safe_int(target_execution_result.get("project_id")) or _safe_int(target_work_item.project_id)
signal_id = _safe_int(target_execution_result.get("signal_id")) or _safe_int(target_work_item.signal_id)
status = (
"succeeded"
if target_work_item.state == STATE_SUCCEEDED
else ("pending_manual" if target_work_item.state == STATE_WAITING_MANUAL else "failed_safe")
)
current_verification = {"verified": target_work_item.state == STATE_SUCCEEDED, "issues": []}
if target_work_item.state == STATE_WAITING_MANUAL:
current_verification = {"verified": False, "issues": ["waiting_manual"]}
elif target_work_item.state == STATE_FAILED:
current_verification = {"verified": False, "issues": ["executor_failed"]}
return {
"status": status,
"followup_reply": reply_text,
"target_work_item_id": int(target_work_item.id),
"target_state": target_work_item.state,
"target_intent": target_intent.name,
"project_id": project_id,
"signal_id": signal_id,
"auto_tasks": target_execution_result.get("auto_tasks") if isinstance(target_execution_result.get("auto_tasks"), list) else [],
}, current_verification
async def _verify_execution(
self,
*,
intent: CanonicalIntent,
execution_result: dict[str, Any],
world_model: dict[str, Any],
) -> dict[str, Any]:
if str(execution_result.get("status") or "").lower() == "failed_safe":
return {"verified": False, "issues": ["executor_failed"]}
if str(execution_result.get("status") or "").lower() == "pending_manual":
return {"verified": False, "issues": ["pending_manual"]}
if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
return {
"verified": bool(
_safe_int(execution_result.get("target_work_item_id")) is not None
and str(execution_result.get("target_state") or "").strip() == STATE_SUCCEEDED
),
"issues": [] if str(execution_result.get("target_state") or "").strip() == STATE_SUCCEEDED else ["waiting_manual"],
}
if intent.name == INTENT_QUERY:
return {"verified": bool(str(execution_result.get("answer") or "").strip()), "issues": []}
if intent.name == INTENT_PROJECT_CREATE:
return {"verified": _safe_int(execution_result.get("project_id")) is not None, "issues": []}
if intent.name in {INTENT_SIGNAL_CREATE, INTENT_SIGNAL_SOURCE_ONBOARD}:
signal_id = _safe_int(execution_result.get("signal_id"))
if signal_id is None:
return {"verified": False, "issues": ["signal_missing"]}
if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD:
workflow = execution_result.get("signal_source_workflow") if isinstance(execution_result.get("signal_source_workflow"), dict) else {}
if str(workflow.get("status") or "").lower() != "completed":
return {"verified": False, "issues": ["signal_source_pending_manual"]}
if intent.project_id is None:
return {"verified": True, "issues": []}
link_exists = (
await self.db.execute(
select(ProjectSignalLink.id).where(
and_(
ProjectSignalLink.tenant_id == self.tenant_id,
ProjectSignalLink.project_id == int(intent.project_id),
ProjectSignalLink.signal_id == int(signal_id),
)
).limit(1)
)
).scalar_one_or_none()
return {"verified": link_exists is not None, "issues": [] if link_exists is not None else ["project_signal_link_missing"]}
return {"verified": False, "issues": ["unsupported_intent"]}
@staticmethod
def _build_review_summary(
*,
intent: CanonicalIntent,
verification: dict[str, Any],
world_model: dict[str, Any],
) -> dict[str, Any]:
return {
"verified": bool(verification.get("verified")),
"issues": verification.get("issues") if isinstance(verification.get("issues"), list) else [],
"next_step": "进入下一轮调度" if verification.get("verified") else "等待人工复核",
"risk_score_after": float((world_model.get("risk_summary") or {}).get("open_risk_count") or 0.0),
"intent": intent.name,
}
async def _maybe_emit_risk_cadence_snapshot(
self,
*,
project_id: Optional[int],
review_summary: dict[str, Any],
) -> None:
if not isinstance(review_summary, dict):
return
risk_after = float(review_summary.get("risk_score_after") or 0.0)
divergences = review_summary.get("divergences") if isinstance(review_summary.get("divergences"), list) else []
if risk_after < 0.7 and not divergences:
return
await self.cadence_service.generate_snapshot(
cadence_type="risk_review",
project_id=int(project_id) if project_id is not None else None,
)
async def _resolve_operator_user(self, *, sender: str, sender_open_id: Optional[str]) -> Optional[User]:
cleaned_sender = str(sender or "").strip()
if cleaned_sender:
user = (
await self.db.execute(
select(User).where(
and_(
User.tenant_id == self.tenant_id,
User.is_active.is_(True),
or_(User.name == cleaned_sender, User.username == cleaned_sender),
)
).limit(1)
)
).scalar_one_or_none()
if user is not None:
return user
preferred = (
await self.db.execute(
select(User).where(
and_(
User.tenant_id == self.tenant_id,
User.is_active.is_(True),
User.status == "active",
User.role.in_(["BOSS", "ADMIN", "PM"]),
)
).order_by(User.id.asc()).limit(1)
)
).scalar_one_or_none()
if preferred is not None:
return preferred
return (
await self.db.execute(
select(User).where(
and_(
User.tenant_id == self.tenant_id,
User.is_active.is_(True),
)
).order_by(User.id.asc()).limit(1)
)
).scalar_one_or_none()
async def _write_processing_log(
self,
*,
source_message_id: str,
work_item: WorkItem,
message_text: str,
route: str,
operation_code: Optional[str],
operation_result: Any,
processing_status: str,
duration_ms: int,
) -> None:
self.db.add(
AIProcessingLog(
tenant_id=self.tenant_id,
feishu_message_id=source_message_id if work_item.source_system == "feishu" else None,
feishu_item_id=None,
raw_message=message_text[:8000],
ai_raw_output=json.dumps(
{
"work_item_id": int(work_item.id),
"intent": work_item.intent,
"state": work_item.state,
"reply_template_key": work_item.reply_template_key,
},
ensure_ascii=False,
),
project_score=None,
signal_confidence=None,
level="ai_pmo",
normalized_text=message_text[:8000],
route=route[:80],
intent=(str(work_item.intent or "")[:40] or None),
executor="ai_pmo_kernel",
operation_code=(operation_code[:80] if operation_code else None),
operation_result=operation_result if isinstance(operation_result, dict) else {"result": operation_result},
reply_source=(str(work_item.reply_template_key or "")[:40] or None),
routing_decision=route[:64],
routing_reason=f"AI PMO kernel route={route}",
reasoning_bullets=[
f"work_item_id={int(work_item.id)}",
f"state={work_item.state}",
f"intent={work_item.intent}",
],
processing_status=processing_status[:20],
error_message=None,
duration_ms=int(duration_ms),
)
)
await self.db.flush()
def _build_capture_result(
self,
*,
work_item: WorkItem,
intent: CanonicalIntent,
execution_result: dict[str, Any],
render: dict[str, Any],
direct_reply_required: bool,
status: str,
classification: str,
) -> dict[str, Any]:
linked_project_ids = [int(work_item.project_id)] if work_item.project_id is not None else []
result = {
"handled": True,
"status": status,
"classification": classification,
"linked_project_ids": linked_project_ids,
"signal_id": _safe_int(execution_result.get("signal_id")) or _safe_int(work_item.signal_id),
"created_project_id": _safe_int(execution_result.get("project_id")) if intent.name == INTENT_PROJECT_CREATE else None,
"auto_tasks": execution_result.get("auto_tasks") if isinstance(execution_result.get("auto_tasks"), list) else execution_result.get("tasks", []),
"ai_trace_id": f"ai-pmo-{int(work_item.id)}",
"results": [
{
"work_item_id": int(work_item.id),
"state": work_item.state,
"intent": intent.name,
"route_status": "queued_for_decision" if work_item.state == STATE_WAITING_MANUAL else "auto_done",
}
],
"intent": intent.name,
"direct_reply_required": bool(direct_reply_required),
"direct_reply_text": str(render.get("text") or "").strip(),
"reply_template_key": str(render.get("template_key") or "").strip(),
"work_item_id": int(work_item.id),
"decision_level": work_item.decision_level,
"review_summary": work_item.review_summary,
}
if intent.name == INTENT_QUERY and isinstance(execution_result.get("rag_result"), dict):
result["rag_result"] = execution_result["rag_result"]
if intent.name == INTENT_PROJECT_CREATE:
raw_payload = execution_result.get("raw_payload") if isinstance(execution_result.get("raw_payload"), dict) else {}
result["project"] = raw_payload.get("project")
if intent.name == INTENT_SIGNAL_SOURCE_ONBOARD:
result["signal_source_workflow"] = execution_result.get("signal_source_workflow")
result["signal_source_orchestration"] = execution_result.get("signal_source_orchestration")
result["ai_execution"] = execution_result.get("signal_source_orchestration")
return result
@staticmethod
def _primary_operation_code(intent_name: str) -> Optional[str]:
if intent_name == INTENT_QUERY:
return "query.answer"
if intent_name == INTENT_PROJECT_CREATE:
return "project.create"
if intent_name == INTENT_SIGNAL_CREATE:
return "signal.enter"
if intent_name == INTENT_SIGNAL_SOURCE_ONBOARD:
return "signal_source.onboard"
if intent_name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}:
return "work_item.resume"
return None
@staticmethod
def _extract_followup_work_item_id(message: str) -> Optional[int]:
match = FOLLOWUP_WORK_ITEM_ID_RE.search(str(message or ""))
return int(match.group(1)) if match else None
@staticmethod
def _build_followup_candidate_clarification(items: list[WorkItem]) -> str:
if not items:
return "当前没有可继续处理的待确认工作项。"
labels = [f"工作项#{int(item.id)}({str(item.intent or '').strip() or 'unknown'})" for item in items[:3]]
return f"当前存在多个待确认工作项:{'、'.join(labels)}。请补充工作项 ID 后再继续处理。"
@staticmethod
def _build_source_message_id(
*,
source_system: str,
source_message_id: Optional[str],
channel_ref: Optional[str],
sender: str,
message_text: str,
) -> str:
explicit = str(source_message_id or "").strip()
if explicit:
return explicit[:128]
basis = f"{source_system}|{channel_ref or ''}|{sender}|{message_text}"
digest = hashlib.sha1(basis.encode("utf-8"), usedforsecurity=False).hexdigest()
return f"{source_system}-{digest[:32]}"
@staticmethod
def _normalize_text(text: str) -> str:
cleaned = re.sub(r"</?at[^>]*>", " ", str(text or ""), flags=re.IGNORECASE)
cleaned = re.sub(r"^@PMFlow\s*", "", cleaned, flags=re.IGNORECASE)
cleaned = re.sub(r"\s+", " ", cleaned).strip()
return cleaned[:4000]
@staticmethod
def _build_transport_mode_reply(message_text: str) -> Optional[str]:
normalized = re.sub(r"\s+", "", str(message_text or "")).lower()
has_long_connection = "长连接" in normalized or "longconnection" in normalized
has_webhook = "webhook" in normalized
if not (has_long_connection and has_webhook):
return None
if not any(token in normalized for token in TRANSPORT_MODE_CORRECTION_HINTS):
return None
return "已按长连接模式处理飞书接入,当前不会走 webhook;这条说明不会再被当成信号源类型变更。"
@staticmethod
def _serialize_project(project: Optional[Project]) -> Optional[dict[str, Any]]:
if project is None:
return None
return {
"id": int(project.id),
"name": str(project.name or ""),
"title": str(project.title or project.name or ""),
"description": str(project.description or ""),
"status": str(project.status or ""),
"owner_id": _safe_int(project.owner_id),
}