mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:11:58 +00:00
a819f92903
三个口子,都实测复现过:
1. 回复"拒绝",工具照样执行。四个节点四套审批解析:工具节点和代码节点对
回复一律 bool(),"拒绝"、"no" 都是真;{"decision": "approve"} 反被当成
拒绝;{"approved": "false"} 也是批准。现在收成一份 engine/approval.py:
只认明确的批准,看不懂的一律按拒绝。填内容的 human input/edit 模式另算——
裸字符串是交稿,只有结构体里明说"不"才驳回。
2. 可写数据源上的 DELETE 从来不问人。数据源工具是动态生成的,查不到
ToolSpec,任何审批模式下都被当成安全;is_dangerous_call 写好了但全仓库
没人调用。现在危险与否逐次判定:registry.call_is_dangerous 统一问,数据源
工具把判定挂在 metadata["dangerous_if"] 上——同一个 db_query,查询直接
跑,可写源上的写要先确认。工具节点、agent 节点、协作成员都走它。
这一步以前没出事,只因为写入从不提交:run_query 走 stream,拿不到结果集就
抛 ResourceClosedError,连接关闭时再回滚——审批通过的写也没落过库。审批接
好之后才让写真正提交(先后不能反,反过来就是一个无审批写库的窗口);
RETURNING 这类带结果集的写照样返回行。工具页的"试一下"是人自己在执行,
不设审批。
3. 协作团队的成员调工具完全不经审批。成员们并行跑在一个节点里、停不下来等
人(恢复时节点整个重放,几个人的 interrupt 对不上号),所以需要确认的调用
不执行,把原因交还给成员让它换做法;节点上显式设成"全部自动放行"才跑。
前端补了这个选项。顺带修了同一处的两个老问题:成员步数不受 max_agent_steps
约束;步数用完时把工具的原始返回当结论交上去(agent 节点在 4057cdc 修过
同一个问题,这里没跟上)——现在补一轮不带工具的收尾。
连带修的:多条审批并存时,交给引擎的是库里存的那份回复,而它存之前被 _safe
截到了 2000 字,解包时又只留 value——人工改过的长稿被砍一截,带了内容的驳回
被当成交稿。现在存全文、只拆自己包的那层 {"value": x}。
test_approval.py 36 例;去掉修复重跑,引擎层 10 例全部失败,各对应一个真问题。
并行驳回那例翻出了一个运行卡死的老 bug,已在上一个提交单独修掉。
338 → 374 passed,连跑三次全绿。
82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
"""人工审批的回复怎么读。全引擎只此一份。
|
|
|
|
以前有四份:human 节点、agent 节点、工具节点、代码节点各读各的,口径不一。
|
|
后果不是风格问题:工具节点和代码节点对字符串一律 bool(),回复"拒绝"、"no"
|
|
都被当成批准,工具真的执行了;而 {"decision": "approve"} 反被判成拒绝。
|
|
|
|
规则只有一条:**只认明确的批准,看不懂的一律按拒绝处理。** 审批这道门放错了
|
|
的代价(危险工具被执行)远大于拦错了的代价(再批一次)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
_YES = frozenset({
|
|
"approve", "approved", "yes", "y", "true", "ok", "allow",
|
|
"同意", "通过", "批准", "允许", "可以",
|
|
})
|
|
_NO = frozenset({
|
|
"reject", "rejected", "deny", "denied", "no", "n", "false",
|
|
"拒绝", "驳回", "不同意", "不通过", "不允许", "不可以",
|
|
})
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Decision:
|
|
#: 明确批准了。审批类的关卡(工具、代码、human 的 approve 模式)只认这个。
|
|
approved: bool
|
|
#: 明确拒绝了。填内容类的关卡(human 的 input / edit 模式)只在这时才停:
|
|
#: 那两种模式的回复本身就是内容,没说"不"就是交了稿。
|
|
rejected: bool
|
|
note: str = ""
|
|
#: 人工改过的工具参数
|
|
args: dict[str, Any] | None = None
|
|
#: 人工改过的代码
|
|
code: str | None = None
|
|
#: human 节点收到的内容(input / edit 模式)
|
|
value: Any = None
|
|
|
|
|
|
def _word(value: Any) -> str:
|
|
return value.strip().lower() if isinstance(value, str) else ""
|
|
|
|
|
|
def _verdict(value: Any) -> bool | None:
|
|
"""True / False / None(没表态)。"""
|
|
if isinstance(value, bool):
|
|
return value
|
|
word = _word(value)
|
|
if word in _YES:
|
|
return True
|
|
if word in _NO:
|
|
return False
|
|
return None
|
|
|
|
|
|
def read_decision(response: Any) -> Decision:
|
|
if isinstance(response, dict):
|
|
verdict = _verdict(response["approved"]) if "approved" in response \
|
|
else _verdict(response.get("decision"))
|
|
args = response.get("args")
|
|
code = response.get("code")
|
|
return Decision(
|
|
approved=verdict is True,
|
|
rejected=verdict is False,
|
|
note=str(response.get("note") or response.get("comment") or ""),
|
|
args=args if isinstance(args, dict) else None,
|
|
code=code if isinstance(code, str) and code else None,
|
|
value=response.get("value"),
|
|
)
|
|
verdict = _verdict(response)
|
|
# 不是一个是/否词的字符串("拒绝,这个表是生产库")既算不上批准,本身又
|
|
# 是理由——留作备注,agent 转告模型时才不会只剩一句"未说明"
|
|
note = response.strip() if isinstance(response, str) and verdict is None else ""
|
|
return Decision(
|
|
approved=verdict is True,
|
|
rejected=verdict is False,
|
|
note=note,
|
|
# 字符串回复本身就是内容:input 模式下用户可能直接回一句话
|
|
value=response,
|
|
)
|