feat: implement publish workflow as evaluation.

This commit is contained in:
FFXN 2026-03-27 17:34:40 +08:00
parent eaa660e12f
commit c14e57ac44
4 changed files with 138 additions and 3 deletions

View File

@ -866,6 +866,54 @@ class PublishedWorkflowApi(Resource):
}
@console_ns.route("/apps/<uuid:app_id>/workflows/publish/evaluation")
class EvaluationPublishedWorkflowApi(Resource):
@console_ns.doc("publish_evaluation_workflow")
@console_ns.doc(description="Publish draft workflow as evaluation workflow")
@console_ns.doc(params={"app_id": "Application ID"})
@console_ns.expect(console_ns.models[PublishWorkflowPayload.__name__])
@console_ns.response(200, "Evaluation workflow published successfully")
@console_ns.response(400, "Invalid workflow or unsupported node type")
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
@edit_permission_required
def post(self, app_model: App):
"""
Publish draft workflow as evaluation workflow.
Evaluation workflows cannot include trigger or human-input nodes.
"""
current_user, _ = current_account_with_tenant()
args = PublishWorkflowPayload.model_validate(console_ns.payload or {})
workflow_service = WorkflowService()
with Session(db.engine) as session:
workflow = workflow_service.publish_evaluation_workflow(
session=session,
app_model=app_model,
account=current_user,
marked_name=args.marked_name or "",
marked_comment=args.marked_comment or "",
)
# Keep workflow_id aligned with the latest published workflow.
app_model_in_session = session.get(App, app_model.id)
if app_model_in_session:
app_model_in_session.workflow_id = workflow.id
app_model_in_session.updated_by = current_user.id
app_model_in_session.updated_at = naive_utc_now()
workflow_created_at = TimestampField().format(workflow.created_at)
session.commit()
return {
"result": "success",
"created_at": workflow_created_at,
}
@console_ns.route("/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
class DefaultBlockConfigsApi(Resource):
@console_ns.doc("get_default_block_configs")

View File

@ -106,6 +106,7 @@ class WorkflowType(StrEnum):
CHAT = "chat"
RAG_PIPELINE = "rag-pipeline"
SNIPPET = "snippet"
EVALUATION = "evaluation"
class WorkflowExecutionStatus(StrEnum):

View File

@ -100,6 +100,7 @@ class WorkflowType(StrEnum):
CHAT = "chat"
RAG_PIPELINE = "rag-pipeline"
SNIPPET = "snippet"
EVALUATION = "evaluation"
@classmethod
def value_of(cls, value: str) -> "WorkflowType":

View File

@ -16,7 +16,7 @@ from core.app.file_access import DatabaseFileAccessController
from core.plugin.impl.model_runtime_factory import create_plugin_model_assembly, create_plugin_provider_manager
from core.repositories import DifyCoreRepositoryFactory
from core.repositories.human_input_repository import FormCreateParams, HumanInputFormRepositoryImpl
from core.trigger.constants import is_trigger_node_type
from core.trigger.constants import TRIGGER_NODE_TYPES, is_trigger_node_type
from core.workflow.human_input_compat import (
DeliveryChannelConfig,
normalize_human_input_node_data_for_graph,
@ -93,6 +93,15 @@ class WorkflowService:
Workflow Service
"""
# Centralized unsupported node types for evaluation workflow publishing.
# Keep this set updated when evaluation workflow constraints change.
EVALUATION_UNSUPPORTED_NODE_TYPES: frozenset[str] = frozenset(
{
BuiltinNodeTypes.HUMAN_INPUT,
*TRIGGER_NODE_TYPES,
}
)
def __init__(self, session_maker: sessionmaker | None = None):
"""Initialize WorkflowService with repository dependencies."""
if session_maker is None:
@ -394,6 +403,82 @@ class WorkflowService:
# return new workflow
return workflow
def publish_evaluation_workflow(
self,
*,
session: Session,
app_model: App,
account: Account,
marked_name: str = "",
marked_comment: str = "",
) -> Workflow:
"""Publish draft workflow as an evaluation workflow version.
Compared to standard publish:
- force published workflow type to ``evaluation``;
- reject graphs containing trigger or human-input nodes.
"""
draft_workflow_stmt = select(Workflow).where(
Workflow.tenant_id == app_model.tenant_id,
Workflow.app_id == app_model.id,
Workflow.version == Workflow.VERSION_DRAFT,
)
draft_workflow = session.scalar(draft_workflow_stmt)
if not draft_workflow:
raise ValueError("No valid workflow found.")
# Validate credentials before publishing, for credential policy check
from services.feature_service import FeatureService
if FeatureService.get_system_features().plugin_manager.enabled:
self._validate_workflow_credentials(draft_workflow)
# validate graph structure
self.validate_graph_structure(graph=draft_workflow.graph_dict)
self._validate_evaluation_workflow_nodes(draft_workflow)
workflow = Workflow.new(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type=WorkflowType.EVALUATION.value,
version=Workflow.version_from_datetime(naive_utc_now()),
graph=draft_workflow.graph,
created_by=account.id,
environment_variables=draft_workflow.environment_variables,
conversation_variables=draft_workflow.conversation_variables,
marked_name=marked_name,
marked_comment=marked_comment,
rag_pipeline_variables=draft_workflow.rag_pipeline_variables,
features=draft_workflow.features,
)
session.add(workflow)
# trigger app workflow events
app_published_workflow_was_updated.send(app_model, published_workflow=workflow)
return workflow
@staticmethod
def _validate_evaluation_workflow_nodes(workflow: Workflow) -> None:
"""Ensure evaluation workflows do not contain unsupported node types."""
disallowed_nodes: list[tuple[str, str]] = []
for node_id, node_data in workflow.walk_nodes():
node_type = node_data.get("type")
if not isinstance(node_type, str):
continue
if node_type in WorkflowService.EVALUATION_UNSUPPORTED_NODE_TYPES:
disallowed_nodes.append((node_id, node_type))
if not disallowed_nodes:
return
formatted_nodes = ", ".join(f"{node_id}:{node_type}" for node_id, node_type in disallowed_nodes)
raise ValueError(
"Evaluation workflow cannot contain trigger or human-input nodes. "
f"Found disallowed nodes: {formatted_nodes}"
)
def _validate_workflow_credentials(self, workflow: Workflow) -> None:
"""
Validate all credentials in workflow nodes before publishing.
@ -1538,8 +1623,8 @@ def _setup_variable_pool(
"workflow_execution_id": str(uuid.uuid4()),
}
# Only add chatflow-specific variables for non-workflow types.
if workflow.type != WorkflowType.WORKFLOW:
# Only add chatflow-specific variables for chat-like workflow types.
if workflow.type not in {WorkflowType.WORKFLOW, WorkflowType.EVALUATION}:
system_variable_values.update(
{
"query": query,