"""AI PMO kernel followup verification mixin.""" from __future__ import annotations from . import ai_pmo_kernel_common as _common for _name, _value in vars(_common).items(): if not _name.startswith("__"): globals()[_name] = _value class KernelFollowupVerificationMixin: async def _verify_execution( self, *, intent: CanonicalIntent, execution_result: dict[str, Any], world_model: dict[str, Any], ) -> dict[str, Any]: normalized_status = str(execution_result.get("status") or "").strip().lower() if normalized_status == "failed_safe": return {"verified": False, "issues": ["executor_failed"]} if normalized_status in {"pending_manual", "pending_confirmation"}: return {"verified": False, "issues": ["pending_manual"]} if normalized_status == "cancelled": return {"verified": True, "issues": []} 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_ids = _normalize_signal_id_list(execution_result.get("signal_ids")) signal_id = _safe_int(execution_result.get("signal_id")) if signal_id is not None and signal_id not in signal_ids: signal_ids = [signal_id, *signal_ids] if not signal_ids: 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": []} linked_signal_ids = ( await self.db.execute( select(ProjectSignalLink.signal_id).where( and_( ProjectSignalLink.tenant_id == self.tenant_id, ProjectSignalLink.project_id == int(intent.project_id), ProjectSignalLink.signal_id.in_(signal_ids), ) ) ) ).scalars().all() linked_set = {int(item) for item in linked_signal_ids if _safe_int(item) is not None} missing = [sid for sid in signal_ids if sid not in linked_set] return { "verified": not missing, "issues": [] if not missing else ["project_signal_link_missing"], } return {"verified": False, "issues": ["unsupported_intent"]} @staticmethod def _should_bypass_policy_gate(*, intent: CanonicalIntent, workflow_route: WorkflowRoute) -> bool: if intent.name in {INTENT_FOLLOWUP_CONFIRM, INTENT_FOLLOWUP_CONTINUE}: return True if workflow_route.family == ROUTE_FAMILY_PROJECT_SIGNAL and workflow_route.action in { ROUTE_ACTION_DISCOVER, ROUTE_ACTION_COMMIT, ROUTE_ACTION_CANCEL, ROUTE_ACTION_REFRESH, }: return True if workflow_route.family == ROUTE_FAMILY_SIGNAL_SOURCE and workflow_route.action == ROUTE_ACTION_ONBOARD: return True return False @classmethod def _normalize_route_from_stored_intent(cls, value: Any) -> str: return RouteResolver.normalize_route_key(str(value or "")) @classmethod def _stored_intent_family(cls, value: Any) -> str: route_key = cls._normalize_route_from_stored_intent(value) family, _ = RouteResolver._split_route_key(route_key) return family @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_feishu_rag_conversation_key( *, source_chat_id: Optional[str], source_sender_open_id: Optional[str], sender: Optional[str], ) -> str: chat = str(source_chat_id or "").strip() if not chat: return "" actor = str(source_sender_open_id or "").strip() if not actor: actor = str(sender or "").strip().lower() actor = re.sub(r"\s+", "", actor) actor = actor[:96] if actor else "unknown" return f"feishu:{chat}:{actor}"