PMFlow/backend/app/services/ai_pmo_kernel_mixins_follow...

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}"