749 lines
23 KiB
Python
749 lines
23 KiB
Python
"""
|
|
Document Workflow Management API
|
|
|
|
This API provides comprehensive workflow automation management including:
|
|
- Workflow creation and configuration
|
|
- Event logging and processing
|
|
- Execution monitoring and control
|
|
- Template management for common workflows
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import List, Optional, Dict, Any, Union
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
|
|
from sqlalchemy.orm import Session, joinedload
|
|
from sqlalchemy import func, or_, and_, desc
|
|
from pydantic import BaseModel, Field
|
|
from datetime import datetime, date, timedelta
|
|
import json
|
|
|
|
from app.database.base import get_db
|
|
from app.auth.security import get_current_user
|
|
from app.models.user import User
|
|
from app.models.document_workflows import (
|
|
DocumentWorkflow, WorkflowAction, WorkflowExecution, EventLog,
|
|
WorkflowTemplate, WorkflowTriggerType, WorkflowActionType,
|
|
ExecutionStatus, WorkflowStatus
|
|
)
|
|
from app.services.workflow_engine import EventProcessor, WorkflowExecutor
|
|
from app.services.query_utils import paginate_with_total
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# Pydantic schemas for API
|
|
class WorkflowCreate(BaseModel):
|
|
name: str = Field(..., max_length=200)
|
|
description: Optional[str] = None
|
|
trigger_type: WorkflowTriggerType
|
|
trigger_conditions: Optional[Dict[str, Any]] = None
|
|
delay_minutes: int = Field(0, ge=0)
|
|
max_retries: int = Field(3, ge=0, le=10)
|
|
retry_delay_minutes: int = Field(30, ge=1)
|
|
timeout_minutes: int = Field(60, ge=1)
|
|
file_type_filter: Optional[List[str]] = None
|
|
status_filter: Optional[List[str]] = None
|
|
attorney_filter: Optional[List[str]] = None
|
|
client_filter: Optional[List[str]] = None
|
|
schedule_cron: Optional[str] = None
|
|
schedule_timezone: str = "UTC"
|
|
priority: int = Field(5, ge=1, le=10)
|
|
category: Optional[str] = None
|
|
tags: Optional[List[str]] = None
|
|
|
|
|
|
class WorkflowUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
status: Optional[WorkflowStatus] = None
|
|
trigger_conditions: Optional[Dict[str, Any]] = None
|
|
delay_minutes: Optional[int] = None
|
|
max_retries: Optional[int] = None
|
|
retry_delay_minutes: Optional[int] = None
|
|
timeout_minutes: Optional[int] = None
|
|
file_type_filter: Optional[List[str]] = None
|
|
status_filter: Optional[List[str]] = None
|
|
attorney_filter: Optional[List[str]] = None
|
|
client_filter: Optional[List[str]] = None
|
|
schedule_cron: Optional[str] = None
|
|
schedule_timezone: Optional[str] = None
|
|
priority: Optional[int] = None
|
|
category: Optional[str] = None
|
|
tags: Optional[List[str]] = None
|
|
|
|
|
|
class WorkflowActionCreate(BaseModel):
|
|
action_type: WorkflowActionType
|
|
action_order: int = Field(1, ge=1)
|
|
action_name: Optional[str] = None
|
|
parameters: Optional[Dict[str, Any]] = None
|
|
template_id: Optional[int] = None
|
|
output_format: str = "DOCX"
|
|
custom_filename_template: Optional[str] = None
|
|
email_template_id: Optional[int] = None
|
|
email_recipients: Optional[List[str]] = None
|
|
email_subject_template: Optional[str] = None
|
|
condition: Optional[Dict[str, Any]] = None
|
|
continue_on_failure: bool = False
|
|
|
|
|
|
class WorkflowActionUpdate(BaseModel):
|
|
action_type: Optional[WorkflowActionType] = None
|
|
action_order: Optional[int] = None
|
|
action_name: Optional[str] = None
|
|
parameters: Optional[Dict[str, Any]] = None
|
|
template_id: Optional[int] = None
|
|
output_format: Optional[str] = None
|
|
custom_filename_template: Optional[str] = None
|
|
email_template_id: Optional[int] = None
|
|
email_recipients: Optional[List[str]] = None
|
|
email_subject_template: Optional[str] = None
|
|
condition: Optional[Dict[str, Any]] = None
|
|
continue_on_failure: Optional[bool] = None
|
|
|
|
|
|
class WorkflowResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
description: Optional[str]
|
|
status: WorkflowStatus
|
|
trigger_type: WorkflowTriggerType
|
|
trigger_conditions: Optional[Dict[str, Any]]
|
|
delay_minutes: int
|
|
max_retries: int
|
|
priority: int
|
|
category: Optional[str]
|
|
tags: Optional[List[str]]
|
|
created_by: Optional[str]
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
last_triggered_at: Optional[datetime]
|
|
execution_count: int
|
|
success_count: int
|
|
failure_count: int
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class WorkflowActionResponse(BaseModel):
|
|
id: int
|
|
workflow_id: int
|
|
action_type: WorkflowActionType
|
|
action_order: int
|
|
action_name: Optional[str]
|
|
parameters: Optional[Dict[str, Any]]
|
|
template_id: Optional[int]
|
|
output_format: str
|
|
condition: Optional[Dict[str, Any]]
|
|
continue_on_failure: bool
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class WorkflowExecutionResponse(BaseModel):
|
|
id: int
|
|
workflow_id: int
|
|
triggered_by_event_id: Optional[str]
|
|
triggered_by_event_type: Optional[str]
|
|
context_file_no: Optional[str]
|
|
context_client_id: Optional[str]
|
|
status: ExecutionStatus
|
|
started_at: Optional[datetime]
|
|
completed_at: Optional[datetime]
|
|
execution_duration_seconds: Optional[int]
|
|
retry_count: int
|
|
error_message: Optional[str]
|
|
generated_documents: Optional[List[Dict[str, Any]]]
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class EventLogCreate(BaseModel):
|
|
event_type: str
|
|
event_source: str
|
|
file_no: Optional[str] = None
|
|
client_id: Optional[str] = None
|
|
resource_type: Optional[str] = None
|
|
resource_id: Optional[str] = None
|
|
event_data: Optional[Dict[str, Any]] = None
|
|
previous_state: Optional[Dict[str, Any]] = None
|
|
new_state: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
class EventLogResponse(BaseModel):
|
|
id: int
|
|
event_id: str
|
|
event_type: str
|
|
event_source: str
|
|
file_no: Optional[str]
|
|
client_id: Optional[str]
|
|
resource_type: Optional[str]
|
|
resource_id: Optional[str]
|
|
event_data: Optional[Dict[str, Any]]
|
|
processed: bool
|
|
triggered_workflows: Optional[List[int]]
|
|
occurred_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class WorkflowTestRequest(BaseModel):
|
|
event_type: str
|
|
event_data: Optional[Dict[str, Any]] = None
|
|
file_no: Optional[str] = None
|
|
client_id: Optional[str] = None
|
|
|
|
|
|
class WorkflowStatsResponse(BaseModel):
|
|
total_workflows: int
|
|
active_workflows: int
|
|
total_executions: int
|
|
successful_executions: int
|
|
failed_executions: int
|
|
pending_executions: int
|
|
workflows_by_trigger_type: Dict[str, int]
|
|
executions_by_day: List[Dict[str, Any]]
|
|
|
|
|
|
# Workflow CRUD endpoints
|
|
@router.post("/workflows/", response_model=WorkflowResponse)
|
|
async def create_workflow(
|
|
workflow_data: WorkflowCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Create a new document workflow"""
|
|
|
|
# Check for duplicate names
|
|
existing = db.query(DocumentWorkflow).filter(
|
|
DocumentWorkflow.name == workflow_data.name
|
|
).first()
|
|
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Workflow with name '{workflow_data.name}' already exists"
|
|
)
|
|
|
|
# Validate cron expression if provided
|
|
if workflow_data.schedule_cron:
|
|
try:
|
|
from croniter import croniter
|
|
croniter(workflow_data.schedule_cron)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Invalid cron expression"
|
|
)
|
|
|
|
# Create workflow
|
|
workflow = DocumentWorkflow(
|
|
name=workflow_data.name,
|
|
description=workflow_data.description,
|
|
trigger_type=workflow_data.trigger_type,
|
|
trigger_conditions=workflow_data.trigger_conditions,
|
|
delay_minutes=workflow_data.delay_minutes,
|
|
max_retries=workflow_data.max_retries,
|
|
retry_delay_minutes=workflow_data.retry_delay_minutes,
|
|
timeout_minutes=workflow_data.timeout_minutes,
|
|
file_type_filter=workflow_data.file_type_filter,
|
|
status_filter=workflow_data.status_filter,
|
|
attorney_filter=workflow_data.attorney_filter,
|
|
client_filter=workflow_data.client_filter,
|
|
schedule_cron=workflow_data.schedule_cron,
|
|
schedule_timezone=workflow_data.schedule_timezone,
|
|
priority=workflow_data.priority,
|
|
category=workflow_data.category,
|
|
tags=workflow_data.tags,
|
|
created_by=current_user.username,
|
|
status=WorkflowStatus.ACTIVE
|
|
)
|
|
|
|
db.add(workflow)
|
|
db.commit()
|
|
db.refresh(workflow)
|
|
|
|
return workflow
|
|
|
|
|
|
@router.get("/workflows/", response_model=List[WorkflowResponse])
|
|
async def list_workflows(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
status: Optional[WorkflowStatus] = Query(None),
|
|
trigger_type: Optional[WorkflowTriggerType] = Query(None),
|
|
category: Optional[str] = Query(None),
|
|
search: Optional[str] = Query(None),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""List workflows with filtering options"""
|
|
|
|
query = db.query(DocumentWorkflow)
|
|
|
|
if status:
|
|
query = query.filter(DocumentWorkflow.status == status)
|
|
|
|
if trigger_type:
|
|
query = query.filter(DocumentWorkflow.trigger_type == trigger_type)
|
|
|
|
if category:
|
|
query = query.filter(DocumentWorkflow.category == category)
|
|
|
|
if search:
|
|
search_filter = f"%{search}%"
|
|
query = query.filter(
|
|
or_(
|
|
DocumentWorkflow.name.ilike(search_filter),
|
|
DocumentWorkflow.description.ilike(search_filter)
|
|
)
|
|
)
|
|
|
|
query = query.order_by(DocumentWorkflow.priority.desc(), DocumentWorkflow.name)
|
|
workflows, _ = paginate_with_total(query, skip, limit, False)
|
|
|
|
return workflows
|
|
|
|
|
|
@router.get("/workflows/{workflow_id}", response_model=WorkflowResponse)
|
|
async def get_workflow(
|
|
workflow_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get a specific workflow by ID"""
|
|
|
|
workflow = db.query(DocumentWorkflow).filter(
|
|
DocumentWorkflow.id == workflow_id
|
|
).first()
|
|
|
|
if not workflow:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Workflow not found"
|
|
)
|
|
|
|
return workflow
|
|
|
|
|
|
@router.put("/workflows/{workflow_id}", response_model=WorkflowResponse)
|
|
async def update_workflow(
|
|
workflow_id: int,
|
|
workflow_data: WorkflowUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Update a workflow"""
|
|
|
|
workflow = db.query(DocumentWorkflow).filter(
|
|
DocumentWorkflow.id == workflow_id
|
|
).first()
|
|
|
|
if not workflow:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Workflow not found"
|
|
)
|
|
|
|
# Update fields that are provided
|
|
update_data = workflow_data.dict(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(workflow, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(workflow)
|
|
|
|
return workflow
|
|
|
|
|
|
@router.delete("/workflows/{workflow_id}")
|
|
async def delete_workflow(
|
|
workflow_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Delete a workflow (soft delete by setting status to archived)"""
|
|
|
|
workflow = db.query(DocumentWorkflow).filter(
|
|
DocumentWorkflow.id == workflow_id
|
|
).first()
|
|
|
|
if not workflow:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Workflow not found"
|
|
)
|
|
|
|
# Soft delete
|
|
workflow.status = WorkflowStatus.ARCHIVED
|
|
db.commit()
|
|
|
|
return {"message": "Workflow archived successfully"}
|
|
|
|
|
|
# Workflow Actions endpoints
|
|
@router.post("/workflows/{workflow_id}/actions", response_model=WorkflowActionResponse)
|
|
async def create_workflow_action(
|
|
workflow_id: int,
|
|
action_data: WorkflowActionCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Create a new action for a workflow"""
|
|
|
|
workflow = db.query(DocumentWorkflow).filter(
|
|
DocumentWorkflow.id == workflow_id
|
|
).first()
|
|
|
|
if not workflow:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Workflow not found"
|
|
)
|
|
|
|
action = WorkflowAction(
|
|
workflow_id=workflow_id,
|
|
action_type=action_data.action_type,
|
|
action_order=action_data.action_order,
|
|
action_name=action_data.action_name,
|
|
parameters=action_data.parameters,
|
|
template_id=action_data.template_id,
|
|
output_format=action_data.output_format,
|
|
custom_filename_template=action_data.custom_filename_template,
|
|
email_template_id=action_data.email_template_id,
|
|
email_recipients=action_data.email_recipients,
|
|
email_subject_template=action_data.email_subject_template,
|
|
condition=action_data.condition,
|
|
continue_on_failure=action_data.continue_on_failure
|
|
)
|
|
|
|
db.add(action)
|
|
db.commit()
|
|
db.refresh(action)
|
|
|
|
return action
|
|
|
|
|
|
@router.get("/workflows/{workflow_id}/actions", response_model=List[WorkflowActionResponse])
|
|
async def list_workflow_actions(
|
|
workflow_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""List actions for a workflow"""
|
|
|
|
actions = db.query(WorkflowAction).filter(
|
|
WorkflowAction.workflow_id == workflow_id
|
|
).order_by(WorkflowAction.action_order, WorkflowAction.id).all()
|
|
|
|
return actions
|
|
|
|
|
|
@router.put("/workflows/{workflow_id}/actions/{action_id}", response_model=WorkflowActionResponse)
|
|
async def update_workflow_action(
|
|
workflow_id: int,
|
|
action_id: int,
|
|
action_data: WorkflowActionUpdate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Update a workflow action"""
|
|
|
|
action = db.query(WorkflowAction).filter(
|
|
WorkflowAction.id == action_id,
|
|
WorkflowAction.workflow_id == workflow_id
|
|
).first()
|
|
|
|
if not action:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Action not found"
|
|
)
|
|
|
|
# Update fields that are provided
|
|
update_data = action_data.dict(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(action, field, value)
|
|
|
|
db.commit()
|
|
db.refresh(action)
|
|
|
|
return action
|
|
|
|
|
|
@router.delete("/workflows/{workflow_id}/actions/{action_id}")
|
|
async def delete_workflow_action(
|
|
workflow_id: int,
|
|
action_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Delete a workflow action"""
|
|
|
|
action = db.query(WorkflowAction).filter(
|
|
WorkflowAction.id == action_id,
|
|
WorkflowAction.workflow_id == workflow_id
|
|
).first()
|
|
|
|
if not action:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Action not found"
|
|
)
|
|
|
|
db.delete(action)
|
|
db.commit()
|
|
|
|
return {"message": "Action deleted successfully"}
|
|
|
|
|
|
# Event Management endpoints
|
|
@router.post("/events/", response_model=dict)
|
|
async def log_event(
|
|
event_data: EventLogCreate,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Log a system event that may trigger workflows"""
|
|
|
|
processor = EventProcessor(db)
|
|
event_id = await processor.log_event(
|
|
event_type=event_data.event_type,
|
|
event_source=event_data.event_source,
|
|
file_no=event_data.file_no,
|
|
client_id=event_data.client_id,
|
|
user_id=current_user.id,
|
|
resource_type=event_data.resource_type,
|
|
resource_id=event_data.resource_id,
|
|
event_data=event_data.event_data,
|
|
previous_state=event_data.previous_state,
|
|
new_state=event_data.new_state
|
|
)
|
|
|
|
return {"event_id": event_id, "message": "Event logged successfully"}
|
|
|
|
|
|
@router.get("/events/", response_model=List[EventLogResponse])
|
|
async def list_events(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
event_type: Optional[str] = Query(None),
|
|
file_no: Optional[str] = Query(None),
|
|
processed: Optional[bool] = Query(None),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""List system events"""
|
|
|
|
query = db.query(EventLog)
|
|
|
|
if event_type:
|
|
query = query.filter(EventLog.event_type == event_type)
|
|
|
|
if file_no:
|
|
query = query.filter(EventLog.file_no == file_no)
|
|
|
|
if processed is not None:
|
|
query = query.filter(EventLog.processed == processed)
|
|
|
|
query = query.order_by(desc(EventLog.occurred_at))
|
|
events, _ = paginate_with_total(query, skip, limit, False)
|
|
|
|
return events
|
|
|
|
|
|
# Execution Management endpoints
|
|
@router.get("/executions/", response_model=List[WorkflowExecutionResponse])
|
|
async def list_executions(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
workflow_id: Optional[int] = Query(None),
|
|
status: Optional[ExecutionStatus] = Query(None),
|
|
file_no: Optional[str] = Query(None),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""List workflow executions"""
|
|
|
|
query = db.query(WorkflowExecution)
|
|
|
|
if workflow_id:
|
|
query = query.filter(WorkflowExecution.workflow_id == workflow_id)
|
|
|
|
if status:
|
|
query = query.filter(WorkflowExecution.status == status)
|
|
|
|
if file_no:
|
|
query = query.filter(WorkflowExecution.context_file_no == file_no)
|
|
|
|
query = query.order_by(desc(WorkflowExecution.started_at))
|
|
executions, _ = paginate_with_total(query, skip, limit, False)
|
|
|
|
return executions
|
|
|
|
|
|
@router.get("/executions/{execution_id}", response_model=WorkflowExecutionResponse)
|
|
async def get_execution(
|
|
execution_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get details of a specific execution"""
|
|
|
|
execution = db.query(WorkflowExecution).filter(
|
|
WorkflowExecution.id == execution_id
|
|
).first()
|
|
|
|
if not execution:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Execution not found"
|
|
)
|
|
|
|
return execution
|
|
|
|
|
|
@router.post("/executions/{execution_id}/retry")
|
|
async def retry_execution(
|
|
execution_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Retry a failed workflow execution"""
|
|
|
|
execution = db.query(WorkflowExecution).filter(
|
|
WorkflowExecution.id == execution_id
|
|
).first()
|
|
|
|
if not execution:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Execution not found"
|
|
)
|
|
|
|
if execution.status not in [ExecutionStatus.FAILED, ExecutionStatus.RETRYING]:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Only failed executions can be retried"
|
|
)
|
|
|
|
# Reset execution for retry
|
|
execution.status = ExecutionStatus.PENDING
|
|
execution.error_message = None
|
|
execution.next_retry_at = None
|
|
execution.retry_count += 1
|
|
|
|
db.commit()
|
|
|
|
# Execute the workflow
|
|
executor = WorkflowExecutor(db)
|
|
success = await executor.execute_workflow(execution_id)
|
|
|
|
return {"message": "Execution retried", "success": success}
|
|
|
|
|
|
# Testing and Management endpoints
|
|
@router.post("/workflows/{workflow_id}/test")
|
|
async def test_workflow(
|
|
workflow_id: int,
|
|
test_request: WorkflowTestRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Test a workflow with simulated event data"""
|
|
|
|
workflow = db.query(DocumentWorkflow).filter(
|
|
DocumentWorkflow.id == workflow_id
|
|
).first()
|
|
|
|
if not workflow:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Workflow not found"
|
|
)
|
|
|
|
# Create a test event
|
|
processor = EventProcessor(db)
|
|
event_id = await processor.log_event(
|
|
event_type=test_request.event_type,
|
|
event_source="workflow_test",
|
|
file_no=test_request.file_no,
|
|
client_id=test_request.client_id,
|
|
user_id=current_user.id,
|
|
event_data=test_request.event_data or {}
|
|
)
|
|
|
|
return {"message": "Test event logged", "event_id": event_id}
|
|
|
|
|
|
@router.get("/stats", response_model=WorkflowStatsResponse)
|
|
async def get_workflow_stats(
|
|
days: int = Query(30, ge=1, le=365),
|
|
db: Session = Depends(get_db),
|
|
current_user: User = Depends(get_current_user)
|
|
):
|
|
"""Get workflow system statistics"""
|
|
|
|
# Basic counts
|
|
total_workflows = db.query(func.count(DocumentWorkflow.id)).scalar()
|
|
active_workflows = db.query(func.count(DocumentWorkflow.id)).filter(
|
|
DocumentWorkflow.status == WorkflowStatus.ACTIVE
|
|
).scalar()
|
|
|
|
total_executions = db.query(func.count(WorkflowExecution.id)).scalar()
|
|
successful_executions = db.query(func.count(WorkflowExecution.id)).filter(
|
|
WorkflowExecution.status == ExecutionStatus.COMPLETED
|
|
).scalar()
|
|
failed_executions = db.query(func.count(WorkflowExecution.id)).filter(
|
|
WorkflowExecution.status == ExecutionStatus.FAILED
|
|
).scalar()
|
|
pending_executions = db.query(func.count(WorkflowExecution.id)).filter(
|
|
WorkflowExecution.status.in_([ExecutionStatus.PENDING, ExecutionStatus.RUNNING])
|
|
).scalar()
|
|
|
|
# Workflows by trigger type
|
|
trigger_stats = db.query(
|
|
DocumentWorkflow.trigger_type,
|
|
func.count(DocumentWorkflow.id)
|
|
).group_by(DocumentWorkflow.trigger_type).all()
|
|
|
|
workflows_by_trigger_type = {
|
|
trigger.value: count for trigger, count in trigger_stats
|
|
}
|
|
|
|
# Executions by day (for the chart)
|
|
cutoff_date = datetime.now() - timedelta(days=days)
|
|
daily_stats = db.query(
|
|
func.date(WorkflowExecution.started_at).label('date'),
|
|
func.count(WorkflowExecution.id).label('count'),
|
|
func.sum(func.case((WorkflowExecution.status == ExecutionStatus.COMPLETED, 1), else_=0)).label('successful'),
|
|
func.sum(func.case((WorkflowExecution.status == ExecutionStatus.FAILED, 1), else_=0)).label('failed')
|
|
).filter(
|
|
WorkflowExecution.started_at >= cutoff_date
|
|
).group_by(func.date(WorkflowExecution.started_at)).all()
|
|
|
|
executions_by_day = [
|
|
{
|
|
'date': row.date.isoformat() if row.date else None,
|
|
'total': row.count,
|
|
'successful': row.successful or 0,
|
|
'failed': row.failed or 0
|
|
}
|
|
for row in daily_stats
|
|
]
|
|
|
|
return WorkflowStatsResponse(
|
|
total_workflows=total_workflows or 0,
|
|
active_workflows=active_workflows or 0,
|
|
total_executions=total_executions or 0,
|
|
successful_executions=successful_executions or 0,
|
|
failed_executions=failed_executions or 0,
|
|
pending_executions=pending_executions or 0,
|
|
workflows_by_trigger_type=workflows_by_trigger_type,
|
|
executions_by_day=executions_by_day
|
|
)
|