Chapter 81
Introduction to Gemini Deep Research Agent
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.Introduction to Gemini Deep Research Agent
| Authors |
|---|
| Holt Skinner |
| Mohan Li |
Overview
The Deep Research Agent on Agent Platform autonomously plans, executes, and synthesizes multi-step research tasks. Using Gemini 3.1 Pro, it bridges the gap between public web data and private enterprise context by grounding research across three distinct, high-fidelity data streams simultaneously.
Deep Research is an agent, not just a model. It is best suited for workloads requiring an "analyst-in-a-box" approach rather than low-latency chat.
Get started
Install Google Gen AI SDK and other required packages
%pip install -U -q "google-genai>=2.4.0" beautifulsoup4Authenticate your notebook environment
If you are running this notebook in Google Colab, run the cell below to authenticate your account.
import sys
if "google.colab" in sys.modules:
from google.colab import auth
auth.authenticate_user()Set Google Cloud project information
To get started using Agent Platform, you must have an existing Google Cloud project and enable the Agent Platform API.
Learn more about setting up a project and a development environment.
import os
from google import genai
# fmt: off
PROJECT_ID = "[your-project-id]" # @param {type: "string", placeholder: "[your-project-id]", isTemplate: true}
# fmt: on
LOCATION = "global" # @param ["global"] {allow-input: true}
if not PROJECT_ID or PROJECT_ID == "[your-project-id]":
PROJECT_ID = str(os.environ.get("GOOGLE_CLOUD_PROJECT"))
client = genai.Client(enterprise=True, project=PROJECT_ID, location=LOCATION)Load Deep Research Agent
AGENT_ID = "deep-research-preview-04-2026"Helper methods and libraries
import base64
import os
import time
from typing import Any
from IPython.display import HTML, Image, display
from bs4 import BeautifulSoup
from google.genai.interactions import (
DocumentContent,
TextContent,
UserInputStep,
)
def contains_html(text: str) -> bool:
"""Checks if the given text contains HTML tags."""
return bool(BeautifulSoup(text, "html.parser").find())
def contains_image(content: Any) -> bool:
"""Checks if the content is a PNG image with data."""
mime_type = getattr(content, "mime_type", None)
if not mime_type and isinstance(content, dict):
mime_type = content.get("mime_type")
return mime_type == "image/png" and (
hasattr(content, "data") or (isinstance(content, dict) and "data" in content)
)
def render_annotation(annotation: Any, idx: int) -> None:
"""Renders a single annotation as an HTML citation."""
if getattr(annotation, "type", None) == "url_citation":
title = getattr(annotation, "title", "Source")
url = getattr(annotation, "url", "#")
display(HTML(f'[cite: {idx}] <a href="{url}" target="_blank">{title}</a>'))
def render_content(content: Any) -> None:
"""Unified function to render different types of content (text, image, annotations)."""
if not content:
return
if isinstance(content, str):
display_markdown(content)
return
# Handle text
text = getattr(content, "text", None)
if not text and isinstance(content, dict):
text = content.get("text")
if text:
if contains_html(text):
display(HTML(text))
else:
print(text, end="\n\n", flush=True)
# Handle images
elif contains_image(content):
data = getattr(content, "data", None)
if not data and isinstance(content, dict):
data = content.get("data")
if data:
display(Image(base64.b64decode(data)))
# Handle annotations
elif annotations := getattr(content, "annotations", None):
if not annotations and isinstance(content, dict):
annotations = content.get("annotations")
for i, ann in enumerate(annotations, 1):
render_annotation(ann, i)
# Handle nested content
else:
sub_content = getattr(content, "content", None)
if not sub_content and isinstance(content, dict):
sub_content = content.get("content")
if isinstance(sub_content, list):
for item in sub_content:
render_content(item)
elif isinstance(sub_content, dict):
render_content(sub_content)
else:
# Final fallback for unexpected content objects
print(content, end="\n\n", flush=True)
def poll_for_results_stream(interaction_id: str) -> None:
"""Streams results for an interaction and polls until completion."""
print(f"✅ Task started! Streaming results for interaction: {interaction_id}\n")
final_interaction = None
try:
stream = client.interactions.get(id=interaction_id, stream=True)
for event in stream:
if interaction := getattr(event, "interaction", None):
final_interaction = interaction
# Process content from delta or step
if content := getattr(event, "delta", getattr(event, "step", None)):
render_content(content)
except Exception as e:
print(f"An error occurred during initial streaming: {e}")
final_interaction = client.interactions.get(id=interaction_id)
if final_interaction is None:
final_interaction = client.interactions.get(id=interaction_id)
# Polling loop for 'in_progress' status
while final_interaction and final_interaction.status == "in_progress":
print(
f"\nInteraction {interaction_id} still in progress. Polling again in 30 seconds...",
end="",
flush=True,
)
time.sleep(30)
final_interaction = client.interactions.get(id=interaction_id)
# Report final status
if final_interaction and final_interaction.status == "completed":
print("\n\n✅ Task complete!")
if usage := getattr(final_interaction, "usage", None):
print("\nUsage Statistics:")
stats = {
"Total Tokens": "total_tokens",
"Input Tokens": "total_input_tokens",
"Output Tokens": "total_output_tokens",
"Thought Tokens": "total_thought_tokens",
}
for label, attr in stats.items():
print(f"- {label}: {getattr(usage, attr, 'N/A')}")
elif final_interaction and final_interaction.status == "requires_action":
print("\n\nInteraction requires action!")
else:
status = getattr(final_interaction, "status", "unknown")
print(f"\n\nInteraction ended with status: {status}")Start a Research Task with Real-time Streaming
The following example demonstrates how to initiate a research task using the SDK.
Note on Duration: Deep Research tasks involve iterative searching and reading. Execution is comprehensive and may take significant time (minutes) to complete.
⚠️ Critical Step: An
interaction_idis returned immediately upon initialization. The id is required to retrieve your results if the request times out or a network interruption occurs during the long-running process.
Configuration Requirements & Limitations:
- You must use background execution (set
background=True); - You must use streaming mode (set
stream=True); - Only text inputs are supported;
- The agent processes single turn queries only. Stateful conversation management is not supported.
You can steer the agent's output by providing specific formatting instructions in your prompt. This allows you to structure reports into specific sections and subsections, include data tables, or adjust tone for different audiences (e.g., "technical," "executive," "casual").
# fmt: off
prompt = "Describe the history of AI Agents from 2024-2026. Give a very brief overview." # @param {type:"string"}
# fmt: on
interaction = client.interactions.create(
agent=AGENT_ID,
input=[
UserInputStep(
type="user_input",
content=[TextContent(type="text", text=prompt)],
)
],
background=True,
tools=[],
)
interaction_id = interaction.id
print(f"\n✅ Task started! Interaction ID: {interaction_id}")Reconnect to or Retrieve an Interaction Stream
The following example demonstrates how to reconnect to an existing research session to retrieve results.
How it works:
- You need to specify an
interaction_idgenerated in the previous step. - If the research is still ongoing, you will receive real-time progress updates.
- If the research is complete, you will receive the full history and final answer.
# @markdown ### 🆔 Interaction ID to fetch:
# @markdown (Leave empty if you intend to fetch the interaction created from the previous cell.)
# fmt: off
INTERACTION_ID = "" # @param {type: "string"}
# fmt: on
interaction_id = INTERACTION_ID or interaction_id
if not interaction_id:
raise RuntimeError("Please specify an interaction ID")
poll_for_results_stream(interaction_id=interaction_id)Multimodal Input
Example: PDF File from URL
Note - You can also use a URI from a Google Cloud Storage Bucket.
For example, try gs://github-repo/2403_05530.pdf
http_url = "https://arxiv.org/pdf/1706.03762" # @param {"type":"string"}
# fmt: off
prompt = "Summarize this document and describe the impact it has had on the world since it was released." # @param {"type":"string"}
# fmt: on
interaction = client.interactions.create(
agent=AGENT_ID,
input=[
UserInputStep(
type="user_input",
content=[
TextContent(type="text", text=prompt),
DocumentContent(
type="document", mime_type="application/pdf", uri=http_url
),
],
)
],
background=True,
tools=[],
)
interaction_id = interaction.id
poll_for_results_stream(interaction_id=interaction_id)Multimodal Output
Generate HTML Output
# fmt: off
prompt = "Generate an HTML table about the Google stock price in the first week of 2026." # @param {"type":"string"}
# fmt: on
interaction = client.interactions.create(
agent=AGENT_ID,
input=[
UserInputStep(
type="user_input",
content=[TextContent(type="text", text=prompt)],
)
],
background=True,
tools=[],
)
interaction_id = interaction.id
poll_for_results_stream(interaction_id=interaction_id)Image Generation
prompt = "Show me a picture of the Eiffel Tower" # @param {"type":"string"}
interaction = client.interactions.create(
agent=AGENT_ID,
input=[
UserInputStep(
type="user_input",
content=[TextContent(type="text", text=prompt)],
)
],
background=True,
tools=[],
)
interaction_id = interaction.id
poll_for_results_stream(interaction_id=interaction_id)Grounding Tools
The Deep Research Agent grounds its responses simultaneously across multiple distinct streams by defining them in the tools array.
By default, the agent has access to information on the public internet using the Google Search and url_context tools, so you don't need to specify them unless you are overriding the default list.
Here is a breakdown of how to configure the specific tool types based on your data sources:
| Grounding Tool | Key | Note |
|---|---|---|
| Google Search | google_search | Grounds the agent on public internet data with enterprise-level privacy filtering. |
| Enterprise Web Search | enterprise_web_search | Grounds the agent on public internet data from trusted sources. |
| Model Context Protocol (MCP) | external_data_mcp | Securely connects to live remote servers like S&P Global, Bloomberg, or Moody's instead of relying on standard web searches. Use the id field to specify the MCP server. |
| Agent Search | vertex_search | Allows the agent to reason across massive internal corpora. You must provide the path via datastore_id. |
MCP Servers
The following example shows connecting an MCP Server without authentication.
# fmt: off
prompt = "Use the SearchGithub tool to search for flashattention implementation." # @param {type:"string"}
# fmt: on
url = "https://mcp.grep.app"
interaction = client.interactions.create(
agent=AGENT_ID,
input=[
UserInputStep(
type="user_input",
content=[TextContent(type="text", text=prompt)],
)
],
background=True,
tools=[
{
"type": "mcp_server",
"name": "test mcp server",
"url": url,
}
],
)
interaction_id = interaction.id
poll_for_results_stream(interaction_id=interaction_id)Here is an example with authentication.
prompt = "Search for papers about flash attention." # @param {type:"string"}
# fmt: off
url = "https://deep-research-mcps.vercel.app/paper-search-mcp" # @param {type:"string"}
token = "Bearer 01727fdfdca3040e9176ff77f3798e3606f4041c60481e3143e859734ec23b13" #@param {type:"string"}
# fmt: on
interaction = client.interactions.create(
agent=AGENT_ID,
input=[
UserInputStep(
type="user_input",
content=[TextContent(type="text", text=prompt)],
)
],
background=True,
tools=[
{
"type": "mcp_server",
"name": "test mcp server",
"url": url,
"headers": {"Authorization": token},
}
],
)
interaction_id = interaction.id
poll_for_results_stream(interaction_id=interaction_id)Enterprise Web Search
prompt = "Research the latest trends and themes in AI." # @param {type:"string"}
interaction = client.interactions.create(
agent=AGENT_ID,
input=[
UserInputStep(
type="user_input",
content=[TextContent(type="text", text=prompt)],
)
],
background=True,
tools=[{"type": "google_search", "search_types": ["enterprise_web_search"]}],
)
interaction_id = interaction.id
poll_for_results_stream(interaction_id=interaction_id)Agent Search
Use the following example to connect Deep Research Agent to an Agent Search data store.
You will need to create a data store and app before proceeding.
Download the following paper and upload it to your data store to test.
ENGINE_ID = "[your-app-id]" # @param {type:"string"}
DATASTORE_ID = "[your-datastore-id]" # @param {type:"string"}
# fmt: off
prompt = "Using the provided search tool, summarize what Borg is." # @param {type:"string"}
# fmt: on
interaction = client.interactions.create(
agent=AGENT_ID,
input=[
UserInputStep(
type="user_input",
content=[TextContent(type="text", text=prompt)],
)
],
background=True,
tools=[
{
"type": "retrieval",
"retrieval_types": ["vertex_ai_search"],
"vertex_ai_search_config": {
"engine": f"projects/{PROJECT_ID}/locations/global/collections/default_collection/engines/{ENGINE_ID}",
"datastores": [
f"projects/{PROJECT_ID}/locations/global/collections/default_collection/dataStores/{DATASTORE_ID}"
],
},
}
],
)
interaction_id = interaction.id
poll_for_results_stream(interaction_id=interaction_id)Best Practices
Giving an autonomous agent access to the web and your files introduces unique dynamics:
- Prompt for Unknowns: Explicitly instruct the agent on how to handle missing data. For example, tell it to state if a figure is unavailable rather than estimating it.
- Prompt Injection Risks: Ensure uploaded files come from trusted sources, as malicious files could contain hidden text designed to manipulate the agent's output.
- Data Exfiltration: Be extremely cautious when asking the agent to summarize sensitive internal data while simultaneously giving it access to browse the public web.
- Verify Citations: While enterprise-grade filtering is applied, always verify the citations provided in the response to ensure web sources are reputable.
