Files
dify/api/services/workflow_runtime/workflow_generator.py
T

1274 lines
59 KiB
Python

from __future__ import annotations
import contextvars
import logging
import threading
import uuid
from collections.abc import Generator, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Literal, Protocol, overload, runtime_checkable
from flask import Flask, current_app
from pydantic import ValidationError
from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker
import contexts
from configs import dify_config
from core.app.app_config.features.file_upload.manager import FileUploadConfigManager
from core.app.apps.base_app_generator import BaseAppGenerator
from core.app.apps.base_app_queue_manager import AppQueueManager, PublishFrom
from core.app.apps.draft_variable_saver import DraftVariableSaver, DraftVariableSaverFactory, NoopDraftVariableSaver
from core.app.apps.exc import GenerateTaskStoppedError
from core.app.apps.workflow.active_workflow_tasks import active_workflow_task
from core.app.apps.workflow.app_config_manager import WorkflowAppConfigManager
from core.app.apps.workflow.generate_response_converter import WorkflowAppGenerateResponseConverter
from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom, WorkflowAppGenerateEntity
from core.app.entities.task_entities import (
WorkflowAppBlockingResponse,
WorkflowAppPausedBlockingResponse,
WorkflowAppStreamResponse,
)
from core.app.layers.pause_state_persist_layer import PauseStateLayerConfig
from core.db.session_factory import session_factory
from core.helper.trace_id_helper import (
extract_external_trace_id_from_args,
extract_parent_trace_context_from_args,
extract_trace_session_id_from_args,
)
from core.repositories import DifyCoreRepositoryFactory
from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository
from core.trigger.constants import is_trigger_node_type
from extensions.ext_database import db
from extensions.tracing import create_trace_scope
from factories import file_factory
from graphon.engine.filter import ResponseStreamFilter
from graphon.engine.layer import Layer
from graphon.enums import NodeType
from graphon.model_runtime.errors.invoke import InvokeAuthorizationError
from graphon.runtime import RuntimeState
from graphon.variable_loader import DUMMY_VARIABLE_LOADER, VariableLoader
from libs.flask_utils import preserve_flask_contexts
from models.account import Account
from models.enums import WorkflowRunTriggeredFrom
from models.model import App, AppMode, EndUser
from models.workflow import Workflow, WorkflowNodeExecutionTriggeredFrom
from repositories.entities.workflow_execution import WorkflowExecutionIdentity
from runtime_contracts.builder.execution import (
BuilderExecutionContext,
BuilderExecutionPolicyError,
BuilderExecutionRecorder,
ConfirmedToolExecutionContext,
JsonToolExecutionContext,
scalar_inputs_digest,
)
from runtime_contracts.builder.native.agent_context import BuilderAgentExecutionContext
from runtime_contracts.builder.native.contracts import NativeTestContextRef
from services.app.file_input_defaults import grant_file_default_access
from services.dify_builder.native_testing.context_binding import NativeContextUnavailable
from services.dify_builder.native_testing.observation_adapters import NativeObservationError, NativeObservationGuard
from services.dify_builder.native_testing.runtime_bridge import NativeRuntimeBridge
from services.workflow_draft_variable_service import DraftVarLoader, WorkflowDraftVariableService
from services.workflow_runtime.node_factory import get_default_root_node_id
from services.workflow_runtime.workflow_pipeline import WorkflowAppGenerateTaskPipeline
from services.workflow_runtime.workflow_queue import WorkflowAppQueueManager
from services.workflow_runtime.workflow_runner import WorkflowAppRunner
if TYPE_CHECKING:
from fields.workflow_run_fields import LoopNodeRunPayload
from services.dify_builder.native_testing.host_execution import NativeHostLaunch
from services.workflow_runtime.base_runner import WorkflowRunDriver
logger = logging.getLogger(__name__)
@runtime_checkable
class _DebugTraceArgs(Protocol):
@property
def trace_session_id(self) -> object: ...
def _extract_trace_session_id_from_debug_args(args: object) -> dict[str, str]:
if isinstance(args, Mapping):
return extract_trace_session_id_from_args(args)
if isinstance(args, _DebugTraceArgs):
try:
trace_session_id = args.trace_session_id
except AttributeError:
return {}
return extract_trace_session_id_from_args({"trace_session_id": trace_session_id})
return {}
def _no_draft_variable_saver(
app_id: str,
node_id: str,
node_type: NodeType,
node_execution_id: str,
enclosing_node_id: str | None = None,
) -> DraftVariableSaver:
"""``DraftVariableSaverFactory`` that never persists draft variables (existing no-op saver)."""
_ = app_id, node_id, node_type, node_execution_id, enclosing_node_id
return NoopDraftVariableSaver()
class BuilderExecutionAdmission(Protocol):
"""Transient service-owned admission, explicitly injected into native execution.
Owns both Builder policy variants: ``admit_raw`` refuses raw metadata for
the context it was built with, and ``__call__`` revalidates the reserved
sidecar and claims the launch identity once before any native node exists.
"""
def __call__(
self,
workflow: Workflow,
app: App,
entity: WorkflowAppGenerateEntity,
root_node_id: str,
resume: bool,
) -> BuilderExecutionRecorder: ...
def admit_raw(self, workflow: Workflow, app: App) -> None: ...
def on_refusal(self, error: BuilderExecutionPolicyError, *, preworker: bool = False) -> None: ...
def on_worker_finished(
self,
*,
context: BuilderExecutionContext | ConfirmedToolExecutionContext | JsonToolExecutionContext,
native_run_id: str,
task_id: str,
recorder: BuilderExecutionRecorder | None,
worker_thread: threading.Thread,
exit_kind: Literal["returned", "policy_refused", "stopped", "failed"],
) -> None: ...
class WorkflowAppGenerator(BaseAppGenerator):
def __init__(self, *, execution_driver: WorkflowRunDriver) -> None:
self._execution_driver = execution_driver
@staticmethod
def _ensure_snippet_start_node_in_worker(*, session: Session, workflow: Workflow) -> Workflow:
"""Re-apply snippet virtual Start injection after worker reloads workflow from DB."""
if workflow.kind_or_standard != "snippet":
return workflow
from models.snippet import CustomizedSnippet
from services.snippet_generate_service import SnippetGenerateService
snippet = session.scalar(
select(CustomizedSnippet).where(
CustomizedSnippet.id == workflow.app_id,
CustomizedSnippet.tenant_id == workflow.tenant_id,
)
)
if snippet is None:
return workflow
return SnippetGenerateService.ensure_start_node_for_worker(workflow, snippet)
@overload
def generate(
self,
*,
execution_identity: WorkflowExecutionIdentity,
app_model: App,
workflow: Workflow,
user: Account | EndUser,
args: Mapping[str, Any],
invoke_from: InvokeFrom,
streaming: Literal[True],
call_depth: int,
workflow_run_id: str | uuid.UUID | None = None,
triggered_from: WorkflowRunTriggeredFrom | None = None,
root_node_id: str | None = None,
graph_engine_layers: Sequence[Layer] = (),
allow_human_input: bool = True,
pause_state_config: PauseStateLayerConfig | None = None,
builder_execution: BuilderExecutionContext
| ConfirmedToolExecutionContext
| JsonToolExecutionContext
| None = None,
builder_execution_admit: BuilderExecutionAdmission | None = None,
builder_agent_execution: BuilderAgentExecutionContext | None = None,
native_test_required: bool = False,
native_test_ref: NativeTestContextRef | None = None,
native_runtime: NativeRuntimeBridge | None = None,
native_host: NativeHostLaunch | None = None,
builder_mocked_node_ids: frozenset[str] = frozenset(),
builder_expected_revision: str | None = None,
builder_tool_credential_bindings: Mapping[str, str] | None = None,
) -> Generator[Mapping[str, Any] | str]: ...
@overload
def generate(
self,
*,
execution_identity: WorkflowExecutionIdentity,
app_model: App,
workflow: Workflow,
user: Account | EndUser,
args: Mapping[str, Any],
invoke_from: InvokeFrom,
streaming: Literal[False],
call_depth: int,
workflow_run_id: str | uuid.UUID | None = None,
triggered_from: WorkflowRunTriggeredFrom | None = None,
root_node_id: str | None = None,
graph_engine_layers: Sequence[Layer] = (),
allow_human_input: bool = True,
pause_state_config: PauseStateLayerConfig | None = None,
builder_execution: BuilderExecutionContext
| ConfirmedToolExecutionContext
| JsonToolExecutionContext
| None = None,
builder_execution_admit: BuilderExecutionAdmission | None = None,
builder_agent_execution: BuilderAgentExecutionContext | None = None,
native_test_required: bool = False,
native_test_ref: NativeTestContextRef | None = None,
native_runtime: NativeRuntimeBridge | None = None,
native_host: NativeHostLaunch | None = None,
builder_mocked_node_ids: frozenset[str] = frozenset(),
builder_expected_revision: str | None = None,
builder_tool_credential_bindings: Mapping[str, str] | None = None,
) -> Mapping[str, Any]: ...
@overload
def generate(
self,
*,
execution_identity: WorkflowExecutionIdentity,
app_model: App,
workflow: Workflow,
user: Account | EndUser,
args: Mapping[str, Any],
invoke_from: InvokeFrom,
streaming: bool,
call_depth: int,
workflow_run_id: str | uuid.UUID | None = None,
triggered_from: WorkflowRunTriggeredFrom | None = None,
root_node_id: str | None = None,
graph_engine_layers: Sequence[Layer] = (),
allow_human_input: bool = True,
pause_state_config: PauseStateLayerConfig | None = None,
builder_execution: BuilderExecutionContext
| ConfirmedToolExecutionContext
| JsonToolExecutionContext
| None = None,
builder_execution_admit: BuilderExecutionAdmission | None = None,
builder_agent_execution: BuilderAgentExecutionContext | None = None,
native_test_required: bool = False,
native_test_ref: NativeTestContextRef | None = None,
native_runtime: NativeRuntimeBridge | None = None,
native_host: NativeHostLaunch | None = None,
builder_mocked_node_ids: frozenset[str] = frozenset(),
builder_expected_revision: str | None = None,
builder_tool_credential_bindings: Mapping[str, str] | None = None,
) -> Mapping[str, Any] | Generator[Mapping[str, Any] | str]: ...
def generate(
self,
*,
execution_identity: WorkflowExecutionIdentity,
app_model: App,
workflow: Workflow,
user: Account | EndUser,
args: Mapping[str, Any],
invoke_from: InvokeFrom,
streaming: bool = True,
call_depth: int = 0,
workflow_run_id: str | uuid.UUID | None = None,
triggered_from: WorkflowRunTriggeredFrom | None = None,
root_node_id: str | None = None,
graph_engine_layers: Sequence[Layer] = (),
allow_human_input: bool = True,
pause_state_config: PauseStateLayerConfig | None = None,
builder_execution: BuilderExecutionContext
| ConfirmedToolExecutionContext
| JsonToolExecutionContext
| None = None,
builder_execution_admit: BuilderExecutionAdmission | None = None,
builder_agent_execution: BuilderAgentExecutionContext | None = None,
native_test_required: bool = False,
native_test_ref: NativeTestContextRef | None = None,
native_runtime: NativeRuntimeBridge | None = None,
native_host: NativeHostLaunch | None = None,
builder_mocked_node_ids: frozenset[str] = frozenset(),
builder_expected_revision: str | None = None,
builder_tool_credential_bindings: Mapping[str, str] | None = None,
) -> Mapping[str, Any] | Generator[Mapping[str, Any] | str]:
from services.dify_builder.native_launch_observation import begin_preparation
from services.dify_builder.native_testing.generation_wiring import validate_native_host
begin_preparation()
if builder_agent_execution is not None:
if (
builder_execution is not None
or native_test_required
or native_test_ref is not None
or native_runtime is not None
or native_host is not None
or invoke_from != InvokeFrom.DEBUGGER
or not isinstance(user, Account)
or app_model.mode != AppMode.WORKFLOW
or call_depth != 0
or (workflow_run_id is not None and str(workflow_run_id) != builder_agent_execution.native_run_id)
):
raise BuilderExecutionPolicyError("unsupported_execution_transport")
workflow_run_id = builder_agent_execution.native_run_id
validate_native_host(
required=native_test_required or native_test_ref is not None,
ref=native_test_ref,
runtime=native_runtime,
host=native_host,
)
native_context = None
native_inputs = None
if native_test_required or native_test_ref is not None:
if native_test_ref is None:
raise NativeContextUnavailable("native_context_missing")
if native_runtime is None:
raise NativeContextUnavailable("native_runtime_unavailable")
native_context = native_runtime.resolve(native_test_ref)
if (
builder_execution is not None
or (app_model.tenant_id, app_model.id, workflow.id, user.id, str(workflow_run_id), call_depth)
!= (
native_context.tenant_id,
native_context.app_id,
native_context.workflow_id,
native_context.actor_id,
native_context.native_run_id,
native_context.call_depth,
)
or workflow.graph_dict != native_context.graph
):
raise NativeContextUnavailable("native_context_mismatch")
assert native_host is not None
guard = native_host.bind_observation()
with native_host.observation_scope():
if (
args.get("files")
or pause_state_config is not None
or graph_engine_layers
or not streaming
or workflow.features_dict.get("text_to_speech", {}).get("enabled")
or any(
args.get(key) is not None
for key in ("trace_session_id", "external_trace_id", "parent_trace_context")
)
):
guard.fail()
native_inputs = native_runtime.prepare_inputs(native_context)
if (
builder_mocked_node_ids
or builder_expected_revision is not None
or builder_tool_credential_bindings is not None
) and (builder_execution is not None or native_context is not None or invoke_from != InvokeFrom.DEBUGGER):
raise BuilderExecutionPolicyError("unsupported_execution_policy")
try:
if builder_execution is not None and (
builder_execution_admit is None
or args.get("files")
or call_depth != 0
or not isinstance(user, Account)
or invoke_from != InvokeFrom.DEBUGGER
or pause_state_config is not None
or graph_engine_layers
):
raise BuilderExecutionPolicyError("unsupported_execution_transport")
if builder_execution is not None:
assert builder_execution_admit is not None
builder_execution_admit.admit_raw(workflow, app_model)
scalar_inputs_digest(args.get("inputs", {}))
except BuilderExecutionPolicyError as error:
if builder_execution_admit is not None:
builder_execution_admit.on_refusal(error, preworker=True)
raise
with self._bind_file_access_scope(tenant_id=app_model.tenant_id, user=user, invoke_from=invoke_from):
files: Sequence[Mapping[str, Any]] = args.get("files") or []
# parse files
# TODO(QuantumGhost): Move file parsing logic to the API controller layer
# for better separation of concerns.
#
# For implementation reference, see the `_parse_file` function and
# `DraftWorkflowNodeRunApi` class which handle this properly.
file_extra_config = FileUploadConfigManager.convert(workflow.features_dict, is_vision=False)
system_files = (
[]
if builder_execution is not None or native_context is not None
else file_factory.build_from_mappings(
mappings=files,
tenant_id=app_model.tenant_id,
config=file_extra_config,
strict_type_validation=invoke_from == InvokeFrom.SERVICE_API,
access_controller=self._file_access_controller,
)
)
# convert to app config
app_config = WorkflowAppConfigManager.get_app_config(
app_model=app_model,
workflow=workflow,
)
inputs: Mapping[str, Any] = native_inputs.inputs if native_inputs is not None else args["inputs"]
extras = {
**extract_external_trace_id_from_args(args),
**extract_parent_trace_context_from_args(args),
**extract_trace_session_id_from_args(args),
}
trace_scope = None
if native_context is None:
trace_scope = create_trace_scope(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
app_tracing=app_model.tracing,
user_id=user.id if isinstance(user, Account) else user.session_id,
extras=extras,
external_tracing_disabled=builder_execution is not None,
)
workflow_run_id = str(workflow_run_id or uuid.uuid4())
root_node_id = root_node_id or get_default_root_node_id(workflow.graph_dict)
root_node_config = workflow.get_node_config_by_id(root_node_id)
root_node_type = workflow.get_node_type_from_node_config(root_node_config)
if builder_execution is not None and root_node_type != "start":
error = BuilderExecutionPolicyError("unsupported_execution_root")
if builder_execution_admit is not None:
builder_execution_admit.on_refusal(error, preworker=True)
raise error
# Trigger inputs are already adapted event data, not Start form fields.
if native_context is None and not is_trigger_node_type(root_node_type):
inputs = self._prepare_user_inputs(
user_inputs=inputs,
variables=app_config.variables,
tenant_id=app_model.tenant_id,
strict_type_validation=invoke_from == InvokeFrom.SERVICE_API,
)
# init application generate entity
application_generate_entity = WorkflowAppGenerateEntity(
task_id=str(uuid.uuid4()),
app_config=app_config,
file_upload_config=file_extra_config,
inputs=inputs,
files=list(native_inputs.files if native_inputs is not None else system_files),
user_id=execution_identity.user_id,
user_from=execution_identity.user_from,
stream=streaming,
invoke_from=invoke_from,
allow_human_input=allow_human_input,
call_depth=call_depth,
builder_execution=builder_execution,
builder_agent_execution=builder_agent_execution,
native_test_required=native_test_required,
native_test_ref=native_test_ref,
builder_mocked_node_ids=builder_mocked_node_ids,
builder_expected_revision=builder_expected_revision,
builder_tool_credential_bindings=builder_tool_credential_bindings,
trace_scope=trace_scope,
workflow_execution_id=workflow_run_id,
extras=extras,
)
if builder_execution is not None:
if scalar_inputs_digest(dict(inputs)) != builder_execution.effective_inputs_digest:
error = BuilderExecutionPolicyError("effective_input_binding_mismatch")
if builder_execution_admit is not None:
builder_execution_admit.on_refusal(error, preworker=True)
raise error
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create repositories
#
# Create session factory
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
# Create workflow execution(aka workflow run) repository
if triggered_from is not None:
# Use explicitly provided triggered_from (for async triggers)
workflow_triggered_from = triggered_from
elif invoke_from == InvokeFrom.DEBUGGER:
workflow_triggered_from = WorkflowRunTriggeredFrom.DEBUGGING
else:
workflow_triggered_from = WorkflowRunTriggeredFrom.APP_RUN
workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository(
session_factory=session_factory,
tenant_id=app_model.tenant_id,
user=user,
app_id=application_generate_entity.app_config.app_id,
triggered_from=workflow_triggered_from,
)
# Create workflow node execution repository
workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository(
session_factory=session_factory,
tenant_id=app_model.tenant_id,
user=user,
app_id=application_generate_entity.app_config.app_id,
triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
)
return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
application_generate_entity=application_generate_entity,
invoke_from=invoke_from,
workflow_execution_repository=workflow_execution_repository,
workflow_node_execution_repository=workflow_node_execution_repository,
streaming=streaming,
root_node_id=root_node_id,
graph_engine_layers=graph_engine_layers,
pause_state_config=pause_state_config,
builder_execution_admit=builder_execution_admit,
native_runtime=native_runtime,
native_host=native_host,
)
def resume(
self,
*,
app_model: App,
workflow: Workflow,
user: Account | EndUser,
application_generate_entity: WorkflowAppGenerateEntity,
graph_runtime_state: RuntimeState,
workflow_execution_repository: WorkflowExecutionRepository,
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
graph_engine_layers: Sequence[Layer] = (),
pause_state_config: PauseStateLayerConfig | None = None,
variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER,
response_stream_filter: ResponseStreamFilter | None = None,
native_runtime: NativeRuntimeBridge | None = None,
native_host: NativeHostLaunch | None = None,
) -> Mapping[str, Any] | Generator[str | Mapping[str, Any]]:
"""
Resume a paused workflow execution using the persisted runtime state.
``trace_scope`` is transient and excluded from generate-entity serialization,
so resumed executions create it again before persistence layers receive the entity.
"""
if application_generate_entity.builder_agent_execution is not None and (
application_generate_entity.builder_execution is not None
or application_generate_entity.native_test_required
or application_generate_entity.native_test_ref is not None
or native_runtime is not None
or native_host is not None
or application_generate_entity.invoke_from != InvokeFrom.DEBUGGER
or not isinstance(user, Account)
or app_model.mode != AppMode.WORKFLOW
or application_generate_entity.call_depth != 0
or application_generate_entity.workflow_execution_id
!= application_generate_entity.builder_agent_execution.native_run_id
):
raise BuilderExecutionPolicyError("unsupported_execution_transport")
if application_generate_entity.native_test_required or application_generate_entity.native_test_ref is not None:
if application_generate_entity.native_test_ref is None:
raise NativeContextUnavailable("native_context_missing")
from services.dify_builder.native_testing.generation_wiring import validate_native_host
validate_native_host(
required=True, ref=application_generate_entity.native_test_ref, runtime=native_runtime, host=native_host
)
assert native_host is not None
native_host.require_resume_bindings(
entity=application_generate_entity,
runtime_state=graph_runtime_state,
response_filter=response_stream_filter,
)
if application_generate_entity.builder_execution is not None:
raise BuilderExecutionPolicyError("unsupported_execution_transport")
if native_host is None and application_generate_entity.trace_scope is None:
application_generate_entity = application_generate_entity.model_copy(
update={
"trace_scope": create_trace_scope(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
app_tracing=app_model.tracing,
user_id=user.id if isinstance(user, Account) else user.session_id,
extras=application_generate_entity.extras,
)
}
)
return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
application_generate_entity=application_generate_entity,
invoke_from=application_generate_entity.invoke_from,
workflow_execution_repository=workflow_execution_repository,
workflow_node_execution_repository=workflow_node_execution_repository,
streaming=application_generate_entity.stream,
variable_loader=variable_loader,
graph_engine_layers=graph_engine_layers,
graph_runtime_state=graph_runtime_state,
pause_state_config=pause_state_config,
response_stream_filter=response_stream_filter,
native_runtime=native_runtime,
native_host=native_host,
)
def _generate(
self,
*,
app_model: App,
workflow: Workflow,
user: Account | EndUser,
application_generate_entity: WorkflowAppGenerateEntity,
invoke_from: InvokeFrom,
workflow_execution_repository: WorkflowExecutionRepository,
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
streaming: bool = True,
variable_loader: VariableLoader = DUMMY_VARIABLE_LOADER,
root_node_id: str | None = None,
graph_engine_layers: Sequence[Layer] = (),
graph_runtime_state: RuntimeState | None = None,
pause_state_config: PauseStateLayerConfig | None = None,
response_stream_filter: ResponseStreamFilter | None = None,
builder_execution_admit: BuilderExecutionAdmission | None = None,
native_runtime: NativeRuntimeBridge | None = None,
native_host: NativeHostLaunch | None = None,
) -> Mapping[str, Any] | Generator[str | Mapping[str, Any]]:
"""
Generate App response.
:param app_model: App
:param workflow: Workflow
:param user: account or end user
:param application_generate_entity: application generate entity
:param invoke_from: invoke from source
:param workflow_execution_repository: repository for workflow execution
:param workflow_node_execution_repository: repository for workflow node execution
:param streaming: is stream
"""
from services.dify_builder.native_testing.generation_wiring import bind_native_generation, validate_native_host
validate_native_host(
required=application_generate_entity.native_test_required
or application_generate_entity.native_test_ref is not None,
ref=application_generate_entity.native_test_ref,
runtime=native_runtime,
host=native_host,
)
if native_host is not None and (graph_engine_layers or application_generate_entity.trace_scope is not None):
raise NativeContextUnavailable("native_observation_unqualified")
if native_host is not None:
if not streaming or pause_state_config is not None:
raise NativeContextUnavailable("native_resume_unavailable")
if graph_runtime_state is not None:
native_host.require_resume_bindings(
entity=application_generate_entity,
runtime_state=graph_runtime_state,
response_filter=response_stream_filter,
)
native_observation = None
if native_host is not None:
from services.dify_builder.native_testing.generation_wiring import preflight_native_observation
native_observation = native_host.bind_observation()
preflight_native_observation(host=native_host, entity=application_generate_entity, workflow=workflow)
with self._bind_file_access_scope(
tenant_id=application_generate_entity.app_config.tenant_id,
user=user,
invoke_from=invoke_from,
):
if native_host is None:
grant_file_default_access(application_generate_entity.app_config.variables)
# init queue manager
queue_manager = WorkflowAppQueueManager(
task_id=application_generate_entity.task_id,
user_id=application_generate_entity.user_id,
invoke_from=application_generate_entity.invoke_from,
app_mode=app_model.mode,
native_observation=native_observation,
native_host=native_host,
)
resolved_response_stream_filter = response_stream_filter or ResponseStreamFilter()
if native_host is not None:
bind_native_generation(
host=native_host,
entity=application_generate_entity,
queue=queue_manager,
response_filter=resolved_response_stream_filter,
workflow_repository=workflow_execution_repository,
node_repository=workflow_node_execution_repository,
)
# new thread with request context and contextvars
context = contextvars.copy_context()
# release database connection, because the following new thread operations may take a long time
db.session.close()
worker_thread = threading.Thread(
target=self._generate_worker,
kwargs={
"flask_app": current_app._get_current_object(), # type: ignore
"application_generate_entity": application_generate_entity,
"queue_manager": queue_manager,
"context": context,
"variable_loader": variable_loader,
"root_node_id": root_node_id,
"workflow_execution_repository": workflow_execution_repository,
"workflow_node_execution_repository": workflow_node_execution_repository,
"graph_engine_layers": graph_engine_layers,
"pause_state_config": pause_state_config,
"graph_runtime_state": graph_runtime_state,
"response_stream_filter": resolved_response_stream_filter,
"builder_execution_admit": builder_execution_admit,
"native_runtime": native_runtime,
"native_host": native_host,
},
)
from services.dify_builder.native_launch_observation import mark_dispatched
mark_dispatched()
if native_host is None:
worker_thread.start()
else:
native_host.start_worker(worker=worker_thread)
draft_var_saver_factory: DraftVariableSaverFactory
if native_host is not None or isinstance(
application_generate_entity.builder_execution, ConfirmedToolExecutionContext
):
# The authorized native Run/node rows are the only retained copy of a
# confirmed Tool's content: no mutable workspace draft-variable copy.
draft_var_saver_factory = _no_draft_variable_saver
else:
draft_var_saver_factory = self._get_draft_var_saver_factory(
invoke_from,
user,
tenant_id=app_model.tenant_id,
)
try:
response = self._handle_response(
application_generate_entity=application_generate_entity,
workflow=workflow,
queue_manager=queue_manager,
user=user,
draft_var_saver_factory=draft_var_saver_factory,
stream=streaming,
native_observation=native_observation,
native_host=native_host,
)
converted_response = WorkflowAppGenerateResponseConverter.convert(
response=response,
invoke_from=invoke_from,
)
except BaseException:
if native_host is None:
self._join_worker_thread(worker_thread)
else:
native_host.request_close(reason="failed")
raise
if native_host is not None:
if not isinstance(converted_response, Generator):
native_host.request_close(reason="failed")
raise NativeContextUnavailable("native_stream_unavailable")
return converted_response
if isinstance(converted_response, Generator):
return self._wrap_stream_with_worker_thread_join(converted_response, worker_thread)
self._join_worker_thread(worker_thread)
return converted_response
def single_iteration_generate(
self,
app_model: App,
workflow: Workflow,
node_id: str,
user: Account | EndUser,
args: Mapping[str, Any],
streaming: bool = True,
*,
execution_identity: WorkflowExecutionIdentity,
session: Session,
) -> Mapping[str, Any] | Generator[str | Mapping[str, Any]]:
"""
Generate App response.
:param app_model: App
:param workflow: Workflow
:param node_id: the node id
:param user: account or end user
:param args: request args
:param streaming: is streamed
:param session: database session supplied by the caller
"""
if not node_id:
raise ValueError("node_id is required")
if args.get("inputs") is None:
raise ValueError("inputs is required")
# convert to app config
app_config = WorkflowAppConfigManager.get_app_config(app_model=app_model, workflow=workflow)
# init application generate entity
application_generate_entity = WorkflowAppGenerateEntity(
task_id=str(uuid.uuid4()),
app_config=app_config,
inputs={},
files=[],
user_id=execution_identity.user_id,
user_from=execution_identity.user_from,
stream=streaming,
invoke_from=InvokeFrom.DEBUGGER,
extras={
"auto_generate_conversation_name": False,
**_extract_trace_session_id_from_debug_args(args),
},
single_iteration_run=WorkflowAppGenerateEntity.SingleIterationRunEntity(
node_id=node_id, inputs=args["inputs"]
),
workflow_execution_id=str(uuid.uuid4()),
)
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create repositories
#
# Create session factory
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
# Create workflow execution(aka workflow run) repository
workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository(
session_factory=session_factory,
tenant_id=app_model.tenant_id,
user=user,
app_id=application_generate_entity.app_config.app_id,
triggered_from=WorkflowRunTriggeredFrom.DEBUGGING,
)
# Create workflow node execution repository
workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository(
session_factory=session_factory,
tenant_id=app_model.tenant_id,
user=user,
app_id=application_generate_entity.app_config.app_id,
triggered_from=WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP,
)
draft_var_srv = WorkflowDraftVariableService(session)
draft_var_srv.prefill_conversation_variable_default_values(workflow, user_id=user.id)
var_loader = DraftVarLoader(
engine=db.engine,
app_id=application_generate_entity.app_config.app_id,
tenant_id=application_generate_entity.app_config.tenant_id,
user_id=user.id,
)
return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
invoke_from=InvokeFrom.DEBUGGER,
application_generate_entity=application_generate_entity,
workflow_execution_repository=workflow_execution_repository,
workflow_node_execution_repository=workflow_node_execution_repository,
streaming=streaming,
variable_loader=var_loader,
pause_state_config=None,
)
def single_loop_generate(
self,
app_model: App,
workflow: Workflow,
node_id: str,
user: Account | EndUser,
args: LoopNodeRunPayload,
streaming: bool = True,
*,
execution_identity: WorkflowExecutionIdentity,
session: Session,
) -> Mapping[str, Any] | Generator[str | Mapping[str, Any]]:
"""
Generate App response.
:param app_model: App
:param workflow: Workflow
:param node_id: the node id
:param user: account or end user
:param args: request args
:param streaming: is streamed
:param session: database session supplied by the caller
"""
if not node_id:
raise ValueError("node_id is required")
if args.inputs is None:
raise ValueError("inputs is required")
# convert to app config
app_config = WorkflowAppConfigManager.get_app_config(app_model=app_model, workflow=workflow)
# init application generate entity
application_generate_entity = WorkflowAppGenerateEntity(
task_id=str(uuid.uuid4()),
app_config=app_config,
inputs={},
files=[],
user_id=execution_identity.user_id,
user_from=execution_identity.user_from,
stream=streaming,
invoke_from=InvokeFrom.DEBUGGER,
extras={
"auto_generate_conversation_name": False,
**_extract_trace_session_id_from_debug_args(args),
},
single_loop_run=WorkflowAppGenerateEntity.SingleLoopRunEntity(node_id=node_id, inputs=args.inputs or {}),
workflow_execution_id=str(uuid.uuid4()),
)
contexts.plugin_tool_providers.set({})
contexts.plugin_tool_providers_lock.set(threading.Lock())
# Create repositories
#
# Create session factory
session_factory = sessionmaker(bind=db.engine, expire_on_commit=False)
# Create workflow execution(aka workflow run) repository
workflow_execution_repository = DifyCoreRepositoryFactory.create_workflow_execution_repository(
session_factory=session_factory,
tenant_id=app_model.tenant_id,
user=user,
app_id=application_generate_entity.app_config.app_id,
triggered_from=WorkflowRunTriggeredFrom.DEBUGGING,
)
# Create workflow node execution repository
workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository(
session_factory=session_factory,
tenant_id=app_model.tenant_id,
user=user,
app_id=application_generate_entity.app_config.app_id,
triggered_from=WorkflowNodeExecutionTriggeredFrom.SINGLE_STEP,
)
draft_var_srv = WorkflowDraftVariableService(session)
draft_var_srv.prefill_conversation_variable_default_values(workflow, user_id=user.id)
var_loader = DraftVarLoader(
engine=db.engine,
app_id=application_generate_entity.app_config.app_id,
tenant_id=application_generate_entity.app_config.tenant_id,
user_id=user.id,
)
return self._generate(
app_model=app_model,
workflow=workflow,
user=user,
invoke_from=InvokeFrom.DEBUGGER,
application_generate_entity=application_generate_entity,
workflow_execution_repository=workflow_execution_repository,
workflow_node_execution_repository=workflow_node_execution_repository,
streaming=streaming,
variable_loader=var_loader,
pause_state_config=None,
)
def _generate_worker(
self,
flask_app: Flask,
application_generate_entity: WorkflowAppGenerateEntity,
queue_manager: AppQueueManager,
context: contextvars.Context,
variable_loader: VariableLoader,
workflow_execution_repository: WorkflowExecutionRepository,
workflow_node_execution_repository: WorkflowNodeExecutionRepository,
root_node_id: str | None = None,
graph_engine_layers: Sequence[Layer] = (),
graph_runtime_state: RuntimeState | None = None,
response_stream_filter: ResponseStreamFilter | None = None,
builder_execution_admit: BuilderExecutionAdmission | None = None,
native_runtime: NativeRuntimeBridge | None = None,
native_host: NativeHostLaunch | None = None,
pause_state_config: PauseStateLayerConfig | None = None,
) -> None:
"""
Generate worker in a new thread.
:param flask_app: Flask app
:param application_generate_entity: application generate entity
:param queue_manager: queue manager
:param workflow_thread_pool_id: workflow thread pool id
:return:
"""
execution_recorder = None
exit_kind: Literal["returned", "policy_refused", "stopped", "failed"] = "failed"
try:
from services.dify_builder.native_testing.context_runtime import native_flask_worker_scope
worker_scope = (
native_flask_worker_scope(host=native_host, flask_app=flask_app, context_vars=context)
if native_host is not None
else preserve_flask_contexts(flask_app, context_vars=context)
)
with worker_scope:
try:
native_context = None
if (
application_generate_entity.native_test_required
or application_generate_entity.native_test_ref is not None
):
if application_generate_entity.native_test_ref is None:
raise NativeContextUnavailable("native_context_missing")
if native_runtime is None:
raise NativeContextUnavailable("native_runtime_unavailable")
native_context = native_runtime.resolve(application_generate_entity.native_test_ref)
if (
application_generate_entity.app_config.tenant_id,
application_generate_entity.app_config.app_id,
application_generate_entity.app_config.workflow_id,
application_generate_entity.user_id,
application_generate_entity.workflow_execution_id,
) != (
native_context.tenant_id,
native_context.app_id,
native_context.workflow_id,
native_context.actor_id,
native_context.native_run_id,
):
raise NativeContextUnavailable("native_context_mismatch")
from services.dify_builder.native_testing.generation_wiring import validate_native_host
validate_native_host(
required=True,
ref=application_generate_entity.native_test_ref,
runtime=native_runtime,
host=native_host,
)
assert native_host is not None
if native_host.generation_bindings.entity is not application_generate_entity:
raise NativeContextUnavailable("native_host_identity_mismatch")
prepared = native_runtime.prepare_inputs(native_context)
if graph_runtime_state is None:
application_generate_entity.inputs = prepared.inputs
application_generate_entity.files = list(prepared.files)
else:
native_host.require_resume_bindings(
entity=application_generate_entity,
runtime_state=graph_runtime_state,
response_filter=response_stream_filter,
)
application_generate_entity.call_depth = native_context.call_depth
with session_factory.create_session() as session:
workflow = session.scalar(
select(Workflow).where(
Workflow.tenant_id == application_generate_entity.app_config.tenant_id,
Workflow.app_id == application_generate_entity.app_config.app_id,
Workflow.id == application_generate_entity.app_config.workflow_id,
)
)
if workflow is None:
if application_generate_entity.builder_execution is not None:
raise BuilderExecutionPolicyError("execution_owner_mismatch")
raise ValueError("Workflow not found")
if application_generate_entity.builder_execution is not None:
if builder_execution_admit is None:
raise BuilderExecutionPolicyError("missing_execution_admission")
app = session.scalar(
select(App).where(
App.id == application_generate_entity.app_config.app_id,
App.tenant_id == application_generate_entity.app_config.tenant_id,
)
)
if app is None:
raise BuilderExecutionPolicyError("execution_owner_mismatch")
if graph_engine_layers:
raise BuilderExecutionPolicyError("unsupported_execution_transport")
if isinstance(application_generate_entity.builder_execution, JsonToolExecutionContext):
# The owned worker read must release its SQL transaction
# before the issuer revalidates daemon metadata.
session.expunge_all()
session.close()
execution_recorder = builder_execution_admit(
workflow,
app,
application_generate_entity,
root_node_id or get_default_root_node_id(workflow.graph_dict),
graph_runtime_state is not None,
)
if execution_recorder is None or not execution_recorder.healthy:
raise BuilderExecutionPolicyError("missing_execution_recorder")
if native_context is not None:
if workflow.graph_dict != native_context.graph:
raise NativeContextUnavailable("native_context_mismatch")
else:
workflow = self._ensure_snippet_start_node_in_worker(session=session, workflow=workflow)
if graph_runtime_state is not None and native_context is None:
self._restore_workflow_run_graph(
session=session,
workflow=workflow,
workflow_run_id=application_generate_entity.workflow_execution_id,
)
user_from = application_generate_entity.user_from
if user_from == UserFrom.END_USER:
end_user = session.scalar(
select(EndUser).where(
EndUser.tenant_id == application_generate_entity.app_config.tenant_id,
EndUser.id == application_generate_entity.user_id,
)
)
system_user_id = end_user.session_id if end_user else ""
else:
system_user_id = application_generate_entity.user_id
if application_generate_entity.builder_expected_revision is not None:
from services.dify_builder.revision import execution_revision
if execution_revision(workflow) != application_generate_entity.builder_expected_revision:
raise BuilderExecutionPolicyError("stale_execution_revision")
runner = WorkflowAppRunner(
application_generate_entity=application_generate_entity,
execution_driver=self._execution_driver,
queue_manager=queue_manager,
variable_loader=variable_loader,
workflow=workflow,
system_user_id=system_user_id,
native_runtime=native_runtime,
native_host=native_host,
root_node_id=root_node_id,
graph_engine_layers=graph_engine_layers,
graph_runtime_state=graph_runtime_state,
response_stream_filter=response_stream_filter,
execution_recorder=execution_recorder,
)
with active_workflow_task(application_generate_entity.task_id):
self._execution_driver(
runner,
pause_state_config,
workflow_execution_repository,
workflow_node_execution_repository,
)
exit_kind = "returned"
except GenerateTaskStoppedError:
exit_kind = "stopped"
logger.warning("Task stopped", exc_info=native_host is None)
pass
except InvokeAuthorizationError:
queue_manager.publish_error(
InvokeAuthorizationError("Incorrect API key provided"), PublishFrom.APPLICATION_MANAGER
)
except NativeContextUnavailable as e:
exit_kind = "policy_refused"
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except ValidationError as e:
if native_host is not None:
queue_manager.publish_error(NativeObservationError(), PublishFrom.APPLICATION_MANAGER)
else:
logger.exception("Validation Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except BuilderExecutionPolicyError as e:
exit_kind = "policy_refused"
if builder_execution_admit is not None:
builder_execution_admit.on_refusal(e)
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except ValueError as e:
if native_host is not None:
queue_manager.publish_error(NativeObservationError(), PublishFrom.APPLICATION_MANAGER)
else:
if dify_config.DEBUG:
logger.exception("Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except Exception as e:
if native_host is not None:
queue_manager.publish_error(NativeObservationError(), PublishFrom.APPLICATION_MANAGER)
else:
logger.exception("Unknown Error when generating")
queue_manager.publish_error(e, PublishFrom.APPLICATION_MANAGER)
except BaseException:
exit_kind = "failed"
if native_host is None:
raise
# Scope entry itself can fail before the inner publisher exists.
# A failed guard must not project another payload. Close the listener
# segment only; physical cleanup remains owned by the host/reader.
from core.app.apps.execution_coordinator import AppExecutionState
native_host.mark_unknown()
try:
from libs.sensitive_http_telemetry import sensitive_http_telemetry
from services.dify_builder.native_testing.context_runtime import NATIVE_LOG_EMITTERS
# Scope entry/exit already restored the worker ContextVars.
# Listener cache cleanup can log swallowed Redis failures, so
# protect it independently of the now-failed observation guard.
with sensitive_http_telemetry(additional_emitters=NATIVE_LOG_EMITTERS):
queue_manager.stop_listen(execution_state=AppExecutionState.TERMINAL)
except BaseException:
raise NativeObservationError() from None
finally:
if native_host is not None:
native_host.worker_finished(worker=threading.current_thread(), exit_kind=exit_kind)
marker = application_generate_entity.builder_execution
if marker is not None and builder_execution_admit is not None:
try:
builder_execution_admit.on_worker_finished(
context=marker,
native_run_id=application_generate_entity.workflow_execution_id,
task_id=application_generate_entity.task_id,
recorder=execution_recorder,
worker_thread=threading.current_thread(),
exit_kind=exit_kind,
)
except BaseException:
# A missing/invalid candidate stays unknown; preserve the primary worker error.
logger.warning("Builder worker completion acknowledgment failed")
def _handle_response(
self,
application_generate_entity: WorkflowAppGenerateEntity,
workflow: Workflow,
queue_manager: AppQueueManager,
user: Account | EndUser,
draft_var_saver_factory: DraftVariableSaverFactory,
stream: bool = False,
*,
native_observation: NativeObservationGuard | None = None,
native_host: NativeHostLaunch | None = None,
) -> WorkflowAppBlockingResponse | WorkflowAppPausedBlockingResponse | Generator[WorkflowAppStreamResponse]:
"""
Handle response.
:param application_generate_entity: application generate entity
:param workflow: workflow
:param queue_manager: queue manager
:param user: account or end user
:param stream: is stream
:param workflow_node_execution_repository: optional repository for workflow node execution
:return:
"""
# init generate task pipeline
generate_task_pipeline = WorkflowAppGenerateTaskPipeline(
application_generate_entity=application_generate_entity,
workflow=workflow,
queue_manager=queue_manager,
user=user,
draft_var_saver_factory=draft_var_saver_factory,
stream=stream,
native_observation=native_observation,
native_host=native_host,
)
try:
return generate_task_pipeline.process()
except ValueError as e:
if native_observation is not None:
native_observation.fail()
if len(e.args) > 0 and e.args[0] == "I/O operation on closed file.": # ignore this error
raise GenerateTaskStoppedError() from e
else:
logger.exception(
"Fails to process generate task pipeline, task_id: %s", application_generate_entity.task_id
)
raise e