import os import json from fastapi import APIRouter, HTTPException, Depends, Body, Path, Query from typing import List, Dict, Any, Optional from fastapi import status import asyncio import uuid from datetime import datetime import logging from dataclasses import dataclass # Import interfaces from modules.lucydom_interface import get_lucydom_interface from modules.auth import get_current_active_user, get_user_context from modules.chat import get_chat_manager # Import models import modules.lucydom_model as lucydom_model # Configure logger logger = logging.getLogger(__name__) # Create router for workflow endpoints router = APIRouter( prefix="/api/workflows", tags=["Workflow"], responses={404: {"description": "Not found"}} ) @dataclass class AppContext: """Context object for all required connections and user information""" mandate_id: int user_id: int interface_data: Any # LucyDOM Interface interface_chat: Any # Chat Manager async def get_context(current_user: Dict[str, Any]) -> AppContext: """ Creates a central context object with all required interfaces Args: current_user: Current user from authentication Returns: AppContext object with all required connections """ mandate_id, user_id = await get_user_context(current_user) interface_data = get_lucydom_interface(mandate_id, user_id) interface_chat = get_chat_manager(mandate_id, user_id) return AppContext( mandate_id=mandate_id, user_id=user_id, interface_data=interface_data, interface_chat=interface_chat ) @router.get("", response_model=List[Dict[str, Any]]) async def list_workflows(current_user: Dict[str, Any] = Depends(get_current_active_user)): """Lists all workflows of the user""" context = await get_context(current_user) # Retrieve workflows for the user workflows = context.interface_data.get_workflows_by_user(context.user_id) return workflows @router.post("/{workflow_id}/user-input", response_model=Dict[str, Any]) async def submit_user_input( workflow_id: Optional[str] = Path(None, description="ID of the workflow (optional)"), user_input: lucydom_model.UserInputRequest = Body(...), current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Allows the user to send inputs for a running workflow or start a new workflow. """ context = await get_context(current_user) # Improved logging logger.info(f"User input for workflow {workflow_id or 'new'} received") try: # Continue or start workflow with the chat manager user_input_dict = { "prompt": user_input.prompt, "list_file_id": user_input.list_file_id } workflow = await context.interface_chat.chat_run(user_input_dict, workflow_id) if not workflow: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Error processing user input" ) return { "workflow_id": workflow.get("id"), "status": "running", "message": "User input received and being processed" } except HTTPException: # Forward HTTP exceptions raise except Exception as e: logger.error(f"Error processing user input: {str(e)}", exc_info=True) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error processing user input: {str(e)}" ) @router.post("/{workflow_id}/stop", response_model=Dict[str, Any]) async def stop_workflow( workflow_id: str = Path(..., description="ID of the workflow to stop"), current_user: Dict[str, Any] = Depends(get_current_active_user) ): """Stops a running workflow""" context = await get_context(current_user) # Load workflow workflow = context.interface_data.get_workflow(workflow_id) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflow_id} not found" ) # Set status to "stopped" workflow["status"] = "completed" workflow["last_activity"] = datetime.now().isoformat() # Update workflow context.interface_data.update_workflow(workflow_id, workflow) return { "workflow_id": workflow_id, "status": "completed", "message": "Workflow has been stopped" } @router.delete("/{workflow_id}", response_model=Dict[str, Any]) async def delete_workflow( workflow_id: str, current_user: Dict[str, Any] = Depends(get_current_active_user) ): """Deletes a workflow""" context = await get_context(current_user) # Delete workflow success = context.interface_data.delete_workflow(workflow_id) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflow_id} not found" ) return { "workflow_id": workflow_id, "message": "Workflow has been deleted" } @router.get("/{workflow_id}/data-statistics", response_model=Dict[str, Any]) async def get_workflow_data_statistics( workflow_id: str, current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Returns statistics about the transferred data volumes for a workflow. """ context = await get_context(current_user) # Load workflow workflow = context.interface_data.get_workflow(workflow_id) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflow_id} not found" ) # Return data statistics data_stats = workflow.get("data_stats", {}) if not data_stats: data_stats = { "total_processing_time": 0.0, "total_token_count": 0, "total_bytes_sent": 0, "total_bytes_received": 0 } return { "workflow_id": workflow_id, "data_stats": data_stats } @router.get("/{workflow_id}/status", response_model=Dict[str, Any]) async def get_workflow_status( workflow_id: str, current_user: Dict[str, Any] = Depends(get_current_active_user) ): """Get the status of a workflow""" context = await get_context(current_user) # Load workflow workflow = context.interface_data.get_workflow(workflow_id) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflow_id} not found" ) # Create status from the loaded workflow status_info = { "id": workflow.get("id"), "name": workflow.get("name"), "status": workflow.get("status"), "started_at": workflow.get("started_at"), "last_activity": workflow.get("last_activity"), "data_stats": workflow.get("data_stats", {}) } return status_info @router.get("/{workflow_id}/logs", response_model=List[Dict[str, Any]]) async def get_workflow_logs( workflow_id: str, current_user: Dict[str, Any] = Depends(get_current_active_user) ): """Get logs of a workflow""" context = await get_context(current_user) # Get logs logs = context.interface_data.get_workflow_logs(workflow_id) if not logs: # Check if the workflow exists workflow = context.interface_data.get_workflow(workflow_id) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflow_id} not found" ) # Return empty log list logs = [] return logs @router.get("/{workflow_id}/messages", response_model=List[Dict[str, Any]]) async def get_workflow_messages( workflow_id: str, current_user: Dict[str, Any] = Depends(get_current_active_user) ): """Get messages of a workflow""" context = await get_context(current_user) # Get messages messages = context.interface_data.get_workflow_messages(workflow_id) if messages is None: # Check if the workflow exists workflow = context.interface_data.get_workflow(workflow_id) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflow_id} not found" ) # Return empty message list messages = [] return messages @router.delete("/{workflow_id}/messages/{message_id}", response_model=Dict[str, Any]) async def delete_workflow_message( workflow_id: str = Path(..., description="ID of the workflow"), message_id: str = Path(..., description="ID of the message to delete"), current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Deletes a single message from a workflow. This function removes the message from the workflow and also from the database. """ context = await get_context(current_user) try: # Check if the workflow exists workflow = context.interface_data.get_workflow(workflow_id) if not workflow: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Workflow with ID {workflow_id} not found" ) # Delete message success = context.interface_data.delete_workflow_message(workflow_id, message_id) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Message with ID {message_id} in workflow {workflow_id} not found" ) return { "workflow_id": workflow_id, "message_id": message_id, "success": True, "message": "Message successfully deleted" } except HTTPException: # Forward known HTTP exceptions raise except Exception as e: # Catch other errors raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error deleting the message: {str(e)}" ) @router.delete("/{workflow_id}/messages/{message_id}/files/{file_id}", response_model=Dict[str, Any]) async def delete_file_from_message( workflow_id: str = Path(..., description="ID of the workflow"), message_id: str = Path(..., description="ID of the message"), file_id: str = Path(..., description="ID of the file to delete"), current_user: Dict[str, Any] = Depends(get_current_active_user) ): """ Deletes a single file reference from a message in the workflow. The file itself is not deleted from the database, only the reference in the message. """ context = await get_context(current_user) # Add detailed logging logger.debug(f"DELETE request: Remove file {file_id} from message {message_id} in workflow {workflow_id}") try: # Remove file from the message success = context.interface_data.delete_file_from_message(workflow_id, message_id, file_id) if not success: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"File with ID {file_id} in message {message_id} not found" ) return { "workflow_id": workflow_id, "message_id": message_id, "file_id": file_id, "success": True, "message": "File successfully deleted from message" } except HTTPException: # Forward HTTP exceptions raise except Exception as e: logger.error(f"Error deleting file: {str(e)}") import traceback traceback_str = traceback.format_exc() logger.error(f"Traceback: {traceback_str}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error deleting file from message: {str(e)}" )