83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
"""``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.<human>.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"]
|