ai-robot-core/ai-service/app/services/flow/flow_service.py

218 lines
6.3 KiB
Python

"""
Script Flow Service for AI Service.
[AC-AISVC-71, AC-AISVC-72, AC-AISVC-73] Flow definition CRUD operations.
"""
import logging
import uuid
from collections.abc import Sequence
from datetime import datetime
from typing import Any
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import col
from app.models.entities import (
ScriptFlow,
ScriptFlowCreate,
ScriptFlowUpdate,
)
logger = logging.getLogger(__name__)
class ScriptFlowService:
"""
[AC-AISVC-71~AC-AISVC-73] Service for managing script flow definitions.
Features:
- Flow CRUD with tenant isolation
- Step validation
- Linked intent rule count tracking
"""
def __init__(self, session: AsyncSession):
self._session = session
async def create_flow(
self,
tenant_id: str,
create_data: ScriptFlowCreate,
) -> ScriptFlow:
"""
[AC-AISVC-71] Create a new script flow with steps.
"""
self._validate_steps(create_data.steps)
flow = ScriptFlow(
tenant_id=tenant_id,
name=create_data.name,
description=create_data.description,
steps=create_data.steps,
is_enabled=create_data.is_enabled,
)
self._session.add(flow)
await self._session.flush()
logger.info(
f"[AC-AISVC-71] Created script flow: tenant={tenant_id}, "
f"id={flow.id}, name={flow.name}, steps={len(flow.steps)}"
)
return flow
async def list_flows(
self,
tenant_id: str,
is_enabled: bool | None = None,
) -> Sequence[ScriptFlow]:
"""
[AC-AISVC-72] List flows for a tenant, optionally filtered by enabled status.
"""
stmt = select(ScriptFlow).where(
ScriptFlow.tenant_id == tenant_id
)
if is_enabled is not None:
stmt = stmt.where(ScriptFlow.is_enabled == is_enabled)
stmt = stmt.order_by(col(ScriptFlow.created_at).desc())
result = await self._session.execute(stmt)
return result.scalars().all()
async def get_flow(
self,
tenant_id: str,
flow_id: uuid.UUID,
) -> ScriptFlow | None:
"""
Get flow by ID with tenant isolation.
"""
stmt = select(ScriptFlow).where(
ScriptFlow.tenant_id == tenant_id,
ScriptFlow.id == flow_id,
)
result = await self._session.execute(stmt)
return result.scalar_one_or_none()
async def get_flow_detail(
self,
tenant_id: str,
flow_id: uuid.UUID,
) -> dict[str, Any] | None:
"""
[AC-AISVC-73] Get flow detail with complete step definitions.
"""
flow = await self.get_flow(tenant_id, flow_id)
if not flow:
return None
linked_rule_count = await self._get_linked_rule_count(tenant_id, flow_id)
return {
"id": str(flow.id),
"name": flow.name,
"description": flow.description,
"steps": flow.steps,
"is_enabled": flow.is_enabled,
"step_count": len(flow.steps),
"linked_rule_count": linked_rule_count,
"created_at": flow.created_at.isoformat(),
"updated_at": flow.updated_at.isoformat(),
}
async def update_flow(
self,
tenant_id: str,
flow_id: uuid.UUID,
update_data: ScriptFlowUpdate,
) -> ScriptFlow | None:
"""
[AC-AISVC-73] Update flow definition.
"""
flow = await self.get_flow(tenant_id, flow_id)
if not flow:
return None
if update_data.name is not None:
flow.name = update_data.name
if update_data.description is not None:
flow.description = update_data.description
if update_data.steps is not None:
self._validate_steps(update_data.steps)
flow.steps = update_data.steps
if update_data.is_enabled is not None:
flow.is_enabled = update_data.is_enabled
flow.updated_at = datetime.utcnow()
await self._session.flush()
logger.info(
f"[AC-AISVC-73] Updated script flow: tenant={tenant_id}, id={flow_id}"
)
return flow
async def delete_flow(
self,
tenant_id: str,
flow_id: uuid.UUID,
) -> bool:
"""Delete a flow definition."""
flow = await self.get_flow(tenant_id, flow_id)
if not flow:
return False
await self._session.delete(flow)
await self._session.flush()
logger.info(
f"Deleted script flow: tenant={tenant_id}, id={flow_id}"
)
return True
async def get_step_count(
self,
tenant_id: str,
flow_id: uuid.UUID,
) -> int:
"""Get the number of steps in a flow."""
flow = await self.get_flow(tenant_id, flow_id)
return len(flow.steps) if flow else 0
async def _get_linked_rule_count(
self,
tenant_id: str,
flow_id: uuid.UUID,
) -> int:
"""Get count of intent rules linked to this flow."""
from app.models.entities import IntentRule
stmt = select(func.count()).select_from(IntentRule).where(
IntentRule.tenant_id == tenant_id,
IntentRule.flow_id == flow_id,
)
result = await self._session.execute(stmt)
return result.scalar() or 0
def _validate_steps(self, steps: list[dict[str, Any]]) -> None:
"""Validate step definitions."""
if not steps:
raise ValueError("Flow must have at least one step")
step_nos = set()
for step in steps:
step_no = step.get("step_no")
if step_no is None:
raise ValueError("Each step must have a step_no")
if step_no in step_nos:
raise ValueError(f"Duplicate step_no: {step_no}")
step_nos.add(step_no)
if not step.get("content"):
raise ValueError(f"Step {step_no} must have content")
next_conditions = step.get("next_conditions", [])
for cond in next_conditions:
if cond.get("goto_step") is None:
raise ValueError(f"next_condition in step {step_no} must have goto_step")