1. 概览
AI 和技术领域的发展速度之快,让任何人都难以跟上。每天都有新模型、新论文和新产品发布。如果有一个摘要代理,每天早上都能获取当天的新闻头条、撰写精辟的摘要并生成 PDF,就能解决这个问题。但以前构建这样的代理意味着要选择框架、在 Python 中定义工具、编写编排循环、打包容器并部署到 Cloud Run。在代理发出任何 Web 请求之前,所有这些操作都已完成。
Gemini API 中的托管式智能体改变了这一局面。您只需编写两个 Markdown 配置文件和一个预构建的渲染器脚本,进行一次 API 调用,即可启动真实的 Ubuntu 沙盒、浏览网页、撰写摘要并生成 PDF。没有容器。无部署。无需编排代码。
在此 Codelab 中,您将构建一个这样的代理:从空函数开始,逐步实现每日摘要功能。
构建内容
- 在真实的 Linux 沙盒中创建并运行首个托管式智能体
- 使用编辑风格、网络来源和 PDF 技能自定义智能体
- 添加了一个安全钩子,用于在破坏性命令运行之前将其屏蔽
- 下载代理生成的 PDF
- 在多轮对话中优化摘要,而无需重新提取网页内容
- 保存代理配置,并在以后的运行中通过 ID 调用该配置
- 通过 Gmail API 将摘要发送到您的收件箱
- 安排代理每天自动运行并发送
所需条件
- Python 3.10 及更高版本
- Gemini API 密钥:aistudio.google.com/api-keys(包含免费层级;建议启用结算功能,以便不间断运行)
2. 什么是 Gemini API 中的托管式智能体?
AI 系统的三个级别
在深入了解代码之前,我们先来看看托管式智能体相对于两种替代方案的适用场景:
级别 | 简介 | 谁管理基础设施? |
标准 LLM | 您提出提示,它会以文本形式回复。没有手、没有记忆、没有网络。 | 不适用:无法自行执行任何操作 |
自托管代理 | 您将 ADK/LangChain/AutoGen + Docker + 工具 + 内存连接起来。 | 您:全部(或像 Agent Engine 这样的受管理的平台) |
受管代理 | 您为其设定目标。Google 会提供一个安全沙盒。智能体可以自主编写代码、运行代码、读取错误、搜索网络和修复 bug。 | Google:所有内容 |
此 Codelab 重点介绍第三行。您提供任务和配置文件。其他一切都由 Google 处理。
您可以使用 ADK + Cloud Run 构建哪些内容
如需构建一个可浏览网页、运行 Python 并生成 PDF 的新闻摘要智能体,您需要使用 ADK + Cloud Run 实现以下所有功能:
# agent.py: define tools and wire up the agent
from google.adk.agents import LlmAgent
from google.adk.tools import google_search, built_in_code_execution
agent = LlmAgent(
name="digest-agent",
model=MODEL,
instruction=AGENTS_MD, # your editorial voice and rules
tools=[google_search, built_in_code_execution],
)
# app.py: serve the agent over HTTP
from google.adk.runners import FastApiRunner
runner = FastApiRunner(agent=agent)
app = runner.app
# pdf_tool.py: custom tool, install reportlab, render PDF
# scraper.py: custom tool, fetch each news source
# streaming.py: wire agent events to your SSE endpoint
# Dockerfile: package everything
FROM python:3.12
COPY . /app
RUN pip install google-adk reportlab requests
CMD ["uvicorn", "app:app", "--host", "0.0.0.0"]
# Deploy to Cloud Run
gcloud run deploy digest-agent \
--image gcr.io/your-project/digest-agent \
--set-secrets GEMINI_API_KEY=gemini-key:latest \
--memory 2Gi
即在代理运行一次之前。您仍然拥有沙盒隔离(因此代理无法损坏您的服务器)、软件包安装、工具调用之间的状态管理以及将事件流式传输到客户端的基础设施。
托管式智能体将其替换为
from google import genai
client = genai.Client()
stream = client.interactions.create(
agent="antigravity-preview-05-2026",
input="Generate the digest.",
stream=True,
environment={
"type": "remote",
"sources": [ # your config files, mounted at startup
{
"type": "inline",
"target": ".agents/AGENTS.md",
"content": AGENTS_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/SKILL.md",
"content": SKILL_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
"content": GENERATE_PDF_PY,
},
],
},
)
ADK + Cloud Run 的要求 | 托管式智能体可为您处理哪些事务 |
容器映像 + Dockerfile + CI/CD | 完全托管的 Ubuntu 沙盒(Python 3.12、Node 22、4 个 CPU / 16 GB RAM) |
Cloud Run 部署 + 扩缩 | 按互动次数提供,在处于非活跃状态 7 天后自动过期 |
沙盒隔离 | 每次互动隔离 |
自定义 PDF 工具 + | 代理在沙盒内安装软件包 |
SSE 流式传输基础架构 |
|
Python 中的工具定义 | 内置工具:网页浏览、代码执行、文件系统 |
工具调用之间的状态管理 | 内置于代理推理循环中 |
您只需编写配置文件(AGENTS.md、SKILL.md、预构建的脚本)并进行一次 API 调用。其他一切都由 Google 处理。
沙盒的运作方式
interactions.create() call
│
▼
Google provisions Ubuntu sandbox (Python 3.12, Node 22, 4 CPU / 16 GB RAM)
│
▼
Agent reasoning loop:
plan → fetch URLs → run Python → write files → reason → repeat
│
▼
Events stream back in real time: tool calls, text chunks, completion
│
▼
interaction.completed → environment_id + interaction_id
沙盒会在处于非活跃状态 7 天后失效。您可以使用 environment_id 继续对话,以优化输出、运行后续任务,或将其派生为已保存的命名代理。
3. 设置
选项 A:Cloud Shell(推荐)
点击下方按钮,在 Google Cloud Shell 中打开此 Codelab。所有依赖项均已预安装。
方案 B:本地设置
git clone https://github.com/Saoussen-CH/tech-digest-managed-agent.git
cd tech-digest-managed-agent
根据需要安装 uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
配置 API 密钥
cp .env.example .env
cloudshell edit .env
设置密钥:
GEMINI_API_KEY=your-key-here
安装依赖项
uv sync
4. 进行首次智能体调用
打开起始文件
cloudshell edit run_digest.py
run_digest() 现在有一个待办事项需要填写,下一步还有三个待办事项。系统已在上方预填充了两个辅助函数:
load_source(path):从相对于脚本的.agents/读取文件。您将在下一个练习中使用它,将编辑风格、PDF 剧本和渲染程序装载到沙盒中。run_stream(stream):处理事件流并返回(environment_id, interaction_id)。您无需自行编写事件循环。
添加内容
TODO 1:将 pass 替换为(暂时忽略 TODO 3 和 4;这些 TODO 用于下一步):
from google import genai
client = genai.Client()
stream = client.interactions.create(
agent=BASE_AGENT,
agent_config={"type": "antigravity", "model": "gemini-3.7-flash"},
input="Fetch the Hacker News front page and list the top 5 stories.",
stream=True,
environment="remote",
)
environment_id, interaction_id = run_stream(stream)
print(f"\nDone. environment_id={environment_id}")
各部分的用途
genai.Client() 从环境中读取 GEMINI_API_KEY。其他所有内容都通过此客户端进行。
interactions.create() 是核心调用。以下四个参数可让此功能正常运行:
agent=BASE_AGENT:选择 Antigravity 智能体 (antigravity-preview-05-2026),这是由 Gemini 3.7 Flash 提供支持的通用托管式智能体。您可以使用agent_config配置底层模型(选项:gemini-3.7-flash、gemini-3.6-flash、gemini-3.5-flash、gemini-3.5-flash-lite)。它默认启用三个内置工具:code_execution(运行 Bash、Python、Node.js)、google_search和url_context(提取和读取网页)。当您传递environment参数时,系统会自动启用文件系统工具(read_file、write_file、list_files)。只需一次调用,即可预配一个完全受管理的 Ubuntu 环境,其中预安装了 Python 3.12、Node.js 22、git、pip 和 curl。没有要构建的容器,也没有要运行的部署。input:相应运行的任务。智能体浏览 Hacker News 并推断结果。environment="remote":为此互动预配全新的云沙盒。stream=True:返回事件的可迭代对象,而不是阻塞。如果不使用此参数,调用会等待 30-90 秒,然后以interaction.output_text形式一次性返回所有输出。借助流式传输,您可以实时查看智能体的推理和行动。流式传输在这里并不是一项高级功能:它是一个合适的默认设置,因为 90 秒的黑框不会向您发出有关代理是否正在工作或卡住的信号。
environment_id 是刚刚运行的沙盒的句柄。在 interaction.completed 之后,沙盒不会关闭,而是会保持运行状态,最长可达 7 天。您可以通过 environment_id 返回到该页面。将其传递给第二次 interactions.create() 调用,代理会在同一文件系统上恢复,并使用相同的文件和已安装的软件包,就像从未离开过一样。下一步使用它来下载 PDF,而无需重新运行代理,再下一步使用它来继续对话。
interaction_id 是刚刚完成的对话轮次的句柄。在下一次调用中将其作为 previous_interaction_id 传递,这样一来,代理就可以完全记住它在此轮对话中说过的话和做过的事。
验证
uv run python run_digest.py
您应该会看到代理运行时的实时输出:
[agent started]
[tool] run_code
Here are the top 5 stories currently on the Hacker News front page, retrieved via the official Hacker News API:
1. **Qwen 3.6 27B is the sweet spot for local development** (471 points)
2. **.self: A new top-level domain designed to support self-hosting** (116 points)
...
Done. environment_id=e3de58774073f75a6ef42924c6ce2e88
即使使用 environment="remote",该 API 也会返回真实的 environment_id。沙盒已运行。缺少的是配置:没有语音、没有技能、没有 PDF 生成器。代理只是将故事打印为文本,然后就停止了。下一步将添加这些内容。
输出的每一行都对应于 run_stream() 中的一个事件:
| 简介 |
|
| 代理提取网址 |
|
| 在沙盒中运行代码的智能体 |
|
| 智能体搜索网页 |
|
| 文件工具和其他工具 |
|
| 代理撰写文字 | 直接流式传输到 stdout |
5. 自定义智能体
该代理没有指令:没有语音、没有技能、没有 PDF 生成器。在此步骤中,您将从 .agents/ 加载配置文件,并将它们装载到沙盒中。
需要更改的内容
对 run_digest.py 进行以下四处更改:
TODO 2:在 load_source() 下方,添加三个模块级常量(这些常量位于 run_digest() 外部,在文件顶部):
AGENTS_MD = load_source(".agents/AGENTS.md")
SKILL_MD = load_source(".agents/skills/digest-pdf/SKILL.md")
GENERATE_PDF_PY = load_source(".agents/skills/digest-pdf/scripts/generate_pdf.py")
打开每个文件,查看您要加载的内容:AGENTS.md 设置编辑风格和工作流规则;SKILL.md 是分步 PDF 剧本;generate_pdf.py 是代理将运行的预构建渲染器。
现在,在 run_digest() 内再进行两项更改:
TODO 3:将 environment 从 "remote" 更改为 sources 字典,并将 input 设置为 "Generate the digest.":
environment={
"type": "remote",
"sources": [
{
"type": "inline",
"target": ".agents/AGENTS.md",
"content": AGENTS_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/SKILL.md",
"content": SKILL_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
"content": GENERATE_PDF_PY,
},
],
},
TODO 4:在 print(f"\nDone. environment_id={environment_id}") 后面添加以下代码行:
save_env(ENVIRONMENT_ID=environment_id, INTERACTION_ID=interaction_id)
save_env 已在 run_digest.py 中定义。它会将这两个 ID 写入 .env,以便下一步无需重新运行代理即可下载 PDF。
每个来源的作用
每个来源都是在代理运行之前启动时装载到沙盒文件系统中的文件。target 路径与 Antigravity 安全带预期找到这些文件的位置一致:
.agents/
├── AGENTS.md ← auto-loaded as global instructions
└── skills/
└── digest-pdf/
├── SKILL.md ← auto-discovered and registered as a skill
└── scripts/
└── generate_pdf.py ← pre-built renderer the agent can run
| 变量 | 安全带如何使用 |
|
| 自动加载为持久性指令:编辑风格、工作流程、执行规则 |
|
| 自动发现并注册为命名技能;代理按名称调用该技能 |
|
| 预建的 PDF 渲染器;代理写入 |
验证
uv run python run_digest.py
现在,运行时间为 1-3 分钟。您应该会看到代理读取配置文件、撰写摘要并保存 PDF:
[agent started]
[tool] read_file (/.agents/skills/digest-pdf/SKILL.md)
[tool] list_files (/.agents/skills/digest-pdf/scripts)
[tool] read_file (/.agents/skills/digest-pdf/scripts/generate_pdf.py)
[tool] run_code
[tool] write_file (/workspace/summaries.json)
[tool] run_code
[tool] delete_file (/tmp/test_scrape.py)
I have successfully generated today's tech news digest and saved the formatted document to /workspace/digest.pdf.
Done. environment_id=4129ffd75574e308748e9425d7ec828f
environment_id 现在是一个实际值:沙盒已使用您的配置文件运行,并且代理已创建 digest.pdf。下一步是在下载前添加安全钩子。
6. 添加安全钩子
借助钩子,您可以在沙盒内之前或之后运行脚本。摘要代理使用 code_execution 运行 Python 脚本,因此 pre_tool_execution 钩子可以拦截这些调用,并在破坏性 shell 命令执行之前将其屏蔽。
运行时从沙盒读取 .agents/hooks.json。在每次调用匹配工具之前,它都会通过管道将调用详细信息传递给 stdin 上的门控脚本。脚本将 {"decision": "allow"} 或 {"decision": "deny", "reason": "..."} 打印到 stdout。拒绝会取消工具调用,智能体看到您的原因后会自行更正。
添加内容
在 run_digest.py 中,在现有的 load_source 调用之后,在顶部附近添加以下两个常量:TODO 5:
import json
HOOKS_JSON = json.dumps({
"safety-gate": {
"pre_tool_execution": [
{
"matcher": "code_execution",
"hooks": [
{
"type": "command",
"command": "python3 /.agents/hooks-scripts/gate.py",
"timeout": 10,
}
],
}
]
}
}, indent=2)
GATE_PY = """\
#!/usr/bin/env python3
import sys, json
data = json.load(sys.stdin)
cmd = str(data.get("tool_call", {}).get("args", {}))
if "rm -rf" in cmd:
print(json.dumps({"decision": "deny", "reason": "Destructive command blocked by safety gate."}))
else:
print(json.dumps({"decision": "allow"}))
"""
TODO 6:在 interactions.create() 内向 sources 列表添加另外两个条目:
{"type": "inline", "target": ".agents/hooks.json", "content": HOOKS_JSON},
{"type": "inline", "target": ".agents/hooks-scripts/gate.py", "content": GATE_PY},
摘要运行中钩子的触发方式
每次智能体调用 code_execution 来运行 Python 脚本或 shell 命令时,运行时都会先将调用详细信息通过管道传输到 gate.py。如果命令包含 rm -rf,钩子会返回 deny,并且代理会收到拒绝原因,然后使用安全替代方案重试。所有其他代码执行调用都会原封不动地传递。
验证
uv run python run_digest.py
输出与之前相同:安全门允许所有正常的 PDF 生成命令。如需确认钩子是否触发,请暂时更改代理输入,让代理运行 rm -rf /tmp/test - 您会看到代理报告该命令被阻止并选择替代方案。
7. 下载 PDF
代理将 digest.pdf 写入了沙盒内的 /workspace/digest.pdf。环境快照可通过 Gemini Files API 以 tar 归档文件的形式提供。
根据需要安装 requests:
uv pip install requests
需要填写的内容
打开 download_pdf.py。它有两个 TODO。
TODO 1:填写 requests.get() 调用:
r = requests.get(
f"https://generativelanguage.googleapis.com/v1beta/files/environment-{environment_id}:download",
params={"alt": "media"},
headers={"x-goog-api-key": api_key},
allow_redirects=True,
)
r.raise_for_status()
网址指向沙盒快照。params={"alt": "media"} 返回原始字节,而不是元数据。您现有的 GEMINI_API_KEY 也会对 Files API 进行身份验证。
TODO 2:从 tar 归档中查找并提取 PDF:
member = next(m for m in tar.getmembers() if m.name.endswith("workspace/digest.pdf"))
tar.extract(member, path=tmp, filter="data")
tar 路径前缀因运行而异,因此请按后缀进行搜索,而不是对确切路径进行硬编码。filter="data" 会抑制 Python 3.13 关于不安全 tar 提取的弃用警告。
验证
uv run python download_pdf.py
Saved digest.pdf (48,231 bytes)
打开同一目录中的 digest.pdf。它包含代理从实时网页生成的格式化摘要。
8. 继续对话
您已拥有 digest.pdf。如果您只是想要该文件,那么到此为止就完成了。此步骤与上述步骤不同,它要求代理更改摘要,而无需重新提取网页。
沙盒仍处于有效状态。智能体仍然具有 /workspace/digest.pdf,并且会记住它总结的每个故事。第二次 interactions.create() 调用会将后续消息发送到同一沙盒中。在此示例中,您要求它在每个故事下方添加“重要性”注释,然后它会就地更新 PDF,而无需重新提取和重新总结。
需要填写的内容
打开 refine_digest.py。它有三个待办事项。
待办事项 1 和 2:填写 interactions.create() 内的两个多轮参数:
environment=environment_id,
previous_interaction_id=interaction_id,
environment=environment_id 恢复包含文件和软件包的同一沙盒。previous_interaction_id=interaction_id 为代理提供对话历史记录。与首次通话相比,其他方面都保持不变。
TODO 3:在事件循环后将新的 interaction_id 持久保存回 .env:
save_env(INTERACTION_ID=interaction_id)
每次调用 interactions.create() 都会生成一个新的 interaction_id。写回意味着下一次运行会将此细化结果作为 previous_interaction_id 传递,从而正确地链接回合。沙盒 ID 永远不会改变,因此无需更新 ENVIRONMENT_ID。
使多轮对话正常运行的两个参数
ID | 保留的内容 | 类比 |
| 文件、已安装的软件包、系统状态:Linux 文件系统上的所有内容 | 在会议之间保持办公桌不变 |
| 对话历史记录:智能体在之前的对话轮次中说过什么和做过什么 | 记住上次会议的讨论内容 |
您可以单独传递任一 ID:
environment_id:重复使用文件和软件包,但发起全新对话。适用于同一工作区中的新任务。previous_interaction_id:继续对话上下文,但在全新的沙盒中进行(文件已消失)。- 两者:完全连续性,这是此步骤所使用的。
不含 environment_id:空白沙盒,无 PDF。不含 previous_interaction_id:没有上下文,代理无法优化特定部分。
验证
uv run python refine_digest.py
该数据流应快速完成,智能体不会重新提取任何内容。完成后:
Refinement done.
Saved digest_v2.pdf (52,418 bytes)
打开 digest_v2.pdf 并将其与 digest.pdf 进行比较。现在,每篇报道都应添加“重要性”行。
9. 持久保存受管理的代理配置
到目前为止,每次调用都已内联传递 AGENTS.md、SKILL.md 和 generate_pdf.py。这样可以正常运行,但您的调用代码会在每次运行时携带完整的文件内容。agents.create() 将配置烘焙到 Google 端已保存的命名代理中。下一次调用只需传递代理 ID:
Inline calls: send sources on every call
Named agent: bake once → invoke by ID, no sources
需要填写的内容
打开 save_agent.py。它有一个 TODO(TODO 1)。
请注意,常量直接从 run_digest.py 导入(无重复):
from run_digest import BASE_AGENT, AGENTS_MD, SKILL_MD, GENERATE_PDF_PY
TODO 1:填写 agents.create() 调用:
agent = client.agents.create(
id="my-digest",
base_agent=BASE_AGENT,
agent_config={
"type": "antigravity",
"model": "gemini-3.7-flash",
},
description="Daily tech digest with editorial voice and PDF generation.",
base_environment={
"type": "remote",
"sources": [
{
"type": "inline",
"target": ".agents/AGENTS.md",
"content": AGENTS_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/SKILL.md",
"content": SKILL_MD,
},
{
"type": "inline",
"target": ".agents/skills/digest-pdf/scripts/generate_pdf.py",
"content": GENERATE_PDF_PY,
},
],
},
)
agent_config 用于设置底层模型。gemini-3.7-flash 是此工作流的默认设置,也是最佳选择;如果您希望以更轻量级或更低成本的方式运行,可以选择 gemini-3.6-flash、gemini-3.5-flash 和 gemini-3.5-flash-lite。
base_environment(而非 environment)是与上一步中的内嵌调用之间的主要区别:来源存储在 Google 端,并在每次后续调用时自动装载。运行一次,而不是在每次摘要运行时都运行。
验证:保存代理
uv run python save_agent.py
Saved: my-digest
my-digest: Daily tech digest with editorial voice and PDF generation.
调用已保存的智能体
打开 invoke_agent.py。它通过 ID 调用已保存的代理,但不包含任何来源:
stream = client.interactions.create(
agent="my-digest",
input="Generate the digest.",
stream=True,
environment="remote",
)
将其与内嵌调用进行比较:agent=BASE_AGENT 被替换为 "my-digest",包含三个内嵌来源的整个 environment 块被替换为 environment="remote"。配置已在 Google 端烘焙完成。
验证:调用已保存的代理
uv run python invoke_agent.py
您应该会看到与内嵌运行相同的直播,但该调用不包含任何源文件。运行后,.env 中的 ENVIRONMENT_ID 和 INTERACTION_ID 会更新,以便您像之前一样继续使用 refine_digest.py。
[agent started]
[tool] read_file
[tool] write_file
[tool] run_code
I have successfully created today's tech news digest.
Done. environment_id=9a1c3e02-...
10. 通过 Gmail 发送
代理已生成摘要并将其保存到 /workspace/digest.pdf。到目前为止,您已在本地下载了该模型。此步骤通过让代理从沙盒内部调用 Gmail REST API,将结果直接发送到您的收件箱。
具体方法:在本地获取 OAuth 2.0 访问令牌,然后在 input 提示中将其传递给代理。代理使用 code_execution 构建附加了 PDF 的 MIME 电子邮件,并将其 POST 到 Gmail API。没有自定义工具,也没有 MCP 服务器注册。
前提条件
在您的 GCP 项目中启用 Gmail API 并创建 OAuth 2.0 客户端 ID:
- 前往 console.cloud.google.com/apis/library/gmail.googleapis.com 并启用 Gmail API。
- 依次前往 API 和服务 > 凭据 > 创建凭据 > OAuth 2.0 客户端 ID。
- 应用类型:桌面应用。下载 JSON 并将其另存为项目根目录中的
credentials.json。
将收件人电子邮件地址添加到 .env:
RECIPIENT_EMAIL=you@gmail.com
根据需要安装身份验证库:
uv sync
需要填写的内容
打开 send_digest.py。它有两个 TODO。
TODO 1:加载或刷新 OAuth 2.0 访问令牌:
creds = None
if TOKEN_FILE.exists():
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
TOKEN_FILE.write_text(creds.to_json())
else:
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=8080, open_browser=False)
TOKEN_FILE.write_text(creds.to_json())
添加 raise NotImplementedError 行后,将其移除。首次运行时,此命令会打开浏览器以显示 OAuth 权限请求页面。令牌会缓存到 .gmail_token.json 中,以供日后运行使用。
TODO 2:将 input="" 替换为电子邮件说明。令牌已在范围内,为 creds.token:
input=(
"Use the Gmail REST API to send an email:\n"
f" To: {recipient}\n"
" Subject: Tech Digest - <today's date in YYYY-MM-DD format>\n"
" Attachment: /workspace/digest.pdf attached as digest.pdf\n\n"
"For the body, read /workspace/summaries.json and format it as a "
"human-readable newsletter, NOT raw JSON. Use this structure:\n"
" Tech Digest - <date>\n\n"
" === <source name> ===\n"
" 1. <title>\n"
" <summary>\n\n"
"Steps:\n"
"1. Parse /workspace/summaries.json and build the formatted body text above.\n"
"2. Read /workspace/digest.pdf as bytes.\n"
"3. Build a MIME multipart message using Python's email library.\n"
"4. Base64url-encode the raw message.\n"
"5. POST to https://gmail.googleapis.com/gmail/v1/users/me/messages/send "
"with Authorization header using this token: "
f"{creds.token}"
),
各部分的用途
该互动会恢复代理已生成 digest.pdf 和 summaries.json 的同一沙盒。previous_interaction_id 为代理提供对话历史记录。
访问令牌在 input 字符串中传递。代理会从提示中读取该值,并在调用 Gmail API 时将其用于 Authorization: Bearer 标头。它绝不会访问您的本地机器或文件系统。
该代理使用 code_execution 在沙盒内编写并运行 Python 脚本:它读取 summaries.json,将其格式化为简报,读取 digest.pdf,构建 MIME 多部分消息,对其进行 base64url 编码,然后通过 POST 请求将其发送到 https://gmail.googleapis.com/gmail/v1/users/me/messages/send。
验证
uv run python send_digest.py
Sending digest...
[agent started]
[tool] read_file (/workspace/summaries.json)
[tool] run_code
[tool] run_code
Email sent successfully.
Email sent. Check your inbox.
检查收件箱。您收到的电子邮件包含简报格式的正文和随附的 digest.pdf。
11. 安排每日跑步
到目前为止,每个步骤都是手动触发的。借助触发器,您可以安排命名代理按 cron 表达式自动运行。代理会在预定时间触发,运行完整的摘要工作流,并且环境会在执行之间保持不变,因此在第一次运行中安装的软件包可在后续每次运行中使用。
Manual: python run_digest.py → runs once, now
Trigger: client.triggers.create() → runs every morning, automatically
需要填写的内容
打开 create_trigger.py。它有一个 TODO。
TODO 1:填写 triggers.create() 调用。触发器每天都会运行完整的工作流:生成摘要并将其发送到您的收件箱。由于访问令牌会在一小时后过期,因此它会从 .gmail_token.json 中注入刷新令牌作为内嵌来源,以便代理可以在每次运行时将其交换为新令牌。
trigger = client.triggers.create(
schedule="0 9 * * *",
time_zone="UTC",
display_name="daily-tech-digest",
max_consecutive_failures=3,
execution_timeout_seconds=600,
interaction={
"agent": "my-digest",
"input": (
f"Generate the daily tech digest following AGENTS.md instructions. "
f"Then send an email to {recipient}:\n"
"- Subject: Tech Digest - <today's date in YYYY-MM-DD format>\n"
"- Body: the content of /workspace/summaries.json formatted as a readable "
"newsletter (NOT raw JSON).\n"
"- Attachment: /workspace/digest.pdf\n\n"
"For Gmail auth: read /workspace/.gmail_creds.json, POST to "
"https://oauth2.googleapis.com/token with grant_type=refresh_token "
"and the client_id, client_secret, refresh_token from the file to get an "
"access_token. Then POST to "
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send "
"with Authorization: Bearer <access_token>."
),
"environment": {
"type": "remote",
"sources": [
{
"type": "inline",
"target": "/workspace/.gmail_creds.json",
"content": gmail_creds,
}
],
},
},
)
默认超时时间为 execution_timeout_seconds=600。max_consecutive_failures=3 在连续 3 次运行失败后自动暂停触发器(API 默认值为 5;3 对于研讨会来说更保守)。
sources 列表将 .gmail_creds.json 注入到沙盒中的 /workspace/.gmail_creds.json。代理读取该令牌,将刷新令牌换成新的访问令牌,然后调用 Gmail API。刷新令牌不会过期,因此每次按计划运行时,无需手动刷新令牌即可正常运行。
添加通话后,移除 raise NotImplementedError 行。
验证
uv run python create_trigger.py
Trigger created: trig_abc123
Next run: 2026-07-23T09:00:00Z
create_trigger.py 会自动将触发器 ID 保存到 .env。
如需在运行后检查执行历史记录,请执行以下操作:
uv run python check_trigger.py
如需立即触发触发器,而不等待下一次预定时间,请执行以下操作:
uv run python fire_trigger.py
如需暂停或删除触发器,请执行以下操作:
uv run python pause_trigger.py
12. 清理
沙盒会在处于非活跃状态 7 天后自动过期。没有要停止的服务器。没有要删除的容器。
如果您保存了代理配置,请将其删除:
uv run python delete_agent.py
13. 总结
您从头开始构建了一个受管理的代理,每次学习一个概念。以下是每项练习的教学内容:
锻炼 | 概念 | Key API |
发出首次调用 | 预配真实的 Linux 沙盒并实时直播其事件 |
|
自定义智能体 | 装载配置文件;在同一运行中将 ID 持久保存到 |
|
添加安全钩 | 在工具调用执行之前拦截它们;拒绝破坏性命令 |
|
下载 PDF | 下载 PDF,无需重新运行代理 |
|
继续对话 | 继续对话,而不重新提取网页内容 |
|
持久保留代理配置 | 持久保留代理配置;按 ID 调用,无需来源 |
|
通过 Gmail 发送 | 在本地获取 OAuth 令牌;将其传递给代理,该代理通过 | OAuth 2.0, |
安排每日运行 | 按 cron 时间表自动运行代理 |
|
键格式
- 一次调用,一个沙盒:
interactions.create()处理所有基础设施(无需部署容器,无需在本地安装软件包) - 渐进式流式传输:
stream=True将 90 秒的黑框转换为工具调用和文本块的实时 Feed - 内嵌来源:将
AGENTS.md、SKILL.md和预构建的脚本装载到沙盒中,无需任何上传或部署步骤 - 利用自动发现功能:系统会自动提取放置在
.agents/中的文件(无需 SDK 配置) - 二维状态:
environment_id跟踪文件和软件包;previous_interaction_id跟踪对话上下文;两者可以单独传递 - 快照下载:环境是一个完整的文件系统 tar,可通过 Gemini Files API 访问
- 命名代理:
agents.create()会永久烘焙配置;未来的调用仅传递代理 ID 和environment="remote",不传递来源 - 钩子:
hooks.json+ 门控脚本在工具调用执行之前拦截它们;deny响应会取消调用,智能体进行自我修正 - 外部 API 调用:在
input提示中传递凭据;智能体通过code_execution在沙盒内编写并运行集成代码 - 触发器:使用
client.triggers.create()按 cron 表达式调度智能体;环境在执行期间保持不变
ADK + Cloud Run 与托管式智能体:差异一览
能力 | ADK + Cloud Run | Gemini API 中的托管式智能体 |
配置沙盒 |
|
|
定义工具 | 向智能体注册的 Python 函数 | 内置:网页浏览、代码执行、文件系统 |
安装软件包 | Dockerfile 中的 | 代理在沙盒中运行 |
流式事件 | 自定义 SSE 基础架构 |
|
继续会话 | 会话数据库 + 上下文注入 |
|
配置文件 | 在代理中硬编码或在启动时注入 | 通过 |
要管理的基础设施 | 容器、Cloud Run、IAM、密钥 | 无 |