""" Workflow routes for the backend API. Implements the endpoints for workflow management according to the state machine. """ import os import json import logging from typing import List, Dict, Any, Optional from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query, Response, status from datetime import datetime # Import auth module import modules.security.auth as auth # Import interfaces import modules.interfaces.lucydomInterface as lucydomInterface import modules.interfaces.msftInterface as msftInterface import modules.interfaces.googleInterface as googleInterface # Import workflow manager from modules.workflow.workflowManager import getWorkflowManager # Import models from modules.interfaces.lucydomModel import ( ChatWorkflow, ChatMessage, ChatLog, ChatStat, ChatDocument, UserInputRequest, getModelAttributes ) # Configure logger logger = logging.getLogger(__name__) # Model attributes for ChatWorkflow workflowAttributes = getModelAttributes(ChatWorkflow) # Create router for workflow endpoints router = APIRouter( prefix="/api/workflows", tags=["Workflow"], responses={404: {"description": "Not found"}} ) def createServiceContainer(currentUser: Dict[str, Any]): """Create a service container with all required interfaces.""" # Get all interfaces interfaceBase = lucydomInterface.getInterface(currentUser) interfaceMsft = msftInterface.getInterface(currentUser) interfaceGoogle = googleInterface.getInterface(currentUser) # Create service container service = type('ServiceContainer', (), { 'base': interfaceBase, 'msft': interfaceMsft, 'google': interfaceGoogle }) return service # API Endpoint for getting all workflows @router.get("", response_model=List[ChatWorkflow]) async def list_workflows( currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """List all workflows for the current user.""" try: # Get service container service = createServiceContainer(currentUser) # Retrieve workflows for the user workflows = service.base.getWorkflowsByUser(currentUser["id"]) return [ChatWorkflow(**workflow) for workflow in workflows] except Exception as e: logger.error(f"Error listing workflows: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error listing workflows: {str(e)}" ) # State 1: Workflow Initialization endpoint @router.post("/start", response_model=ChatWorkflow) async def start_workflow( workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"), userInput: UserInputRequest = Body(...), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """ Starts a new workflow or continues an existing one. Corresponds to State 1 in the state machine documentation. """ try: # Get service container service = createServiceContainer(currentUser) # Get workflow manager workflowManager = await getWorkflowManager(service) # Start or continue workflow workflow = await workflowManager.workflowStart(userInput, workflowId) return workflow except Exception as e: logger.error(f"Error in start_workflow: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) # State 8: Workflow Stopped endpoint @router.post("/{workflowId}/stop", response_model=ChatWorkflow) async def stop_workflow( workflowId: str = Path(..., description="ID of the workflow to stop"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Stops a running workflow.""" try: # Get service container service = createServiceContainer(currentUser) # Get workflow manager workflowManager = await getWorkflowManager(service) # Stop workflow workflow = await workflowManager.workflowStop(workflowId) return workflow except Exception as e: logger.error(f"Error in stop_workflow: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) # State 11: Workflow Reset/Deletion endpoint @router.delete("/{workflowId}", response_model=Dict[str, Any]) async def delete_workflow( workflowId: str = Path(..., description="ID of the workflow to delete"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Deletes a workflow and its associated data.""" try: # Get service container service = createServiceContainer(currentUser) # Verify workflow exists workflow = service.base.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflowId} not found" ) # Check if user has permission to delete if workflow.get("_userId") != currentUser["id"]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to delete this workflow" ) # Delete workflow success = service.base.deleteWorkflow(workflowId) if not success: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to delete workflow" ) return { "id": workflowId, "message": "Workflow and associated data deleted successfully" } except HTTPException: raise except Exception as e: logger.error(f"Error deleting workflow: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error deleting workflow: {str(e)}" ) # API Endpoint for workflow status @router.get("/{workflowId}/status", response_model=ChatWorkflow) async def get_workflow_status( workflowId: str = Path(..., description="ID of the workflow"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Get the current status of a workflow.""" try: # Get service container service = createServiceContainer(currentUser) # Retrieve workflow workflow = service.base.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflowId} not found" ) return ChatWorkflow(**workflow) except HTTPException: raise except Exception as e: logger.error(f"Error getting workflow status: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error getting workflow status: {str(e)}" ) # API Endpoint for workflow logs with selective data transfer @router.get("/{workflowId}/logs", response_model=List[ChatLog]) async def get_workflow_logs( workflowId: str = Path(..., description="ID of the workflow"), logId: Optional[str] = Query(None, description="Optional log ID to get only newer logs"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Get logs for a workflow with support for selective data transfer.""" try: # Get service container service = createServiceContainer(currentUser) # Verify workflow exists workflow = service.base.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflowId} not found" ) # Get all logs allLogs = service.base.getWorkflowLogs(workflowId) # Apply selective data transfer if logId is provided if logId: # Find the index of the log with the given ID logIndex = next((i for i, log in enumerate(allLogs) if log.get("id") == logId), -1) if logIndex >= 0: # Return only logs after the specified log return [ChatLog(**log) for log in allLogs[logIndex + 1:]] return [ChatLog(**log) for log in allLogs] except HTTPException: raise except Exception as e: logger.error(f"Error getting workflow logs: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error getting workflow logs: {str(e)}" ) # API Endpoint for workflow messages with selective data transfer @router.get("/{workflowId}/messages", response_model=List[ChatMessage]) async def get_workflow_messages( workflowId: str = Path(..., description="ID of the workflow"), messageId: Optional[str] = Query(None, description="Optional message ID to get only newer messages"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Get messages for a workflow with support for selective data transfer.""" try: # Get service container service = createServiceContainer(currentUser) # Verify workflow exists workflow = service.base.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflowId} not found" ) # Get all messages allMessages = service.base.getWorkflowMessages(workflowId) # Apply selective data transfer if messageId is provided if messageId: # Find the index of the specified message based on messageIds array messageIds = workflow.get("messageIds", []) if messageId in messageIds: messageIndex = messageIds.index(messageId) # Return messages from this index onwards based on the messageIds order filteredMessages = [] for msgId in messageIds[messageIndex:]: message = next((msg for msg in allMessages if msg.get("id") == msgId), None) if message: filteredMessages.append(message) return [ChatMessage(**msg) for msg in filteredMessages] # Sort messages by sequenceNo allMessages.sort(key=lambda x: x.get("sequenceNo", 0)) return [ChatMessage(**msg) for msg in allMessages] except HTTPException: raise except Exception as e: logger.error(f"Error getting workflow messages: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error getting workflow messages: {str(e)}" ) # Document Management Endpoints @router.delete("/{workflowId}/messages/{messageId}", response_model=Dict[str, Any]) async def delete_workflow_message( workflowId: str = Path(..., description="ID of the workflow"), messageId: str = Path(..., description="ID of the message to delete"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Delete a message from a workflow.""" try: # Get service container service = createServiceContainer(currentUser) # Verify workflow exists and belongs to user workflow = service.base.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflowId} not found" ) # Delete the message success = service.base.deleteWorkflowMessage(workflowId, messageId) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Message with ID {messageId} not found in workflow {workflowId}" ) # Update workflow's messageIds messageIds = workflow.get("messageIds", []) if messageId in messageIds: messageIds.remove(messageId) service.base.updateWorkflow(workflowId, {"messageIds": messageIds}) return { "workflowId": workflowId, "messageId": messageId, "message": "Message deleted successfully" } except HTTPException: raise except Exception as e: logger.error(f"Error deleting message: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error deleting message: {str(e)}" ) @router.delete("/{workflowId}/messages/{messageId}/files/{fileId}", response_model=Dict[str, Any]) async def delete_file_from_message( workflowId: str = Path(..., description="ID of the workflow"), messageId: str = Path(..., description="ID of the message"), fileId: str = Path(..., description="ID of the file to delete"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Delete a file reference from a message in a workflow.""" try: # Get service container service = createServiceContainer(currentUser) # Verify workflow exists and belongs to user workflow = service.base.getWorkflow(workflowId) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflowId} not found" ) # Delete file reference from message success = service.base.deleteFileFromMessage(workflowId, messageId, fileId) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"File with ID {fileId} not found in message {messageId}" ) return { "workflowId": workflowId, "messageId": messageId, "fileId": fileId, "message": "File reference deleted successfully" } except HTTPException: raise except Exception as e: logger.error(f"Error deleting file reference: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error deleting file reference: {str(e)}" ) # File preview and download routes @router.get("/files/{fileId}/preview", response_model=ChatDocument) async def preview_file( fileId: str = Path(..., description="ID of the file to preview"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Get file metadata and a preview of the file content.""" try: # Get service container service = createServiceContainer(currentUser) # Get file metadata file = service.base.getFile(fileId) if not file: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"File with ID {fileId} not found" ) # Get file data (limited for preview) fileData = service.base.getFileData(fileId) if fileData is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"File data not found for file ID {fileId}" ) # For text-based files, return a preview of the content mimeType = file.get("mimeType", "application/octet-stream") isText = mimeType.startswith("text/") or mimeType in [ "application/json", "application/xml", "application/javascript" ] previewData = None # Get base64Encoded flag from database fileDataEntries = service.base.db.getRecordset("fileData", recordFilter={"id": fileId}) if fileDataEntries and "base64Encoded" in fileDataEntries[0]: # Use the flag from the database base64Encoded = fileDataEntries[0]["base64Encoded"] else: # Determine based on file type (fallback for older data) base64Encoded = not isText if isText: # Convert to string without trim for preview if isinstance(fileData, bytes): try: filePreview = fileData.decode('utf-8') previewData = filePreview except UnicodeDecodeError: # Try other encodings for encoding in ['latin-1', 'cp1252', 'iso-8859-1']: try: filePreview = fileData.decode(encoding) previewData = filePreview break except UnicodeDecodeError: continue # For images, return base64 encoded data if mimeType.startswith("image/"): import base64 previewData = base64.b64encode(fileData).decode('utf-8') base64Encoded = True # Create ChatDocument instance return ChatDocument( id=fileId, fileId=fileId, fileName=file.get("name"), fileSize=file.get("size"), mimeType=mimeType, contents=[{ "sequenceNr": 1, "name": file.get("name"), "mimeType": mimeType, "data": previewData, "metadata": { "base64Encoded": base64Encoded, "isPreviewable": isText or mimeType.startswith("image/") } }] ) except HTTPException: raise except Exception as e: logger.error(f"Error previewing file: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error previewing file: {str(e)}" ) @router.get("/files/{fileId}/download") async def download_file( fileId: str = Path(..., description="ID of the file to download"), currentUser: Dict[str, Any] = Depends(auth.getCurrentActiveUser) ): """Download a file.""" try: # Get service container service = createServiceContainer(currentUser) # Get file data fileInfo = service.base.downloadFile(fileId) if not fileInfo: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"File with ID {fileId} not found" ) # Return file as response return Response( content=fileInfo["content"], media_type=fileInfo["contentType"], headers={ "Content-Disposition": f"attachment; filename={fileInfo['name']}" } ) except HTTPException: raise except Exception as e: logger.error(f"Error downloading file: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error downloading file: {str(e)}" )