Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f8fea5e
feat: add ggsql visualization tool
cpsievert Apr 8, 2026
d7a3223
refactor: remove developer-facing viz API from public surface
cpsievert Apr 8, 2026
9a87ee6
feat: viz polish, prompt best practices, and collapsed query param
cpsievert Apr 8, 2026
afebc15
refactor: simplify viz preload and trim brittle tests
cpsievert Apr 8, 2026
610ea48
fix(prompts): polish ggsql-syntax.md — LABEL wording, PLACE/DRAW dist…
cpsievert Apr 8, 2026
29e731b
refactor(viz): simplify chart feedback path
cpsievert Apr 9, 2026
ca6b75d
refactor(imports): hoist stdlib/hard-dep imports, keep only optional …
cpsievert Apr 9, 2026
fa6f966
refactor(prompts): restructure system and tool prompts for unified ca…
cpsievert Apr 9, 2026
7cd9f9e
fix(prompts): favor viz over redundant tables, collapse preparatory q…
cpsievert Apr 9, 2026
8c4a781
docs: add viz tool and collapsed query param to changelog
cpsievert Apr 9, 2026
127a066
fix: address Copilot PR review feedback
cpsievert Apr 9, 2026
014f279
chore: update github remotes
cpsievert Apr 16, 2026
8623756
merge: resolve conflicts with main (deferred client + ggsql viz)
cpsievert Apr 16, 2026
a8aeae4
fix: add stream_content stub to DummyProvider for chatlas 0.16.0 compat
cpsievert Apr 16, 2026
b253363
fix(viz): use deepcopy in fit_chart_to_container to avoid mutating input
cpsievert Apr 16, 2026
f51c718
fix(viz): use as_narwhals in to_polars to fix ibis source compatibility
cpsievert Apr 16, 2026
2e04dd2
fix(prompts): improve column casing guidance for Snowflake uppercase …
cpsievert Apr 16, 2026
372b8e6
fix(viz): lowercase DataFrame columns before DuckDB registration
cpsievert Apr 16, 2026
db17b55
Merge branch 'main' into feat/ggsql-integration
cpsievert Apr 17, 2026
abdaa9a
feat: add truncate_error for capping tool error messages
cpsievert Apr 18, 2026
878f42d
fix(prompts): update ggsql syntax guide and bump to v0.2.4
cpsievert Apr 18, 2026
b1ad65c
refactor: rename visualize_query tool to visualize
cpsievert Apr 20, 2026
20fa032
Update example to use shiny express
cpsievert Apr 20, 2026
b6ba1a8
Update pkg-py/src/querychat/static/js/viz-preload.js
cpsievert Apr 20, 2026
e5f35be
Wrap ggsql syntax reference in XML tag
cpsievert Apr 20, 2026
0bec143
fix(prompts): gate visual exploration mention on visualize tool presence
cpsievert Apr 20, 2026
eebd82a
fix(prompts): add trailing comma note to ggsql syntax important notes
cpsievert Apr 20, 2026
231e3ef
fix(prompts): restore when-to-use/when-not-to-use in tool descriptions
cpsievert Apr 20, 2026
d4b9d20
refactor(prompts): move viz tool-use instructions into tool description
cpsievert Apr 20, 2026
5fd68d2
fix(tests): update prompt tests for moved viz instructions
cpsievert Apr 20, 2026
2fc4303
fix(prompts): add missing ggsql syntax from skill reference
cpsievert Apr 20, 2026
a5b2372
Fix pkg-py/docs/tools.qmd link
cpsievert Apr 20, 2026
0f2314d
Fix typo
cpsievert Apr 20, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@ renv.lock
# Planning documents (local only)
docs/plans/

# Screenshot capture script (local only)
pkg-py/docs/_screenshots/

