Skip to content

feat(py): opt-in "maps" capability for visualizations - #323

Draft
cpsievert wants to merge 13 commits into
mainfrom
feat/maps-capability-py
Draft

cpsievert wants to merge 13 commits into
mainfrom
feat/maps-capability-py

Conversation

@cpsievert

@cpsievert cpsievert commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Merge blocker: requires an unpublished ggsql fix (spatial validate). Merge only after a ggsql release containing it is published and the ggsql>= pin is bumped.

Adds an opt-in "maps" capability to querychat for Python, letting the LLM draw choropleths and point maps through ggsql's spatial support (spec in local docs/superpowers/specs/2026-10-12-maps-capability-design.md).

Stacked on #321 — the ggsql:world join pattern requires full-query execution through ggsql.CachingReader. The diff against ggsql-caching-reader additionally shows a few main commits because #321's branch is behind main; it will clean up when #321 merges and this retargets.

How it works:

  • Opt-in: tools=("default", "maps"). "default" is a new sentinel that expands to the default tool set (also fixes the existing "must restate every tool to add one" problem); "maps" is a capability modifier that validation requires alongside "visualize".
  • Runtime: when the viz reader is first created, the tool pre-warms the DuckDB icu extension (all sessions — DuckDB's CLI/Python client load it by default, so LLM-written SQL assumes it) and, for maps-enabled clients, the spatial extension (INSTALL/LOAD via ggsql setup statements, plus a smoke query). Spatial failure (e.g. no network for the one-time download) warns and marks maps unavailable for the session; ICU failure warns and continues.
  • Prompt: a new ggsql-spatial.md partial is included in the system prompt only when maps is enabled. It teaches the ggsql:world choropleth-join pattern (verified to execute ggsql-side, so it works against any backend), lon/lat point maps, named projections, and guardrails (no INSTALL/LOAD from the LLM; ST_* functions are allowed only on the ggsql compute connection, not in data queries).
  • Maps-disabled guidance: when visualize is on but maps is not, the system prompt tells the LLM that maps exist, forbids spatial syntax, and gives it the tools=("default", "maps") enablement message plus a lon/lat-scatter fallback.
  • Error UX: after a failed spatial pre-warm, ggsql's extension-not-found (or st_* catalog) error is translated into actionable "setup failed, suggest a non-map chart instead" guidance instead of a raw error.

Known limitation: bookmark-restored viz widgets use a separate reader that is not pre-warmed, so restored spatial charts may fail on restore (they skip with the existing restore warning). Wiring maps into the restore path can follow later.

Verification

  • All new behavior is TDD-covered: tools normalization (sentinel expansion, maps-requires-visualize, deps-missing drop), prompt rendering with/without maps (spatial partial vs. maps-disabled note), ICU/spatial pre-warm success/failure, error translation for the setup-failed state, and wiring (pre-warms run once per client).
  • Full suite: 917 passed, 33 skipped.
  • Every spatial claim in the prompt partial was verified against ggsql 0.5.2 (ggsql:world schema, cross-source join staging, extension auto-load behavior).

Pass tools=("default", "maps") to let the LLM draw choropleths (via the
builtin ggsql:world dataset) and point maps through ggsql's spatial
support:

- New ggsql-spatial prompt partial, rendered into the system prompt when
  maps are enabled.
- The "default" tools entry expands to the default tool set, so extra
  capabilities no longer require restating every tool.
- The DuckDB spatial extension is pre-warmed when the viz reader is
  first created with maps enabled; if setup fails, the session
  continues without maps.
- Spatial-extension errors are translated into actionable guidance for
  the LLM (do not retry with DRAW spatial / ggsql:world; suggest a
  non-map visualization instead) when maps are disabled or setup
  failed.
prewarm_spatial routed INSTALL/LOAD spatial through execute(), which
targets the user's primary data source. Those connections are typically
locked down (filesystem access disabled), so the install failed and the
"maps" capability was silently disabled — and even where it succeeded it
would take no effect on the connection that matters.

Spatial SQL actually executes on the CachingReader's private in-memory
cache connection (the compute surface), so install/load the extension
there via the new ggsql.CachingReader.execute_sql_cached binding
(posit-dev/ggsql-python#14) and keep the smoke query on execute(), where
it exercises the full spatial path: extension load, builtin ggsql:world
registration, spatial layer execution.

Without a ggsql build that has execute_sql_cached, prewarm fails with a
warning and maps stay unavailable, so this degrades gracefully.
… app

The LLM kept putting lon/lat/size in global VISUALISE mappings, which
every layer inherits — and the spatial background layer only accepts
geometry/fill/stroke/opacity, so the query failed with "Layer 'spatial'
does not support the `lat`, `size`, `lon` mappings". The prompt now
states explicitly that point aesthetics belong on DRAW point MAPPING.

Also adds examples/13-maps-app.py: a Shiny Express demo with two live
datasets (USGS real-time earthquakes, World Bank GDP) exercising the
point map, choropleth, and orthographic globe patterns.

Note: the globe suggestion currently triggers a suspected OOM in
ggsql's orthographic projection of ggsql:world (under investigation);
it is kept here as a repro.
cpsievert added a commit to posit-dev/ggsql-python that referenced this pull request Oct 14, 2026
* feat: add execute_sql_cached binding to CachingReader

Exposes the Rust CachingReader's compute surface (execute_sql_cached,
present since ggsql 0.5.2) to Python.

Setup statements that ggsql's derived SQL depends on must run on the
private cache connection where that SQL executes — e.g. INSTALL/LOAD of
DuckDB extensions such as spatial. Routing them through execute_sql
sends them to the primary instead, where locked-down connections reject
them and they take no effect on the cache.

This is needed by querychat's opt-in "maps" capability to prewarm the
DuckDB spatial extension on the compute surface
(posit-dev/querychat#323).

* docs: changelog entry for CachingReader.execute_sql_cached

* docs: reorder changelog entries
ggsql's bundled DuckDB starts extension-free, while the DuckDB CLI and
Python client load ICU by default — so SQL that works everywhere else
(e.g. `TIMESTAMPTZ - INTERVAL`, the standard way to write temporal
filters) failed on the viz engine with a binder error. Seen in the wild:
an LLM-generated earthquake map query failed repeatedly on
`-(TIMESTAMP WITH TIME ZONE, INTERVAL)` before recovering.

prewarm_spatial (now prewarm_compute_surface, since it is no longer
spatial-only) also installs/loads ICU on the CachingReader's cache
connection, alongside spatial. Cost: a one-time ~15 MB download per
machine per DuckDB version, then ~60 ms per startup. Unlike spatial,
ICU failure is non-fatal (maps don't need it): warn and continue.
A previous commit on this branch made viz_dep() use a dep_version()
helper whose definition was never committed, so importing
querychat._viz_tools raised ImportError and the Python CI job on #323
failed at collection time. Restore the direct __version__ usage.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant