gateway/modules/routes/routeWorkflows.py

687 lines
No EOL
26 KiB
Python

"""
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 interfaces
from modules.interfaces.lucydomInterface import getInterface as getInterfaceLucydom
from modules.interfaces.msftInterface import getInterface as getInterfaceMsft
from modules.security.auth import getCurrentActiveUser
from modules.workflow.workflowManager import getWorkflowManager
# Import models
from modules.interfaces import lucydomModel as Models
# Configure logger
logger = logging.getLogger(__name__)
# Create router for workflow endpoints
router = APIRouter(
prefix="/api/workflows",
tags=["Workflow"],
responses={404: {"description": "Not found"}}
)
# State 1: Workflow Initialization endpoint
@router.post("/start", response_model=Dict[str, Any])
async def startWorkflow(
workflowId: Optional[str] = Query(None, description="Optional ID of the workflow to continue"),
userInput: Models.UserInputRequest = Body(...),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""
Starts a new workflow or continues an existing one.
Corresponds to State 1 in the state machine documentation.
"""
try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
interfaceMsft = getInterfaceMsft(currentUser)
# Convert the user input to a dictionary
userInputDict = {
"prompt": userInput.prompt,
"listFileId": userInput.listFileId
}
# Get workflow manager with interface
workflowManager = await getWorkflowManager(interfaceBase, interfaceMsft)
# Start or continue workflow using the workflow manager
workflow = await workflowManager.workflowStart(userInputDict, workflowId)
logger.info("User Input received. Answer:", workflow)
return {
"id": workflow.get("id"),
"status": workflow.get("status", "running"),
"message": "Workflow initialized and processing started"
}
except Exception as e:
logger.error(f"Error starting workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error starting workflow: {str(e)}"
)
# State 8: Workflow Stopped endpoint
@router.post("/{workflowId}/stop", response_model=Dict[str, Any])
async def stopWorkflow(
workflowId: str = Path(..., description="ID of the workflow to stop"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Stops a running workflow."""
try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
interfaceMsft = getInterfaceMsft(currentUser)
# Verify workflow exists and belongs to user
workflow = interfaceBase.getWorkflow(workflowId)
if not workflow:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workflow with ID {workflowId} not found"
)
if workflow.get("_userId") != currentUser["id"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to stop this workflow"
)
# Stop the workflow
workflowManager = await getWorkflowManager(interfaceBase, interfaceMsft)
stoppedWorkflow = await workflowManager.workflowStop(workflowId)
return {
"id": workflowId,
"status": stoppedWorkflow.get("status", "stopped"),
"message": "Workflow has been stopped"
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error stopping workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error stopping workflow: {str(e)}"
)
# State 11: Workflow Reset/Deletion endpoint
@router.delete("/{workflowId}", response_model=Dict[str, Any])
async def deleteWorkflow(
workflowId: str = Path(..., description="ID of the workflow to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Deletes a workflow and its associated data."""
try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists
workflow = interfaceBase.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 = interfaceBase.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 getting all workflows
@router.get("", response_model=List[Dict[str, Any]])
async def listWorkflows(
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""List all workflows for the current user."""
try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
# Retrieve workflows for the user
workflows = interfaceBase.getWorkflowsByUser(currentUser["id"])
return 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)}"
)
# API Endpoint for workflow status
@router.get("/{workflowId}/status", response_model=Dict[str, Any])
async def getWorkflowStatus(
workflowId: str = Path(..., description="ID of the workflow"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get the current status of a workflow."""
try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
# Retrieve workflow
workflow = interfaceBase.getWorkflow(workflowId)
if not workflow:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workflow with ID {workflowId} not found"
)
# Create status response
statusInfo = {
"id": workflow.get("id"),
"name": workflow.get("name"),
"status": workflow.get("status"),
"startedAt": workflow.get("startedAt"),
"lastActivity": workflow.get("lastActivity"),
"currentRound": workflow.get("currentRound", 1),
"dataStats": workflow.get("dataStats", {})
}
return statusInfo
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[Dict[str, Any]])
async def getWorkflowLogs(
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(getCurrentActiveUser)
):
"""Get logs for a workflow with support for selective data transfer."""
try:
# Get interface with current user context
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists
workflow = interfaceBase.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 = interfaceBase.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 allLogs[logIndex + 1:]
return 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[Dict[str, Any]])
async def getWorkflowMessages(
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(getCurrentActiveUser)
):
"""Get messages for a workflow with support for selective data transfer."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists
workflow = interfaceBase.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 = interfaceBase.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 filteredMessages
# Sort messages by sequenceNo
allMessages.sort(key=lambda x: x.get("sequenceNo", 0))
return 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 deleteWorkflowMessage(
workflowId: str = Path(..., description="ID of the workflow"),
messageId: str = Path(..., description="ID of the message to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Delete a message from a workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists and belongs to user
workflow = interfaceBase.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 = interfaceBase.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)
interfaceBase.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 deleteFileFromMessage(
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(getCurrentActiveUser)
):
"""Delete a file reference from a message in a workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Verify workflow exists and belongs to user
workflow = interfaceBase.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 = interfaceBase.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=Dict[str, Any])
async def previewFile(
fileId: str = Path(..., description="ID of the file to preview"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get file metadata and a preview of the file content."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get file metadata
file = interfaceBase.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 = interfaceBase.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 = interfaceBase.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
# Return file metadata with preview and base64Encoded flag
return {
"id": fileId,
"name": file.get("name"),
"mimeType": mimeType,
"size": file.get("size"),
"creationDate": file.get("creationDate"),
"isPreviewable": isText or mimeType.startswith("image/"),
"preview": previewData,
"base64Encoded": base64Encoded
}
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 downloadFile(
fileId: str = Path(..., description="ID of the file to download"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Download a file."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get file data
fileInfo = interfaceBase.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)}"
)
@router.get("/workflows", response_model=List[Dict[str, Any]])
async def getWorkflows(currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)):
"""Get all workflows for the mandate."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get all workflows for the mandate
workflows = interfaceBase.getWorkflowsByMandate(currentUser.get("_mandateId"))
return workflows
except Exception as e:
logger.error(f"Error getting workflows: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting workflows: {str(e)}"
)
@router.post("/workflows", response_model=Dict[str, Any])
async def createWorkflow(
workflow: Dict[str, Any],
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Create a new workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Create workflow
newWorkflow = interfaceBase.createWorkflow(workflow)
return newWorkflow
except Exception as e:
logger.error(f"Error creating workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error creating workflow: {str(e)}"
)
@router.get("/workflows/{workflowId}", response_model=Dict[str, Any])
async def getWorkflow(
workflowId: str = Path(..., description="ID of the workflow"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Get a specific workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get workflow
workflow = interfaceBase.getWorkflow(workflowId)
if not workflow:
raise HTTPException(status_code=404, detail="Workflow not found")
return workflow
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting workflow: {str(e)}"
)
@router.put("/workflows/{workflowId}", response_model=Dict[str, Any])
async def updateWorkflow(
workflow: Dict[str, Any],
workflowId: str = Path(..., description="ID of the workflow to update"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Update an existing workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get workflow
existingWorkflow = interfaceBase.getWorkflow(workflowId)
if not existingWorkflow:
raise HTTPException(status_code=404, detail="Workflow not found")
# Update workflow
updatedWorkflow = interfaceBase.updateWorkflow(workflowId, workflow)
return updatedWorkflow
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error updating workflow: {str(e)}"
)
@router.delete("/workflows/{workflowId}")
async def deleteWorkflow(
workflowId: str = Path(..., description="ID of the workflow to delete"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Delete a workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get workflow
workflow = interfaceBase.getWorkflow(workflowId)
if not workflow:
raise HTTPException(status_code=404, detail="Workflow not found")
# Delete workflow
success = interfaceBase.deleteWorkflow(workflowId)
if not success:
raise HTTPException(status_code=500, detail="Failed to delete workflow")
return {"status": "success"}
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)}"
)
@router.post("/workflows/{workflowId}/files/{fileId}")
async def addFileToWorkflow(
workflowId: str = Path(..., description="ID of the workflow"),
fileId: str = Path(..., description="ID of the file to add"),
currentUser: Dict[str, Any] = Depends(getCurrentActiveUser)
):
"""Add a file to a workflow."""
try:
# Get admin user for workflow operations
interfaceBase = getInterfaceLucydom(currentUser)
# Get workflow
workflow = interfaceBase.getWorkflow(workflowId)
if not workflow:
raise HTTPException(status_code=404, detail="Workflow not found")
# Get file
file = interfaceBase.getFile(fileId)
if not file:
raise HTTPException(status_code=404, detail="File not found")
# Add file to workflow
success = interfaceBase.addFileToWorkflow(workflowId, fileId)
if not success:
raise HTTPException(status_code=500, detail="Failed to add file to workflow")
return {"status": "success"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error adding file to workflow: {str(e)}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error adding file to workflow: {str(e)}"
)