Files
dify/api/services/human_input_v2/composition.py
T
8cbb6c0130 feat: HITL IM integration (#44864)
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-10-30 02:43:31 +00:00

105 lines
4.6 KiB
Python

"""Production composition for shared Human Input v2 services."""
from __future__ import annotations
from collections.abc import Callable
from datetime import timedelta
from typing import override
from sqlalchemy import select
from sqlalchemy.orm import Session
from configs import dify_config
from core.app.entities.app_invoke_entities import DifyRunContext, InvokeFrom, UserFrom
from core.db.session_factory import session_factory
from core.human_input_v2.shared.values import AccountId, TenantId
from core.workflow.nodes.human_input_v2.entities import HumanInputNodeData
from core.workflow.nodes.human_input_v2.runtime import HumanInputDeliveryError, HumanInputRuntime, PreparedForm
from core.workflow.system_variables import SystemVariableKey, get_system_text
from graphon.runtime.runtime_state_protocol import ReadOnlyVariablePool
from libs.datetime_utils import ensure_naive_utc
from models.workflow import WorkflowRun
from .delivery_service import HumanInputDeliveryService
from .email_client import build_email_client
from .form_service import AccountInitiator, EndUserInitiator, FormExecutionContext, HumanInputFormService
from .node_data_migration import HumanInputNodeDataMigrationService
from .runtime import DifyHumanInputRuntime
from .workspace_member_email_lookup import SQLAlchemyWorkspaceMemberEmailLookup
def build_human_input_delivery_service(
*, tenant_id: TenantId, session_factory: Callable[[], Session]
) -> HumanInputDeliveryService:
return HumanInputDeliveryService(
session_factory=session_factory,
email_client=build_email_client(tenant_id=tenant_id, session_factory=session_factory),
)
def build_human_input_node_data_migration_service() -> HumanInputNodeDataMigrationService:
"""Compose the read-only migration service for one Console request."""
return HumanInputNodeDataMigrationService(
member_email_lookup=SQLAlchemyWorkspaceMemberEmailLookup(session_factory.create_session),
)
class WorkflowHumanInputRuntime(HumanInputRuntime):
"""Bind services when execution starts, after the run has been persisted.
Graph construction precedes the run-start persistence layer. In particular,
do not read the run or calculate a new global deadline in NodeFactory.
"""
def __init__(self, context: DifyRunContext) -> None:
self._context = context
@override
def prepare_form(
self,
*,
node_execution_id: str,
node_data: HumanInputNodeData,
variable_pool: ReadOnlyVariablePool,
) -> PreparedForm:
run_id = get_system_text(variable_pool, SystemVariableKey.WORKFLOW_EXECUTION_ID)
if run_id is None:
raise HumanInputDeliveryError("Human Input requires a persisted workflow run")
with session_factory.create_session() as session:
run = session.scalar(
select(WorkflowRun).where(
WorkflowRun.id == run_id,
WorkflowRun.tenant_id == self._context.tenant_id,
WorkflowRun.app_id == self._context.app_id,
)
)
if run is None:
raise HumanInputDeliveryError("Human Input workflow run was not found")
deadline = ensure_naive_utc(run.created_at) + timedelta(
seconds=dify_config.HUMAN_INPUT_GLOBAL_TIMEOUT_SECONDS
)
initiator: AccountInitiator | EndUserInitiator | None = None
if self._context.user_from == UserFrom.ACCOUNT:
initiator = AccountInitiator(AccountId(self._context.user_id))
elif self._context.invoke_from in (InvokeFrom.WEB_APP, InvokeFrom.SERVICE_API):
initiator = EndUserInitiator(self._context.user_id)
context = FormExecutionContext(
tenant_id=TenantId(self._context.tenant_id),
app_id=self._context.app_id,
workflow_run_id=run_id,
global_timeout_deadline=deadline,
initiator=initiator,
debugging_account_id=(
AccountId(self._context.user_id)
if self._context.invoke_from == InvokeFrom.DEBUGGER and self._context.user_from == UserFrom.ACCOUNT
else None
),
)
return DifyHumanInputRuntime(
form_service=HumanInputFormService(session_factory=session_factory.get_session_maker(), context=context),
delivery_service=build_human_input_delivery_service(
tenant_id=context.tenant_id, session_factory=session_factory.create_session
),
).prepare_form(node_execution_id=node_execution_id, node_data=node_data, variable_pool=variable_pool)