PMFlow/backend/app/services/ai_pmo_workflow_runtime.py

242 lines
7.4 KiB
Python

"""AI PMO workflow handlers and registry."""
from __future__ import annotations
from typing import Optional, TYPE_CHECKING
from ..models import ConversationSession, IngestEvent, Project, WorkItem
from ..schemas.world_model import WorldModel
from .ai_pmo_intent_routing import (
INTENT_FOLLOWUP_CONTINUE,
ROUTE_ACTION_DISCOVER,
ROUTE_FAMILY_FOLLOWUP,
ROUTE_FAMILY_PROJECT_CREATE,
ROUTE_FAMILY_PROJECT_SIGNAL,
ROUTE_FAMILY_QUERY,
ROUTE_FAMILY_SIGNAL_SOURCE,
CanonicalIntent,
WorkflowRoute,
_safe_int,
)
if TYPE_CHECKING:
from .ai_pmo_kernel_service import AIPMOKernelService
class ContextAssembler:
"""拼装 world model 和工作流上下文,不承担执行逻辑。"""
def __init__(self, kernel: "AIPMOKernelService") -> None:
self.kernel = kernel
async def assemble_world_model(
self,
*,
sender: str,
message_text: str,
project_hint_id: Optional[int],
session: Optional[ConversationSession],
) -> tuple[Optional[Project], WorldModel]:
query_tables: set[str] = set()
project = await self.kernel._resolve_project(
message_text=message_text,
project_hint_id=project_hint_id,
query_tables=query_tables,
session=session,
)
world_model = await self.kernel.build_world_model(
tenant_id=self.kernel.tenant_id,
project_id=_safe_int(getattr(project, "id", None)),
sender=sender,
project=project,
pre_queried_tables=query_tables,
)
return project, world_model
class WorkflowHandler:
"""工作流处理器基类。"""
def __init__(self, kernel: "AIPMOKernelService") -> None:
self.kernel = kernel
async def start(
self,
*,
route: WorkflowRoute,
intent: CanonicalIntent,
message_text: str,
sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
channel_ref: Optional[str],
) -> dict[str, object]:
raise NotImplementedError
async def resume(
self,
*,
route: WorkflowRoute,
target_work_item: WorkItem,
target_ingest: IngestEvent,
followup_sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
message_text: str,
world_model: dict[str, object],
) -> tuple[dict[str, object], dict[str, object]]:
return await self.kernel._resume_followup_target(
target_work_item=target_work_item,
target_ingest=target_ingest,
followup_intent=CanonicalIntent(
name=INTENT_FOLLOWUP_CONTINUE,
confidence=0.9,
project_id=_safe_int(target_work_item.project_id),
signal_title="",
signal_summary="",
metadata={
"workflow_route": route.key,
"workflow_family": route.family,
"workflow_action": route.action,
},
),
followup_sender=followup_sender,
operator_user_id=operator_user_id,
source_system=source_system,
source_message_id=source_message_id,
world_model=world_model,
message_text=message_text,
workflow_route=route,
)
class QueryWorkflowHandler(WorkflowHandler):
async def start(
self,
*,
route: WorkflowRoute,
intent: CanonicalIntent,
message_text: str,
sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
channel_ref: Optional[str],
) -> dict[str, object]:
_ = route
return await self.kernel.executor.execute(
intent=intent,
message_text=message_text,
sender=sender,
operator_user_id=operator_user_id,
source_system=source_system,
source_message_id=source_message_id,
channel_ref=channel_ref,
)
class ProjectCreateWorkflowHandler(WorkflowHandler):
async def start(
self,
*,
route: WorkflowRoute,
intent: CanonicalIntent,
message_text: str,
sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
channel_ref: Optional[str],
) -> dict[str, object]:
_ = route
return await self.kernel.executor.execute(
intent=intent,
message_text=message_text,
sender=sender,
operator_user_id=operator_user_id,
source_system=source_system,
source_message_id=source_message_id,
channel_ref=channel_ref,
)
class SignalSourceWorkflowHandler(WorkflowHandler):
async def start(
self,
*,
route: WorkflowRoute,
intent: CanonicalIntent,
message_text: str,
sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
channel_ref: Optional[str],
) -> dict[str, object]:
_ = route
return await self.kernel.executor.execute(
intent=intent,
message_text=message_text,
sender=sender,
operator_user_id=operator_user_id,
source_system=source_system,
source_message_id=source_message_id,
channel_ref=channel_ref,
)
class ProjectSignalWorkflowHandler(WorkflowHandler):
async def start(
self,
*,
route: WorkflowRoute,
intent: CanonicalIntent,
message_text: str,
sender: str,
operator_user_id: Optional[int],
source_system: str,
source_message_id: str,
channel_ref: Optional[str],
) -> dict[str, object]:
_ = sender
_ = operator_user_id
if route.action == ROUTE_ACTION_DISCOVER:
return await self.kernel.executor.discover_project_signal_candidates(
intent=intent,
message_text=message_text,
source_system=source_system,
source_message_id=source_message_id,
channel_ref=channel_ref,
)
return await self.kernel.executor.execute(
intent=intent,
message_text=message_text,
sender=sender,
operator_user_id=operator_user_id,
source_system=source_system,
source_message_id=source_message_id,
channel_ref=channel_ref,
)
class WorkflowRegistry:
"""按 family.action 路由处理器。"""
def __init__(self, kernel: "AIPMOKernelService") -> None:
self.kernel = kernel
self._handlers: dict[str, WorkflowHandler] = {
ROUTE_FAMILY_QUERY: QueryWorkflowHandler(kernel),
ROUTE_FAMILY_PROJECT_CREATE: ProjectCreateWorkflowHandler(kernel),
ROUTE_FAMILY_PROJECT_SIGNAL: ProjectSignalWorkflowHandler(kernel),
ROUTE_FAMILY_SIGNAL_SOURCE: SignalSourceWorkflowHandler(kernel),
ROUTE_FAMILY_FOLLOWUP: WorkflowHandler(kernel),
}
def get_handler(self, *, route: WorkflowRoute) -> WorkflowHandler:
handler = self._handlers.get(route.family)
if handler is None:
return self._handlers[ROUTE_FAMILY_QUERY]
return handler