Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a068ee8
Add standalone multi-table querychat demo (fine-jewelry retailer)
cpsievert Sep 22, 2026
64a2cf1
Enable visualization tool in multi-table standalone demo
cpsievert Sep 22, 2026
ac4041a
Switch multi-table standalone demo from pandas to polars
cpsievert Sep 22, 2026
40f328e
Work around a ggsql date-coercion bug in the multi-table standalone demo
cpsievert Sep 22, 2026
74878bd
test: layer-level sources succeed; shared source reads (failing)
cpsievert Oct 12, 2026
bc0b804
feat: route ggsql viz execution through ggsql.CachingReader
cpsievert Oct 12, 2026
6cc5ae4
test: pass raw query strings to execute_ggsql
cpsievert Oct 12, 2026
314a80c
deps: require ggsql>=0.5.2 for viz
cpsievert Oct 12, 2026
bb758ac
refactor: own the CachingReader in the viz tool closure
cpsievert Oct 12, 2026
544530c
fix: restore viz widgets through a CachingReader
cpsievert Oct 12, 2026
4eac7eb
docs: viz cache behavior, staleness caveat, and GGSQL_CACHE_* env vars
cpsievert Oct 12, 2026
5d78889
r: Increment dev version 0.4.0.9000
gadenbuie Sep 29, 2026
36b6d0c
fix(pkg-r): skip tests that assume Suggests packages are installed (#…
gadenbuie Sep 29, 2026
6ccec6b
r/v0.4.1 (#319)
gadenbuie Sep 30, 2026
b6bd3db
r: bump dev version to v0.4.1.9000
gadenbuie Sep 30, 2026
c3f24b9
feat(prompts): add ggsql-spatial partial for maps capability
cpsievert Oct 12, 2026
f4f8779
feat(py): add default sentinel and maps modifier to tools
cpsievert Oct 12, 2026
a4bb7f7
feat(py): render ggsql-spatial partial when maps enabled
cpsievert Oct 12, 2026
d06507c
docs: maps capability
cpsievert Oct 12, 2026
1435441
feat(py): pre-warm spatial extension when maps enabled
cpsievert Oct 12, 2026
84d21d4
feat(py): translate spatial errors when maps disabled
cpsievert Oct 12, 2026
b89789d
`devtools::document()` (GitHub Actions)
cpsievert Oct 12, 2026
8adcb06
fix(py): widen create_model field spec annotations for pyright
cpsievert Oct 12, 2026
de7d660
fix(py): ruff lint cleanups (test style, drop stale noqa from #321)
cpsievert Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ README.html
test-results/
shiny_bookmarks/
python-package/examples/titanic.db
pkg-py/examples/13-multi-table-standalone/data/
.quarto
*.db

Expand Down
6 changes: 6 additions & 0 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- New opt-in `"maps"` capability: pass `tools=("default", "maps")` to let the LLM draw choropleths (via the builtin `ggsql:world` dataset) and point maps through ggsql's spatial support. The `"default"` tools entry expands to the default tool set, so extra capabilities no longer require restating every tool.

## [0.9.0] - 2026-09-13

### New features
Expand Down
30 changes: 30 additions & 0 deletions pkg-py/docs/visualize.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,38 @@ Each chart has controls in its footer:
Note that visualization queries are independent of any active dashboard filter set by the `update` tool.
They always run against the full dataset.

### Caching

To keep iterative chart refinement fast, querychat memoizes the data portion of visualization queries: when the LLM issues identical SQL within 5 minutes (for example, when only the visual encoding changes), the cached result is reused instead of re-reading your data source.
Each chat session has its own cache, bounded at 512 MiB, which is freed when the session ends.

This assumes the data doesn't change mid-session.
If it can — for example, update queries are enabled (`QUERYCHAT_ENABLE_UPDATE_QUERIES=true`), or another process writes to the database — charts may lag those changes by up to the TTL below.

Tune or disable the cache with environment variables (set before the app starts):

| Variable | Default | Purpose |
|---|---|---|
| `GGSQL_CACHE_TTL` | `300` | Seconds a cached result is treated as fresh |
| `GGSQL_CACHE_MAX_BYTES` | `512mb` | Per-session memory budget before least-recently-used eviction |
| `GGSQL_CACHE_DISABLED` | unset | Set to any non-empty value except `0` to always re-read the source |

Learn more about the ggsql grammar at [ggsql.org](https://ggsql.org/).

## Maps

querychat can draw choropleths and point maps through ggsql's spatial support.
Maps are opt-in because enabling them downloads the DuckDB `spatial` extension (a one-time, cached download) the first time they are set up:

```{.python}
qc = QueryChat(titanic(), "titanic", tools=("default", "maps"))
```

The `"default"` entry expands to the default tool set, so you don't have to restate every tool to add maps.

With maps enabled, the LLM can answer geographic questions: "map revenue by country" joins your data with the builtin `ggsql:world` country boundaries (on ISO codes or country names), and "map our store locations" draws lon/lat points over a world outline.
If a map query is attempted while maps are not enabled, the tool explains how to enable them.

## See also

- [Tools](tools.qmd) — Understand what querychat can do under the hood
Expand Down
90 changes: 90 additions & 0 deletions pkg-py/examples/13-multi-table-standalone/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# /// script
# dependencies = [
# "querychat[polars,viz]",
# "anthropic",
# ]
# ///
"""Multi-table querychat demo: a synthetic fine-jewelry retailer.

Five related tables -- diamonds, settings, customers, orders, and
order_items -- are registered with a single QueryChat instance so the LLM
can query any one of them or write joins across them (e.g. an "Engagement
Ring" order item joins to both its diamond and its setting).

Data is generated once and cached to ./data/*.parquet; later runs read the
cache instead of regenerating (see generate_data.py).

Usage:
uv run app.py

Requires ANTHROPIC_API_KEY to be set (change client= below to use a
different provider).
"""

from pathlib import Path

from generate_data import load_tables
from querychat import QueryChat

GREETING = """\
## Brilliant Retail: Jewelry Explorer 💎

Welcome! You're chatting with a synthetic fine-jewelry retailer's data --
**diamonds**, **settings** (ring mountings), **customers**, **orders**, and
**order_items**, all linked together.

**Get started**

<ul>
<li><span class="suggestion">Show me a few example rows from each table</span></li>
<li><span class="suggestion">What's the average price of a round diamond over 1 carat?</span></li>
</ul>

**Explore across tables**

<ul>
<li><span class="suggestion">Which setting style is most often paired with princess-cut diamonds in engagement rings?</span></li>
<li><span class="suggestion">What's the average order value by acquisition channel?</span></li>
<li><span class="suggestion">Show total revenue by month over the last year</span></li>
</ul>
"""

DATA_DESCRIPTION = """\
Brilliant Retail is an online fine-jewelry retailer. This dataset covers its
diamond and setting catalog alongside customer order history.
"""

# Works around a ggsql bug (https://github.com/posit-dev/ggsql/issues/<TBD>):
# a date/timestamp column produced by an expression like DATE_TRUNC(...) fails
# ggsql's `SCALE ... VIA date` coercion unless it's explicitly cast to DATE.
EXTRA_INSTRUCTIONS = """\
When a `querychat_visualize` query buckets a date or timestamp column (e.g.
via DATE_TRUNC), always CAST the resulting expression to DATE in the SQL, for
example:

SELECT CAST(DATE_TRUNC('month', order_date) AS DATE) AS month, ...

A column left as a raw TIMESTAMP will fail when referenced by
`SCALE ... VIA date` with a "Cannot coerce column ... to date" error.
"""

tables = load_tables(Path(__file__).parent / "data")

qc = QueryChat(
tables["diamonds"],
"diamonds",
greeting=GREETING,
data_description=DATA_DESCRIPTION,
data_dict=Path(__file__).parent / "data-dict.yaml",
extra_instructions=EXTRA_INSTRUCTIONS,
client="anthropic",
)
qc.add_table(tables["settings"], "settings")
qc.add_table(tables["customers"], "customers")
qc.add_table(tables["orders"], "orders")
qc.add_table(tables["order_items"], "order_items")

app = qc.app()

if __name__ == "__main__":
app.run()
178 changes: 178 additions & 0 deletions pkg-py/examples/13-multi-table-standalone/data-dict.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
version: "0.1.0"

tables:
diamonds:
description: >
Loose diamond inventory. One row per diamond, graded on the 4 Cs
(carat, cut, color, clarity) and certified by an independent lab.
columns:
- name: id
type: number(id)
constraints: [primary_key]
description: Unique diamond identifier.
- name: shape
type: enum
values: [Round, Princess, Cushion, Oval, Emerald, Radiant, Asscher, Marquise, Pear, Heart]
description: Cut shape of the diamond.
- name: carat
type: number(quantity)
units: carat
description: Weight of the diamond.
- name: cut
type: enum
values: [Super Ideal, Ideal, Very Good, Good]
description: Cut quality grade -- how well the diamond's facets interact with light.
- name: color
type: enum
values: [D, E, F, G, H, I, J]
description: Color grade on the standard D (colorless) to J (faint color) scale.
- name: clarity
type: enum
values: [FL, IF, VVS1, VVS2, VS1, VS2, SI1, SI2]
description: >
Clarity grade, from FL (flawless) to SI2 (slight inclusions visible
under magnification).
- name: certified_by
type: enum
values: [GIA, IGI, AGS]
description: Independent grading lab that certified the diamond.
- name: price
type: number(currency)
units: USD
description: Price of the loose diamond.

settings:
description: Ring mountings that a diamond can be set into. One row per setting.
columns:
- name: id
type: number(id)
constraints: [primary_key]
description: Unique setting identifier.
- name: style
type: enum
values: [Solitaire, Halo, Pave, Three-Stone, Vintage, Bezel]
description: Setting style.
- name: metal
type: enum
values: [Platinum, 18K White Gold, 18K Yellow Gold, 18K Rose Gold, Palladium]
description: Metal the setting is made of.
- name: band_width_mm
type: number(quantity)
units: mm
description: Width of the ring band.
- name: base_price
type: number(currency)
units: USD
description: Price of the setting alone, without a diamond.

customers:
description: One row per customer.
columns:
- name: id
type: number(id)
constraints: [primary_key]
description: Unique customer identifier.
- name: name
type: string
description: Customer's full name.
- name: state
type: string
description: US state abbreviation of the customer's shipping address.
- name: join_date
type: date
description: Date the customer created their account.
- name: acquisition_channel
type: enum
values: [Organic Search, Paid Social, Referral, Email, Direct, Showroom Walk-in]
description: How the customer first found the retailer.

orders:
description: One row per order.
columns:
- name: id
type: number(id)
constraints: [primary_key]
description: Unique order identifier.
- name: customer_id
type: number(id)
constraints: [foreign_key]
description: Customer who placed the order. Joins to customers.id.
- name: order_date
type: date
description: Date the order was placed.
- name: status
type: enum
values: [Delivered, Shipped, Processing, Returned]
description: Current fulfillment status of the order.
- name: channel
type: enum
values: [Online, Showroom]
description: Where the order was placed.

order_items:
description: >
Line items within an order. One row per item purchased. An item's
diamond_id and setting_id are populated according to its item_type:
"Engagement Ring" has both, "Loose Diamond" has only diamond_id,
"Wedding Band" has only setting_id, and "Earrings"/"Necklace" have
neither -- their price is not decomposed into components.
columns:
- name: id
type: number(id)
constraints: [primary_key]
description: Unique order item identifier.
- name: order_id
type: number(id)
constraints: [foreign_key]
description: Order this item belongs to. Joins to orders.id.
- name: item_type
type: enum
values: [Engagement Ring, Loose Diamond, Wedding Band, Earrings, Necklace]
description: Kind of item purchased.
- name: diamond_id
type: number(id)
constraints: [foreign_key]
description: >
Diamond included in this item, if any. Joins to diamonds.id. Null
unless item_type is "Engagement Ring" or "Loose Diamond".
- name: setting_id
type: number(id)
constraints: [foreign_key]
description: >
Setting included in this item, if any. Joins to settings.id. Null
unless item_type is "Engagement Ring" or "Wedding Band".
- name: price
type: number(currency)
units: USD
description: Price of this line item.

relationships:
- description: Each order belongs to one customer.
cardinality: many-to-one
join: orders.customer_id = customers.id
- description: Each order has one or more line items.
cardinality: one-to-many
join: orders.id = order_items.order_id
- description: An order item may include one diamond (Engagement Ring or Loose Diamond).
cardinality: many-to-one
join: order_items.diamond_id = diamonds.id
- description: An order item may include one setting (Engagement Ring or Wedding Band).
cardinality: many-to-one
join: order_items.setting_id = settings.id

glossary:
4 Cs: The standard diamond grading criteria -- carat (weight), cut, color, and clarity.
carat: Unit of diamond weight; 1 carat = 200 milligrams. Larger carat generally means higher price.
cut: >
How well a diamond's facets interact with light, independent of its
shape. Graded Super Ideal > Ideal > Very Good > Good in this dataset.
color: >
Diamond color grade from D (colorless, most valuable) to J (faint color,
least valuable) in this dataset.
clarity: >
Presence of internal/external flaws, from FL (flawless) down to SI2
(inclusions visible under 10x magnification).
setting: >
The metal mounting that holds a diamond, e.g. a ring band. Sold
separately as-is, or combined with a diamond into an Engagement Ring
order item.
Loading
Loading