"""``human_input`` node: suspends the run and validates the resumed payload.""" from __future__ import annotations from typing import Any from deerflow.workflows.errors import WorkflowError from deerflow.workflows.expressions import render_template from deerflow.workflows.runtime.context import RunContext, WorkflowPaused from deerflow.workflows.schemas import NodeResult, WorkflowNode DEFAULT_ACTIONS = ("submit",) def _form_schema(node: WorkflowNode) -> dict[str, Any]: schema = node.config.get("formSchema") or node.config.get("form_schema") or {} return schema if isinstance(schema, dict) else {} def _actions(node: WorkflowNode) -> list[str]: raw = node.config.get("actions") actions = [str(a) for a in raw] if isinstance(raw, list) and raw else list(DEFAULT_ACTIONS) return actions class HumanInputNodeExecutor: """First pass raises :class:`WorkflowPaused`; the resume pass consumes the payload the API stored on the context and validates it against the form.""" async def execute(self, node: WorkflowNode, ctx: RunContext) -> NodeResult: payload = ctx.resume_payload if not payload or str(payload.get("nodeId") or "") != node.id: raise WorkflowPaused( node.id, { "nodeId": node.id, "name": node.name, "prompt": render_template(str(node.config.get("prompt") or ""), ctx.state()), "formSchema": _form_schema(node), "actions": _actions(node), }, ) action = str(payload.get("action") or "submit") allowed = _actions(node) if action not in allowed: raise WorkflowError( "WORKFLOW_HUMAN_INPUT_INVALID", f"不支持的操作:{action}", node_id=node.id, details={"allowed": allowed}, ) values = payload.get("values") values = values if isinstance(values, dict) else {} # 独立页的协助卡允许用户只填写自然语言 message。把它收敛进 values # 后,后续节点即可通过 {{ nodes..data.values.message }} 显式引用。 message = payload.get("message") if isinstance(message, str) and message.strip() and not values.get("message"): values = {**values, "message": message.strip()} schema = _form_schema(node) required = schema.get("required") if isinstance(required, list) and action != "cancel": missing = [str(key) for key in required if values.get(str(key)) in (None, "")] if missing: raise WorkflowError( "WORKFLOW_HUMAN_INPUT_INVALID", f"缺少必填项:{'、'.join(missing)}", node_id=node.id, details={"missing": missing}, ) if action == "cancel": raise WorkflowError( "WORKFLOW_CANCELLED", "用户在人工节点选择终止运行", node_id=node.id, ) # One-shot: a later human node in the same run must pause again. ctx.resume_payload = None return NodeResult(data={"action": action, "values": values}) __all__ = ["HumanInputNodeExecutor"]