mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:31:58 +00:00
85 lines
4.8 KiB
Python
85 lines
4.8 KiB
Python
"""助手的节点手册要教「驳回」出口(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
|