Dual-Engine RAG Support Assistant on Google Cloud: Public Docs, Private Runbooks
Build a support assistant that queries Vertex AI Search and the Developer Knowledge API together, then lets Gemini reconcile advice with a customer's own rules.
This article is published in English.
Anyone working a support queue against an SLA knows the cost of switching tabs: one for Google Cloud's public documentation, another for the customer's own runbook, and a mental merge of the two. Asking a generic AI assistant to shortcut that process often returns an answer that is right on paper and wrong for the customer in front of you.
The fix described here is a dual-engine retrieval-augmented setup. A ticket is sent in parallel to two sources, public Google Cloud docs and a private store of customer runbooks, and a Gemini model reconciles what comes back into a resolution guide that respects the customer's rules. Picture finding a well-rated recipe online and having a nutritionist rework it for your diet before you cook.
The build uses four pieces:
- Two retrieval paths that run side by side, one for generic technical advice and one for strict internal policy.
- Vertex AI Search as the private store, indexing business rules and constraints straight from runbook files.
- The Developer Knowledge API as the public source, returning grounded recommendations and troubleshooting steps from official documentation.
- Gemini as the reasoning layer, screening public solutions against private rules so the output is a guide a support engineer can safely follow.
Why generic advice goes wrong
Take a ticket like this one:
Acme reports roughly five seconds of cold-start delay on their Cloud Run frontend. What should we do?
An assistant grounded only in public docs will almost certainly say to set min-instances to 1. That is the textbook remedy for cold starts, and it is also a problem for this client, for two reasons:
- A budget cap. Acme limits the frontend service to a single instance at most, so an always-on instance doesn't fit.
- A process rule. Acme's runbook forbids changing min-instances without written sign-off from its lead architect, Sarah Connor.
What's needed is a system that fetches the public guidance on Cloud Run startup latency, checks each suggestion against Acme's private constraints, and only then tells the engineer about any conflict and proposes an alternative that complies.
Prerequisites
Have these ready before starting:
- A Google Cloud project with billing turned on.
- The gcloud CLI, installed and authenticated on your machine.
- Python 3.10 or newer.
Cost and quotas
The tutorial is meant to stay inside free tiers and developer quotas for each service:
- Cloud Storage. One small runbook file sits well within the Always Free storage allowance. Charges arise only if you go past it or generate egress.
- Vertex AI Search (Gemini Agent Search). New accounts get a free trial tier. Once trial credits run out, queries are billed pay-as-you-go per 1,000 queries, so check the current pricing page.
- Developer Knowledge API. Free to enable and use within the standard developer limits. Request allocations can be watched in Google Cloud's quota pages.
- Gemini 3.5 Flash. Free within the usual developer limits in Google AI Studio. Paid billing starts only if you upgrade for higher concurrency and rate limits.
Preparing the Google Cloud environment
Before the assistant itself, the underlying infrastructure needs setting up. One bash script handles three jobs:
- Enabling APIs. It turns on Cloud Storage, Vertex AI Agent Builder (
discoveryengine), Developer Knowledge and the Generative Language API, so the orchestrator can reach every endpoint it needs. - Creating the Discovery Engine service identity. It pre-creates the Google-managed service agent (
service-PROJECT_NUMBER@gcp-sa-discoveryengine.iam.gserviceaccount.com), so the background indexer can be given IAM permissions right away, with no first-time console click required. - Setting up storage and access. It creates the Cloud Storage bucket that will hold the customer runbook and grants
roles/storage.objectViewerto two principals: the background search agent, which needs it for indexing, and your own account, so you can inspect the bucket in the console.
Paste the script into a terminal:
# 1. Initialize variables from active gcloud config
export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
export BUCKET_NAME="${PROJECT_ID}-internal-kb"
export USER_EMAIL=$(gcloud config get-value account)
# 2. Enable all required GCP APIs
gcloud services enable \
storage-api.googleapis.com \
discoveryengine.googleapis.com \
developerknowledge.googleapis.com \
generativelanguage.googleapis.com \
--project=${PROJECT_ID}
# 3. Provision the Discovery Engine Service Identity programmatically
# (This creates the background service account without requiring a manual Console click)
gcloud beta services identity create \
--service=discoveryengine.googleapis.com \
--project=${PROJECT_ID}
# 4. Create the GCS bucket
gcloud storage buckets create gs://${BUCKET_NAME} --project=${PROJECT_ID} --location=us-central1
# 5. Grant Storage Object Viewer to the Agent Search Service Agent (for background indexing)
gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \
--member="serviceAccount:@gcp-sa-discoveryengine.iam.gserviceaccount.com">service-${PROJECT_NUMBER}@gcp-sa-discoveryengine.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
# 6. Grant Storage Object Viewer to your active Console user (for frontend validation)
gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \
--member="user:${USER_EMAIL}" \
--role="roles/storage.objectViewer"
echo "Setup complete! All APIs enabled and permissions configured successfully."
The values you may want to adjust:
PROJECT_IDis your project ID. When the project is already set in gcloud,gcloud config get-value projectreturns it automatically.PROJECT_NUMBERis the project's unique number.BUCKET_NAMEnames the bucket for runbooks and defaults to${PROJECT_ID}-internal-kb.USER_EMAILis the address of the active Google Cloud account that gets viewer access in the console.
Building the private knowledge engine
The private side is a small search engine holding a sample customer runbook.
Writing and uploading the runbook
Create the runbook file locally, then copy it to the bucket:
# Create the client runbook file with .txt extension
cat << 'EOF' > client-acme-runbook.txt
# Client Acme Corporation - Cloud Run Deployment Runbook
* **Client ID:** ACME-404
* **SLA Tier:** Platinum (4-hour resolution target)
* **Infrastructure Specifications:**
* Frontend Service Name: `acme-frontend-prod`
* Regional Deployment: `us-central1`
* Max Instances: `1` (Hard cap imposed due to strict budget constraints)
* CPU Allocation: `CPU allocated only during request processing`
* Backend Database: Cloud SQL PostgreSQL (`acme-db-prod`)
* Networking: Serverless VPC (Virtual Private Cloud) Access connector (`acme-vpc-connector`)
* **Deployment Constraints:**
* Deployed strictly via GitHub Actions workflow (`.github/workflows/deploy.yml`). Manual console updates are forbidden.
* Direct access to the production DB requires using the Bastion Host `acme-bastion-prod`.
* **Escalation Path:**
* Primary Contact: Sarah Connor (Lead Architect, sarah@acme-corp.com)
* Secondary Contact: PagerDuty escalation policy `acme-prod-critical`
* **Common Issues & Custom Rules:**
* **High Cold Starts:** High cold-start latency due to CPU throttling during scale-up. Do not modify the `min-instances` count without approval from Sarah Connor, as it breaches billing thresholds.
* **Database Connection Timeouts:** If timeouts occur, verify that the Serverless VPC Access connector is in a `READY` state before attempting database restarts.
EOF
# Upload the file to your bucket
gcloud storage cp client-acme-runbook.txt gs://${BUCKET_NAME}/
The upload command uses the bucket name from the setup step, ${PROJECT_ID}-internal-kb unless you changed it. The runbook is deliberately packed with the facts that matter: Acme's hard cap of one instance, its approval rule for min-instances, its database and VPC connector names, its deployment-only-through-GitHub-Actions policy, and the escalation contacts.
Creating an Agent Search data store
The data store is what makes the runbook searchable. In the Google Cloud console:
- Open Agent Builder (also labelled Vertex AI Search).
- From the left navigation, choose Data Stores, then Create data store.
- As the source, pick Cloud Storage.
- For the folder path, enter
gs://PROJECT_ID-internal-kb/*with your real project ID in place ofPROJECT_ID. - Pick Unstructured documents (PDF, HTML, TXT and similar) as the type of data and continue.
- Name the data store
acme-internal-kb. - Click Create.
- On the Data Stores list, copy and keep the Data Store ID.
Creating a search app for serving
Queries from code go through a Search App (an Engine), which acts as the serving layer on top of the data store:
- In Agent Builder, open Apps.
- Click Create app.
- Under Search on the creation page, click Create.
- Call the app
acme-search-app. - For the external organisation name, enter
Acme Corporation. - Leave the location at the default, global, and continue.
- On the Data Stores step, tick
acme-internal-kb. - Click Create.
- When it finishes, open the app from the Apps list and copy its Engine ID (also called App ID) from the details page. It looks something like
acme-search-app_1234567890.
Connecting the public side: the Developer Knowledge API
This API gives grounded access to official Google Cloud documentation and developer guides. The steps below create an API key, restrict it, and confirm access with one curl request.
Creating an API key
- In the console, go to Credentials.
- Choose Create credentials, then API key.
- Preferably restrict the key to the Developer Knowledge API alone.
- Store the key in an environment variable:
export DEVELOPERKNOWLEDGE_API_KEY="DEVELOPERKNOWLEDGE_API_KEY
Put your own generated key in place of the placeholder.
Testing with curl
A quick query confirms the key works and that you can reach Google's developer docs:
curl -X POST "https://developerknowledge.googleapis.com/v1:answerQuery?key=$DEVELOPERKNOWLEDGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "How do I reduce Cloud Run cold starts?"}'
A structured JSON reply containing an answer drawn from official documentation means it's working.
The orchestration script
Next comes a Python script that asks both engines the same question and hands the results to Gemini for the final answer.
Installing libraries
pip install google-cloud-discoveryengine google-generativeai requests
Creating the orchestrator
The following command writes app.py. The script sends the incoming ticket text to both search paths, slots the two results into a prompt template, and passes the filled prompt to Gemini:
cat << 'EOF' > app.py
import os
import sys
import requests
import google.generativeai as genai
from google.cloud import discoveryengine_v1beta as discoveryengine
# Configuration - Replace with your details
PROJECT_ID = os.environ.get("PROJECT_ID")
ENGINE_ID = os.environ.get("ENGINE_ID")
LOCATION = "global"
MODEL_NAME = os.environ.get("GEMINI_MODEL", "gemini-3.5-flash")
# Initialize Gemini Client (Uses GEMINI_API_KEY env variable)
genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
model = genai.GenerativeModel(MODEL_NAME)
# 1. Helper to query the Private KB (Agent Search)
def query_private_kb(query_text: str):
client = discoveryengine.SearchServiceClient()
serving_config = f"projects/{PROJECT_ID}/locations/{LOCATION}/collections/default_collection/engines/{ENGINE_ID}/servingConfigs/default_search"
request = discoveryengine.SearchRequest(
serving_config=serving_config,
query=query_text,
page_size=3,
)
response = client.search(request)
results = []
for result in response.results:
doc_data = result.document.derived_struct_data
if "snippets" in doc_data:
for s in doc_data["snippets"]:
results.append(s.get("snippet", ""))
return "\n".join(results)
# 2. Helper to query the Public KB (Developer Knowledge API)
def query_public_gcp_kb(query_text: str):
api_key = os.environ.get("DEVELOPERKNOWLEDGE_API_KEY")
url = f"https://developerknowledge.googleapis.com/v1:answerQuery?key={api_key}"
headers = {"Content-Type": "application/json"}
payload = {"query": query_text}
try:
response = requests.post(url, json=payload, headers=headers)
response.raise_for_status()
data = response.json()
return data.get("answer", {}).get("answerText", "No answer found.")
except Exception as e:
return f"Error querying Developer Knowledge API: {e}"
# 3. Main Orchestration
def run_support_assistant(ticket_query: str):
print(f"\n[Ticket Query]: {ticket_query}")
# Dynamically search private client specs using the ticket query
print("Searching private database…")
private_context = query_private_kb(ticket_query)
# Dynamically search public GCP advice using the ticket query
print("Searching public Google Cloud docs…")
public_context = query_public_gcp_kb(ticket_query)
# Format system prompt template
prompt = f"""
You are a Senior Cloud Support Assistant. Your task is to resolve the support ticket.
CRITICAL INSTRUCTIONS:
1. You MUST read the [Private Runbook Context] for specific client configurations, limits, and rules.
2. You MUST cross-reference all generic GCP recommendations against the client's private constraints. If a generic recommendation (e.g., increasing min-instances) violates a client limit or rule (e.g., budget caps or requiring specific manager approvals), you MUST call this out explicitly and provide the compliant fallback option.
3. Refer to specific client contacts (e.g., Sarah Connor), services, databases, and network connectors mentioned in the runbook when explaining the steps.
[Private Runbook Context]:
{private_context}
[Official GCP Guidance]:
{public_context}
[Support Ticket Query]:
{ticket_query}
Provide a precise troubleshooting guide for the support engineer.
"""
response = model.generate_content(prompt)
print("\n[AI Assistant Resolution Guide]:")
print(response.text)
if __name__ == "__main__":
if not all([PROJECT_ID, ENGINE_ID, os.environ.get("DEVELOPERKNOWLEDGE_API_KEY"), os.environ.get("GEMINI_API_KEY")]):
print("Please set your environment variables: PROJECT_ID, ENGINE_ID, DEVELOPERKNOWLEDGE_API_KEY, and GEMINI_API_KEY.")
else:
# Allow passing the ticket query as a command line argument
default_query = "Client Acme is complaining about 5-second cold start latency on their frontend service. How can we fix it?"
query = sys.argv[1] if len(sys.argv) > 1 else default_query
run_support_assistant(ticket_query=query)
EOF
The prompt does the real work. It tells the model to read the private runbook for client-specific settings and rules, and to compare every generic recommendation with those constraints. If a public recommendation, such as raising min-instances, breaks a client limit or approval rule, the model must say so openly and offer a compliant fallback. It must also name the client's actual contacts, services, databases and network connectors when it explains the steps.
Running the test
Set the environment variables and launch the script:
export PROJECT_ID="PROJECT_ID"
export ENGINE_ID="ENGINE_ID"
export DEVELOPERKNOWLEDGE_API_KEY="DEVELOPERKNOWLEDGE_API_KEY"
export GEMINI_API_KEY="GEMINI_API_KEY"
# Run with the default query
python3 app.py
# Or pass a custom query as a command line argument
python3 app.py "Client Acme is complaining about database connection timeouts on their Cloud SQL backend."
Substitute real values:
PROJECT_IDis your project ID.ENGINE_IDis the Engine/App ID of the Vertex AI Search app.DEVELOPERKNOWLEDGE_API_KEYis the key you generated for the Developer Knowledge API.GEMINI_API_KEYis a key from Google AI Studio. Google's documentation explains how to get a Gemini API key.
What the output looks like
Expect something along these lines:
[AI Assistant Resolution Guide]:
### **Acme Support Ticket: Resolution & Troubleshooting Guide**
**Ticket ID:** #ACM-98431
**Service:** `acme-frontend` (Cloud Run)
**Issue:** 5-second cold start latency
**Assigned To:** Cloud Support Engineering Team
- -
### **CRITICAL COMPLIANCE WARNING (Read Before Acting)**
According to Acme's operational rules:
* **The Conflict:** Standard GCP documentation suggests setting `min_instances` (e.g., to 1 or more) to eliminate cold starts. However, **Acme operates under a strict budget cap policy that prohibits idle/always-on instances** for the frontend service without explicit business justification.
* **The Rule:** You **MUST NOT** increase `min_instances` on `acme-frontend` without prior written approval from **Sarah Connor** (Acme's Technical Lead & Budget Approver).
* **Compliant Fallback:** If Sarah Connor does not approve the budget increase for `min_instances`, you must resolve the latency strictly through **code-level optimizations, CPU adjustments, startup probes, and connection tuning** as detailed below.
- -
### **Step-by-Step Troubleshooting & Resolution Guide**
#### **Phase 1: Triage and Governance Check**
1. **Contact Sarah Connor:** Reach out to Sarah Connor to request a budget exception for setting `min_instances = 1`.
2. **Fallback implementation:** If denied, proceed strictly with the budget-compliant fallback steps below to optimize the cold start duration within the 0-instance scaling limit.
#### **Phase 2: Network & Connection Optimization**
1. **Inspect VPC Connector Latency:** `acme-frontend` routes traffic to the database via the **`acme-vpc-connector`** (Serverless VPC Access). Ensure that the connector throughput is not bottlenecked.
2. **Database Connection Lazy-Loading:** The frontend connects to **`acme-production-db`** (Cloud SQL). Do not open the connection pool until the first actual handler request arrives to minimize container boot time.
…
What the result shows
The answer combines the two sources, and each contributes something different:
- Public documentation, via the Developer Knowledge API, supplies the technical fixes: turning on
cpu-boost, setting concurrency to80, or adding a/healthzstartup probe. - The private knowledge base, via Agent Search, adds business logic, security constraints and operational detail. It points out that the standard advice (
min_instances = 1) breaks Acme's budget rules. It names Sarah Connor as the person who must approve any exception. It refers to Acme's actual database and VPC connector.
Without both engines, a generic model would just recommend raising min_instances. A junior support engineer might act on it right away, and in an enterprise that could break budget limits, trip billing alerts and skip architectural approval. Grounding the model in both public and private context yields fixes that are technically sound and also compliant with organisational policy.
Hardening it for production
A few changes would be sensible before this serves real tickets:
- Keep secrets in a secrets manager. Put API keys in Secret Manager and load them at runtime, not in plain-text environment variables.
- Use least-privilege IAM. Run the workload under a dedicated service account holding only
roles/discoveryengine.viewerandroles/storage.objectViewer, and limit API keys to specific APIs and trusted network addresses. - Automate ingestion. Link sources such as Jira, Confluence, SharePoint or Google Drive to the data store through Vertex AI Search connectors, and drop the manual uploads.
- Filter by metadata. Pass client IDs and system tags in the
SearchRequestfilter so results are narrowed to one customer's environment. - Cache common lookups. Keep frequent documentation answers in Memorystore for Redis to cut latency and quota use.
Cleaning up
To avoid ongoing charges, remove what the tutorial created.
Search app and data store
A data store can't be deleted while an app is attached, so delete the app first. In the console:
- Go to Agent Builder, then Apps.
- Find
acme-search-app, open More actions (three dots) and choose Delete. - Type the app's name in the confirmation dialog and confirm.
- Open Data Stores in the navigation menu.
- Find
acme-internal-kb, open More actions, and choose Delete. - Confirm.
The storage bucket
Removing the bucket's contents is one command:
export BUCKET_NAME="${PROJECT_ID}-internal-kb"
gcloud storage rm - recursive gs://${BUCKET_NAME}/
API keys
- Go to APIs & Services, then Credentials.
- Under API Keys, find the key made for the Developer Knowledge API.
- Click the trash icon next to it and confirm.
Local files
Delete the scripts and the sample runbook from your machine:
rm -f app.py app_test.py client-acme-runbook.txt