1. Panoramica
Il panorama dell'AI e della tecnologia si muove più velocemente di quanto chiunque possa tenere traccia. Ogni giorno vengono rilasciati nuovi modelli, articoli e prodotti. Un agente di riepilogo che recupera i titoli di oggi, scrive riepiloghi concisi e genera un PDF ogni mattina risolverebbe il problema, ma in passato la creazione di un agente di questo tipo richiedeva la scelta di un framework, la definizione di strumenti in Python, la scrittura di un ciclo di orchestrazione, il packaging di un container e il deployment su Cloud Run. Tutto questo prima che l'agente avesse effettuato una sola richiesta web.
Managed Agents nell'API Gemini cambia l'equazione. Scrivi due file di configurazione Markdown e uno script di rendering predefinito, effettua una chiamata API e viene avviata una sandbox Ubuntu reale, che naviga sul web, scrive i riepiloghi e genera un PDF. Nessun contenitore. Nessun deployment. Nessun codice di orchestrazione.
In questo codelab creerai esattamente questo agente: da una funzione vuota a un riepilogo giornaliero funzionante, un concetto alla volta.
Cosa creerai
- Crea ed esegui il tuo primo agente gestito in un sandbox Linux reale
- Personalizzare l'agente con la linea editoriale, le fonti web e una skill PDF
- Aggiungi un meccanismo di sicurezza per bloccare i comandi distruttivi prima che vengano eseguiti
- Scaricare il PDF generato dall'agente
- Perfezionare il riepilogo in una conversazione multi-turno senza recuperare nuovamente il web
- Salva la configurazione dell'agente e richiamala per ID nelle esecuzioni future
- Inviare il riepilogo alla tua casella di posta tramite l'API Gmail
- Pianifica l'esecuzione e l'invio automatico dell'agente ogni giorno
Che cosa ti serve
- Python 3.10+
- Una chiave API Gemini: aistudio.google.com/api-keys (livello senza costi incluso; fatturazione consigliata per esecuzioni senza interruzioni)
2. Che cosa sono gli agenti gestiti nell'API Gemini?
Tre livelli di sistemi AI
Prima di passare al codice, ecco dove si inseriscono gli agenti gestiti rispetto alle due alternative:
Livello | Descrizione | Chi gestisce l'infrastruttura? |
LLM standard | Tu inserisci un prompt e lui risponde con un testo. Nessuna mano, nessuna memoria, nessun internet. | N/A: non può fare nulla da solo |
Agente self-hosted | Colleghi ADK/LangChain/AutoGen + Docker + strumenti + memoria. | Tu: tutto (o una piattaforma gestita come Agent Engine) |
Agente gestito | Assegni un obiettivo. Google esegue il provisioning di una sandbox sicura. L'agente scrive il codice, lo esegue, legge gli errori, esegue ricerche sul web e corregge i bug in modo autonomo. | Google: tutto |
Questo codelab riguarda la terza riga. Fornisci un file di attività e di configurazione. Google si occupa di tutto il resto.
Cosa creeresti con ADK + Cloud Run
Per creare un agente di riepilogo delle notizie che naviga sul web, esegue Python e genera un PDF, avresti bisogno di tutto questo con 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
ovvero prima che l'agente venga eseguito per la prima volta. Mantieni l'isolamento della sandbox (in modo che l'agente non possa danneggiare il server), l'installazione dei pacchetti, la gestione dello stato tra le chiamate agli strumenti e l'infrastruttura di streaming per inviare eventi a un client.
Con cosa lo sostituisce 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,
},
],
},
)
Requisiti di ADK + Cloud Run | Cosa gestisce Managed Agents per te |
Immagine container + Dockerfile + CI/CD | Sandbox Ubuntu completamente gestita (Python 3.12, Node 22, 4 CPU / 16 GB di RAM) |
Deployment e scalabilità di Cloud Run | Fornito per interazione, scade automaticamente dopo 7 giorni di inattività |
Isolamento della sandbox | Isolamento per interazione |
Strumento PDF personalizzato + | L'agente installa i pacchetti all'interno della sandbox |
Infrastruttura di streaming SSE |
|
Definizioni degli strumenti in Python | Strumenti integrati: navigazione web, esecuzione del codice, file system |
Gestione dello stato tra le chiamate di strumenti | Integrato nel ciclo di ragionamento dell'agente |
Scrivi i file di configurazione (AGENTS.md, SKILL.md, uno script predefinito) ed esegui una chiamata API. Google si occupa di tutto il resto.
Come funziona la sandbox
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
La sandbox viene mantenuta per 7 giorni di inattività. Puoi riprenderla con environment_id per perfezionare l'output, eseguire attività di follow-up o creare una copia in un agente denominato salvato.
3. Configura
Opzione A: Cloud Shell (consigliata)
Fai clic sul pulsante di seguito per aprire questo codelab in Google Cloud Shell. Tutte le dipendenze sono preinstallate.
Opzione B: configurazione locale
git clone https://github.com/Saoussen-CH/tech-digest-managed-agent.git
cd tech-digest-managed-agent
Se necessario, installa uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
Configurare la chiave API
cp .env.example .env
cloudshell edit .env
Imposta la chiave:
GEMINI_API_KEY=your-key-here
Installa le dipendenze
uv sync
4. Effettuare la prima chiamata dell'agente
Aprire il file iniziale
cloudshell edit run_digest.py
run_digest() ha un TODO da compilare ora e altri tre per il passaggio successivo. Sopra sono già precompilati due helper:
load_source(path): legge un file da.agents/rispetto allo script. Lo utilizzerai nel prossimo esercizio per montare la linea editoriale, la guida pratica PDF e il renderer nella sandbox.run_stream(stream): elabora il flusso di eventi e restituisce(environment_id, interaction_id). Non devi scrivere personalmente il ciclo degli eventi.
Cosa aggiungere
TODO 1: sostituisci pass con (ignora per ora i TODO 3 e 4, che verranno utilizzati nel passaggio successivo):
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}")
A cosa serve ogni parte
genai.Client() legge GEMINI_API_KEY dall'ambiente. Tutto il resto passa attraverso questo client.
interactions.create() è la chiamata principale. Perché funzioni, sono necessari quattro parametri:
agent=BASE_AGENT: seleziona l'agente Antigravity (antigravity-preview-05-2026), un agente gestito per uso generico basato su Gemini 3.7 Flash per impostazione predefinita. Puoi configurare il modello sottostante utilizzandoagent_config(opzioni:gemini-3.7-flash,gemini-3.6-flash,gemini-3.5-flash,gemini-3.5-flash-lite). Sono disponibili tre strumenti integrati attivati per impostazione predefinita:code_execution(esegui Bash, Python, Node.js),google_searcheurl_context(recupera e leggi pagine web). Gli strumenti del file system (read_file,write_file,list_files) vengono attivati automaticamente quando passi il parametroenvironment. Una chiamata esegue il provisioning di un ambiente Ubuntu completamente gestito con Python 3.12, Node.js 22, git, pip e curl preinstallati. Nessun container da creare, nessun deployment da eseguire.input: l'attività per questa esecuzione. L'agente sfoglia Hacker News e analizza i risultati.environment="remote": esegue il provisioning di una nuova sandbox cloud per questa interazione.stream=True: restituisce un iterabile di eventi anziché bloccare. Senza, la chiamata attende 30-90 secondi e restituisce tutto l'output contemporaneamente comeinteraction.output_text. Con lo streaming, vedi il motivo dell'agente e agisci in tempo reale. Lo streaming non è una funzionalità avanzata: è l'impostazione predefinita corretta, perché una scatola nera di 90 secondi non fornisce alcun segnale sul funzionamento o sul blocco dell'agente.
environment_id è un handle della sandbox appena eseguita. Dopo interaction.completed, la sandbox non viene chiusa, ma rimane attiva per un massimo di 7 giorni. Il environment_id ti consente di tornare alla pagina. Trasferiscilo a una seconda interactions.create() chiamata e l'agente riprende lo stesso file system, con gli stessi file e pacchetti installati, come se non fosse mai uscito. Il passaggio successivo lo utilizza per scaricare il PDF senza eseguire nuovamente l'agente, mentre il passaggio successivo lo utilizza per continuare la conversazione.
interaction_id è un handle del turno di conversazione appena completato. Passalo come previous_interaction_id nella chiamata successiva e l'agente avrà piena memoria di ciò che ha detto e fatto in questo turno.
Verifica
uv run python run_digest.py
Dovresti vedere l'output in tempo reale mentre l'agente lavora:
[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
L'API restituisce un environment_id reale anche con environment="remote". La sandbox è stata eseguita. Ciò che manca è la configurazione: nessuna voce, nessuna skill, nessun generatore di PDF. L'agente ha stampato le storie come testo e si è fermato. Il passaggio successivo aggiunge questi dati.
Ogni riga di output corrisponde a un evento di run_stream():
| Descrizione | Cosa stampa |
| agente che recupera un URL |
|
| agente che esegue il codice nella sandbox |
|
| agente che cerca sul web |
|
| strumenti per i file e altri |
|
| L'agente sta scrivendo un testo | trasmesso direttamente a stdout |
5. Personalizzare l'agente
L'agente non aveva istruzioni: nessuna voce, nessuna skill, nessun generatore di PDF. In questo passaggio carichi i file di configurazione da .agents/ e li monti nella sandbox.
Cosa modificare
Apporta quattro modifiche a run_digest.py:
TODO 2: sotto load_source(), aggiungi le tre costanti a livello di modulo (si trovano al di fuori di run_digest(), nella parte superiore del file):
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")
Apri ogni file per vedere cosa stai caricando: AGENTS.md imposta le regole del workflow e la linea editoriale; SKILL.md è la guida pratica PDF passo passo; generate_pdf.py è il renderer predefinito che verrà eseguito dall'agente.
Ora apporta altre due modifiche all'interno di run_digest():
TODO 3: modifica environment da "remote" al dizionario delle origini e imposta input su "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: aggiungi questa riga subito dopo print(f"\nDone. environment_id={environment_id}"):
save_env(ENVIRONMENT_ID=environment_id, INTERACTION_ID=interaction_id)
save_env è già definito in run_digest.py. Scrive entrambi gli ID in .env in modo che il passaggio successivo possa scaricare il PDF senza eseguire nuovamente l'agente.
A cosa serve ogni origine
Ogni origine è un file montato nel file system della sandbox all'avvio prima dell'esecuzione dell'agente. I percorsi target corrispondono a quelli in cui la pettorina Antigravity prevede di trovarli:
.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
Percorso | Variabile | Cosa fa la pettorina |
|
| Caricamento automatico come istruzioni persistenti: linea editoriale, workflow, regole di esecuzione |
|
| Rilevata e registrata automaticamente come competenza denominata; l'agente la richiama per nome |
|
| Renderer PDF predefinito; l'agente scrive |
Verifica
uv run python run_digest.py
L'esecuzione ora richiede 1-3 minuti. Dovresti vedere l'agente leggere i file di configurazione, scrivere i riepiloghi e salvare il 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 ora è un valore reale: la sandbox è stata eseguita con i file di configurazione e l'agente ha creato digest.pdf. Il passaggio successivo aggiunge un controllo di sicurezza prima del download.
6. Aggiungere un aggancio di sicurezza
Gli hook ti consentono di eseguire uno script all'interno della sandbox prima o dopo ogni chiamata allo strumento. L'agente di riepilogo utilizza code_execution per eseguire script Python, quindi un hook pre_tool_execution può intercettare queste chiamate e bloccare i comandi shell distruttivi prima che vengano eseguiti.
Il runtime legge .agents/hooks.json dalla sandbox. Prima di ogni chiamata dello strumento di corrispondenza, i dettagli della chiamata vengono inviati allo script di gate su stdin. Lo script stampa {"decision": "allow"} o {"decision": "deny", "reason": "..."} in stdout. Un rifiuto annulla la chiamata dello strumento e l'agente vede il tuo motivo e si corregge.
Cosa aggiungere
TODO 5: in run_digest.py, aggiungi queste due costanti nella parte superiore, dopo le chiamate load_source esistenti:
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: aggiungi altre due voci all'elenco sources all'interno di interactions.create():
{"type": "inline", "target": ".agents/hooks.json", "content": HOOKS_JSON},
{"type": "inline", "target": ".agents/hooks-scripts/gate.py", "content": GATE_PY},
Come vengono attivati gli hook durante l'esecuzione del digest
Ogni volta che l'agente chiama code_execution per eseguire uno script Python o un comando shell, il runtime invia i dettagli della chiamata a gate.py. Se il comando contiene rm -rf, l'hook restituisce deny e l'agente riceve il motivo del rifiuto e riprova con un'alternativa sicura. Tutte le altre chiamate di esecuzione del codice vengono trasmesse invariate.
Verifica
uv run python run_digest.py
L'output è identico a prima: il meccanismo di sicurezza consente tutti i normali comandi di generazione di PDF. Per verificare che l'hook venga attivato, modifica temporaneamente l'input dell'agente per chiedergli di eseguire rm -rf /tmp/test. L'agente segnalerà che il comando è stato bloccato e sceglierà un'alternativa.
7. Scarica il PDF
L'agente ha scritto digest.pdf a /workspace/digest.pdf all'interno della sandbox. Lo snapshot dell'ambiente è disponibile come archivio tar tramite l'API Gemini Files.
Se necessario, installa requests:
uv pip install requests
Cosa compilare
Apri download_pdf.py. Ha due cose da fare.
TODO 1: compila la chiamata 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()
L'URL indirizza lo snapshot della sandbox. params={"alt": "media"} restituisce byte non elaborati anziché metadati. Il tuo GEMINI_API_KEY esistente autentica anche l'API Files.
TODO 2: trova ed estrai il PDF dall'archivio tar:
member = next(m for m in tar.getmembers() if m.name.endswith("workspace/digest.pdf"))
tar.extract(member, path=tmp, filter="data")
Il prefisso del percorso tar varia a seconda delle esecuzioni, quindi esegui la ricerca per suffisso anziché codificare il percorso esatto. filter="data" elimina l'avviso di ritiro di Python 3.13 relativo all'estrazione non sicura di tar.
Verifica
uv run python download_pdf.py
Saved digest.pdf (48,231 bytes)
Apri digest.pdf nella stessa directory. Contiene il riepilogo formattato generato dall'agente dalle pagine web pubblicate.
8. Continua la conversazione
Hai già digest.pdf. Se volevi solo il file, hai finito. Questo passaggio riguarda un'altra operazione: chiedere all'agente di modificare il riepilogo senza recuperare nuovamente il web.
La sandbox è ancora attiva. L'agente ha ancora /workspace/digest.pdf e ricorda ogni storia che ha riassunto. Una seconda chiamata interactions.create() invia un messaggio di follow-up nella stessa sandbox. Qui chiedi di aggiungere una nota "Perché è importante" sotto ogni storia e il PDF viene aggiornato sul posto, senza recupero e riepilogo.
Cosa compilare
Apri refine_digest.py. Ha tre cose da fare.
TODO 1 e 2: compila i due parametri multi-turn all'interno di interactions.create():
environment=environment_id,
previous_interaction_id=interaction_id,
environment=environment_id ripristina la stessa sandbox con i relativi file e pacchetti. previous_interaction_id=interaction_id fornisce all'agente la cronologia delle conversazioni. Non cambierà nient'altro rispetto alla prima chiamata.
TODO 3: mantieni il nuovo interaction_id in .env dopo il ciclo di eventi:
save_env(INTERACTION_ID=interaction_id)
Ogni chiamata interactions.create() produce un nuovo interaction_id. La riscrittura significa che l'esecuzione successiva passa questo perfezionamento come previous_interaction_id, concatenando correttamente le svolte. L'ID sandbox non cambia mai, quindi non è necessario aggiornare ENVIRONMENT_ID.
I due parametri che consentono il funzionamento della conversazione multi-turno
ID | Che cosa conserva | Analogia |
| File, pacchetti installati, stato del sistema: tutto ciò che si trova nel file system Linux | Mantenere la stessa scrivania tra una riunione e l'altra |
| Cronologia della conversazione: cosa ha detto e fatto l'agente nei turni precedenti | Ricordare cosa è stato discusso nell'ultima riunione |
Puoi superare la verifica di uno dei due ID in modo indipendente:
environment_id: riutilizza file e pacchetti, ma avvia una nuova conversazione. Utile per una nuova attività nello stesso spazio di lavoro.previous_interaction_id: continua il contesto della conversazione, ma in una nuova sandbox (i file non sono più presenti).- Entrambi: continuità completa, che è ciò che utilizza questo passaggio.
Senza environment_id: sandbox vuota, nessun PDF. Senza previous_interaction_id: nessun contesto, l'agente non può perfezionare una sezione specifica.
Verifica
uv run python refine_digest.py
Lo streaming dovrebbe essere veloce, l'agente non recupera nulla. Al termine:
Refinement done.
Saved digest_v2.pdf (52,418 bytes)
Apri digest_v2.pdf e confrontalo con digest.pdf. Ogni storia dovrebbe ora avere una riga "Perché è importante".
9. Persistenza di una configurazione dell'agente gestita
Tutte le chiamate finora hanno superato AGENTS.md, SKILL.md e generate_pdf.py inline. Funziona, ma il codice di chiamata trasporta l'intero contenuto del file a ogni esecuzione. agents.create() incorpora la configurazione in un agente denominato salvato sul lato di Google. La chiamata successiva passa solo l'ID agente:
Inline calls: send sources on every call
Named agent: bake once → invoke by ID, no sources
Cosa compilare
Apri save_agent.py. Contiene un TODO (TODO 1).
Tieni presente che le costanti vengono importate direttamente da run_digest.py (senza duplicazione):
from run_digest import BASE_AGENT, AGENTS_MD, SKILL_MD, GENERATE_PDF_PY
TODO 1: compila la chiamata 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 imposta il modello sottostante. gemini-3.7-flash è l'opzione predefinita e la scelta migliore per questo flusso di lavoro; gemini-3.6-flash, gemini-3.5-flash e gemini-3.5-flash-lite sono disponibili se vuoi un'esecuzione più leggera o a costi inferiori.
base_environment (non environment) è la differenza principale rispetto alla chiamata inline nel passaggio precedente: le origini vengono archiviate lato Google e montate automaticamente a ogni invocazione futura. Esegui una volta, non a ogni esecuzione del riepilogo.
Verifica: salva l'agente
uv run python save_agent.py
Saved: my-digest
my-digest: Daily tech digest with editorial voice and PDF generation.
Richiamare l'agente salvato
Apri invoke_agent.py. Chiama l'agente salvato per ID senza origini:
stream = client.interactions.create(
agent="my-digest",
input="Generate the digest.",
stream=True,
environment="remote",
)
Confronta questo risultato con la chiamata inline: agent=BASE_AGENT viene sostituito da "my-digest" e l'intero blocco environment con tre fonti inline viene sostituito da environment="remote". La configurazione è già integrata nel sistema di Google.
Verifica: richiama l'agente salvato
uv run python invoke_agent.py
Dovresti vedere lo stesso live streaming dell'esecuzione in linea, ma la chiamata non contiene file di origine. Dopo la corsa, ENVIRONMENT_ID e INTERACTION_ID in .env vengono aggiornati in modo da poter continuare con refine_digest.py come prima.
[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. Invia tramite Gmail
L'agente ha generato il riepilogo e lo ha salvato in /workspace/digest.pdf. Finora l'hai scaricato localmente. Questo passaggio lo invia direttamente alla tua posta in arrivo facendo in modo che l'agente chiami l'API REST di Gmail dall'interno della sandbox.
L'approccio: ottieni un token di accesso OAuth 2.0 localmente e lo passi all'agente nel prompt input. L'agente utilizza code_execution per creare un'email MIME con il PDF allegato e inviarla all'API Gmail. Nessuno strumento personalizzato, nessuna registrazione del server MCP.
Prerequisiti
Abilita l'API Gmail nel tuo progetto GCP e crea un ID client OAuth 2.0:
- Vai alla pagina console.cloud.google.com/apis/library/gmail.googleapis.com e attiva l'API Gmail.
- Vai ad API e servizi > Credenziali > Crea credenziali > ID client OAuth 2.0.
- Tipo di applicazione: App desktop. Scarica il file JSON e salvalo come
credentials.jsonnella radice del progetto.
Aggiungi l'email del destinatario a .env:
RECIPIENT_EMAIL=you@gmail.com
Se necessario, installa le librerie di autenticazione:
uv sync
Cosa compilare
Apri send_digest.py. Ha due cose da fare.
TODO 1: carica o aggiorna un token di accesso 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())
Rimuovi la riga raise NotImplementedError dopo averla aggiunta. Al primo avvio, si apre un browser per la schermata per il consenso OAuth. Il token viene memorizzato nella cache in .gmail_token.json per le esecuzioni future.
TODO 2: sostituisci input="" con le istruzioni via email. Il token è già nell'ambito come 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}"
),
A cosa serve ogni parte
L'interazione riprende la stessa sandbox in cui l'agente ha già generato digest.pdf e summaries.json. previous_interaction_id fornisce all'agente la cronologia delle conversazioni.
Il token di accesso viene passato nella stringa input. L'agente lo legge dal prompt e lo utilizza nell'intestazione Authorization: Bearer quando chiama l'API Gmail. Non tocca mai la macchina locale o il file system.
L'agente utilizza code_execution per scrivere ed eseguire uno script Python all'interno della sandbox: legge summaries.json, lo formatta come newsletter, legge digest.pdf, crea un messaggio MIME multipart, lo codifica in base64url e lo invia tramite POST a https://gmail.googleapis.com/gmail/v1/users/me/messages/send.
Verifica
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.
Controlla la posta in arrivo. L'email arriva con il corpo formattato come newsletter e digest.pdf allegato.
11. Pianificare le corse giornaliere
Finora ogni passaggio è stato attivato manualmente. I trigger consentono di pianificare l'esecuzione automatica dell'agente denominato in base a un'espressione cron. L'agente viene eseguito all'ora pianificata, esegue il flusso di lavoro di riepilogo completo e l'ambiente persiste tra le esecuzioni, quindi i pacchetti installati alla prima esecuzione sono disponibili in ogni esecuzione successiva.
Manual: python run_digest.py → runs once, now
Trigger: client.triggers.create() → runs every morning, automatically
Cosa compilare
Apri create_trigger.py. Ha un TODO.
TODO 1: fill in the triggers.create() call. Il trigger esegue l'intero flusso di lavoro ogni giorno: genera il riepilogo E lo invia alla tua casella di posta. Poiché i token di accesso scadono dopo un'ora, viene inserito il token di aggiornamento da .gmail_token.json come origine in linea, in modo che l'agente possa scambiarlo con un token nuovo a ogni esecuzione.
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 è il timeout predefinito. max_consecutive_failures=3 mette in pausa automaticamente il trigger dopo 3 esecuzioni non riuscite di fila (il valore predefinito dell'API è 5; 3 è più conservativo per un workshop).
L'elenco sources inserisce .gmail_creds.json nella sandbox in /workspace/.gmail_creds.json. L'agente lo legge, scambia il token di aggiornamento con un nuovo token di accesso e chiama l'API Gmail. I token di aggiornamento non scadono, quindi questa operazione funziona a ogni esecuzione pianificata senza alcun aggiornamento manuale del token.
Rimuovi la riga raise NotImplementedError dopo aver aggiunto la chiamata.
Verifica
uv run python create_trigger.py
Trigger created: trig_abc123
Next run: 2026-07-23T09:00:00Z
create_trigger.py salva automaticamente l'ID trigger in .env.
Per controllare la cronologia di esecuzione dopo una sessione:
uv run python check_trigger.py
Per attivare il trigger immediatamente senza attendere il successivo orario pianificato:
uv run python fire_trigger.py
Per mettere in pausa o eliminare il trigger:
uv run python pause_trigger.py
12. Elimina
La sandbox scade automaticamente dopo 7 giorni di inattività. Nessun server da arrestare. Nessun contenitore da eliminare.
Se hai salvato una configurazione dell'agente, eliminala:
uv run python delete_agent.py
13. Riepilogo
Hai creato un agente gestito da zero, un concetto alla volta. Ecco cosa insegnava ogni esercizio:
Esercizio | Concetto | API Key |
Effettuare la prima chiamata | Esegui il provisioning di una sandbox Linux reale e trasmetti in streaming i relativi eventi in diretta |
|
Personalizzare l'agente | Monta i file di configurazione; mantieni gli ID in |
|
Aggiungere un gancio di sicurezza | Intercettare le chiamate agli strumenti prima che vengano eseguite; negare i comandi distruttivi |
|
Scarica il PDF | Scaricare il PDF senza eseguire di nuovo l'agente | API Gemini Files |
Continua la conversazione | Continua la conversazione senza recuperare di nuovo il web |
|
Mantieni configurazione agente | Persistenza della configurazione dell'agente; richiamo per ID, nessuna origine necessaria |
|
Invia tramite Gmail | Ottieni un token OAuth localmente e passalo all'agente, che chiama l'API REST Gmail tramite | OAuth 2.0, |
Pianificare le esecuzioni giornaliere | Esegui l'agente automaticamente in base a una pianificazione cron |
|
Pattern chiave
- Una chiamata, una sandbox:
interactions.create()gestisce tutta l'infrastruttura (nessun container da eseguire il deployment, nessun pacchetto da installare localmente) - Streaming progressivo:
stream=Truetrasforma una scatola nera di 90 secondi in un feed pubblicato di chiamate di strumenti e blocchi di testo - Origini inline: monta
AGENTS.md,SKILL.mde script predefiniti nella sandbox senza alcun passaggio di caricamento o deployment - Rilevamento automatico dell'imbracatura: i file inseriti in
.agents/vengono rilevati automaticamente (non è richiesta alcuna configurazione dell'SDK) - Stato bidimensionale:
environment_idtiene traccia di file e pacchetti;previous_interaction_idtiene traccia del contesto della conversazione; entrambi possono essere passati in modo indipendente - Download dello snapshot: l'ambiente è un tar completo del file system, accessibile tramite l'API Gemini Files
- Agenti denominati:
agents.create()incorpora la configurazione in modo permanente; le chiamate future trasmettono solo l'ID agente eenvironment="remote", senza origini - Hook:
hooks.json+ uno strumento di intercettazione degli script di gate intercetta le chiamate prima dell'esecuzione; una rispostadenyannulla la chiamata e l'agente si autocorregge - Chiamate API esterne: passa una credenziale nel prompt
input; l'agente scrive ed esegue il codice di integrazione all'interno della sandbox tramitecode_execution - Trigger: pianifica un agente in base a un'espressione cron con
client.triggers.create(); l'ambiente persiste tra le esecuzioni
ADK + Cloud Run e agenti gestiti: la differenza in sintesi
Capacità | ADK + Cloud Run | Managed Agents nell'API Gemini |
Esegui il provisioning di una sandbox |
|
|
Definisci gli strumenti | Funzioni Python registrate con l'agente | Funzionalità integrate: navigazione web, esecuzione di codice, file system |
Installa pacchetti |
| L'agente esegue |
Eventi di streaming | Infrastruttura SSE personalizzata |
|
Continuare una sessione | Database della sessione + inserimento del contesto |
|
File di configurazione | Codificati nell'agente o inseriti all'avvio | Montato tramite |
Infrastruttura da gestire | Container, Cloud Run, IAM, secret | Nessuno |