Corso
Il team di Gemini ha recentemente introdotto Gemini 2.5 Computer Use, un modello specializzato che può vedere uno schermo in tempo reale e agire su di esso cliccando, digitando, scrollando e navigando sul web come un operatore umano.
In questa guida salteremo i benchmark astratti e costruiremo qualcosa di pratico: un'app Streamlit che usa Computer Use per controllare un browser reale, cercare su Google annunci di lavoro, applicare un filtro ed esportare i risultati in CSV senza alcuna API di ricerca di terze parti.
In questo tutorial imparerai a:
- Configurare il loop di Gemini 2.5 Computer Use
- Eseguire l'agente con Playwright (Chromium), usando un'allowlist e un'eventuale conferma umana per i passaggi rischiosi.
- Costruire una UI Streamlit con log delle azioni in tempo reale e screenshot dopo ogni turno.
- Fare scraping dei risultati organici e scaricare un CSV.
Alla fine avrai un agente per la ricerca di lavoro che selezionerà per te annunci accurati.

Se vuoi saperne di più su Gemini 2.5, ti consiglio di dare un'occhiata al nostro tutorial su Gemini 2.5 Pro, che copre funzionalità, test, accesso, benchmark e altro. Dovresti anche informarti su Gemini 3 Pro, l'ultimo modello di Google.
Che cos'è Gemini 2.5 Computer Use?
Gemini 2.5 Computer Use è un modello (anteprima) e uno strumento specializzato nella Gemini API che ti consente di creare agenti che controllano il browser. Invece di chiamare API specifiche del sito, il modello lavora dagli screenshot. Può vedere cosa c'è sulla pagina e poi agire emettendo azioni UI come navigate, click_at, scroll_document, ecc.
Il client riceve queste azioni proposte, le esegue e rimanda una nuova combinazione di screenshot e URL così che il modello possa decidere il passo successivo.
Come funziona il loop di Computer Use

Fonte: Documentazione di Gemini
Ecco come funziona, da end-to-end, il loop dell'agente mostrato sopra:
- Invia una richiesta: inizia inviando una richiesta che include lo strumento Computer Use, un prompt con l'obiettivo e uno screenshot iniziale dell'interfaccia utente.
- Ottieni una risposta: il modello risponde con una narrazione e una o più function call (come
click_atotype_text_at) e una decisione di sicurezza. - Esecuzione: poi esegui le azioni proposte, cioè se l'azione è contrassegnata come regolare/consentita possiamo eseguirla; se richiede conferma, chiediamo all'utente e procediamo solo con approvazione esplicita.
- Restituisci lo stato: dopo aver eseguito l'azione, restituiamo lo stato al modello inviando una
FunctionResponseche contiene uno screenshot aggiornato e l'URL corrente. - Ripeti: ripetiamo questo ciclo
vedi–decidi–agisci–osservafinché il compito non è completato, si verifica un errore o l'utente o il modello scelgono di interrompere.
Crea un agente di ricerca lavoro con Gemini 2.5 Computer Use e Streamlit
In questa sezione costruiremo un agente di ricerca lavoro basato su Streamlit che guida un browser reale usando Gemini 2.5 Computer Use e Playwright, quindi esporta i risultati in CSV.
Ecco come funziona:
- L'utente inserisce un elenco di keyword di lavoro nella sidebar di Streamlit.
- L'agente istruisce Gemini ad aprire Google, eseguire ogni query e applicare un filtro temporale.
- L'agente si ferma alla prima pagina dei risultati organici e mostra screenshot live insieme ai log delle azioni ad ogni turno.
- Quindi estrae titoli, link e snippet dalla pagina 1 e li aggrega su tutte le keyword.
- Infine, renderizza una tabella dei risultati insieme a un file CSV scaricabile.
Sotto il cofano, l'app impone un dominio in allowlist per una navigazione sicura, supporta una conferma umana opzionale per passaggi rischiosi e usa il loop di Computer Use per un debug agevole.
Prima di iniziare:
Alcuni casi d'uso responsabili e policy dei siti includono:
- Ambito dell'automazione: questa demo automatizza solo Google Search (in allowlist) e fa scraping della prima pagina di risultati organici.
- Rispetta i termini del sito: non automatizzare siti con Termini di servizio restrittivi e non tentare mai di bypassare i CAPTCHA. Se compare un CAPTCHA, completalo manualmente e poi lascia che il modello continui.
- Sicurezza con l'umano nel loop: se Gemini propone un'azione rischiosa (come un'interazione con CAPTCHA, acquisti), l'app si ferma per impostazione predefinita. Procederà solo se abiliti esplicitamente l'auto-conferma.
Passaggio 1: Prerequisiti
- Python 3.10+
- Una chiave API di Gemini da Google AI Studio
- macOS / Windows / Linux con Playwright Chromium installato
Passaggio 1.1: Import
Per prima cosa, assicurati di avere installati i seguenti pacchetti:
python -m venv .venv && source .venv/bin/activate
pip install streamlit google-genai playwright python-dotenv
playwright install chromium
I comandi sopra creano un ambiente virtuale e installano tutte le dipendenze principali necessarie per creare l'app, ovvero Streamlit per la UI, google-genai per chiamare la Gemini API, Playwright per l'automazione del browser, e python-dotenv per caricare le variabili d'ambiente e Chromium per Playwright.
Passaggio 1.2: Configurazione della chiave API di Gemini
Ora che abbiamo installato le dipendenze, impostiamo la chiave API di Gemini da AI Studio.
- Accedi ad AI Studio e cerca “Get API Key”
- Clicca su Create API key
- Seleziona un progetto Google Cloud esistente o creane uno con fatturazione abilitata.

- Clicca semplicemente su link a billing account e aggiungi i dati della carta per la verifica.
- Infine, copia la chiave API da AI Studio e salvala.
Ora crea un file .env nella cartella del progetto e aggiungi lì la tua chiave API:
GOOGLE_API_KEY=YOUR_REAL_KEY
ALLOWED_HOSTS=google.com
Il pacchetto python-dotenv caricherà GOOGLE_API_KEY a runtime così che l'app possa chiamare il modello Gemini 2.5 Computer Use.
Passaggio 2: Configurazione del modello
Ora prepariamo l'esecuzione, che include import, costanti, ID del modello e autenticazione. Questo assicura che l'app comunichi con Gemini e che la navigazione resti in sicurezza entro un dominio in allowlist.
import os, io, time, csv, base64, urllib.parse
from typing import List, Dict, Tuple
import streamlit as st
from dotenv import load_dotenv
from playwright.sync_api import sync_playwright
from google import genai
from google.genai import types
from google.genai.types import Content, Part
W, H = 1440, 900
MODEL = "gemini-2.5-computer-use-preview-10-2025"
load_dotenv()
API_KEY = os.getenv("GOOGLE_API_KEY")
if not API_KEY:
st.stop()
ALLOWED_HOSTS = {h.strip().lower() for h in os.getenv("ALLOWED_HOSTS", "google.com").split(",") if h.strip()}
client = genai.Client(api_key=API_KEY)
Partiamo importando le librerie principali come Streamlit, Playwright, google-genai e piccole utility (dotenv, os, io, csv, time, urllib) per configurazione e I/O.
Un viewport fisso (W, H = 1440 × 900) mantiene coerenti gli screenshot e la mappatura delle coordinate per Computer Use, e il modello in anteprima è impostato su gemini-2.5-computer-use-preview-10-2025.
La chiave API è caricata con load_dotenv() e os.getenv("GOOGLE_API_KEY"). La navigazione è impostata da una allowlist (di default google.com) per sicurezza.
Infine, genai.Client(api_key=API_KEY) inizializza l'SDK di Gemini così che l'app possa eseguire il loop dell'agente e renderizzare i risultati in Streamlit.
Passaggio 3: Creazione di funzioni helper
Prima di passare al cuore dell'app, dobbiamo impostare alcune funzioni helper. Aiutano con i controlli di sicurezza, la conversione delle coordinate, l'esecuzione delle azioni per le function call di Gemini, lo scraping della SERP e l'esportazione dei risultati in CSV.
Passaggio 3.1: Sicurezza ed esecuzione delle azioni
Iniziamo aggiungendo un helper backend che abilita un controllo sicuro del browser. Include una funzione per verificare la allowlist dei domini e limitare la navigazione, una funzione per convertire le coordinate e mappare la griglia 0–999 del modello al viewport fisso, e un dispatcher di azioni che interpreta le function call di Computer Use ed esegue le azioni tramite Playwright.
def host_allowed(url: str) -> bool:
try:
netloc = urllib.parse.urlparse(url).netloc.lower()
return any(netloc.endswith(allowed) for allowed in ALLOWED_HOSTS)
except Exception:
return False
def denorm(v: int, size: int) -> int:
return int(v/1000*size)
def exec_calls(candidate, page, viewport, *, approve_all=False) -> List[Tuple[str, Dict]]:
W, H = viewport
results = []
for part in candidate.content.parts:
fc = getattr(part, "function_call", None)
if not fc:
continue
name, args = fc.name, (fc.args or {})
sd = args.get("safety_decision")
if sd and sd.get("decision") == "require_confirmation":
reason = sd.get("explanation", "Model flagged a risky action.")
log_box.warning(f"[SAFETY requires confirmation] {reason}")
if not approve_all:
st.stop()
results.append((name, {"safety_acknowledgement": "true"}))
if name == "navigate":
target = args.get("url", "")
if target and not host_allowed(target):
log_box.error(f"[BLOCKED] Non-allowlisted host: {target}")
results.append((name, {"error": "blocked_by_allowlist"}))
continue
try:
if name == "open_web_browser":
pass
elif name == "navigate":
page.goto(args["url"], timeout=30000)
elif name == "search":
page.goto("https://www.google.com", timeout=30000)
elif name == "click_at":
page.mouse.click(denorm(args["x"], W), denorm(args["y"], H))
elif name == "hover_at":
page.mouse.move(denorm(args["x"], W), denorm(args["y"], H))
elif name == "type_text_at":
x, y = denorm(args["x"], W), denorm(args["y"], H)
page.mouse.click(x, y)
if args.get("clear_before_typing", True):
page.keyboard.press("Meta+A"); page.keyboard.press("Backspace")
page.keyboard.type(args["text"])
if args.get("press_enter", True):
page.keyboard.press("Enter")
elif name == "scroll_document":
page.mouse.wheel(0, 800 if args["direction"] == "down" else -800)
elif name == "key_combination":
page.keyboard.press(args["keys"])
page.wait_for_load_state("networkidle", timeout=10000)
results.append((name, {}))
time.sleep(0.6)
except Exception as e:
results.append((name, {"error": str(e)}))
return results
Vediamo come ogni funzione si inserisce nella pipeline:
host_allowed(): analizza l'URL di destinazione, normalizza l'hostname in minuscolo e verifica se termina con uno dei domini inALLOWED_HOSTSelencati nel file.env.denorm(): converte le coordinate del puntatore in pixel reali per il viewport corrente. È cruciale per clic, hover e digitazione precisi, soprattutto quando cambi dimensioni dello schermo o esegui in headless.exec_calls(): scansiona la risposta del modello alla ricerca di function call e le inoltra a Playwright. Blocca le navigate verso host non in allowlist, esegue le azioni supportate, attende lo stato di rete idle per stabilizzare la pagina e registra l'esito per azione (inclusi gli errori). Questi risultati vengono poi usati nel passaggio successivo per costruire leFunctionResponsesda inviare al modello.
Insieme, queste funzioni impongono confini, mantengono corretta la matematica del puntatore e registrano risultati granulari.
Passaggio 3.2: Feedback post-azione e acquisizione dati
Dopo l'esecuzione delle azioni, l'agente necessita di feedback per decidere il prossimo passo e restituire output utili. Questo step fornisce tre funzioni helper come segue:
def fr_from(page, results):
shot = page.screenshot(type="png")
url = page.url
frs = []
for name, result in results:
frs.append(types.FunctionResponse(
name=name,
response={"url": url, **result},
parts=[types.FunctionResponsePart(
inline_data=types.FunctionResponseBlob(mime_type="image/png", data=shot)
)]
))
return frs, shot
def scrape_google_serp(page, max_items=10):
items = []
anchors = page.locator('div#search a:has(h3)')
count = min(anchors.count(), max_items)
for i in range(count):
a = anchors.nth(i)
title = a.locator('h3').inner_text()
link = a.get_attribute('href')
snippet = ""
snips = page.locator('div#search .VwiC3b')
if snips.count() > i:
snippet = snips.nth(i).inner_text()
items.append({"title": title, "link": link, "snippet": snippet})
return items
def to_csv_download(rows: List[Dict], name="results.csv"):
if not rows:
return None
out = io.StringIO()
writer = csv.DictWriter(out, fieldnames=["keyword", "title", "link", "snippet"])
writer.writeheader(); writer.writerows(rows)
b = out.getvalue().encode("utf-8")
href = f"data:text/csv;base64,{base64.b64encode(b).decode()}"
st.download_button("Download CSV", data=b, file_name=name, mime="text/csv")
Vediamo come funziona ciascuna funzione di post-azione:
fr_from(): cattura uno screenshot PNG aggiornato e l'URL corrente, quindi costruisce unaFunctionResponseper ogni azione eseguita. Questo incorpora contesto visivo per il turno di inferenza successivo e preserva una traccia verificabile di quanto accaduto.scrape_google_serp(): estraiamo titoli, link e snippet dalla prima pagina dei risultati di Google usando selettori robusti comea:has(h3)per titoli/link e.VwiC3bper gli snippet. Lo scraper limita i risultati amax_itemse restituisce righe pulite e strutturate, pronte per analisi o export.to_csv_download(): genera un CSV in memoria con intestazioni coerenti e lo espone tramite la funzionedownload_button()di Streamlit.
Ora che abbiamo tutte le funzioni helper, possiamo costruire un'app Streamlit attorno ad esse.
Passaggio 4: Costruire l'applicazione Streamlit
Questo passaggio collega la UI di Streamlit al loop dell'agente di Computer Use. Renderizza l'interfaccia, gestisce come l'utente interagisce con l'agente e assicura che l'output corrisponda alle sue esigenze.
st.set_page_config(page_title="Jobs Search with Gemini Computer Use", layout="wide")
st.title("Jobs Search with Gemini 2.5 Computer Use")
with st.sidebar:
st.markdown("**Search inputs**")
default_kw = "data scientist remote\nml engineer \ngenai research india"
kw_text = st.text_area("Job keywords", value=default_kw, height=120)
query_mode = st.selectbox("Query style", ["<kw> jobs", 'site:linkedin.com/jobs "<kw>"'], index=0)
use_past_week = st.checkbox('Ask agent to set "Past week" filter', value=True)
turns = st.slider("Max agent turns per keyword", 3, 12, 6)
auto_confirm = st.checkbox("Auto-approve risky actions", value=False, help="If model requests confirmation (e.g., CAPTCHA), auto-approve instead of pausing.")
run_btn = st.button("Run search")
log_col, shot_col = st.columns([0.45, 0.55])
log_box = log_col.container(height=520)
shot_box = shot_col.container(height=520)
table_box = st.container()
if run_btn:
keywords = [k.strip() for k in kw_text.splitlines() if k.strip()]
all_rows = []
log_box.write("Initializing browser...")
pw = sync_playwright().start()
browser = pw.chromium.launch(headless=False)
ctx = browser.new_context(viewport={"width": W, "height": H})
page = ctx.new_page()
try:
for kw in keywords:
log_box.subheader(f"▶ {kw}")
page.goto("https://www.google.com", timeout=30000)
initial_shot = page.screenshot(type="png")
base_query = f'{kw} jobs' if query_mode == "<kw> jobs" else f'site:linkedin.com/jobs "{kw}"'
goal = (
f'Search Google for "{base_query}". '
f'{"Open Tools and set time filter to Past week. " if use_past_week else ""}'
'Stop when first-page results are fully visible; do NOT open the Jobs panel.'
)
contents = [Content(role="user", parts=[Part(text=goal), Part.from_bytes(data=initial_shot, mime_type="image/png")])]
cfg = types.GenerateContentConfig(
tools=[types.Tool(computer_use=types.ComputerUse(
environment=types.Environment.ENVIRONMENT_BROWSER
))]
)
for turn in range(turns):
log_box.caption(f"Turn {turn+1}: thinking…")
resp = client.models.generate_content(model=MODEL, contents=contents, config=cfg)
cand = resp.candidates[0]
contents.append(cand.content)
narr = " ".join([p.text for p in cand.content.parts if getattr(p, "text", None)])
if narr:
log_box.info(narr[:400])
fcs = [p.function_call for p in cand.content.parts if getattr(p, "function_call", None)]
if not fcs:
log_box.success("Agent stopped proposing actions.")
break
for fc in fcs:
log_box.write(f"→ {fc.name} {fc.args or {}}")
results = exec_calls(cand, page, (W, H), approve_all=auto_confirm)
frs, shot = fr_from(page, results)
contents.append(Content(role="user", parts=[Part(function_response=fr) for fr in frs]))
shot_box.image(shot, caption=page.url, width='stretch')
rows = scrape_google_serp(page)
for r in rows:
r["keyword"] = kw
all_rows.extend(rows)
log_box.success(f"{kw}: collected {len(rows)} results")
if all_rows:
table_box.dataframe(all_rows, width='stretch')
to_csv_download(all_rows, name="jobs_google_results.csv")
else:
st.warning("No rows collected. Try fewer keywords or fewer turns.")
finally:
browser.close()
pw.stop()
Capiremo ora nel dettaglio questa pipeline:
- L'app parte configurando la pagina con layout ampio e impostando un titolo chiaro, così l'utente capisce subito cosa fa lo strumento.
- Nella sidebar, l'utente fornisce le keyword, sceglie lo stile della query, un filtro temporale, il numero massimo di turni dell'agente per keyword e abilita o disabilita l'auto-conferma per azioni rischiose. Poi il pulsante Run search avvia il flusso.
- L'area principale è divisa in due colonne: a sinistra un contenitore “log” che mostra aggiornamenti testuali in streaming e a destra un contenitore “screenshot” che visualizza immagini live a ogni turno. Sotto c'è un contenitore separato per la tabella finale dei risultati.
- Quando l'utente clicca su Run search, l'interfaccia scrive i messaggi di avanzamento nel pannello dei log, aggiorna il pannello degli screenshot man mano che arrivano nuove immagini e, dopo aver elaborato tutte le keyword, mostra i risultati aggregati come dataframe.
Per eseguire l'app, lancia semplicemente il seguente comando bash nel terminale:
python -m streamlit run app.py
Suggerimento: avvia sempre Streamlit dalla tua venv per evitare errori “ModuleNotFoundError”.
Conclusione
Questo tutorial mostra come creare un agente visivo per la ricerca di lavoro usando Gemini 2.5 Computer Use per controllare un browser, applicare filtri ed esportare i risultati tramite una UI Streamlit.
Da qui puoi estendere il flusso aggiungendo paginazione per attraversare più pagine, ampliando le capacità dello scraper con date di pubblicazione, località e nomi delle aziende, oppure mettendo in allowlist una job board sandbox.
Se vuoi saperne di più su computer use e agentic AI, dai un'occhiata al corso Introduction to AI Agents.
Sono una Google Developers Expert in ML (Gen AI), una Kaggle 3x Expert e una Women Techmakers Ambassador con oltre 3 anni di esperienza nel tech. Ho co-fondato una startup health-tech nel 2020 e sto conseguendo un master in informatica al Georgia Tech, con specializzazione in machine learning.


