gateway/routes/workflows.py
2025-04-24 11:14:43 +02:00

371 lines
No EOL
12 KiB
Python

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: str = Path(..., 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.
"""
logger.debug("CHECK 01...")
context = await get_context(current_user)
logger.debug("CHECK 02...")
# Improved logging
logger.info(f"User input for workflow '{workflow_id}' received")
if workflow_id == "new": workflow_id = None #agreed placeholder with frontend
logger.debug("CHECK trying...")
try:
# Continue or start workflow with the chat manager
user_input_dict = {
"prompt": user_input.prompt,
"list_file_id": user_input.list_file_id
}
logger.debug("CHECK start Workflow ...")
workflow = await context.interface_chat.chat_run(user_input_dict, workflow_id)
logger.debug("CHECK end Workflow ...")
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)}"
)