| """ |
| Advanced Workflow API Endpoints |
| Multi-input, multi-step, multi-output workflow support with state management |
| """ |
|
|
| import asyncio |
| from datetime import datetime |
| import logging |
| from typing import Any, Dict, List, Optional |
| from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException |
| from pydantic import BaseModel, Field |
|
|
| from .advanced_workflow_system import ( |
| AdvancedWorkflowDefinition, |
| ExecutionEngine, |
| InputParameter, |
| ParameterType, |
| ParameterValidator, |
| StateManager, |
| WorkflowState, |
| WorkflowStep, |
| ) |
| from .workflow_template_manager import WorkflowTemplateManager, get_workflow_template_manager |
|
|
| logger = logging.getLogger(__name__) |
| router = APIRouter() |
|
|
| |
| state_manager = StateManager() |
| execution_engine = ExecutionEngine(state_manager) |
| template_manager = get_workflow_template_manager() |
|
|
| |
| class CreateWorkflowRequest(BaseModel): |
| name: str |
| description: str |
| category: str = "general" |
| tags: List[str] = [] |
| input_schema: List[Dict[str, Any]] = [] |
| steps: List[Dict[str, Any]] = [] |
| output_config: Optional[Dict[str, Any]] = None |
|
|
| class StartWorkflowRequest(BaseModel): |
| workflow_id: str |
| inputs: Dict[str, Any] = {} |
|
|
| class UpdateWorkflowRequest(BaseModel): |
| inputs: Dict[str, Any] = {} |
|
|
| class WorkflowStepRequest(BaseModel): |
| step_id: str |
| inputs: Dict[str, Any] = {} |
|
|
| class WorkflowTemplate(BaseModel): |
| template_id: str |
| name: str |
| description: str |
| category: str |
| input_schema: List[Dict[str, Any]] |
| steps: List[Dict[str, Any]] |
| tags: List[str] = [] |
|
|
| |
| def serialize_workflow(workflow: AdvancedWorkflowDefinition) -> Dict[str, Any]: |
| """Convert workflow to serializable dict""" |
| return { |
| "workflow_id": workflow.workflow_id, |
| "name": workflow.name, |
| "description": workflow.description, |
| "version": workflow.version, |
| "category": workflow.category, |
| "tags": workflow.tags, |
| "input_schema": [param.dict() for param in workflow.input_schema], |
| "steps": [step.dict() for step in workflow.steps], |
| "output_config": workflow.output_config.dict() if workflow.output_config else None, |
| "state": workflow.state.value, |
| "current_step": workflow.current_step, |
| "created_at": workflow.created_at.isoformat(), |
| "updated_at": workflow.updated_at.isoformat(), |
| "created_by": workflow.created_by |
| } |
|
|
| |
| @router.post("/workflows", response_model=Dict[str, Any]) |
| async def create_workflow(request: CreateWorkflowRequest): |
| """Create a new advanced workflow""" |
| try: |
| |
| workflow_data = { |
| "workflow_id": f"workflow_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{hash(request.name) % 10000}", |
| "name": request.name, |
| "description": request.description, |
| "category": request.category, |
| "tags": request.tags, |
| "input_schema": [InputParameter(**param) for param in request.input_schema], |
| "steps": [WorkflowStep(**step) for step in request.steps], |
| "output_config": request.output_config |
| } |
|
|
| |
| workflow = await execution_engine.create_workflow(workflow_data) |
|
|
| return { |
| "status": "success", |
| "workflow_id": workflow.workflow_id, |
| "workflow": serialize_workflow(workflow) |
| } |
|
|
| except Exception as e: |
| logger.error(f"Failed to create workflow: {e}") |
| raise HTTPException(status_code=400, detail=str(e)) |
|
|
| @router.get("/workflows") |
| async def list_workflows( |
| state: Optional[WorkflowState] = None, |
| category: Optional[str] = None, |
| tags: Optional[str] = None, |
| sort_by: str = "updated_at", |
| sort_order: str = "desc", |
| limit: Optional[int] = None, |
| offset: int = 0 |
| ): |
| """ |
| List workflows with comprehensive filtering and sorting. |
| |
| Query Parameters: |
| - state: Filter by workflow state (draft, running, completed, etc.) |
| - category: Filter by category |
| - tags: Comma-separated list of tags (workflows must have ALL specified tags) |
| - sort_by: Field to sort by (updated_at, created_at, name) |
| - sort_order: Sort order (asc or desc) |
| - limit: Maximum number of workflows to return |
| - offset: Number of workflows to skip |
| """ |
| try: |
| |
| status_filter = None |
| if state is not None: |
| status_filter = state.value if isinstance(state, WorkflowState) else state |
|
|
| |
| tags_list = None |
| if tags: |
| tags_list = [t.strip() for t in tags.split(",") if t.strip()] |
|
|
| |
| workflows = state_manager.list_workflows( |
| status=status_filter, |
| category=category, |
| tags=tags_list, |
| sort_by=sort_by, |
| sort_order=sort_order, |
| limit=limit, |
| offset=offset |
| ) |
|
|
| |
| total_workflows = len(state_manager.list_workflows( |
| status=status_filter, |
| category=category, |
| tags=tags_list |
| )) |
|
|
| |
| return { |
| "workflows": workflows, |
| "total": total_workflows, |
| "offset": offset, |
| "limit": limit if limit is not None else len(workflows), |
| "filters": { |
| "state": status_filter, |
| "category": category, |
| "tags": tags_list |
| } |
| } |
|
|
| except Exception as e: |
| logger.error(f"Failed to list workflows: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.get("/workflows/{workflow_id}", response_model=Dict[str, Any]) |
| async def get_workflow(workflow_id: str): |
| """Get workflow details""" |
| try: |
| state = state_manager.load_state(workflow_id) |
| if not state: |
| raise HTTPException(status_code=404, detail="Workflow not found") |
|
|
| workflow = AdvancedWorkflowDefinition(**state) |
|
|
| return { |
| "status": "success", |
| "workflow": serialize_workflow(workflow), |
| "execution_status": execution_engine.get_workflow_status(workflow_id) |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to get workflow {workflow_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.post("/workflows/{workflow_id}/start", response_model=Dict[str, Any]) |
| async def start_workflow(workflow_id: str, request: StartWorkflowRequest): |
| """Start or resume workflow execution""" |
| try: |
| |
| state = state_manager.load_state(workflow_id) |
| if not state: |
| raise HTTPException(status_code=404, detail="Workflow not found") |
|
|
| workflow = AdvancedWorkflowDefinition(**state) |
|
|
| |
| validation_errors = [] |
| for param in workflow.input_schema: |
| if param.name in request.inputs: |
| is_valid, error_msg = ParameterValidator.validate_parameter(param, request.inputs[param.name]) |
| if not is_valid: |
| validation_errors.append(f"{param.name}: {error_msg}") |
|
|
| if validation_errors: |
| raise HTTPException(status_code=400, detail={ |
| "type": "validation_error", |
| "errors": validation_errors |
| }) |
|
|
| |
| result = await execution_engine.start_workflow(workflow_id, request.inputs) |
|
|
| return { |
| "status": "success", |
| "result": result |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to start workflow {workflow_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.post("/workflows/{workflow_id}/pause", response_model=Dict[str, Any]) |
| async def pause_workflow(workflow_id: str): |
| """Pause workflow execution""" |
| try: |
| success = execution_engine.pause_workflow(workflow_id) |
|
|
| if not success: |
| raise HTTPException(status_code=400, detail="Workflow cannot be paused") |
|
|
| return { |
| "status": "success", |
| "message": "Workflow paused" |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to pause workflow {workflow_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.post("/workflows/{workflow_id}/resume", response_model=Dict[str, Any]) |
| async def resume_workflow(workflow_id: str, request: UpdateWorkflowRequest): |
| """Resume paused workflow execution""" |
| try: |
| result = execution_engine.resume_workflow(workflow_id, request.inputs) |
|
|
| return { |
| "status": "success", |
| "result": result |
| } |
|
|
| except Exception as e: |
| logger.error(f"Failed to resume workflow {workflow_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.post("/workflows/{workflow_id}/cancel", response_model=Dict[str, Any]) |
| async def cancel_workflow(workflow_id: str): |
| """Cancel workflow execution""" |
| try: |
| success = execution_engine.cancel_workflow(workflow_id) |
|
|
| if not success: |
| raise HTTPException(status_code=400, detail="Workflow cannot be cancelled") |
|
|
| return { |
| "status": "success", |
| "message": "Workflow cancelled" |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to cancel workflow {workflow_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.get("/workflows/{workflow_id}/status", response_model=Dict[str, Any]) |
| async def get_workflow_status(workflow_id: str): |
| """Get current workflow execution status""" |
| try: |
| status = execution_engine.get_workflow_status(workflow_id) |
|
|
| if not status: |
| raise HTTPException(status_code=404, detail="Workflow not found") |
|
|
| return { |
| "status": "success", |
| "workflow_status": status |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to get workflow status {workflow_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.get("/workflows/{workflow_id}/step/{step_id}", response_model=Dict[str, Any]) |
| async def get_workflow_step(workflow_id: str, step_id: str): |
| """Get specific workflow step details""" |
| try: |
| state = state_manager.load_state(workflow_id) |
| if not state: |
| raise HTTPException(status_code=404, detail="Workflow not found") |
|
|
| workflow = AdvancedWorkflowDefinition(**state) |
|
|
| |
| step = next((s for s in workflow.steps if s.step_id == step_id), None) |
| if not step: |
| raise HTTPException(status_code=404, detail="Step not found") |
|
|
| |
| step_result = workflow.step_results.get(step_id, None) |
|
|
| return { |
| "status": "success", |
| "step": step.dict(), |
| "result": step_result, |
| "is_current_step": workflow.current_step == step_id |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to get workflow step {workflow_id}/{step_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.post("/workflows/{workflow_id}/step/{step_id}/execute", response_model=Dict[str, Any]) |
| async def execute_workflow_step(workflow_id: str, step_id: str, request: WorkflowStepRequest): |
| """Execute a specific workflow step with provided inputs""" |
| try: |
| state = state_manager.load_state(workflow_id) |
| if not state: |
| raise HTTPException(status_code=404, detail="Workflow not found") |
|
|
| workflow = AdvancedWorkflowDefinition(**state) |
|
|
| |
| step = next((s for s in workflow.steps if s.step_id == step_id), None) |
| if not step: |
| raise HTTPException(status_code=404, detail="Step not found") |
|
|
| |
| step_inputs = request.inputs |
| step_inputs.update(workflow.user_inputs) |
|
|
| |
| validation_errors = [] |
| for param in step.input_parameters: |
| if param.name in step_inputs: |
| is_valid, error_msg = ParameterValidator.validate_parameter(param, step_inputs[param.name]) |
| if not is_valid: |
| validation_errors.append(f"{param.name}: {error_msg}") |
|
|
| if validation_errors: |
| raise HTTPException(status_code=400, detail={ |
| "type": "validation_error", |
| "errors": validation_errors |
| }) |
|
|
| |
| result = await execution_engine._execute_step(workflow, step) |
|
|
| |
| workflow.step_results[step_id] = result |
| workflow.updated_at = datetime.now() |
| state_manager.save_state(workflow_id, workflow.dict()) |
|
|
| return { |
| "status": "success", |
| "step_result": result |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to execute workflow step {workflow_id}/{step_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.get("/workflows/{workflow_id}/inputs/required", response_model=Dict[str, Any]) |
| async def get_required_inputs(workflow_id: str): |
| """Get required inputs for the workflow""" |
| try: |
| state = state_manager.load_state(workflow_id) |
| if not state: |
| raise HTTPException(status_code=404, detail="Workflow not found") |
|
|
| workflow = AdvancedWorkflowDefinition(**state) |
|
|
| |
| missing_inputs = execution_engine._get_missing_inputs(workflow, workflow.user_inputs) |
|
|
| return { |
| "status": "success", |
| "required_inputs": [param.dict() for param in missing_inputs], |
| "current_step": workflow.current_step, |
| "workflow_state": workflow.state.value |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to get required inputs {workflow_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| |
| @router.get("/workflows/templates", response_model=List[Dict[str, Any]]) |
| async def list_workflow_templates( |
| category: Optional[str] = None, |
| tags: Optional[List[str]] = None, |
| active_only: bool = True |
| ): |
| """List available workflow templates""" |
| try: |
| templates = template_manager.list_templates( |
| category=category, |
| tags=tags, |
| active_only=active_only |
| ) |
|
|
| return [template.dict() for template in templates] |
|
|
| except Exception as e: |
| logger.error(f"Failed to list workflow templates: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.post("/workflows/templates", response_model=Dict[str, Any]) |
| async def create_workflow_template(template: Dict[str, Any]): |
| """Create a workflow template""" |
| try: |
| created_template = template_manager.create_template(template) |
|
|
| return { |
| "status": "success", |
| "template_id": created_template.template_id, |
| "template": created_template.dict() |
| } |
|
|
| except ValueError as e: |
| raise HTTPException(status_code=400, detail=str(e)) |
| except Exception as e: |
| logger.error(f"Failed to create workflow template: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.post("/workflows/from-template", response_model=Dict[str, Any]) |
| async def create_workflow_from_template( |
| template_id: str, |
| workflow_data: Dict[str, Any] |
| ): |
| """Create a new workflow from a template""" |
| try: |
| |
| workflow_definition = template_manager.create_workflow_from_template( |
| template_id=template_id, |
| workflow_data=workflow_data |
| ) |
|
|
| |
| workflow = await execution_engine.create_workflow(workflow_definition) |
|
|
| return { |
| "status": "success", |
| "workflow_id": workflow.workflow_id, |
| "template_id": template_id, |
| "workflow": serialize_workflow(workflow) |
| } |
|
|
| except ValueError as e: |
| raise HTTPException(status_code=404, detail=str(e)) |
| except Exception as e: |
| logger.error(f"Failed to create workflow from template {template_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| |
| @router.get("/workflows/parameter-types", response_model=List[str]) |
| async def get_parameter_types(): |
| """Get available parameter types""" |
| return [param_type.value for param_type in ParameterType] |
|
|
| @router.post("/workflows/validate-parameters", response_model=Dict[str, Any]) |
| async def validate_parameters( |
| parameters: List[Dict[str, Any]], |
| inputs: Dict[str, Any] |
| ): |
| """Validate input parameters""" |
| try: |
| results = {} |
|
|
| for param_data in parameters: |
| param = InputParameter(**param_data) |
| value = inputs.get(param.name) |
|
|
| is_valid, error_msg = ParameterValidator.validate_parameter(param, value) |
|
|
| results[param.name] = { |
| "valid": is_valid, |
| "error": error_msg, |
| "type": param.type.value, |
| "required": param.required |
| } |
|
|
| return { |
| "status": "success", |
| "validation_results": results, |
| "all_valid": all(r["valid"] for r in results.values()) |
| } |
|
|
| except Exception as e: |
| logger.error(f"Failed to validate parameters: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| |
| @router.get("/workflows/{workflow_id}/export", response_model=Dict[str, Any]) |
| async def export_workflow(workflow_id: str): |
| """Export workflow definition""" |
| try: |
| state = state_manager.load_state(workflow_id) |
| if not state: |
| raise HTTPException(status_code=404, detail="Workflow not found") |
|
|
| |
| export_data = state.copy() |
| export_data.pop("step_results", None) |
| export_data.pop("execution_context", None) |
| export_data.pop("state", None) |
| export_data.pop("current_step", None) |
|
|
| return { |
| "status": "success", |
| "workflow_definition": export_data |
| } |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to export workflow {workflow_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @router.post("/workflows/import", response_model=Dict[str, Any]) |
| async def import_workflow(workflow_definition: Dict[str, Any]): |
| """Import workflow definition""" |
| try: |
| |
| workflow_definition["workflow_id"] = f"imported_{datetime.now().strftime('%Y%m%d_%H%M%S')}" |
| workflow_definition["state"] = WorkflowState.DRAFT |
|
|
| workflow = await execution_engine.create_workflow(workflow_definition) |
|
|
| return { |
| "status": "success", |
| "workflow_id": workflow.workflow_id, |
| "workflow": serialize_workflow(workflow) |
| } |
|
|
| except Exception as e: |
| logger.error(f"Failed to import workflow: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |