Introduction

Part 1 showed the fastest way to connect Gemini Enterprise to live Oracle data without building a custom natural-language-to-SQL stack. This post is for developers who want to extend that foundation. Here, the Oracle AI Database Agent remains the governed Oracle data plane and SQL execution engine, while a custom ADK orchestrator running on Cloud Run adds composition: parallel data gathering, web enrichment, chart generation, and document delivery.

That distinction matters. You are not replacing the Oracle AI Database Agent from Part 1, and you are not copying Oracle data into a separate middleware layer just to make AI work. You are using the Oracle agent as one specialized agent inside a broader workflow, which allows you to give the model reliable, governed access to the system of record without building and maintaining custom data plumbing.

By the end of this post, you will have a deployable reference architecture on Cloud Run that demonstrates how A2A lets multiple agents decompose a task, run in parallel, and synthesize a result that would be cumbersome to build inside a single agent.

Target audience:

  • Google Cloud developers building agentic applications with ADK
  • Developers who want to use the Oracle AI Database Agent as a governed Oracle data-access component inside broader workflows
  • Engineers evaluating A2A as an integration pattern for production systems

What we are building

The Oracle Sales Intelligence Hub is a developer reference application that can answer complex business questions about Oracle sales data by orchestrating five specialized agents. The Oracle AI Database Agent from Part 1 handles Oracle data access; the other agents add external context, visualization, synthesis, and delivery

  • oracle_analyst Queries Oracle AI Database (Sales History schema) via the A2A remote agent
  • web_analyst Searches the web for external market context using Gemini web search grounding
  • charts_agent Generates a revenue bar chart and geographic map as PNG images
  • synthesis_agent Combines all inputs into a structured executive briefing
  • docs_writer_agent Uploads the full briefing to Cloud Storage and returns a signed URL

A user types a single question such as, “Give me a Q3 FY2025 briefing for the UK market” and receives a briefing document with charts, market context, and a shareable link, all within a single Gemini Enterprise chat response.


Agent topology

The agents are arranged in a SequentialAgent pipeline with a ParallelAgent at the data gathering stage:

root_agent (LlmAgent is the entry point)
└── briefing_pipeline (SequentialAgent)
├── data_gathering_team (ParallelAgent)
│ ├── oracle_analyst this refers to Oracle Database AI
Agent[RemoteA2aAgent]
│ └── web_analyst for Gemini web search grounding
├── charts_agent for bar chart png
├── synthesis_agent to combine Oracle + web + charts
└── docs_writer_agent to store in Cloud Storage

The ParallelAgent runs oracle_analyst and web_analyst simultaneously. This is one of the key values of multi-agent: neither agent waits for the other, cutting total latency roughly in half compared to sequential execution.


Prerequisites

From Part 1 of this blog post

  • Oracle agent URL: base URL of the Oracle NL2SQL A2A agent
  • Oracle token URL: OAuth token endpoint for your database
  • Oracle client ID and client secret : from the OAuth registration step
  • Select AI profile: configured with the Sales History (SH) schema

GCP setup requirements

  • GCP project with the following APIs enabled:
    • Vertex AI API
    • Cloud Run API
    • Cloud Build API
    • Secret Manager API
    • Cloud Storage API
    • Maps Static API
  • Service account with roles: Vertex AI User, Secret Manager Secret Accessor, Storage Object Admin, Logs Writer, Service Account Token Creator
  • Cloud Storage bucket: multi-region recommended for signed URL delivery
  • Google Maps API key: restricted to Maps Static API (GCP Console → APIs & Services → Credentials)
  • gcloud CLI installed and authenticated

Project structure

oracle-sales-hub/
├── orchestrator/
│ ├── demo_sales_hub/ ADK agent package
│ │ ├── __init__.py
│ │ └── agent.py all agent definitions
│ ├── oracle_auth.py Oracle OAuth client factory
│ ├── gcs_tool.py Cloud Storage upload
│ ├── charts_tool.py Bar chart + Maps API map
│ ├── main.py ADK FastAPI entry point
│ ├── requirements.txt
│ └── Dockerfile
└── infra/
└── deploy.sh Cloud Run deployment script

Authentication note: Part 1 described the managed user-facing flow, where queries run under each user’s Oracle Database identity. This reference architecture introduces a developer-owned orchestrator on Cloud Run and uses service-to-service authentication between that orchestrator and the Oracle remote agent.

Step 1: Oracle OAuth authentication

The Oracle Database AI Agent uses OAuth 2.0 with the password grant (resource owner password credentials). This grant allows the orchestrator to obtain a token using a database username and password. The oracle_auth.py module implements an httpx.Auth class that fetches a Bearer token, caches it, and refreshes it 60 seconds before expiry. This is wired into ADK’s RemoteA2aAgent via the a2a_client_factory parameter.

# oracle_auth.py (key excerpt)
class OracleOAuthAuth(httpx.Auth):
def __init__(self, token_url, client_id, client_secret, username,
password):
self.token_url = token_url
self.client_id = client_id
self.client_secret = client_secret
self.username = username
self.password = password
def _get_token(self) -> str:
resp = client.post(self.token_url, data={
"grant_type": "password",
"username": self.username,
"password": self.password,
"client_id": self.client_id,
"client_secret": self.client_secret,
})
return resp.json()["access_token"]
def auth_flow(self, request):
request.headers["Authorization"] = f"Bearer {self._get_token()}"
yield request
def get_oracle_client_factory(token_url, client_id, client_secret,
username, password):
auth = OracleOAuthAuth(token_url, client_id, client_secret, username,
password)
async_client = httpx.AsyncClient(
timeout=httpx.Timeout(timeout=60.0),
auth=auth,
headers={"Content-Type": "application/json"},
)
return
ClientFactory(ClientConfig(httpx_client=async_client))

This pattern is reusable for any A2A remote agent that requires non-Google OAuth. If you are integrating other third-party A2A agents, implement the same httpx. Auth subclass pattern with your own token endpoint and grant type.

Step 2: Connecting to the Oracle NL2SQL A2A agent

The Oracle Database AI Agent exposes an A2A endpoint at a path specific to Oracle’s implementation. Oracle uses:

ORACLE_AGENT_URL/agents/{agent-name}

We pass a pre-built AgentCard object directly to RemoteA2aAgent instead of a URL. This bypasses the HTTP fetch entirely

from a2a.types import AgentCard, AgentSkill,
AgentCapabilities
def _make_oracle_remote_agent(name: str) -> RemoteA2aAgent:
agent_card = AgentCard(
name="Oracle AI Database Agent",
description="Queries Oracle Autonomous AI Database using natural
language.",
url=ORACLE_AGENT_URL + AGENT_CARD_PATH,
version="1.0.0",
default_input_modes=["text/plain"],
default_output_modes=["text/plain"],
capabilities=AgentCapabilities(streaming=False),
skills=[AgentSkill(
id="nl2sql",
name="NL2SQL Query",
description="Converts natural language to SQL and queries
Oracle.",
tags=["oracle", "sql", "database"],
examples=["Show revenue by channel for Q3 FY2025"],
)],
)
return RemoteA2aAgent(
name=name,
description="Queries Oracle Autonomous AI Database using natural
language.",
a2a_client_factory=get_oracle_client_factory(
token_url=ORACLE_TOKEN_URL,
client_id=ORACLE_CLIENT_ID,
client_secret=ORACLE_CLIENT_SECRET,
username=ORACLE_USERNAME,
password=ORACLE_PASSWORD,
),
agent_card=agent_card, # pre-built object, no HTTP fetch
use_legacy=True,
)

It is important to note that NL2SQL and Oracle query execution still happen on the Oracle side. Your Cloud Run orchestrator does not translate natural language to SQL itself; it delegates Oracle data retrieval to the Oracle AI Database Agent through A2A.

Step 3: Running Oracle and web search in parallel

ADK’s ParallelAgent runs all sub-agents simultaneously and collects their outputs into the shared conversation context. The data_gathering_team runs the Oracle query and the web search at the same time:

data_gathering_team = ParallelAgent(
name="data_gathering_team",
sub_agents=[oracle_analyst, web_analyst],
)

Each sub-agent prefixes its output with a clear header, so the downstream synthesis agent can reliably locate each data source.

Step 4: Generating visual charts

Gemini Enterprise renders PNG images inline in chat when the agent declares image/png as an output mode in its agent card. The charts_tool.py module generates two images:

Bar chart

Generated with Matplotlib. Shows FY2024 vs FY2025 side-by-side bars per sales channel (Direct Sales, Tele Sales, etc.) with YoY percentage.

# charts_tool.py (bar chart excerpt)
bars_2024 = ax.bar(x - bar_width/2, fy2024_vals, bar_width,
label="FY2024", color="#B5D4F4")
bars_2025 = ax.bar(x + bar_width/2, fy2025_vals, bar_width,
label="FY2025", color="#185FA5")
# YoY % label above each FY2025 bar
for i, (v24, v25) in enumerate(zip(fy2024_vals, fy2025_vals)):
yoy = ((v25 - v24) / v24) * 100
color = "#3B6D11" if yoy >= 0 else "#A32D2D"
ax.text(x[i] + bar_width/2, v25, f"+{yoy:.1f}%",
ha="center", color=color, fontweight="bold")

Revenue map for geographic distribution

Generated using the Google Maps Static API. Country centroids are overlaid with circle markers sized and colored by revenue intensity. The map auto-centers and zooms based on which countries appear in the Oracle results.

# charts_tool.py (map excerpt)
params = {
"size": "800x500",
"scale": "2", # retina quality
"maptype": "roadmap",
"center": f"{center_lat},{center_lon}",
"zoom": str(zoom),
"key": MAPS_API_KEY,
}
# Add a circle marker per country, colored by revenue intensity
for country, revenue in mapped.items():
lat, lon = COUNTRY_COORDS[country]
markers.append(f"color:{color}|size:{size}|{lat},{lon}")

Both images are uploaded to Cloud Storage and returned as signed URLs. The charts_agent runs after data_gathering_team completes, so it has access to the Oracle data it needs to parse revenue figures. The URLs appear in the Gemini Enterprise chat response as clickable links, users click to open the full-resolution chart in a new tab.

Step 5: Synthesizing the briefing

The synthesis agent receives three upstream inputs via the shared conversation context and combines them into a structured Markdown briefing. The output briefing follows a fixed structure with sections such as sales performance, geo distribution, market context, and recommendation

Step 6: Delivering the briefing via Cloud Storage

The docs_writer_agent calls upload_briefing_to_gcs() which converts the briefing to a HTML page and uploads it to your Cloud Storage bucket. For simplicity, we push the file to publicly readable storage. The final response in Gemini Enterprise chat includes the structured briefing text in Markdown, image links with bar chart and revenue map and a clickable URL to the full HTML report on Cloud Storage

Step 7: Declaring image output in the agent card

Declare image/png as a supported output mode in the agent card JSON so Gemini Enterprise knows this agent can return image content. In practice, Gemini Enterprise renders images sent as binary blob attachments in the A2A response. Images referenced only as GCS URLs appear as clickable links rather than inline renders. For this demo, charts appear as clickable links that open in a new tab, and the full HTML briefing report includes the charts embedded inline.

The agent card JSON you paste into Gemini Enterprise must include these fields:

{
"protocolVersion": "0.1",
"name": "Oracle Sales Intelligence Hub",
"description": "Answers business questions about Oracle sales
data...",
"url": "https://YOUR_CLOUD_RUN_URL",
"version": "1.0.0",
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain", "image/png"],
"capabilities": { "streaming": false },
"skills": [{
"id": "sales_briefing",
"name": "Sales Briefing",
"description": "Generates an executive sales briefing...",
"tags": ["sales", "oracle", "analytics", "briefing"],
"examples": ["Give me a Q3 FY2025 briefing for the UK market",
"demo"]
}]
}

Step 8: Serving the agent with to_a2a()

ADK’s get_fast_api_app() serves agents via ADK’s own API format, not the A2A JSON-RPC standard that Gemini Enterprise expects. To expose a proper A2A-compliant endpoint, use ADK’s to_a2a() function instead. Pass a Runner configured with session and artifact services:

# main.py
import os
import uvicorn
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from google.adk.artifacts import InMemoryArtifactService
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from demo_sales_hub.agent import root_agent
port = int(os.environ.get("PORT", "8080"))
cloud_run_url = os.environ.get("CLOUD_RUN_URL", "")
host = cloud_run_url.replace("https://", "").replace("http://",
"").rstrip("/")
runner = Runner(
agent=root_agent,
app_name=root_agent.name,
session_service=InMemorySessionService(),
artifact_service=InMemoryArtifactService(),
)
app = to_a2a(
agent=root_agent,
host=host,
port=443,
protocol="https",
runner=runner,
)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=port)

Step 9: Deploying to Cloud Run

The deployment script handles all setup steps automatically. Fill in the placeholders at the top of the script and run it from your project root.

Environment variables

VariableDescription
PROJECT_IDYour GCP project ID
REGIONCloud Run region e.g. us-east4
ORACLE_AGENT_URLBase URL of the Oracle AI Database Agent
ORACLE_TOKEN_URLOracle OAuth token endpoint
ORACLE_CLIENT_IDOAuth client ID from Oracle registration
ORACLE_CLIENT_SECRETOAuth client secret (stored in Secret Manager)
ORACLE_USERNAMEOracle database username for password grant authentication
ORACLE_PASSWORDOracle database password (stored in Secret Manager)
GCS_BUCKET_NAMECloud Storage bucket for briefing output
MAPS_API_KEYGoogle Maps Static API key (stored in Secret Manager)
CLOUD_RUN_URLYour Cloud Run service public URL (used to build agent card)
SERVICE_ACCOUNT_EMAILService account email for GCS signed URL generation
GOOGLE_GENAI_USE_VERTEXAISet to true to use Vertex AI instead of Gemini API key
GOOGLE_CLOUD_PROJECTGCP project ID passed to Vertex AI SDK
GOOGLE_CLOUD_LOCATIONSet to global for broadest Gemini model availability
GEMINI_MODELSet to gemini-2.5-flash

