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 工具 + | Agent 會在沙箱內安裝套件 |
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 中開啟這個程式碼研究室。所有依附元件都已預先安裝。
選項 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() 中的事件:
| 簡介 |
|
| 代理程式擷取網址 |
|
| 代理在沙箱中執行程式碼 |
|
| 代理程式搜尋網路 |
|
| 檔案工具和其他 |
|
| 服務專員正在輸入文字 | 直接串流至 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
| 變數 | 安全帶如何與安全座椅搭配使用 |
|
| 自動載入為持續性指令:編輯觀點、工作流程、執行規則 |
|
| 自動探索並註冊為具名技能;代理程式會依名稱叫用該技能 |
|
| 預建的 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 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 | 保留內容 | 比喻 |
| 檔案、已安裝的套件、系統狀態: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 建構 MIME 電子郵件,並附上 PDF 檔案,然後將該郵件 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 中,以供日後執行。
待辦事項 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 沙箱,並即時串流其事件 |
|
自訂代理程式 | 掛接設定檔;在同一次執行中將 ID 保留在 |
|
新增安全掛鉤 | 在工具呼叫執行前攔截,拒絕破壞性指令 |
|
下載 PDF | 下載 PDF,不必重新執行代理程式 | Gemini Files API |
延續對話 | 繼續對話,不必重新擷取網頁 |
|
保留代理程式設定 | 保存代理設定;依 ID 叫用,無須來源 |
|
透過 Gmail 傳送 | 在本地取得 OAuth 權杖,並傳遞至代理程式,代理程式會透過 | OAuth 2.0, |
排定每日執行時間 | 依據 Cron 排程自動執行代理程式 |
|
鍵模式
- 一次呼叫,一個沙箱:
interactions.create()會處理所有基礎架構 (不必部署容器,也不必在本機安裝套件) - 漸進式串流:
stream=True將 90 秒的黑方塊轉換為工具呼叫和文字區塊的現行動態饋給 - 內嵌來源:將
AGENTS.md、SKILL.md和預先建構的指令碼掛載至沙箱,不必上傳或部署 - 自動探索 Harness:系統會自動挑選
.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 與 Managed Agents:差異總覽
功能 | ADK + Cloud Run | Gemini API 的受管理代理程式 |
佈建沙箱 |
|
|
定義工具 | 向代理程式註冊的 Python 函式 | 內建:網頁瀏覽、執行程式碼、檔案系統 |
安裝套件 | Dockerfile 中的 | 代理程式在沙箱中執行 |
串流事件 | 自訂 SSE 基礎架構 |
|
繼續執行工作階段 | 工作階段資料庫 + 脈絡資料注入 |
|
設定檔 | 在代理程式中硬式編碼,或在啟動時注入 | 透過「 |
管理基礎架構 | 容器、Cloud Run、IAM、密鑰 | 無 |