主题
使用 agent 自有中断提示 Slack 用户
从 Managed Deep Agents 工具发布一个 Slack 表单,暂停运行,并在用户提交表单时恢复它。
agent 自有中断是由你自己的代码创建的暂停。一个工具发布一条你在 Block Kit 中设计的 Slack 消息或模态框,然后调用 interrupt()。当用户按下按钮或提交模态框时,Managed Deep Agents 会将答案作为 user_prompt_response 渠道事件交付给部署,并用它恢复被暂停的运行。你可以在运行中途向用户请求结构化输入,例如一次批准、一个选择或一组表单字段,而无需构建 Slack 应用或你自己的 webhook。
这与 interrupt_on 创建的平台自有中断不同。那些会在工具调用前暂停,并由 Slack 或 Studio 为你渲染批准卡片。使用 agent 自有中断时,你拥有这些 block、表单状态以及答案到达后会发生什么。平台只负责把答案带回来。有关平台自有中断,请参阅人在回路。
NOTE
Managed Deep Agents 处于公开 beta 阶段,仅在 US 区域的 LangSmith Cloud 上可用。
NOTE
agent 自有中断需要 managed-deepagents>=0.8.0,以及部署 agent 上的一个 Slack 渠道。表单只在 Slack 中渲染。LangSmith Studio 显示原始的中断值。
工作原理
- 一次运行从一条 Slack 消息开始。你的工具从
runtime.channel读取对话,并使用部署的 bot token 将一条 Block Kit 消息发布到同一个 Slack 线程中。 - 工具调用
interrupt(),传入一个带有相关性 ID 的值。线程停在检查点。 - 用户按下按钮或提交模态框。Slack 将该交互发送到 Managed Deep Agents,后者将其作为
user_prompt_response事件转发给部署。 - SDK 将该事件与停泊的中断匹配并恢复运行。
interrupt()返回用户的响应,工具继续执行。
以这种方式暂停的工具在运行恢复时会从顶部再次执行,这是标准的 LangGraph 中断行为。对发布做防护,使其只发生一次。参阅发布表单并暂停。
发布表单并暂停
该工具需要三样东西:要发布到的 Slack 对话、bot token,以及将表单与中断关联起来的相关性 ID。
- 对话:
runtime.channel.raw_event是启动该运行的 Slack 事件。它带有channel、ts,对于线程消息还带有thread_ts。发布到那个线程中,以便答案路由回同一个 agent 线程。 - Bot token:
connections.get("mda/slack-bot-token", {"type": "agent"})解析 Managed Deep Agents 为该部署所配置的 Slack 应用的 token。该 slug 是保留的,无法通过mda connections create创建。 - 相关性 ID:将同一个字符串放在每个按钮的
value(作为 JSON)中,以及你传给interrupt()的值里的INTERRUPT_CORRELATION_KEY下。
内置 Slack 渠道的 runtime.channel.post 只发送文本,因此请直接通过 Slack Web API 发布 Block Kit。
python
import json
import uuid
import httpx
from langchain.tools import tool
from langgraph.types import interrupt
from managed_deepagents import (
INTERRUPT_CORRELATION_KEY,
ManagedDeepAgentRuntime,
connections,
)
@tool(parse_docstring=True)
async def request_approval(amount: str, runtime: ManagedDeepAgentRuntime) -> str:
"""Ask the requester to approve or decline a spend.
Args:
amount: The spend to approve, such as "$1,200".
"""
channel = runtime.channel
if channel is None or channel.provider != "slack":
return "Approval forms are available only in Slack."
correlation_id = str(uuid.uuid4())
# The tool runs again when the interrupt resumes. Post only on the first pass.
if channel.event["type"] != "user_prompt_response":
await post_approval_form(channel.raw_event, amount, correlation_id)
response = interrupt({INTERRUPT_CORRELATION_KEY: correlation_id, "amount": amount})
return f"The requester chose {response['action']} for {amount}."
async def post_approval_form(raw_event: dict, amount: str, correlation_id: str) -> None:
bot_token = await connections.get("mda/slack-bot-token", {"type": "agent"})
value = json.dumps({INTERRUPT_CORRELATION_KEY: correlation_id})
blocks = [
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"Approve a spend of *{amount}*?"},
},
{
"type": "actions",
"elements": [
{
"type": "button",
"action_id": "approve_spend",
"text": {"type": "plain_text", "text": "Approve"},
"value": value,
},
{
"type": "button",
"action_id": "decline_spend",
"text": {"type": "plain_text", "text": "Decline"},
"value": value,
},
],
},
]
async with httpx.AsyncClient(timeout=30.0) as client:
result = await client.post(
"https://slack.com/api/chat.postMessage",
headers={"Authorization": f"Bearer {bot_token}"},
json={
"channel": raw_event["channel"],
"thread_ts": raw_event.get("thread_ts") or raw_event["ts"],
"text": f"Approve a spend of {amount}?",
"blocks": blocks,
},
)
body = result.json()
if not body.get("ok"):
raise RuntimeError(f"Slack rejected the form: {body.get('error')}")像对待任何其他自定义工具那样,在 agent 定义中注册该工具。中断继承托管的检查点器,因此无需额外的持久化设置。
消息中按钮的任何 action_id 都有效,除了以 mda_modal:: 开头的那个。那个前缀会改为打开一个模态框。参阅打开模态框。
读取响应
interrupt() 返回规范化的响应。它只包含每个提供商共有的字段:
| 字段 | 类型 | 含义 |
|---|---|---|
provider | "slack" | 交付答案的渠道提供商。 |
action | string | 按钮的 action_id,或模态框的 callback_id。 |
value | JSON,可选 | 按钮的 value 字符串,或模态框的 state.values 对象。对平台不透明。 |
correlation_id | string,可选 | 当按钮的 value 是带有 mda_correlation_id 键的 JSON 时存在。模态框提交从不携带它。 |
同样的响应在运行上下文上以 runtime.channel.event 可用,其 type 设为 user_prompt_response。Slack 逐字的交互负载在 runtime.channel.raw_event 上。对于在消息内含输入表单的情况,用户的条目位于 raw_event["state"]["values"],按 block ID 和 action ID 作为键。
Managed Deep Agents 只读取 Slack 交互负载中的第一个动作,因此一次按下必须只对应一个事件。
响应如何找到它的中断
Slack 不知道某个按钮属于哪个中断,因此 SDK 会读取停泊在线程上的中断并做出判断:
| 停泊的中断 | 响应上的 correlation_id | 结果 |
|---|---|---|
| 一个 | 缺失 | 恢复该中断。 |
| 一个或多个 | 存在且恰好匹配一个 | 恢复匹配的中断。 |
| 多个 | 缺失 | 什么都不运行。SDK 记录该歧义并确认该事件。 |
| 一个或多个 | 存在但不匹配任何一个,或匹配多个 | 什么都不运行。启动一个回合会放弃停泊的检查点。 |
| 无 | 任意 | 在不带新消息的情况下启动一次全新运行。响应只能在 runtime.channel.event 上获取。 |
当 agent 可能在一个线程上停泊多个中断时,务必带上相关性 ID。没有它,一个线程只有在恰好一个中断待处理时才能被回答。
打开模态框
模态框比消息提供更多空间,并支持完整的 Block Kit 输入元素集。Slack 把模态框称为 view。你编写 view,Managed Deep Agents 打开它。
Slack 会为每次点击签发一个单次使用的 trigger_id,它大约在三秒后过期,并且它是打开模态框的唯一授权。将该点击转发给部署并等待一次运行会错过那个窗口,因此 Managed Deep Agents 直接从点击中已有的内容打开 view。agent 不会被告知这次点击。
要打开模态框,发布一个 action_id 以 mda_modal:: 开头、且 value 为完整 view(作为 JSON)的按钮:
json
{
"type": "button",
"action_id": "mda_modal::approve_spend",
"text": { "type": "plain_text", "text": "Review" },
"value": "{\"type\":\"modal\",\"title\":{\"type\":\"plain_text\",\"text\":\"Approve spend\"},\"submit\":{\"type\":\"plain_text\",\"text\":\"Submit\"},\"blocks\":[...]}"
}当用户按下提交时,部署会收到一个 user_prompt_response,其 action 设为 view 的 callback_id(等于按钮的 action_id),value 设为 view 的 state.values。随后模态框关闭。
view 上有两个字段属于平台:
callback_id:设为按钮的action_id,以便提交可被识别。private_metadata:保存模态框被打开时所在的加密渠道和线程。模态框提交不携带渠道或线程,因此这是回到对话的唯一路径。如果你的 view 已经设置了private_metadata,模态框将不会打开。
每个其他字段都按你编写的那样原样发送到 Slack。Managed Deep Agents 不校验 view,因此一个被 Slack 拒绝的 view 不会向用户显示任何内容。
模态框提交从不携带 correlation_id。模态框只有在恰好一个中断停泊时才能回答一个线程。
整个 view 必须能放进按钮的 value 中,而 Slack 将其上限设为 2000 个字符。当工具发布消息时,Slack 会拒绝一个过大的按钮,因此失败会出现在工具调用中,而不是点击时。
发布但不暂停
一个工具可以发布一个表单并让运行结束,而不调用 interrupt()。随后提交会在不带新消息的情况下启动一次全新运行,你的代码从 runtime.channel.event 读取答案。平台上没有任何东西在跟踪是否有一个提示待处理,因此你的应用自己掌握待处理表单的状态、过期,以及当表单打开时后续聊天消息意味着什么。
当预期用户接下来要做的是回答该提示时,更倾向于使用 interrupt()。
限制与保留名称
平台保留的名称:
| 名称 | 位置 | 含义 |
|---|---|---|
mda_modal:: 前缀 | 按钮 action_id | 该点击打开一个模态框,且永不到达 agent。 |
mda_correlation_id | 按钮 value 内的键 | 将一次点击匹配到一个停泊的中断。导出为 INTERRUPT_CORRELATION_KEY。 |
callback_id | 应用定义的 view | 设为按钮的 action_id。 |
private_metadata | 应用定义的 view | 由平台设置。提供它会阻止模态框打开。 |
mda/slack-bot-token | 连接 slug | 部署的 Slack bot token。无法用 CLI 创建或覆盖。 |
Managed Deep Agents 在转发 view 前不会检查的 Slack 限制:
| 限制 | 值 |
|---|---|
按钮 value | 2000 个字符 |
| 模态框标题 | 24 个字符 |
| 每个 view 的 blocks | 100 |
callback_id | 255 个字符 |
超过最后三项会使模态框无法打开,且不向用户显示任何内容。
了解失败模式
- 模态框不打开。 view JSON 格式错误、超过某个 Slack 限制,或
trigger_id已过期。在 Slack 中按下按钮没有任何可见效果。失败会在平台上记录日志。 - 用户关闭了模态框。 中断保持停泊。没有取消路径,也没有过期。
- 用户在线程中回复而不是使用表单。 该消息走普通的消息路径,不会参考中断状态。运行以新消息开始,停泊的检查点被放弃,而卡片仍保留其可用按钮。
- 两个人按下同一个按钮。 两个交互都会被转发。第一个恢复中断,第二个启动一个全新回合。没有任何东西对它们去重。
- 多个中断停泊且响应没有相关性 ID。 什么都不运行。由于模态框提交从不携带相关性 ID,模态框无法回答有两个停泊中断的线程。
- 提交无法返回
response_action。 没有逐字段校验错误,没有链式模态框,也没有原地模态框更新。
另请参阅
- 人在回路:在选定的工具调用前暂停,使用平台渲染的批准卡片。
- 将 Managed Deep Agent 连接到 Slack:添加用于交付表单响应的 Slack 渠道。
- 管理连接:
connections.get(...)如何解析 agent 自有的凭据。 - 添加自定义工具:注册发布该表单的工具。