Run the deployment

chmod +x infra/deploy.sh
./infra/deploy.sh

The script performs these steps:

  • Sets the active GCP project
  • Enables all required GCP APIs
  • Grants Cloud Build service account deployment permissions
  • Stores secrets in Secret Manager (Oracle client secret, Maps API key)
  • Builds and pushes the Docker image via Cloud Build (no Docker required locally)
  • Deploys the Cloud Run service with Zero Trust authentication (–no-allow-unauthenticated)
  • Grants the current user Cloud Run invoker access for testing

Step 10: Connecting to Gemini Enterprise

Once deployed, a Gemini Enterprise App Admin registers with your Cloud Run orchestrator as a custom A2A agent. Think of this as a second layer above the Oracle AI Database Agent from Part 1: Gemini Enterprise talks to your orchestrator, and your orchestrator in turn talks to the Oracle agent.

There are two separate connections in this architecture. The Oracle OAuth credentials from Part 1 are used by the orchestrator when it calls the Oracle AI Database Agent. Access from Gemini Enterprise to your Cloud Run service should match however you expose the custom agent endpoint. In the demo deployment in this post, the service is made reachable, so Gemini Enterprise can invoke it.

  • Open the Gemini Enterprise Admin Console: go to Apps → Google Workspace → Gemini Enterprise
  • Navigate to Agents then Add Agent then Custom agent via A2A
  • Paste the agent card JSON: replace YOUR_CLOUD_RUN_URL with your actual service URL. The url field must point to your Cloud Run service base URL.
  • Enter OAuth credentials: Gemini Enterprise prompts for Client ID, Client Secret, Authorization URL, and Token URL. Use the same Oracle OAuth credentials from Part 1.
  • Grant access: in the agent’s Permissions panel, add the users, groups, or OUs that should have access
  • Open Cloud Run to allUsers: Gemini Enterprise must be able to reach your service. Run:
gcloud run services add-iam-policy-binding oracle-sales-hub \
--region=YOUR_REGION \
--member="allUsers" \
--role="roles/run.invoker"

Note: this is reset on each redeployment. Add –allow-unauthenticated to the gcloud run deploy command in deploy.sh to make it permanent.


Demo output

Chat

The image shows the demo chat output
The chat output

Charts (opens in new tab)

Visualization chart shows the revenue by sales channel - Financial Year 2024 vs Financial Year 2025
Visualization chart
The image shows the visualization of the results on the map: the revenue by sales channel - Financial Year 2024 vs Financial Year 2025
The output results on the map

Troubleshooting

IssueResolution
Container fails to start on Cloud RunCheck logs: gcloud logging read with resource.type=cloud_run_revision. Most common cause is a missing environment variable causing a crash at import time.
ModuleNotFoundError: google.adk.a2aThe A2A extras are not installed. Change google-adk>=0.4.0 to google-adk[a2a]>=0.4.0 in requirements.txt and redeploy.
Agent card returns 404 from curlADK’s get_fast_api_app() does not expose A2A endpoints. Switch to to_a2a() in main.py as described in Step 8.
Cloud Run returns 403 to Gemini EnterpriseGemini Enterprise cannot reach the service. Run: gcloud run services add-iam-policy-binding oracle-sales-hub –member=allUsers –role=roles/run.invoker
Gemini model 404 / publisher model not foundgemini-2.0-flash is not available for new projects. Use gemini-2.5-flash and set GOOGLE_CLOUD_LOCATION=global in deploy.sh.
Signed URL error: credentials just contain a tokenCompute Engine credentials cannot sign URLs directly. Add SERVICE_ACCOUNT_EMAIL env var, update gcs_tool.py to use access_token parameter, and grant roles/iam.serviceAccountTokenCreator to the service account.
Oracle A2A returns ADB-00015 Invalid credentialThe client_id or client_secret is incorrect or expired. Re-run the Oracle OAuth registration curl to generate fresh credentials.
Gemini Enterprise OAuth error unauthorizedGemini Enterprise sends authorization code flow without PKCE. If your Oracle OAuth client is registered with PKCE, register a new client without using token_endpoint_auth_method: client_secret_post.

Links