# Playwright MCP
.playwright-mcp/

Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,15 @@ make py-build
make py-docs
```

Before finishing your implementation or committing any code, you should run:
Before committing any Python code, you must run all three checks and confirm they pass:

```bash
uv run ruff check --fix pkg-py --config pyproject.toml
make py-check-types
make py-check-tests
```

To get help with making sure code adheres to project standards.
Do not commit or push until all three pass.

### R Package

Expand Down
10 changes: 6 additions & 4 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### New features

* Added a `"visualize"` tool that lets the LLM create inline Altair charts from natural language requests using [ggsql](https://github.com/posit-dev/ggsql) — a SQL extension for declarative data visualization. Include it via `tools=("query", "visualize")` (or alongside `"update"`). Charts render inline in the chat with fullscreen support, a "Show Query" toggle, and Save as PNG/SVG. Install the optional dependencies with `pip install querychat[viz]`. (#219)

* The `querychat_query` tool now accepts an optional `collapsed` parameter. When `collapsed=True`, the result card starts collapsed so preparatory or exploratory queries don't clutter the conversation. The LLM is guided to use this automatically when running queries before a visualization.

* Added support for Snowflake Semantic Views. When connected to Snowflake (via SQLAlchemy or Ibis), querychat automatically discovers available Semantic Views and includes their definitions in the system prompt. This helps the LLM generate correct queries using the `SEMANTIC_VIEW()` table function with certified business metrics and dimensions. (#200)

* `QueryChat()` now supports deferred chat client initialization. Pass `client=` to `server()` to provide a session-scoped chat client, enabling use cases where API credentials are only available at session time (e.g., Posit Connect managed OAuth tokens). When no `client` is specified anywhere, querychat resolves a sensible default from the `QUERYCHAT_CLIENT` environment variable (or `"openai"`). (#205)

### Improvements

* When a custom `prompt_template` is provided that doesn't contain Mustache references to `{{schema}}`, the expensive `get_schema()` call is now skipped entirely. This allows users with large databases to avoid slow startup by providing their own prompt that includes schema information inline (or omits it). (#208)

### New features

* Added support for Snowflake Semantic Views. When connected to Snowflake (via SQLAlchemy or Ibis), querychat automatically discovers available Semantic Views and includes their definitions in the system prompt. This helps the LLM generate correct queries using the `SEMANTIC_VIEW()` table function with certified business metrics and dimensions. (#200)

## [0.5.1] - 2026-01-23

### New features
Expand Down
3 changes: 3 additions & 0 deletions pkg-py/docs/_quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ website:
- models.qmd
- data-sources.qmd
- context.qmd
- visualize.qmd
- section: "Build custom apps"
contents:
- build-intro.qmd
Expand Down Expand Up @@ -114,6 +115,8 @@ quartodoc:
signature_name: short
- name: tools.tool_reset_dashboard
signature_name: short
- name: tools.tool_visualize
signature_name: short

filters:
- "interlinks"
Expand Down
8 changes: 8 additions & 0 deletions pkg-py/docs/build.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ from querychat.data import titanic
qc = QueryChat(titanic(), "titanic")
```

::: {.callout-tip}
### Visualization support

querychat supports an optional visualization tool that lets the LLM create inline charts.
Enable it by including `"visualize"` in the `tools` parameter.
See [Visualizations](visualize.qmd) for details.
:::

::: {.callout-note collapse="true"}

## Quick start with `.app()`
Expand Down
Binary file added pkg-py/docs/images/viz-bar-chart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pkg-py/docs/images/viz-fullscreen.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pkg-py/docs/images/viz-scatter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added pkg-py/docs/images/viz-show-query.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions pkg-py/docs/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ querychat can also handle more general questions about the data that require cal

![](/images/quickstart-summary.png){fig-alt="Screenshot of the querychat's app with a summary statistic inlined in the chat." class="lightbox shadow rounded mb-3"}

