mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 01:41:57 +00:00
62aac187d8
MCP / 自定义工具的审批三档(tools/trust.py) - 平台看不出这两类工具做什么,以前审批关卡一律当它们安全、照跑不误。现在默认每次调用都等审批; 工具页可以逐个改成「始终允许 · 门控把关」或「始终允许」,审批卡上的「始终允许」等于前者 - 门控把关:每次调用先由门控模型审一遍工具说明、参数和节点任务;判可疑、出错、超时、答不成格式, 一律交还给人,不会变成放行。判定包进 checkpoint,审批恢复重放时不再问一遍 - agent 节点、工具节点、协作团队三处关卡都接上;团队成员停不下来等人,门控没放行的调用不执行 - 每次运行在发起时快照档位(runs.tool_trust),恢复和接着跑沿用同一份;正式运行不看档位,一律等审批; 升级前发起的运行照旧不问——它们可能正停在某个审批上,多问一次答复就会对错号 - 接口:GET /api/tools 带 trust / trust_key,PUT /api/tools/trust;设置新增门控模型;新事件 tool.gated - 界面:工具页三选一、审批卡「始终允许」、设置页门控模型、运行记录解码 tool.gated; 顺手修了 MCP 工具详情 404、工具审批被驳回却显示成放行 智能体护栏(engine/guards.py),取代固定的「最多 12 步」 - 步数只留兜底:默认 100,设置里可改;以前画布自动写进节点的 12 按「没填」处理 - 同一个工具、同样的参数第二次不再执行,把上次的结果交还给它;连续 3 步拿不到新信息就收尾 - 每个节点的令牌预算(默认 200 万)和金额预算(默认不限),都可以在设置里改成不限 - 上下文用到窗口的 60% 先压缩早期的工具结果,到 85% 就收尾;临近任何一个上限先提醒模型一次 - 下游结构化校验失败时写明是上游哪个 agent 提前收了尾;复核信号带上收尾原因, 问数据页只在真是步数用满时才给「放宽步数重跑」 - 上限在发起时快照(runs.agent_limits);升级前发起的运行一字不改走旧逻辑 检查脚本 - check-all 默认 4 道并行:量时间最多的两项互不重叠;并行时没过的最后单独重跑一遍,重跑才过的 单列成「偶发」。16 项全量从约 21 分钟降到约 7 分钟(加上一次重跑约 10 分钟);--lanes 1 回到串行 顺带修的 - 设置接口里的 limits 改为只读:库里存着旧版设置页写进去的一份(max_agent_steps: 25),从不生效, 却盖住了真实上限,问数据页照着错的数许诺 - test_kb 里嵌入模型那条测试不再真连 api.openai.com(代理慢时一条要等近 4 分钟) - check-stream 读滚动容器前先等它挂上,并行跑时不再偶发 验证(沙箱):tsc 干净,生产构建通过,pytest 1655 passed(两遍),check-all 16 项检查、3562 项全过; 新增后端测试 53 条,关键逻辑做了 22 处变异验证、全部抓到。
103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from langgraph.config import get_stream_writer
|
|
|
|
from app.core.events import EventType
|
|
from app.engine.schema import GraphNode, GraphSpec
|
|
from app.engine.state import GraphState, template_context
|
|
from app.engine.expressions import render_deep, render_template
|
|
|
|
|
|
@dataclass
|
|
class RunContext:
|
|
"""一次运行的全局上下文,编译时闭包进每个节点。"""
|
|
|
|
run_id: str
|
|
thread_id: str
|
|
spec: GraphSpec
|
|
workflow_id: str | None = None
|
|
# 深度 > 0 表示当前在子图里,用来防止子图无限递归
|
|
depth: int = 0
|
|
memory_scope: str = "default"
|
|
collection: str = "default"
|
|
#: 工具审批策略的全局默认(设置里的「危险工具默认需要人工确认」)。节点和图级
|
|
#: defaults 都没配 approval 时才用它;为空表示老运行,按节点各自的缺省走
|
|
approval_default: str | None = None
|
|
#: MCP / 自定义工具信任三档的快照(tools/trust.py)。None 是升级前发起的运行,
|
|
#: 这两类工具照旧不问人
|
|
tool_trust: dict[str, str] | None = None
|
|
#: agent 护栏的上限快照(engine/guards.py)。None 是升级前发起的运行,走旧逻辑
|
|
agent_limits: dict[str, Any] | None = None
|
|
extra: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class NodeContext:
|
|
"""单个节点执行时能拿到的东西。"""
|
|
|
|
node: GraphNode
|
|
run: RunContext
|
|
|
|
@property
|
|
def config(self) -> dict[str, Any]:
|
|
return self.node.data.config
|
|
|
|
def emit(self, event_type: EventType | str, **data: Any) -> None:
|
|
"""往 LangGraph 的 custom 流里发一条事件。
|
|
|
|
runner 在另一端消费,转成落库的 RunEvent 并广播给前端 —— 画布上的节点高亮、
|
|
token 流、工具调用卡片全都来自这里。
|
|
"""
|
|
try:
|
|
writer = get_stream_writer()
|
|
except Exception: # noqa: BLE001 - 不在图执行上下文里(比如单元测试)
|
|
return
|
|
writer(
|
|
{
|
|
"__agentlab__": True,
|
|
"type": str(event_type),
|
|
"node_id": self.node.id,
|
|
"ts": time.time(),
|
|
"data": data,
|
|
}
|
|
)
|
|
|
|
def render(self, value: Any, state: GraphState) -> Any:
|
|
"""用当前状态渲染配置里的模板。"""
|
|
return render_deep(value, template_context(state))
|
|
|
|
def render_str(self, value: str | None, state: GraphState) -> str:
|
|
return render_template(value or "", template_context(state))
|
|
|
|
def cfg(self, key: str, default: Any = None) -> Any:
|
|
"""读节点配置,没有就回退到图级 defaults。"""
|
|
value = self.config.get(key)
|
|
if value in (None, ""):
|
|
return self.run.spec.defaults.get(key, default)
|
|
return value
|
|
|
|
def approval_mode(self, fallback: str = "dangerous") -> str:
|
|
"""工具审批策略:节点 > 图级 defaults > 全局设置 > 节点类型自己的缺省。
|
|
|
|
审批恢复时节点会整个重放、再算一遍要不要问人,所以这里只能取决于
|
|
这次运行固定下来的东西——全局默认在发起时就定死在 RunContext 上了。
|
|
"""
|
|
return self.cfg("approval") or self.run.approval_default or fallback
|
|
|
|
def actor(self) -> str | None:
|
|
"""回复这个节点审批的人(设置里的署名)。没署名、或者不是恢复出来的,就是 None。"""
|
|
return (self.run.extra.get("actors") or {}).get(self.node.id)
|
|
|
|
|
|
class NodeError(RuntimeError):
|
|
"""节点执行失败。带上 node_id 方便前端定位到具体的卡片。"""
|
|
|
|
def __init__(self, node_id: str, message: str, *, retryable: bool = False) -> None:
|
|
super().__init__(message)
|
|
self.node_id = node_id
|
|
self.retryable = retryable
|