""" Sessions Controller for Mid Platform. [AC-IDMP-09] Session mode switch endpoint: POST /mid/sessions/{sessionId}/mode """ import logging from typing import Annotated from fastapi import APIRouter, Depends, Path from fastapi.responses import JSONResponse from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_session from app.core.tenant import get_tenant_id from app.models.mid.schemas import ( SessionMode, SwitchModeRequest, SwitchModeResponse, ) from app.services.memory import MemoryService logger = logging.getLogger(__name__) router = APIRouter(prefix="/mid", tags=["Mid Platform Sessions"]) _session_modes: dict[str, SessionMode] = {} class CancelFlowResponse(BaseModel): """Response for cancel flow operation.""" success: bool message: str session_id: str @router.post( "/sessions/{sessionId}/cancel-flow", operation_id="cancelActiveFlow", summary="Cancel active flow", description=""" Cancel the active flow for a session. Use this when you encounter "Session already has an active flow" error. """, responses={ 200: {"description": "Flow cancelled successfully", "model": CancelFlowResponse}, 404: {"description": "No active flow found"}, }, ) async def cancel_active_flow( sessionId: Annotated[str, Path(description="Session ID")], session: Annotated[AsyncSession, Depends(get_session)], ) -> CancelFlowResponse: """ Cancel the active flow for a session. This endpoint allows you to cancel any active flow instance so that a new flow can be started. """ tenant_id = get_tenant_id() if not tenant_id: from app.core.exceptions import MissingTenantIdException raise MissingTenantIdException() logger.info( f"[Cancel Flow] Cancelling active flow: tenant={tenant_id}, session={sessionId}" ) try: from app.services.flow.engine import FlowEngine flow_engine = FlowEngine(session) cancelled = await flow_engine.cancel_flow( tenant_id=tenant_id, session_id=sessionId, reason="User requested cancellation via API", ) if cancelled: logger.info(f"[Cancel Flow] Flow cancelled: session={sessionId}") return CancelFlowResponse( success=True, message="Active flow cancelled successfully", session_id=sessionId, ) else: logger.info(f"[Cancel Flow] No active flow found: session={sessionId}") return CancelFlowResponse( success=True, message="No active flow found for this session", session_id=sessionId, ) except Exception as e: logger.error(f"[Cancel Flow] Failed to cancel flow: {e}") return CancelFlowResponse( success=False, message=f"Failed to cancel flow: {str(e)}", session_id=sessionId, ) @router.post( "/sessions/{sessionId}/mode", operation_id="switchSessionMode", summary="Switch session mode", description=""" [AC-IDMP-09] Switch session mode between BOT_ACTIVE and HUMAN_ACTIVE. When mode is HUMAN_ACTIVE, dialogue responses will route to transfer mode. """, responses={ 200: {"description": "Mode switched successfully", "model": SwitchModeResponse}, 400: {"description": "Invalid request"}, }, ) async def switch_session_mode( sessionId: Annotated[str, Path(description="Session ID")], switch_request: SwitchModeRequest, session: Annotated[AsyncSession, Depends(get_session)], ) -> SwitchModeResponse: """ [AC-IDMP-09] Switch session mode. Modes: - BOT_ACTIVE: Bot handles responses - HUMAN_ACTIVE: Transfer to human agent """ tenant_id = get_tenant_id() if not tenant_id: from app.core.exceptions import MissingTenantIdException raise MissingTenantIdException() logger.info( f"[AC-IDMP-09] Mode switch: tenant={tenant_id}, " f"session={sessionId}, mode={switch_request.mode.value}" ) try: memory_service = MemoryService(session) await memory_service.get_or_create_session( tenant_id=tenant_id, session_id=sessionId, ) session_key = f"{tenant_id}:{sessionId}" _session_modes[session_key] = switch_request.mode logger.info( f"[AC-IDMP-09] Mode switched: session={sessionId}, " f"mode={switch_request.mode.value}, reason={switch_request.reason}" ) return SwitchModeResponse( session_id=sessionId, mode=switch_request.mode, ) except Exception as e: logger.error(f"[AC-IDMP-09] Mode switch failed: {e}") return SwitchModeResponse( session_id=sessionId, mode=switch_request.mode, ) def get_session_mode(tenant_id: str, session_id: str) -> SessionMode: """Get current session mode.""" session_key = f"{tenant_id}:{session_id}" return _session_modes.get(session_key, SessionMode.BOT_ACTIVE) def clear_session_mode(tenant_id: str, session_id: str) -> None: """Clear session mode (reset to BOT_ACTIVE).""" session_key = f"{tenant_id}:{session_id}" if session_key in _session_modes: del _session_modes[session_key]