使用 Gemini API 的 Managed Agents 建構每日科技摘要代理程式

1. 總覽

AI 和科技領域的發展速度之快,無人能及。每天都有新模型、論文和產品推出。如果每天早上都能取得當日新聞摘要,並以精簡的文字撰寫摘要,然後產生 PDF,就能解決這個問題。但過去要建構這類摘要代理程式,就得先選擇框架、在 Python 中定義工具、編寫自動化調度管理迴圈、封裝容器,然後部署至 Cloud Run。在代理程式發出任何網頁要求之前,所有這些作業都已完成。

Gemini API 的 Managed Agents 改變了這個等式。您會編寫兩個 Markdown 設定檔和一個預先建構的算繪器指令碼,發出一個 API 呼叫,然後啟動實際的 Ubuntu 沙箱、瀏覽網頁、撰寫摘要並產生 PDF。沒有容器。無需部署。沒有自動化調度管理程式碼。

在本程式碼研究室中,您將從空白函式開始,逐步建構出可運作的每日摘要代理程式。

建構目標

  • 在實際的 Linux 沙箱中,建立並執行第一個受管理代理
  • 自訂 AI 代理的編輯觀點、網路來源和 PDF 技能
  • 新增安全防護機制,在破壞性指令執行前加以封鎖
  • 下載代理程式生成的 PDF
  • 在多輪對話中精修摘要,不必重新擷取網頁內容
  • 儲存代理程式設定,並在日後執行時依 ID 叫用
  • 透過 Gmail API 將摘要傳送至收件匣
  • 排定代理每天自動執行並傳送

軟硬體需求

  • Python 3.10 以上版本
  • Gemini API 金鑰:aistudio.google.com/api-keys (內含免費方案;建議啟用帳單功能,確保執行作業不中斷)

2. 什麼是 Gemini API 的 Managed Agents?

AI 系統的三個層級

開始編寫程式碼前,請先瞭解 Managed Agents 相對其他兩種替代方案的適用情況:

等級

簡介

誰負責管理基礎架構?

標準 LLM

你輸入提示,它會以文字回覆。不需動手、不需記憶、不需網路。

不適用:無法自行執行任何動作

自行代管的代理程式

您會將 ADK/LangChain/AutoGen + Docker + 工具 + 記憶體連接起來。

您:所有項目 (或 Agent Engine 等代管平台)

受管理代理程式

你為其設定目標,Google 會提供安全沙箱。代理會編寫程式碼、執行程式碼、讀取錯誤、搜尋網路,並自主修正錯誤。

Google:所有內容

本程式碼研究室將介紹第三列。您提供工作和設定檔,其餘工作交由 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

也就是在代理程式執行一次之前。您仍擁有沙箱隔離功能 (因此代理程式不會損壞伺服器)、套件安裝、工具呼叫之間的狀態管理,以及將事件傳送至用戶端的串流基礎架構。

Managed Agents 的替代方案

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 的必要條件

Managed Agents 為您處理的事項

容器映像檔 + Dockerfile + CI/CD

全代管 Ubuntu 沙箱 (Python 3.12、Node 22、4 個 CPU / 16 GB RAM)

Cloud Run 部署作業 + 擴縮

每次互動都會提供,閒置 7 天後自動失效

沙箱隔離

每次互動都隔離

自訂 PDF 工具 + pip install

Agent 會在沙箱內安裝套件

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 中開啟這個程式碼研究室。所有依附元件都已預先安裝。

在 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() 有一個 TODO 現在要填寫,下一個步驟還有三個。系統已在上方預先填入兩位幫手:

  • load_source(path):從相對於指令碼的 .agents/ 讀取檔案。您會在下一個練習中使用這個檔案,將編輯觀點、PDF 教戰手冊和轉譯器掛接到沙箱。
  • run_stream(stream):處理事件串流並傳回 (environment_id, interaction_id)。您不需要自行編寫事件迴圈。

應加入的資訊

TODO 1:將 pass 替換為 (暫時忽略 TODO 3 和 4,這些是下一個步驟要處理的):

    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() 呼叫,代理程式就會在相同檔案系統中繼續執行,並使用相同檔案和已安裝的套件,就像從未離開一樣。下一個步驟會使用這個 ID 下載 PDF,不必重新執行代理程式,再下一個步驟則會使用這個 ID 繼續對話。

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" 變更為來源字典,並將 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,
                },
            ],
        },

待辦事項 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 hook 可以攔截這些呼叫,並在執行前封鎖破壞性 Shell 指令。

執行階段會從沙箱讀取 .agents/hooks.json。每次呼叫比對工具前,系統都會將呼叫詳細資料透過管道傳送至 stdin 上的閘道指令碼。指令碼會將 {"decision": "allow"} 或 {"decision": "deny", "reason": "..."} 列印到 stdout。拒絕會取消工具呼叫,代理程式會看到你的原因並自行修正。

應加入的資訊

TODO 5:在 run_digest.py 中,於現有的 load_source 呼叫後方,在頂端附近新增這兩個常數:

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},

摘要執行期間的觸發 Hook 方式

每當代理程式呼叫 code_execution 執行 Python 指令碼或殼層指令時,執行階段會先將呼叫詳細資料傳送至 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。其中有三個 TODO。

TODO 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 建構 MIME 電子郵件,並附上 PDF 檔案,然後將該郵件 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 中,以供日後執行。

待辦事項 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() 呼叫。觸發條件每天都會執行完整的工作流程:產生摘要並傳送至收件匣。由於存取權杖會在 1 小時後過期,因此系統會從 .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 清單會在 /workspace/.gmail_creds.json 將 .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,不必重新執行代理程式

Gemini Files API :download in download_pdf.py

延續對話

繼續對話,不必重新擷取網頁

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 秒的黑方塊轉換為工具呼叫和文字區塊的現行動態饋給
  3. 內嵌來源:將 AGENTS.md、SKILL.md 和預先建構的指令碼掛載至沙箱,不必上傳或部署
  4. 自動探索 Harness:系統會自動挑選 .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 與 Managed Agents:差異總覽

功能

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、密鑰

無

後續步驟