querychat can also create visualizations, powered by [ggsql](https://ggsql.org/) and [Altair](https://altair-viz.github.io/).
With the [visualization tool](visualize.qmd) enabled, ask for a chart and it appears inline in the conversation:

![](/images/viz-bar-chart.png){fig-alt="Screenshot of querychat with an inline bar chart showing survival rate by passenger class." class="lightbox shadow rounded mb-3"}

## Web frameworks

While the examples above use [Shiny](https://shiny.posit.co/py/), querychat also supports [Streamlit](https://streamlit.io/), [Gradio](https://gradio.app/), and [Dash](https://dash.plotly.com/). Each framework has its own `QueryChat` class under the relevant sub-module, but the methods and properties are mostly consistent across all of them.
Expand Down
37 changes: 36 additions & 1 deletion pkg-py/docs/tools.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ querychat combines [tool calling](https://posit-dev.github.io/chatlas/get-starte

One important thing to understand generally about querychat's tools is they are Python functions, and that execution happens on _your machine_, not on the LLM provider's side. In other words, the SQL queries generated by the LLM are executed locally in the Python process running the app.

querychat provides the LLM access two tool groups:
querychat provides the LLM access to three tool groups:

1. **Data updating** - Filter and sort data (without sending results to the LLM).
2. **Data analysis** - Calculate summaries and return results for interpretation by the LLM.
Expand Down Expand Up @@ -52,6 +52,40 @@ app = qc.app()
![](/images/quickstart-summary.png){fig-alt="Screenshot of the querychat's app with a summary statistic inlined in the chat." class="lightbox shadow rounded mb-3"}


## Data visualization

When a user asks for a chart or visualization, the LLM generates a [ggsql](https://ggsql.org/) query — standard SQL extended with a `VISUALISE` clause — and requests a call to the `visualize` tool.
This tool:

1. Executes the SQL portion of the query
2. Renders the `VISUALISE` clause as an Altair chart
3. Displays the chart inline in the chat

Unlike the data updating tools, visualization queries don't affect the dashboard filter.
They query the full dataset independently, and each call produces a new inline chart message in the chat.

The inline chart includes controls for fullscreen viewing, saving as PNG/SVG, and a "Show Query" toggle that reveals the underlying ggsql code.

To use the visualization tool, first install the `viz` extras:

```bash
pip install "querychat[viz]"
```

Then include `"visualize"` in the `tools` parameter (it is not enabled by default):

```{.python filename="viz-app.py"}
from querychat import QueryChat
from querychat.data import titanic

qc = QueryChat(titanic(), "titanic", tools=("query", "update", "visualize"))
app = qc.app()
```

![](/images/viz-scatter.png){fig-alt="Screenshot of querychat with an inline scatter plot." class="lightbox shadow rounded mb-3"}

See [Visualizations](visualize.qmd) for more details.

## View the source

If you'd like to better understand how the tools work and how the LLM is prompted to use them, check out the following resources:
Expand All @@ -65,3 +99,4 @@ If you'd like to better understand how the tools work and how the LLM is prompte
- [`prompts/tool-update-dashboard.md`](https://github.com/posit-dev/querychat/blob/main/pkg-py/src/querychat/prompts/tool-update-dashboard.md)
- [`prompts/tool-reset-dashboard.md`](https://github.com/posit-dev/querychat/blob/main/pkg-py/src/querychat/prompts/tool-reset-dashboard.md)
- [`prompts/tool-query.md`](https://github.com/posit-dev/querychat/blob/main/pkg-py/src/querychat/prompts/tool-query.md)
- [`prompts/tool-visualize.md`](https://github.com/posit-dev/querychat/blob/main/pkg-py/src/querychat/prompts/tool-visualize.md)
104 changes: 104 additions & 0 deletions pkg-py/docs/visualize.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: Visualizations
lightbox: true
---

querychat can create charts inline in the chat.
When you ask a question that benefits from a visualization, the LLM writes a query using [ggsql](https://ggsql.org/) — a SQL-like visualization grammar — and renders an [Altair](https://altair-viz.github.io/) chart directly in the conversation.

## Getting started

Visualization requires two steps:

1. **Install the `viz` extras:**

```bash
pip install "querychat[viz]"
```

2. **Include `"visualize"` in the `tools` parameter:**

```{.python filename="app.py"}
from querychat import QueryChat
from querychat.data import titanic

qc = QueryChat(titanic(), "titanic", tools=("query", "update", "visualize"))
app = qc.app()
Comment thread
cpsievert marked this conversation as resolved.
```

Ask something like "Show me survival rate by passenger class as a bar chart" and querychat will generate and display the chart inline:

![](/images/viz-bar-chart.png){fig-alt="Bar chart showing survival rate by passenger class." class="lightbox shadow rounded mb-3"}

## Choosing tools

The `tools` parameter controls which capabilities the LLM has access to.
By default, only `"query"` and `"update"` are enabled — visualization must be opted into explicitly.

To enable only query and visualization (no dashboard filtering):

```{.python}
qc = QueryChat(titanic(), "titanic", tools=("query", "visualize"))
```
Comment thread
cpsievert marked this conversation as resolved.

See [Tools](tools.qmd) for a full reference on available tools and what each one does.

## Custom apps

The example below shows a minimal custom Shiny app using only the `"query"` and `"visualize"` tools.
It omits `"update"` to focus entirely on data analysis and visualization rather than dashboard filtering:

```{.python filename="app.py"}
{{< include /../examples/10-viz-app.py >}}
```

## What you can ask for

querychat can generate a wide range of chart types.
Some example prompts:

- "Show me a bar chart of survival rate by passenger class"
- "Scatter plot of age vs fare, colored by survival"
- "Line chart of average fare over time"
- "Histogram of passenger ages"
- "Facet survival rate by class and sex"

The LLM chooses an appropriate chart type based on your question, but you can always be specific.
If you ask for a bar chart, you'll get a bar chart.

![](/images/viz-scatter.png){fig-alt="Scatter plot of age vs fare colored by survival status." class="lightbox shadow rounded mb-3"}

::: {.callout-tip}
If you don't like the chart, ask the LLM to adjust it — for example, "make the dots bigger" or "use a log scale on the y-axis".
:::

## Chart controls

Each chart has controls in its footer:

**Fullscreen** — Click the expand icon to view the chart in fullscreen mode.

![](/images/viz-fullscreen.png){fig-alt="A chart displayed in fullscreen mode." class="lightbox shadow rounded mb-3"}

**Save** — Download the chart as a PNG or SVG file.

**Show Query** — Expand the footer to see the ggsql query used to generate the chart.

![](/images/viz-show-query.png){fig-alt="A chart with the Show Query footer expanded, showing the ggsql query." class="lightbox shadow rounded mb-3"}

## How it works

1. **The LLM generates a ggsql query** — a SQL-like grammar that describes both data transformation and visual encoding in a single statement.
2. **The SQL is executed** — querychat runs the data portion of the query against your data source locally.
3. **The VISUALISE clause is rendered** — the result is passed to Altair, which produces a Vega-Lite chart specification.
4. **The chart appears inline** — the chart is streamed back into the conversation as an interactive widget.

Note that visualization queries are independent of any active dashboard filter set by the `update` tool.
They always run against the full dataset.

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

## See also

- [Tools](tools.qmd) — Understand what querychat can do under the hood
- [Provide context](context.qmd) — Help the LLM understand your data better
17 changes: 17 additions & 0 deletions pkg-py/examples/10-viz-app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from querychat.express import QueryChat
from querychat.data import titanic

from shiny.express import ui, app_opts

# Omits "update" tool — this demo focuses on query + visualization only
qc = QueryChat(
titanic(),
"titanic",
tools=("query", "visualize")
)

qc.ui()

ui.page_opts(fillable=True, title="QueryChat Visualization Demo")

app_opts(bookmark_store="url")
22 changes: 19 additions & 3 deletions pkg-py/src/querychat/_icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,35 @@

from shiny import ui

ICON_NAMES = Literal["arrow-counterclockwise", "funnel-fill", "terminal-fill", "table"]
ICON_NAMES = Literal[
"arrow-counterclockwise",
"bar-chart-fill",
"chevron-down",
"download",
"funnel-fill",
"graph-up",
"terminal-fill",
"table",
]


def bs_icon(name: ICON_NAMES) -> ui.HTML:
def bs_icon(name: ICON_NAMES, cls: str = "") -> ui.HTML:
"""Get Bootstrap icon SVG by name."""
if name not in BS_ICONS:
raise ValueError(f"Unknown Bootstrap icon: {name}")
return ui.HTML(BS_ICONS[name])
svg = BS_ICONS[name]
if cls:
svg = svg.replace('class="', f'class="{cls} ', 1)
return ui.HTML(svg)
Comment thread
cpsievert marked this conversation as resolved.


BS_ICONS = {
"arrow-counterclockwise": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-arrow-counterclockwise" style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img"><path fill-rule="evenodd" d="M8 3a5 5 0 1 1-4.546 2.914.5.5 0 0 0-.908-.417A6 6 0 1 0 8 2v1z"></path><path d="M8 4.466V.534a.25.25 0 0 0-.41-.192L5.23 2.308a.25.25 0 0 0 0 .384l2.36 1.966A.25.25 0 0 0 8 4.466z"></path></svg>',
"bar-chart-fill": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-bar-chart-fill" style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img"><path d="M1 11a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1zm5-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1zm5-5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1z"/></svg>',
"chevron-down": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-chevron-down" style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img"><path fill-rule="evenodd" d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708"/></svg>',
"download": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-download" style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z"/></svg>',
"funnel-fill": '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel-fill" viewBox="0 0 16 16"><path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5z"/></svg>',
"graph-up": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-graph-up" style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img"><path fill-rule="evenodd" d="M0 0h1v15h15v1H0zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07"/></svg>',
"terminal-fill": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-terminal-fill " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M0 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3zm9.5 5.5h-3a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1zm-6.354-.354a.5.5 0 1 0 .708.708l2-2a.5.5 0 0 0 0-.708l-2-2a.5.5 0 1 0-.708.708L4.793 6.5 3.146 8.146z"></path></svg>',
"table": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="bi bi-table " style="height:1em;width:1em;fill:currentColor;vertical-align:-0.125em;" aria-hidden="true" role="img" ><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2zm15 2h-4v3h4V4zm0 4h-4v3h4V8zm0 4h-4v3h3a1 1 0 0 0 1-1v-2zm-5 3v-3H6v3h4zm-5 0v-3H1v2a1 1 0 0 0 1 1h3zm-4-4h4V8H1v3zm0-4h4V4H1v3zm5-3v3h4V4H6zm4 4H6v3h4V8z"></path></svg>',
}
Loading
Loading