使用 Gemini API 中的托管式智能体构建每日科技摘要智能体

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 工具 + pip install

代理在沙盒内安装软件包

SSE 流式传输基础架构

stream=True 返回一个事件可迭代对象

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. 设置

点击下方按钮,在 Google Cloud Shell 中打开此 Codelab。所有依赖项均已预安装。

在 Cloud Shell 中打开

方案 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() 中的一个事件:

step.type

简介

run_stream() 打印的内容

"url_context_call"

代理提取网址

[tool] url_context (https://...)

"code_execution_call"

在沙盒中运行代码的智能体

[tool] run_code

"google_search_call"

智能体搜索网页

[tool] google_search

"function_call"

文件工具和其他工具

[tool] read_file (/workspace/...)

step.delta(delta.type == "text")

代理撰写文字

直接流式传输到 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

target 路径

变量

安全带如何使用

.agents/AGENTS.md

AGENTS_MD

自动加载为持久性指令:编辑风格、工作流程、执行规则

.agents/skills/digest-pdf/SKILL.md

SKILL_MD

自动发现并注册为命名技能;代理按名称调用该技能

.agents/skills/digest-pdf/scripts/generate_pdf.py

GENERATE_PDF_PY

预建的 PDF 渲染器;代理写入 summaries.json,然后运行此脚本

验证

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

保留的内容

类比

environment=environment_id

文件、已安装的软件包、系统状态:Linux 文件系统上的所有内容

在会议之间保持办公桌不变

previous_interaction_id=interaction_id

对话历史记录:智能体在之前的对话轮次中说过什么和做过什么

记住上次会议的讨论内容

您可以单独传递任一 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:

  1. 前往 console.cloud.google.com/apis/library/gmail.googleapis.com 并启用 Gmail API。
  2. 依次前往 API 和服务 > 凭据 > 创建凭据 > OAuth 2.0 客户端 ID。
  3. 应用类型:桌面应用。下载 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 沙盒并实时直播其事件

interactions.create(agent, input, environment, stream=True)、event.event_type

自定义智能体

装载配置文件;在同一运行中将 ID 持久保存到 .env

environment.sources、save_env

添加安全钩

在工具调用执行之前拦截它们;拒绝破坏性命令

hooks.json、pre_tool_execution、gate.py

下载 PDF

下载 PDF,无需重新运行代理

download_pdf.py 中的 Gemini Files API :download

继续对话

继续对话,而不重新提取网页内容

environment=environment_id、previous_interaction_id=interaction_id

持久保留代理配置

持久保留代理配置;按 ID 调用,无需来源

agents.create()、agents.list()

通过 Gmail 发送

在本地获取 OAuth 令牌;将其传递给代理,该代理通过 code_execution 调用 Gmail REST API

OAuth 2.0,client.interactions.create(input=...)

安排每日运行

按 cron 时间表自动运行代理

client.triggers.create(schedule, time_zone, interaction)

键格式

  1. 一次调用,一个沙盒:interactions.create() 处理所有基础设施(无需部署容器,无需在本地安装软件包)
  2. 渐进式流式传输:stream=True 将 90 秒的黑框转换为工具调用和文本块的实时 Feed
  3. 内嵌来源:将 AGENTS.md、SKILL.md 和预构建的脚本装载到沙盒中,无需任何上传或部署步骤
  4. 利用自动发现功能:系统会自动提取放置在 .agents/ 中的文件(无需 SDK 配置)
  5. 二维状态:environment_id 跟踪文件和软件包;previous_interaction_id 跟踪对话上下文;两者可以单独传递
  6. 快照下载:环境是一个完整的文件系统 tar,可通过 Gemini Files API 访问
  7. 命名代理:agents.create() 会永久烘焙配置;未来的调用仅传递代理 ID 和 environment="remote",不传递来源
  8. 钩子:hooks.json + 门控脚本在工具调用执行之前拦截它们;deny 响应会取消调用,智能体进行自我修正
  9. 外部 API 调用:在 input 提示中传递凭据;智能体通过 code_execution 在沙盒内编写并运行集成代码
  10. 触发器:使用 client.triggers.create() 按 cron 表达式调度智能体;环境在执行期间保持不变

ADK + Cloud Run 与托管式智能体:差异一览

能力

ADK + Cloud Run

Gemini API 中的托管式智能体

配置沙盒

docker build + gcloud run deploy

interactions.create()

定义工具

向智能体注册的 Python 函数

内置:网页浏览、代码执行、文件系统

安装软件包

Dockerfile 中的 pip install

代理在沙盒中运行 pip install

流式事件

自定义 SSE 基础架构

stream=True

继续会话

会话数据库 + 上下文注入

environment_id + previous_interaction_id

配置文件

在代理中硬编码或在启动时注入

通过 environment.sources 安装

要管理的基础设施

容器、Cloud Run、IAM、密钥

无

后续步骤