ai-robot-core/ai-service/app/api/admin/script_flows.py

203 lines
6.0 KiB
Python

"""
Script Flow Management API.
[AC-AISVC-71, AC-AISVC-72, AC-AISVC-73] Script flow CRUD endpoints.
[AC-AISVC-101, AC-AISVC-102] Flow simulation endpoints.
"""
import logging
import uuid
from typing import Any
from fastapi import APIRouter, Depends, Header, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_session
from app.models.entities import ScriptFlowCreate, ScriptFlowUpdate
from app.services.flow.flow_service import ScriptFlowService
from app.services.flow.tester import ScriptFlowTester
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin/script-flows", tags=["Script Flows"])
def get_tenant_id(x_tenant_id: str = Header(..., alias="X-Tenant-Id")) -> str:
"""Extract tenant ID from header."""
if not x_tenant_id:
raise HTTPException(status_code=400, detail="X-Tenant-Id header is required")
return x_tenant_id
@router.get("")
async def list_flows(
tenant_id: str = Depends(get_tenant_id),
is_enabled: bool | None = None,
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-72] List all script flows for a tenant.
"""
logger.info(f"[AC-AISVC-72] Listing script flows for tenant={tenant_id}, is_enabled={is_enabled}")
service = ScriptFlowService(session)
flows = await service.list_flows(tenant_id, is_enabled)
data = []
for f in flows:
linked_rule_count = await service._get_linked_rule_count(tenant_id, f.id)
data.append({
"id": str(f.id),
"name": f.name,
"description": f.description,
"step_count": len(f.steps),
"is_enabled": f.is_enabled,
"linked_rule_count": linked_rule_count,
"created_at": f.created_at.isoformat(),
"updated_at": f.updated_at.isoformat(),
})
return {"data": data}
@router.post("", status_code=201)
async def create_flow(
body: ScriptFlowCreate,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-71] Create a new script flow.
"""
logger.info(f"[AC-AISVC-71] Creating script flow for tenant={tenant_id}, name={body.name}")
service = ScriptFlowService(session)
try:
flow = await service.create_flow(tenant_id, body)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return {
"id": str(flow.id),
"name": flow.name,
"description": flow.description,
"step_count": len(flow.steps),
"is_enabled": flow.is_enabled,
"created_at": flow.created_at.isoformat(),
"updated_at": flow.updated_at.isoformat(),
}
@router.get("/{flow_id}")
async def get_flow_detail(
flow_id: uuid.UUID,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-73] Get script flow detail with complete step definitions.
"""
logger.info(f"[AC-AISVC-73] Getting flow detail for tenant={tenant_id}, id={flow_id}")
service = ScriptFlowService(session)
detail = await service.get_flow_detail(tenant_id, flow_id)
if not detail:
raise HTTPException(status_code=404, detail="Flow not found")
return detail
@router.put("/{flow_id}")
async def update_flow(
flow_id: uuid.UUID,
body: ScriptFlowUpdate,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-73] Update script flow definition.
"""
logger.info(f"[AC-AISVC-73] Updating flow for tenant={tenant_id}, id={flow_id}")
service = ScriptFlowService(session)
try:
flow = await service.update_flow(tenant_id, flow_id, body)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
if not flow:
raise HTTPException(status_code=404, detail="Flow not found")
return {
"id": str(flow.id),
"name": flow.name,
"description": flow.description,
"step_count": len(flow.steps),
"is_enabled": flow.is_enabled,
"created_at": flow.created_at.isoformat(),
"updated_at": flow.updated_at.isoformat(),
}
@router.delete("/{flow_id}", status_code=204)
async def delete_flow(
flow_id: uuid.UUID,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> None:
"""
Delete a script flow.
"""
logger.info(f"Deleting flow for tenant={tenant_id}, id={flow_id}")
service = ScriptFlowService(session)
success = await service.delete_flow(tenant_id, flow_id)
if not success:
raise HTTPException(status_code=404, detail="Flow not found")
class FlowSimulateRequest(BaseModel):
"""Request body for flow simulation."""
userInputs: list[str]
@router.post("/{flow_id}/simulate")
async def simulate_flow(
flow_id: uuid.UUID,
body: FlowSimulateRequest,
tenant_id: str = Depends(get_tenant_id),
session: AsyncSession = Depends(get_session),
) -> dict[str, Any]:
"""
[AC-AISVC-101, AC-AISVC-102] Simulate flow execution and analyze coverage.
This endpoint simulates the flow execution with provided user inputs
without modifying any database state. It returns:
- Step-by-step simulation results
- Coverage analysis (covered steps, coverage rate)
- Detected issues (dead loops, low coverage, etc.)
"""
logger.info(
f"[AC-AISVC-101] Simulating flow for tenant={tenant_id}, "
f"flow_id={flow_id}, inputs_count={len(body.userInputs)}"
)
service = ScriptFlowService(session)
flow = await service.get_flow(tenant_id, flow_id)
if not flow:
raise HTTPException(status_code=404, detail="Flow not found")
if not flow.steps:
raise HTTPException(status_code=400, detail="Flow has no steps")
tester = ScriptFlowTester()
result = tester.simulate_flow(flow, body.userInputs)
return tester.to_dict(result)