124 lines
5.6 KiB
Python
124 lines
5.6 KiB
Python
"""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}"
|