Google's Conversational Analytics API lets you build a chat-with-your-data feature into your own application, backed by a data agent that answers questions against a data source. When that source is a Looker explore, every answer is generated through your LookML semantic layer: the same joins, metrics, labels and row-level security your dashboards use. This tutorial prepares a model for it, creates a data agent, asks it questions from Python, and covers evaluation and guardrails. It assumes a Looker instance, a Google Cloud project with billing, and the Gemini Data Analytics API enabled.
What the API exposes
The API (package google.cloud.geminidataanalytics) has two services: DataAgentService, for creating and managing agents, and DataChatService, for conversations. An agent is configured with a published context: a system instruction (business context in plain language) and one or more datasource references. For Looker, a datasource reference names an instance URL, a LookML model and an explore, plus credentials (an API key or an OAuth token for a user whose permissions the agent inherits). Chat requests stream back a sequence of messages: the agent's interpretation of the question, the Looker query it built, the data, a chart specification and a natural-language answer.
Because the agent queries Looker through the API as a specific user, row-level security and field access are exactly what that user would see in an Explore. That is the whole reason to put Looker behind the agent rather than pointing it at a table.
Step 1: prepare the model
The agent reads field names, labels and descriptions to decide what to query. Prepare one explore deliberately before exposing anything:
explore: orders {
label: "Orders"
description: "One row per order. Use for revenue, order counts and customer questions. Revenue is net of refunds."
always_filter: { filters: [orders.created_date: "90 days"] }
access_filter: { field: users.region user_attribute: region }
join: users {
sql_on: ${orders.user_id} = ${users.id} ;;
relationship: many_to_one
}
}
view: orders {
dimension: id { primary_key: yes hidden: yes sql: ${TABLE}.id ;; }
dimension: user_id { hidden: yes sql: ${TABLE}.user_id ;; }
dimension_group: created {
type: time
timeframes: [date, week, month, quarter, year]
label: "Order"
description: "When the order was placed. 'Last month' means the previous calendar month."
sql: ${TABLE}.created_at ;;
}
dimension: status {
description: "Order status: complete, pending, cancelled, returned."
sql: ${TABLE}.status ;;
}
measure: count {
type: count
label: "Number of Orders"
description: "Count of orders. Synonyms: orders, order volume."
}
measure: total_revenue {
type: sum
sql: ${TABLE}.net_amount ;;
value_format_name: usd_0
label: "Revenue"
description: "Net revenue in USD after refunds. Synonyms: sales, income, net revenue."
}
}
The rules: hide keys and implementation fields; one measure per business concept with synonyms in the description; explicit definitions of ambiguous words ("last month"); always_filter so an open-ended question does not scan years; and access_filter so security lives in the model.
Step 2: create a data agent
pip install google-cloud-geminidataanalytics
gcloud auth application-default login
from google.cloud import geminidataanalytics as gda
PROJECT = "my-analytics-project"
LOCATION = "global"
AGENT_ID = "orders-agent"
agent_client = gda.DataAgentServiceClient()
# Looker datasource: the agent queries this explore as the user behind these credentials
looker_ref = gda.LookerExploreReference(
looker_instance_uri="https://looker.example.com",
lookml_model="ecommerce",
explore="orders",
)
credentials = gda.Credentials(
oauth=gda.OAuthCredentials(
secret=gda.OAuthCredentials.SecretBased(
client_id=LOOKER_CLIENT_ID, # API key of a dedicated, least-privilege Looker user
client_secret=LOOKER_CLIENT_SECRET,
)
)
)
datasources = gda.DatasourceReferences(
looker=gda.LookerExploreReferences(explore_references=[looker_ref], credentials=credentials)
)
context = gda.Context(
system_instruction=(
"You answer questions about e-commerce orders for the finance team. "
"Revenue is always net of refunds. When a question has no time range, use the last 90 days "
"and say so. If a question cannot be answered from the Orders explore, say that instead of guessing."
),
datasource_references=datasources,
)
agent = gda.DataAgent(
name=f"projects/{PROJECT}/locations/{LOCATION}/dataAgents/{AGENT_ID}",
data_analytics_agent=gda.DataAnalyticsAgent(published_context=context),
)
agent_client.create_data_agent(
request=gda.CreateDataAgentRequest(
parent=f"projects/{PROJECT}/locations/{LOCATION}",
data_agent_id=AGENT_ID,
data_agent=agent,
)
)
The class names follow the current client library; the API is still evolving, so check the Conversational Analytics API documentation for the version you install. Using a dedicated Looker user for the credentials means the agent can only see what that user can see; for per-end-user security, pass each end user's own OAuth token or map users to Looker user attributes in your application layer.
Step 3: ask questions
chat_client = gda.DataChatServiceClient()
def ask(question: str, conversation_id: str = "finance-demo"):
request = gda.ChatRequest(
parent=f"projects/{PROJECT}/locations/{LOCATION}",
messages=[gda.Message(user_message=gda.UserMessage(text=question))],
conversation_reference=gda.ConversationReference(
conversation=f"projects/{PROJECT}/locations/{LOCATION}/conversations/{conversation_id}",
data_agent_context=gda.DataAgentContext(
data_agent=f"projects/{PROJECT}/locations/{LOCATION}/dataAgents/{AGENT_ID}",
),
),
)
for reply in chat_client.chat(request=request):
msg = reply.system_message
if msg.text:
print("TEXT:", "".join(msg.text.parts))
if msg.data and msg.data.query:
print("QUERY:", msg.data.query) # the Looker query the agent built
if msg.data and msg.data.result:
print("ROWS:", msg.data.result)
if msg.chart and msg.chart.result:
print("CHART:", msg.chart.result) # Vega-Lite style spec you can render
ask("What was revenue by region last quarter compared with the quarter before?")
ask("Which of those regions had the most cancelled orders?")
Create the conversation first (create_conversation) if your version requires it; a conversation keeps context so follow-up questions resolve "those regions" correctly. Log the QUERY output: it is the Looker query the agent generated, expressed in model fields, and it is what you will compare against expected answers.
Step 4: evaluation
Do not ship on vibes. Build a question set with the business team: thirty to fifty real questions, each with the expected Looker query (fields and filters) and the expected number from a trusted dashboard. Then:
import json
with open("eval_questions.json") as f:
cases = json.load(f) # [{"question": ..., "expected_fields": [...], "expected_value": ...}]
passed = 0
for case in cases:
fields, value = run_and_capture(case["question"]) # wrap ask() to return the query fields and headline value
ok = set(fields) == set(case["expected_fields"]) and abs(value - case["expected_value"]) < 0.005 * abs(case["expected_value"])
passed += ok
if not ok:
print("FAIL", case["question"], fields, value)
print(f"{passed}/{len(cases)} passed")
Run it after every model change and after every library upgrade. Failures almost always point at the model: a missing synonym, an ambiguous label, an unhidden field that hijacked a question. Fix the LookML, not the prompt.
Guardrails
- Least privilege: the agent's Looker user gets
access_dataandsee_lookml_dashboardson one model set; nothing else. - Curated exposure: only explores prepared as in step 1. Keep the rest out of the agent's model set.
- Row-level security in the model, never in the system instruction.
- Always-filters to cap scan ranges; aggregate tables for the common rollups so AI-driven query bursts do not dominate your warehouse bill.
- Show the query. In your UI, display the fields and filters the agent used; users catch misinterpretations instantly when they can see them.
- Refusal is a feature. Instruct the agent to say when a question cannot be answered from the explore.
Building a chat-with-your-data feature on Looker and want the model curated and the evaluation harness in place before launch? That is the engagement described on our AI & Gemini in Looker page. Contact us.