Files
deerflow-code/offline-backend-20260512/backend/packages/harness/deerflow/workflows/nodes/human.py
T
董宏凯 1706a24182 feat(workflow): 增加会话级多智能体任务规划运行能力
提交范围聚焦于工作流编排新增:新增 workflow_planning 路由与前后端运行入口(workflow_planning.py、workflow_proposal_planner.py、workflow_deep_research_adapter.py),支持基于会话的提案-规划-执行流程,满足任务驱动式多智能体协作。

完成网关层运行时适配:扩展 workflow_resource_checks、workflow_executor、workflow_runs、workflow_agent_runner 与依赖注入,新增/更新配置(config.yaml、workflow_config),并补充 lead_agent 路径与节点实现,确保 Planner 与普通工作流运行态衔接一致。

补齐持久化与建模:新增 workflow_planning 持久化模块与 MySQL migration(20260831_01_workflow_planning_sessions),注册 SQL 与模型、内存 store,并扩展错误/表达式/validator/节点语义以承载新会话态输入输出。

增强文档与测试:新增会话式多智能体编排中文说明、更新开发文档与 AGENTS 指南、补充 workflow_engine/workflow_runs 回归测试,提高新路径的行为可验证性;同时留存 seed 资产(workflow-planner)便于一体化部署。
2026-09-01 11:47:30 +08:00

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"]