Skip to content

RAK0152/looker-api2

Repository files navigation

Looker Conversational Extension

A React Looker extension that lets users chat with their dashboards and explores in natural language, with dynamic Vega-Lite charts rendered inline. Backed by Google Cloud's Conversational Analytics API (geminidataanalytics.googleapis.com). No LookML models or views are written by this project — only a minimal manifest.lkml is required to register the extension with Looker.


1. Deploy to Looker

The Looker Extension Framework loads exactly one JavaScript bundle from a LookML project. You have two paths: a dev loop that points Looker at a local Vite dev server (instant reload while you iterate), and a production deploy that ships the prebuilt bundle inside a LookML project.

1.1 Prerequisites

Requirement Notes
Looker instance with Extension Framework enabled Available on most modern Looker (Google Cloud Core) instances. Confirm under Admin → Labs.
Looker user with develop permission Needed to create the LookML project that hosts the extension manifest.
Google Cloud project with the Conversational Analytics API enabled Enable geminidataanalytics.googleapis.com in APIs & Services → Library.
Service account or user OAuth grant with roles/geminidataanalytics.dataAgentUser The Looker user attribute approach below uses the calling user's OAuth token; the service-account approach uses a Workload Identity binding.
Node 18+ and npm For building the bundle locally.

1.2 Configure Looker user attributes

The extension expects two pieces of runtime config that come from Looker user attributes (so different teams / users can target different GCP projects without rebuilding):

In Looker → Admin → User Attributes → Create user attribute:

Name Type Data type Default Notes
google_cloud_project_id String None your-gcp-project-id The GCP project that hosts the Conversational Analytics API.

If you skip this step, the extension falls back to VITE_GOOGLE_CLOUD_PROJECT_ID from your local .env — useful for development, but in production you should always use the user attribute so the value isn't baked into the bundle.

1.3 Build the production bundle

cd looker-api2
npm install --legacy-peer-deps
npm run build

This produces dist/bundle.js (~1.5 MB, ~480 KB gzipped) — a single self-contained file with React, the Looker SDKs, @looker/components, and Vega-Lite all inlined.

1.4 Create the LookML project that hosts the extension

  1. In Looker, click Develop → Manage LookML Projects → New LookML Project.

  2. Name it looker_conversational_extension. Pick Blank Project as the starting point.

  3. Switch to Development Mode (toggle in the top-right).

  4. Inside the new project, create a file called manifest.lkml and paste the contents of manifest.lkml from this repo.

  5. Switch the manifest from dev to prod mode by editing these two lines:

    # Comment out the dev URL:
    # url: "http://localhost:8080/src/index.tsx"
    # Uncomment the file reference:
    file: "bundle.js"
  6. Drag-and-drop dist/bundle.js into the LookML project's file tree. Looker stores it as a static file alongside the manifest.

  7. Save & Commit & Deploy to Production.

The extension now shows up in the Looker UI under Applications → Conversational Chat.

1.5 Dev loop (live reload)

While iterating locally:

cp .env.example .env       # set VITE_GOOGLE_CLOUD_PROJECT_ID for preview
npm run dev                # Vite dev server on http://localhost:8080

Keep the manifest in this mode:

url: "http://localhost:8080/src/index.tsx"
# file: "bundle.js"

Now every save triggers HMR inside the Looker iframe. Chrome may block the mixed-content load (Looker is HTTPS, Vite is HTTP) — when this happens, click the shield icon in the address bar and allow insecure content for the Looker tab.

