让你的代理通过电子邮件协商会议时间 - DEV Community
让你的代理通过电子邮件协商会议时间 - DEV Community
大多数"AI日程助手"演示都是作弊的。它们展示一个已经知道时间的模型,调用一次"创建事件",然后鞠躬谢幕。那不是日程安排——那是有聊天界面的数据录入。真正的日程安排是一种协商:有人问"你周二有空吗?",你查一下,回复"周二满了,周三下午2点怎么样?",对方再讨价还价,最终两个人类(或一个代理与一个人)达成一个大家都不讨厌的时间段。这是一个多轮对话,最终以日历事件结束。
我之前写过关于给你的代理设置日历的文章——配置一个真实邮箱,托管事件,回复邀请。那篇文章止于单一的create调用。本文则关注该调用之前的所有内容:来回沟通中时间被决定的过程。创建事件是故事的最后一句话,而非整个情节。
载体是Nylas Agent Account。它是一个拥有自己电子邮件地址和日历的授权,因此可以成为协商中的实际参与者,而不是一个在人类背后偷看的机器人。我在开发Nylas CLI,所以下面的终端命令正是我在原型设计这类循环时实际使用的命令。
有一点我要提前说明,因为这是最常见的错误方向:Scheduler 不适用于 Agent Accounts。 没有可用性配置API,没有预定页面,没有/v3/scheduling/*。这在支持端点中有文档记录。所以如果你本能地想"直接指向Scheduler",停下——那扇门对这个提供商来说是锁着的。你确实拥有的是该授权自己的空闲/忙碌状态以及Events API,这足以让你自己构建整个协商过程。本文将展示如何实现。
协商循环的实际样貌
撇开AI不谈,会议协商是一个包含四个步骤的状态机:
- 入站消息提出时间(或要求代理提出一些时间)。它以电子邮件的形式出现在一个线程中。
- 代理检查自己的空闲/忙碌状态,在请求的时间窗口内查找空闲时段。
- 代理在线程内回复,提出一个提议——接受请求的时间之一,或提出自己的反建议。
- 对方再次回复。使用新的约束条件回到步骤2,直到有人说"好",然后创建事件。
这四个步骤中有三个直接映射到Nylas原语——入站邮件、空闲/忙碌、在线程内回复——第四个是事件创建。不是Nylas调用的部分是中间的决策:从电子邮件中解析"周三下午可以,但2点之前不行"并将其转化为具体的时间段。那是你的应用逻辑——一个LLM(大语言模型)调用或你自己拥有的解析器。我将在整个过程中诚实地说明这个边界,因为假装API为你做推理正是我抱怨的那种演示魔法。
好消息是数据平面永远不会改变。Agent Account 只是一个带有grant_id的授权,所以下面的每个调用都是标准的/v3/grants/{grant_id}/...端点。没有新的东西要学——如果你使用过Nylas电子邮件和日历API,你已经知道这些动词。
开始之前
你需要:
- 一个Agent Account(一个授权)。使用
nylas agent account create support@yourcompany.com或POST /v3/connect/custom创建一个。快速入门中有详细介绍。 - 它的
grant_id,以及一个导出为NYLAS_API_KEY的Nylas API密钥。 - 一种接收入站邮件的方式。订阅
message.createdwebhook,以便代理在请求到达后几秒内做出响应,或者按一定频率轮询GET /messages以进行批处理流。两者都支持。
刚接触Agent Accounts?先阅读什么是Agent Accounts——本文的其余部分假设你有一个可用的账户。
所有API示例使用美国基础主机https://api.us.nylas.com和bearer令牌。如果你的应用位于欧盟区域,请替换为api.eu.nylas.com。
步骤1:读取入站请求
当有人给代理发邮件——"我们下周能抽出30分钟吗?"——那是一个带有thread_id的message.created webhook。整个协商都发生在那个线程上,所以thread_id是你后续每一步都要携带的关键。参见代理的电子邮件线程了解为什么线程(而非主题行)是稳定的标识符。
获取邮件正文,以便你的代码可以提取请求的时间窗口:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages?thread_id=<THREAD_ID>&limit=5" \
--header "Authorization: Bearer $NYLAS_API_KEY"
从CLI操作:
nylas email threads show <THREAD_ID>
……并完整读取特定消息:
nylas email read <MESSAGE_ID>
步骤 2:检查智能体自身的忙碌/空闲状态
现在智能体会在请求的时间范围内查看自己的日历。这一步让智能体成为真正的参与者:它有既定任务,且不应提议已被占用的时段。
API 端点为 POST /v3/grants/{grant_id}/calendars/free-busy。传入时间窗口和要检查的邮箱地址——智能体自身的地址用于检查其日历:
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/calendars/free-busy" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"start_time": 1750766400,
"end_time": 1751025600,
"emails": ["support@yourcompany.com"]
}'
响应返回该地址在时间窗口内的忙碌时间段列表(Unix 时间戳 start_time/end_time 对)。空闲时段则是它们之间的空隙——计算这些空隙同样由你的代码完成。free/busy 告诉你哪些时间已被占用;你决定哪些可被视作可提议时段(仅限工作时间?30 分钟粒度?会议间缓冲时间?)。
CLI 通过 nylas calendar availability check 封装了相同调用:
# 检查智能体自身在时间窗口内的忙碌块
nylas calendar availability check \
--emails support@yourcompany.com \
--start "next monday 9am" \
--end "next friday 6pm"
此外还有 nylas calendar availability find,它更进一步,直接呈现空闲会议时段——当你在原型开发阶段且不想编写空闲时段查找逻辑时非常方便。但在生产环境的协商循环中,你通常需要从 check(或 API)获取原始的忙碌块,以便自己的代码控制如何选择时段。
一个微妙但重要的点:此处的 free/busy 检查的是智能体的日历。如果你还想检查人类的空闲情况,且该人类位于你应用可访问的已授权 Google 或 Microsoft 账户上,你可以在同一个 emails 数组中包含其地址——但同一请求中的所有地址必须属于同一提供商。然而,对于纯邮件的协商,你通常无法编程访问对方日历。这正是你通过邮件协商而非直接查询对方 free/busy 的原因。善加利用:提议、让对方还价、达成一致。
步骤 3:通过线程内回复提议时间
智能体已获取空闲时段。现在它需要提议这些时段——而提议是现有线程中的邮件回复,而非日历邀请。这正是与“直接创建事件”方法的核心区别所在。你尚未预定任何事,而是抛出两三个选项并等待。
在线程内回复至关重要,因为它能将整个协商保留在同一对话中,位于对方收件箱里,这样他们会很自然地再次回复。当你传递 reply_to_message_id 时,Nylas 会为你保留线程头(In-Reply-To、References):
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/messages/send" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"reply_to_message_id": "<INBOUND_MESSAGE_ID>",
"to": [{ "email": "alex@acme.com" }],
"subject": "Re: 30 minutes next week",
"body": "周二我已排满,但周三东部时间下午 2:00 或 3:30 有空,或者周四东部时间上午 11:00 有空。这些时间您方便吗?"
}'
CLI 版本更简洁,因为 reply 会自动获取原始邮件并填充收件人、主题和线程信息:
nylas email reply <INBOUND_MESSAGE_ID> \
--body "周二我已排满,但周三东部时间下午 2:00 或 3:30 有空,或者周四东部时间上午 11:00 有空。这些时间您方便吗?"
邮件正文中的措辞——如何表达选项、选择提供多少个选项、匹配请求者的偏好——由你的 LLM 根据你在步骤 2 中计算出的空闲时段生成。发送和线程处理由 Nylas 完成;措辞和选择哪些时段则由你决定。
步骤 4:读取对方还价并达成一致
这一步大多数演示完全跳过,但正是它让整个过程成为真正的协商。对方回复:“周三下午 2 点不行,你能改成周三早上吗?” 该回复会再次触发同一线程上的 message.created。你的处理程序在状态存储中查找 thread_id,发现该对话正处于协商过程中,然后带着新约束(“周三早上”)循环回到步骤 2。
有两个因素能让这个循环在生产环境中得以存活:
- 在线程上保持状态。 将
thread_id映射到你的协商状态——你上次提议的时间窗口、已进行的轮数、是否正在等待回复。将其存放在持久化存储中,而非内存中;这些对话可能持续数小时或数天。线程指南 提供了对此的成熟模式。 - 限制协商轮数。 决定例如三轮反提案后仍未达成一致时的处理方式:转交给人工,或发送一条类似 Scheduler 风格的“以下是我接下来五个空闲时段,请选择一个”消息并停止协商。两端都用 LLM 来回无休止地讨论,只会白白消耗每日发送配额。
每一轮仅仅是重复步骤 2 和 3——在缩小后的时间窗口内重新检查空闲/忙碌状态,并在原邮件线程中回复一个更紧凑的提案。当一方的回复是接受时,你们就达成了共识;你的解析器检测接受的方式与检测原始请求相同:通过阅读邮件。
步骤 5:预订商定的时段
一旦双方都同意某个具体时间,现在创建事件——这是其他演示中首先展示的那个单一调用。由于 agent 是真正的日历参与者,创建时使用 notify_participants=true 会从 agent 的地址发送正常的 ICS 邀请。请求方会在 Gmail 或 Outlook 中收到它,像对待任何同事一样点击接受。
calendar_id 是创建时的必需查询参数;对于 agent 的默认日历,使用 primary:
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/events?calendar_id=primary¬ify_participants=true" \
--header "Authorization: Bearer $NYLAS_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"title": "Acme <> Support sync",
"when": { "start_time": 1750944600, "end_time": 1750946400 },
"participants": [
{ "email": "alex@acme.com" }
]
}'
CLI 对应命令如下:
nylas calendar events create \
--calendar primary \
--title "Acme <> Support sync" \
--start "2026-06-26 14:00" \
--end "2026-06-26 14:30" \
--participant alex@acme.com
执行此命令后,一个 event.created webhook 会到达 agent 的日历,当请求方点击接受时,他们的响应会回流到 agent 的...