from typing import Literal from uuid import UUID from flask_restx import Resource from pydantic import BaseModel, Field, field_validator from configs import dify_config from controllers.common.errors import NotFoundError from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models from controllers.console import console_ns from controllers.console.app.wraps import get_app_model from controllers.console.flask_admission import console_account_admission from controllers.console.wraps import ( RBACPermission, RBACResourceScope, model_validate, ) from extensions.ext_application_services import application_services from fields.base import ResponseModel from fields.workflow_run_fields import ( AdvancedChatWorkflowRunPaginationResponse, WorkflowRunCountResponse, WorkflowRunDetailResponse, WorkflowRunNodeExecutionListResponse, WorkflowRunNodeExecutionResponse, WorkflowRunPaginationResponse, ) from libs.custom_inputs import time_duration from libs.helper import dump_response, uuid_value from machinery.context import RequestContext from models import App, AppMode, WorkflowRunTriggeredFrom from services.workflow_run_service import WorkflowRunListArgs def _build_backstage_input_url(form_token: str | None) -> str | None: if not form_token: return None base_url = dify_config.APP_WEB_URL if not base_url: return None return f"{base_url.rstrip('/')}/form/{form_token}" class WorkflowRunListQuery(BaseModel): last_id: str | None = Field(default=None, description="Last run ID for pagination") limit: int = Field(default=20, ge=1, le=100, description="Number of items per page (1-100)") status: Literal["running", "succeeded", "failed", "stopped", "partial-succeeded"] | None = Field( default=None, description="Workflow run status filter" ) triggered_from: Literal["debugging", "app-run"] | None = Field( default=None, description="Filter by trigger source: debugging or app-run. Default: debugging" ) @field_validator("last_id") @classmethod def validate_last_id(cls, value: str | None) -> str | None: if value is None: return value return uuid_value(value) class WorkflowRunCountQuery(BaseModel): status: Literal["running", "succeeded", "failed", "stopped", "partial-succeeded"] | None = Field( default=None, description="Workflow run status filter" ) time_range: str | None = Field( default=None, description=( "Filter by time range (optional): e.g., 7d (7 days), 4h (4 hours), " "30m (30 minutes), 30s (30 seconds). Filters by created_at field." ), ) triggered_from: Literal["debugging", "app-run"] | None = Field( default=None, description="Filter by trigger source: debugging or app-run. Default: debugging" ) @field_validator("time_range") @classmethod def validate_time_range(cls, value: str | None) -> str | None: if value is None: return value return time_duration(value) def _workflow_run_list_args(req_data: WorkflowRunListQuery) -> WorkflowRunListArgs: args: WorkflowRunListArgs = {"limit": req_data.limit} if req_data.last_id is not None: args["last_id"] = req_data.last_id if req_data.status is not None: args["status"] = req_data.status return args def _triggered_from(value: str | None) -> WorkflowRunTriggeredFrom: return WorkflowRunTriggeredFrom(value) if value else WorkflowRunTriggeredFrom.DEBUGGING class HumanInputPauseTypeResponse(ResponseModel): type: Literal["human_input"] form_id: str backstage_input_url: str | None = None class PausedNodeResponse(ResponseModel): node_id: str node_title: str pause_type: HumanInputPauseTypeResponse class WorkflowPauseDetailsResponse(ResponseModel): paused_at: str | None = None paused_nodes: list[PausedNodeResponse] register_schema_models( console_ns, WorkflowRunListQuery, WorkflowRunCountQuery, ) register_response_schema_models( console_ns, AdvancedChatWorkflowRunPaginationResponse, WorkflowRunPaginationResponse, WorkflowRunCountResponse, WorkflowRunDetailResponse, WorkflowRunNodeExecutionResponse, WorkflowRunNodeExecutionListResponse, HumanInputPauseTypeResponse, PausedNodeResponse, WorkflowPauseDetailsResponse, ) @console_ns.route("/apps//advanced-chat/workflow-runs") class AdvancedChatAppWorkflowRunListApi(Resource): @console_ns.doc("get_advanced_chat_workflow_runs") @console_ns.doc(description="Get advanced chat workflow run list") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.doc(params=query_params_from_model(WorkflowRunListQuery)) @console_ns.response( 200, "Workflow runs retrieved successfully", console_ns.models[AdvancedChatWorkflowRunPaginationResponse.__name__], ) @console_account_admission( rbac_resource_scope=RBACResourceScope.APP, rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, ) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @model_validate(WorkflowRunListQuery) def get(self, req_data: WorkflowRunListQuery, request_context: RequestContext, app_model: App): """ Get advanced chat app workflow run list """ result = application_services().workflow_runs.get_paginate_advanced_chat_workflow_runs( request_context, app_id=app_model.id, args=_workflow_run_list_args(req_data), triggered_from=_triggered_from(req_data.triggered_from), ) return dump_response(AdvancedChatWorkflowRunPaginationResponse, result) @console_ns.route("/apps//advanced-chat/workflow-runs/count") class AdvancedChatAppWorkflowRunCountApi(Resource): @console_ns.doc("get_advanced_chat_workflow_runs_count") @console_ns.doc(description="Get advanced chat workflow runs count statistics") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.doc(params=query_params_from_model(WorkflowRunCountQuery)) @console_ns.response( 200, "Workflow runs count retrieved successfully", console_ns.models[WorkflowRunCountResponse.__name__], ) @console_account_admission( rbac_resource_scope=RBACResourceScope.APP, rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, ) @get_app_model(mode=[AppMode.ADVANCED_CHAT]) @model_validate(WorkflowRunCountQuery) def get(self, req_data: WorkflowRunCountQuery, request_context: RequestContext, app_model: App): """ Get advanced chat workflow runs count statistics """ result = application_services().workflow_runs.get_workflow_runs_count( request_context, app_id=app_model.id, status=req_data.status, time_range=req_data.time_range, triggered_from=_triggered_from(req_data.triggered_from), ) return dump_response(WorkflowRunCountResponse, result) @console_ns.route("/apps//workflow-runs") class WorkflowRunListApi(Resource): @console_ns.doc("get_workflow_runs") @console_ns.doc(description="Get workflow run list") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.doc(params=query_params_from_model(WorkflowRunListQuery)) @console_ns.response( 200, "Workflow runs retrieved successfully", console_ns.models[WorkflowRunPaginationResponse.__name__], ) @console_account_admission( rbac_resource_scope=RBACResourceScope.APP, rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @model_validate(WorkflowRunListQuery) def get(self, req_data: WorkflowRunListQuery, request_context: RequestContext, app_model: App): """ Get workflow run list """ result = application_services().workflow_runs.get_paginate_workflow_runs( request_context, app_id=app_model.id, args=_workflow_run_list_args(req_data), triggered_from=_triggered_from(req_data.triggered_from), ) return dump_response(WorkflowRunPaginationResponse, result) @console_ns.route("/apps//workflow-runs/count") class WorkflowRunCountApi(Resource): @console_ns.doc("get_workflow_runs_count") @console_ns.doc(description="Get workflow runs count statistics") @console_ns.doc(params={"app_id": "Application ID"}) @console_ns.doc(params=query_params_from_model(WorkflowRunCountQuery)) @console_ns.response( 200, "Workflow runs count retrieved successfully", console_ns.models[WorkflowRunCountResponse.__name__], ) @console_account_admission( rbac_resource_scope=RBACResourceScope.APP, rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) @model_validate(WorkflowRunCountQuery) def get(self, req_data: WorkflowRunCountQuery, request_context: RequestContext, app_model: App): """ Get workflow runs count statistics """ result = application_services().workflow_runs.get_workflow_runs_count( request_context, app_id=app_model.id, status=req_data.status, time_range=req_data.time_range, triggered_from=_triggered_from(req_data.triggered_from), ) return dump_response(WorkflowRunCountResponse, result) @console_ns.route("/apps//workflow-runs/") class WorkflowRunDetailApi(Resource): @console_ns.doc("get_workflow_run_detail") @console_ns.doc(description="Get workflow run detail") @console_ns.doc(params={"app_id": "Application ID", "run_id": "Workflow run ID"}) @console_ns.response( 200, "Workflow run detail retrieved successfully", console_ns.models[WorkflowRunDetailResponse.__name__], ) @console_ns.response(404, "Workflow run not found") @console_account_admission( rbac_resource_scope=RBACResourceScope.APP, rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, request_context: RequestContext, app_model: App, run_id: UUID): """ Get workflow run detail """ workflow_run = application_services().workflow_runs.get_workflow_run( request_context, app_id=app_model.id, run_id=str(run_id), ) if workflow_run is None: raise NotFoundError("Workflow run not found") return dump_response(WorkflowRunDetailResponse, workflow_run) @console_ns.route("/apps//workflow-runs//node-executions") class WorkflowRunNodeExecutionListApi(Resource): @console_ns.doc("get_workflow_run_node_executions") @console_ns.doc(description="Get workflow run node execution list") @console_ns.doc(params={"app_id": "Application ID", "run_id": "Workflow run ID"}) @console_ns.response( 200, "Node executions retrieved successfully", console_ns.models[WorkflowRunNodeExecutionListResponse.__name__], ) @console_ns.response(404, "Workflow run not found") @console_account_admission( rbac_resource_scope=RBACResourceScope.APP, rbac_permission=RBACPermission.APP_CREATE_AND_MANAGEMENT, ) @get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW]) def get(self, request_context: RequestContext, app_model: App, run_id: UUID): """ Get workflow run node execution list """ node_executions = application_services().workflow_runs.get_workflow_run_node_executions( request_context, app_id=app_model.id, run_id=str(run_id), ) return dump_response(WorkflowRunNodeExecutionListResponse, {"data": node_executions}) @console_ns.route("/workflow//pause-details") class ConsoleWorkflowPauseDetailsApi(Resource): """Console API for getting workflow pause details.""" @console_ns.doc("get_workflow_pause_details") @console_ns.doc(description="Get workflow pause details") @console_ns.doc(params={"workflow_run_id": "Workflow run ID"}) @console_ns.response( 200, "Workflow pause details retrieved successfully", console_ns.models[WorkflowPauseDetailsResponse.__name__], ) @console_ns.response(404, "Workflow run not found") @console_account_admission() def get(self, request_context: RequestContext, workflow_run_id: str): """ Get workflow pause details. GET /console/api/workflow//pause-details Returns information about why and where the workflow is paused. """ details = application_services().workflow_runs.get_pause_details( request_context, workflow_run_id=workflow_run_id, ) if details is None: raise NotFoundError("Workflow run not found") return ( dump_response( WorkflowPauseDetailsResponse, { "paused_at": details.paused_at.isoformat() + "Z" if details.paused_at else None, "paused_nodes": [ { "node_id": node.node_id, "node_title": node.node_title, "pause_type": { "type": "human_input", "form_id": node.form_id, "backstage_input_url": _build_backstage_input_url(node.form_token), }, } for node in details.paused_nodes ], }, ), 200, )