1.6 Smoke test

  1. Open the extension from the Looker sidebar.
  2. The picker should list your dashboards and explores. If it doesn't, check the browser console — most failures are missing core_api_methods entitlements in the manifest.
  3. Pick a dashboard you trust (one whose query model/view are valid LookML — the API needs those even though you don't author them).
  4. Ask "Top 5 by revenue last month — show as a bar chart."
  5. You should see a streamed response with text, then a Vega-Lite chart rendered inline.

If the API call fails with a 403, the Looker host's egress to geminidataanalytics.googleapis.com isn't authenticated. See §4 Authentication below.


2. Architecture

┌─────────────────────────────────────────────────────────────┐
│ Looker (browser, HTTPS)                                     │
│                                                             │
│   ┌─────────────────────────────────────────────────────┐   │
│   │ Extension iframe (sandboxed)                        │   │
│   │                                                     │   │
│   │   index.tsx                                         │   │
│   │     └─ ExtensionProvider40                          │   │
│   │           └─ ComponentsProvider                     │   │
│   │                 └─ App                              │   │
│   │                     ├─ ContextPicker  ──── coreSDK ─┼───┼──► /api/4.0/dashboards
│   │                     │  (Looker SDK)                 │   │    /api/4.0/lookml_models
│   │                     │                               │   │
│   │                     └─ ChatView                     │   │
│   │                         ├─ MessageList              │   │
│   │                         ├─ MessageBubble            │   │
│   │                         │   ├─ VegaChart  (vega-lite)
│   │                         │   └─ DataTable           │   │
│   │                         └─ ChatInput               │   │
│   │                                                     │   │
│   └──────────────── extensionSDK.serverProxy ───────────┼───┼──► geminidataanalytics
│                                                         │   │    .googleapis.com/v1beta
│                                                         │   │    /…:chat
└─────────────────────────────────────────────────────────┘

2.1 Why these choices

  • Extension Framework, not a standalone web app. Looker signs requests to its own API on behalf of the calling user, which means no API keys live in our code or browser. The same serverProxy mechanism authenticates outbound calls to Google APIs.
  • Conversational Analytics API, not bring-your-own-LLM. This API understands Looker's semantic layer natively — it can resolve explores, generate SQL, run queries through Looker, and return Vega-Lite chart specs in one shot.
  • Vega-Lite, not Recharts. The API returns Vega-Lite specs as part of its response. Compiling Vega-Lite → Vega lets us render the API's output verbatim with no manual mapping.
  • @looker/components, not Tailwind. Matches Looker's host UI so the extension feels native inside the iframe.

2.2 Why no LookML models

Even though we're chatting with dashboards, this project never authors LookML models, views, or explores. The Conversational Analytics API references existing explores that already live in your Looker instance — we discover them at runtime via coreSDK.all_lookml_models() and coreSDK.dashboard(id). The only LookML file in this repo is manifest.lkml, which is not a data model — it's just a registration descriptor (project name, entitlements, URL of the bundle).


3. Code map

Path Role
manifest.lkml Registers the extension with Looker; declares allowed core_api_methods and external_api_urls.
index.html Vite entry HTML — Looker loads this in dev mode.
vite.config.ts Single-file bundle config + shimMissingExports workaround for @looker/design-tokens.
src/index.tsx Mounts ExtensionProvider40 and the Looker ComponentsProvider.
src/App.tsx Resolves runtime config from user attributes, then toggles between picker and chat.
src/types.ts ChatMessage, ChatPart, ExploreRef, DashboardRef.
src/services/userConfig.ts Reads google_cloud_project_id via extensionSDK.userAttributeGetItem. Falls back to Vite env for local dev.
src/services/lookerSdkService.ts fetchDashboards, fetchAllExplores, loadDashboardExploreRefs — all call coreSDK (Looker 4.0 SDK) via the Extension SDK proxy.
src/services/conversationalApi.ts Builds the :chat request and posts it through extensionSDK.serverProxy. Parses the response (JSON array or NDJSON stream) into ChatPart[].
src/components/ContextPicker.tsx Two tabs: Dashboard (pick one — its underlying explore refs are auto-resolved) and Explore (pick any explore directly).
src/components/ChatView.tsx Header, scrolling message list, input. Maintains chat history in component state.
src/components/MessageBubble.tsx Renders all message part kinds: text, chart, data, sql, error.
src/components/MessageList.tsx Auto-scrolls on new message; shows a spinner while waiting.
src/components/ChatInput.tsx Text input + Send button. Enter submits, Shift+Enter not handled (single-line input).
src/components/VegaChart.tsx Compiles Vega-Lite → Vega with react-vega. Adds a sensible default size if the spec omits one.
src/components/DataTable.tsx Renders raw row data when the API returns a data part (capped at 20 rows for UI).

3.1 Conversational API request shape

Built in src/services/conversationalApi.ts:

POST https://geminidataanalytics.googleapis.com/v1beta
     /projects/{PROJECT_ID}/locations/global:chat

{
  "parent": "projects/{PROJECT_ID}/locations/global",
  "messages": [
    { "userMessage": { "text": "Top 5 by revenue last month" } }
  ],
  "inlineContext": {
    "datasourceReferences": {
      "looker": {
        "exploreReferences": [
          { "lookerInstanceUri": "https://my-co.cloud.looker.com",
            "lookmlModel": "ecommerce",
            "explore":     "order_items" }
        ]
      }
    },
    "options": { "chart": { "image": { "noImage": {} } } }
  }
}

The response is a stream of systemMessage envelopes — text deltas, chart specs, data tables, SQL. We collect them all and turn each into a ChatPart.

3.2 Conversation history strategy

The current implementation re-sends prior user turns on each request (the API stitches assistant context internally via inlineContext). For long-running threads, switch to a persistent conversation ID (projects/.../conversations/{id}) — the API supports this but it's not wired up here.


4. Authentication

There are two viable patterns; pick one based on how your Looker host is hosted.

4.1 Looker (Google Cloud Core) → ADC

If your Looker instance is the Google Cloud Core SKU, the extension server can sign outbound calls with Application Default Credentials from the Looker host's service account. This is the path the manifest is set up for:

external_api_urls: [ "https://geminidataanalytics.googleapis.com" ]

Grant the Looker host's service account roles/geminidataanalytics.dataAgentUser on the GCP project, and serverProxy handles the bearer token automatically. No client-side credentials.

4.2 Self-hosted Looker → user OAuth

For self-hosted Looker, you need to either:

  • Add an OAuth grant in the manifest (oauth2_urls + scoped_user_attributes) and prompt each user once.
  • Or stand up a small backend that mints tokens (a thin Cloud Run service) and have the extension call it instead. The current code already routes everything through serverProxy so retargeting the URL is a one-line change.

5. Local development

npm install --legacy-peer-deps   # see §6 for why
cp .env.example .env             # set GCP project + Looker instance URI
npm run dev                      # http://localhost:8080

Without Looker, the app will display a config error (it can't resolve extensionSDK). To exercise just the Vega rendering or picker UI in isolation, run the bundle inside Looker's dev mode (§1.5).

5.1 Scripts

Script What it does
npm run dev Vite dev server with HMR on port 8080.
npm run build TypeScript check + Vite production build → dist/bundle.js.
npm run preview Serves the built bundle locally.

6. Known gotchas

Issue Fix
npm install fails with ERESOLVE … peer react@^17.0.2 Use npm install --legacy-peer-deps. The Looker SDK's peer range is stale; it works fine with React 18.
useHistory is not exported by react-router-dom at build time Pin react-router-dom@^5.3.4. Looker Extension SDK v24 still imports v5's API.
"Elevations" is not exported by … design-tokens Already handled — the Vite config sets rollupOptions.shimMissingExports: true to shim TypeScript-only re-exports.
Mixed-content warning when loading dev bundle Allow insecure content for the Looker tab in Chrome, or run Vite with HTTPS (vite --https) and trust the self-signed cert.
Dashboard picker shows zero explores The dashboard's underlying queries are SQL Runner / Looks rather than explore-based. Pick from the Explore tab instead.
403 from Conversational API The Looker host's service account is missing roles/geminidataanalytics.dataAgentUser on the GCP project.

7. What's not built

Deliberate omissions, so the repo stays focused on the picker + chat flow:

  • Conversation persistence. History is in-memory; a refresh wipes it. Add a per-user IndexedDB cache or hit the API's conversations resource if you need durability.
  • Multi-turn references (e.g., "now break that down by region"). The API supports follow-ups via persistent conversation IDs — wire that in if you need it.
  • Streaming UI. Responses are buffered and rendered when complete. To stream, replace serverProxy with a fetch against a Cloud Run proxy that supports SSE.
  • Drill-down into the chart. Vega-Lite click events aren't piped back into a follow-up query. react-vega's signalListeners is the integration point.
  • Per-explore allowlists. Anyone with extension access can ask anything about any explore the Looker user can see. If you need finer control, gate fetchAllExplores by a user-attribute allowlist.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors