mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:51:59 +00:00
助手的节点手册讲驳回出口:哪些节点能接(审批、补充输入、编辑稿件、动作确认、代码和工具审批、双盲裁决),连线写 sourceHandle="rejected",什么时候接(驳回后要通知、退回修改、走另一条路),不接时怎样(运行结束、记为被驳回;审批模式这一路到此为止;写了驳回即终止就别接),驳回那条路必须走到成果节点或结束,失败重试、出错时继续不管驳回。以前手册只有一句审批的 approved / rejected,助手画出来的图驳回就整轮结束
This commit is contained in:
@@ -327,7 +327,19 @@ skip_if。这些字段按受限的 Python 表达式求值,变量直接写路
|
||||
- edges 里每条边 {source, target, sourceHandle?}
|
||||
- branch 节点的出边必须带 sourceHandle,取值是 cases 里的 key,另外要有一条 sourceHandle="default" 兜底
|
||||
- loop 节点:sourceHandle="body" 连循环体,循环体最后一个节点再连回 loop 节点;sourceHandle="done" 连循环结束后的去向
|
||||
- human 节点(approve 模式):sourceHandle 用 "approved" 和 "rejected"
|
||||
- human 节点(approve 模式):批准走 sourceHandle "approved",驳回走下面的「驳回」出口
|
||||
- 「驳回」出口:会被人驳回的节点可以多接一条 sourceHandle="rejected" 的边,驳回(包括超时驳回)之后沿它往下走。
|
||||
能接的只有:human(审批、补充输入、编辑稿件三种模式)、tool(工具调用审批、写外部系统前的动作确认)、
|
||||
code(代码执行审批)、dual_check(双盲复算的裁决);别的节点接了画布校验会报错。
|
||||
· 什么时候接:驳回之后还要做事——通知申请人(接一个通知或写说明的节点,再到成果节点)、退回修改(连回要改的上游
|
||||
那一步,改完再到这个节点问一次)、改走另一条路(比如出一份「已退回」的成果)。驳回时这一步照常结束,下游用
|
||||
{{ nodes.关卡id.note }} 取驳回备注、{{ nodes.关卡id.actor }} 取谁驳回的;动作确认被驳回时写入不会发出
|
||||
· 不接时:补充输入、编辑稿件、动作确认、代码和工具审批、双盲裁决被驳回,运行就此结束、记为被驳回(是人的决定,不算失败);
|
||||
审批模式不接时这一路到审批为止、不再往下走(运行照常结束,没有成果)。审批写了 stop_on_reject(驳回即终止运行)就不要再接驳回出口:
|
||||
驳回时运行直接结束,那条边走不到
|
||||
· 驳回那条路必须走到成果节点,或者在一个不再往下连的节点结束(退回修改绕回上游、改完再问一次也算),否则驳回之后
|
||||
只能一直绕下去,发布检查会报错
|
||||
· 驳回不是故障:「失败重试」「出错时继续」不管驳回,不要为了驳回之后接着走去配它们
|
||||
- 一个节点连出多条普通边 = 并行执行,多条边汇入同一节点 = 等待汇聚
|
||||
"""
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
"""助手的节点手册要教「驳回」出口(docs/design/orchestration-round/00-总方案.md §3)。
|
||||
|
||||
手册里没写的出口,助手就不会接:驳回之后要通知申请人、退回修改、改走另一条路的,画出来的图只会在驳回时整轮结束。
|
||||
这里不只查关键词,还拿校验认的那一份(会被驳回的节点类型、出口的 sourceHandle、发布检查的「走不到结束」)来对,
|
||||
再照手册的写法搭一张图,校验、发布检查都过。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from app.api.copilot import NODE_REFERENCE
|
||||
from app.engine.schema import GATE_TYPES, REJECT_HANDLE, GraphSpec, validate_graph
|
||||
|
||||
|
||||
def _reject_entry() -> str:
|
||||
start = NODE_REFERENCE.index("「驳回」出口")
|
||||
return NODE_REFERENCE[start:NODE_REFERENCE.index("\n- 一个节点连出多条普通边", start)]
|
||||
|
||||
|
||||
def test_the_guide_names_every_node_that_can_be_rejected_and_the_handle():
|
||||
entry = _reject_entry()
|
||||
assert f'sourceHandle="{REJECT_HANDLE}"' in entry
|
||||
# 能接驳回出口的节点类型(schema.GATE_TYPES)一个不漏,按 type 字段写
|
||||
assert all(f"{t}" in entry for t in GATE_TYPES), [t for t in GATE_TYPES if str(t) not in entry]
|
||||
# 六种关卡都点到:审批、补充输入、编辑稿件、动作确认、代码 / 工具审批、双盲裁决
|
||||
for kind in ("审批", "补充输入", "编辑稿件", "动作确认", "代码执行审批", "工具调用审批", "裁决"):
|
||||
assert kind in entry, kind
|
||||
|
||||
|
||||
def test_the_guide_says_when_to_connect_it_and_what_happens_without_it():
|
||||
entry = _reject_entry()
|
||||
# 什么时候接:驳回后要通知、退回修改、走另一条路
|
||||
assert "通知" in entry and "退回修改" in entry and "另一条路" in entry
|
||||
# 不接时:运行结束、记被驳回(不算失败);审批模式没设驳回即终止的老规矩照实说
|
||||
assert "记为被驳回" in entry and "不算失败" in entry
|
||||
assert "stop_on_reject" in entry
|
||||
# 驳回那条路必须走到成果节点或结束(发布检查 reject_exit.no_end),绕回去改完再问一次也可以
|
||||
assert "成果节点" in entry and "不再往下连" in entry and "发布检查" in entry
|
||||
# 驳回不是故障:失败重试、出错时继续不管它
|
||||
assert "失败重试" in entry
|
||||
# 驳回之后下游取得到谁、备注
|
||||
assert "{{ nodes.关卡id.note }}" in entry
|
||||
|
||||
|
||||
def test_the_guide_does_not_leak_internal_names():
|
||||
entry = _reject_entry()
|
||||
for internal in ("reject_exit", "NodeRejected", "end_reason", "cancelled", "__decision__", "GATE_TYPES"):
|
||||
assert internal not in entry, internal
|
||||
|
||||
|
||||
def test_the_old_one_line_rule_is_replaced_not_duplicated():
|
||||
# 以前只有一句「human 节点(approve 模式):sourceHandle 用 approved 和 rejected」:驳回出口说一处,不留两种说法
|
||||
assert len(re.findall(r'sourceHandle="rejected"', NODE_REFERENCE)) == 1
|
||||
|
||||
|
||||
def _node(nid: str, ntype: str, **config):
|
||||
return {"id": nid, "type": ntype, "position": {"x": 0, "y": 0}, "data": {"label": nid, "config": config}}
|
||||
|
||||
|
||||
def test_a_graph_drawn_the_way_the_guide_says_passes_the_checks():
|
||||
"""照手册写:审批批准走 approved,驳回接 sourceHandle="rejected" 到「通知申请人」再到一个成果节点;工具的确认被驳回
|
||||
退回上游修改(连回起草,改完再问一次)。画布校验、各级发布检查都不报驳回出口的问题。"""
|
||||
from app.engine.governance import lint_for_publish
|
||||
|
||||
graph = {
|
||||
"nodes": [_node("start", "input"), _node("draft", "llm", prompt="起草"),
|
||||
_node("gate", "human", mode="approve", title="可以吗"),
|
||||
_node("send", "tool", tool="calculator", args={"expression": "1+1"}, approval="always"),
|
||||
_node("tell", "llm", prompt="通知申请人:{{ nodes.gate.note }}"),
|
||||
_node("done", "output", fields=[{"name": "结果", "value": "{{ nodes.send.text }}"}]),
|
||||
_node("back", "output", fields=[{"name": "结果", "value": "{{ nodes.tell.text }}"}])],
|
||||
"edges": [{"source": "start", "target": "draft"}, {"source": "draft", "target": "gate"},
|
||||
{"source": "gate", "target": "send", "sourceHandle": "approved"},
|
||||
{"source": "gate", "target": "tell", "sourceHandle": "rejected"},
|
||||
{"source": "tell", "target": "back"},
|
||||
{"source": "send", "target": "done"},
|
||||
{"source": "send", "target": "draft", "sourceHandle": "rejected"}],
|
||||
}
|
||||
spec = GraphSpec.model_validate(graph)
|
||||
codes = {i.code for i in validate_graph(spec).issues if i.level == "error"}
|
||||
assert not {c for c in codes if c and "reject" in c}, codes
|
||||
for level in ("draft", "published", "governed"):
|
||||
issues = lint_for_publish(spec, level=level).issues
|
||||
assert not [i for i in issues if (i.code or "").startswith("reject_exit")], level
|
||||
Reference in New Issue
Block a user