Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
11 changes: 6 additions & 5 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Improvements

* The `tools` parameter now uses `"filter"` as the preferred name (instead of `"update"`) for the dashboard-filtering tool group. The default is now `("filter", "query")`. The legacy name `"update"` is still accepted everywhere. (#222)
* 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)
Comment thread
cpsievert marked this conversation as resolved.

### 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)
* 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 `"filter"`; `"update"` remains a legacy alias). 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)

* `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)

* 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)

* 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.

### 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)

## [0.5.1] - 2026-01-23

### New features
Expand Down
12 changes: 6 additions & 6 deletions pkg-py/src/querychat/_dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def __init__(
*,
greeting: Optional[str | PathType] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | PathType] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | PathType] = None,
Expand All @@ -113,7 +113,7 @@ def __init__(
*,
greeting: Optional[str | PathType] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | PathType] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | PathType] = None,
Expand All @@ -129,7 +129,7 @@ def __init__(
*,
greeting: Optional[str | PathType] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | PathType] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | PathType] = None,
Expand All @@ -145,7 +145,7 @@ def __init__(
*,
greeting: Optional[str | PathType] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | PathType] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | PathType] = None,
Expand All @@ -161,7 +161,7 @@ def __init__(
*,
greeting: Optional[str | PathType] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | PathType] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | PathType] = None,
Expand All @@ -176,7 +176,7 @@ def __init__(
*,
greeting: Optional[str | PathType] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | PathType] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | PathType] = None,
Expand Down
12 changes: 6 additions & 6 deletions pkg-py/src/querychat/_gradio.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -106,7 +106,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -121,7 +121,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -136,7 +136,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -151,7 +151,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -165,7 +165,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand Down
31 changes: 16 additions & 15 deletions pkg-py/src/querychat/_querychat_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@

from ._viz_tools import VisualizeData

TOOL_GROUPS = Literal["update", "query", "visualize"]
DEFAULT_TOOLS: tuple[TOOL_GROUPS, ...] = ("update", "query")
TOOL_GROUPS = Literal["filter", "update", "query", "visualize"]
DEFAULT_TOOLS: tuple[TOOL_GROUPS, ...] = ("filter", "query")
Comment thread
cpsievert marked this conversation as resolved.

class QueryChatBase(Generic[IntoFrameT]):
"""
Expand Down Expand Up @@ -177,8 +177,9 @@ def client(
Parameters
----------
tools
Which tools to include: `"update"`, `"query"`, `"visualize"`,
or a combination.
Which tools to include: `"filter"`, `"query"`, `"visualize"`,
or a combination. The legacy name `"update"` is still accepted
as an alias for `"filter"`.
update_dashboard
Callback when update_dashboard tool succeeds.
reset_dashboard
Expand Down Expand Up @@ -306,25 +307,25 @@ def create_client(client: str | chatlas.Chat | None) -> chatlas.Chat:

def normalize_tools(
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None | MISSING_TYPE,
default: tuple[TOOL_GROUPS, ...] | None,
default: tuple[TOOL_GROUPS, ...] | set[str] | None,
*,
check_deps: bool = True,
) -> tuple[TOOL_GROUPS, ...] | None:
) -> set[str] | None:
Comment thread
cpsievert marked this conversation as resolved.
if tools is None or tools == ():
result = None
resolved = None
elif isinstance(tools, MISSING_TYPE):
result = default
resolved = set(default) if default is not None else None
elif isinstance(tools, str):
result = (tools,)
elif isinstance(tools, tuple):
result = tools
resolved = {tools}
else:
result = tuple(tools)
resolved = set(tools)
Comment thread
cpsievert marked this conversation as resolved.
if resolved is not None:
resolved = {"update" if t == "filter" else t for t in resolved}
if not check_deps:
return result
if has_viz_tool(result) and not has_viz_deps():
return resolved
if has_viz_tool(resolved) and not has_viz_deps():
raise ImportError(
"Visualization tools require ggsql, altair, shinywidgets, and "
"vl-convert-python. Install them with: pip install querychat[viz]"
)
return result
return resolved
19 changes: 11 additions & 8 deletions pkg-py/src/querychat/_shiny.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class QueryChat(QueryChatBase[IntoFrameT]):
**Privacy-focused mode:** Only allow dashboard filtering, ensuring the LLM
can't see any raw data.
```python
qc = QueryChat(df, "my_data", tools="update")
qc = QueryChat(df, "my_data", tools="filter")
qc.app()
```

Expand Down Expand Up @@ -97,15 +97,18 @@ class QueryChat(QueryChatBase[IntoFrameT]):
defaults to `"openai"`.
tools
Which querychat tools to include in the chat client by default. Can be:
- A single tool string: `"update"` or `"query"`
- A tuple of tools: `("update", "query", "visualize")`
- A single tool string: `"filter"` or `"query"`
- A tuple of tools: `("filter", "query", "visualize")`
- `None` or `()` to disable all tools

Default is `("update", "query")`. The visualization tool (`"visualize"`)
Default is `("filter", "query")`. The visualization tool (`"visualize"`)
can be opted into by including it in the tuple.

Set to `"update"` to prevent the LLM from accessing data values, only
allowing dashboard filtering without answering questions.
Pass only `"filter"` to restrict the LLM to dashboard filtering,
omitting both the `"query"` and `"visualize"` tools so the LLM
cannot access or display any raw data values.

The legacy name `"update"` is still accepted as an alias for `"filter"`.

The tools can be overridden per-client by passing a different `tools`
parameter to the `.client()` method.
Expand Down Expand Up @@ -142,7 +145,7 @@ def __init__(
id: Optional[str] = None,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand Down Expand Up @@ -620,7 +623,7 @@ def __init__(
id: Optional[str] = None,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand Down
3 changes: 1 addition & 2 deletions pkg-py/src/querychat/_shiny_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from shiny import Inputs, Outputs, Session

from ._datasource import DataSource
from ._querychat_base import TOOL_GROUPS
from ._viz_tools import VisualizeData
from .types import UpdateDashboardData

Expand Down Expand Up @@ -123,7 +122,7 @@ def mod_server(
greeting: str | None,
client: Callable[..., chatlas.Chat],
enable_bookmarking: bool,
tools: tuple[TOOL_GROUPS, ...] | None = None,
tools: set[str] | None = None,
) -> ServerValues[IntoFrameT]:
# Reactive values to store state
sql = ReactiveStringOrNone(None)
Expand Down
12 changes: 6 additions & 6 deletions pkg-py/src/querychat/_streamlit.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -81,7 +81,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -96,7 +96,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -111,7 +111,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -126,7 +126,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand All @@ -140,7 +140,7 @@ def __init__(
*,
greeting: Optional[str | Path] = None,
client: Optional[str | chatlas.Chat] = None,
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("update", "query"),
tools: TOOL_GROUPS | tuple[TOOL_GROUPS, ...] | None = ("filter", "query"),
data_description: Optional[str | Path] = None,
categorical_threshold: int = 20,
extra_instructions: Optional[str | Path] = None,
Expand Down
5 changes: 2 additions & 3 deletions pkg-py/src/querychat/_system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

if TYPE_CHECKING:
from ._datasource import DataSource
from ._querychat_base import TOOL_GROUPS


class QueryChatSystemPrompt:
Expand Down Expand Up @@ -62,12 +61,12 @@ def __init__(
self.categorical_threshold = categorical_threshold
self.data_source = data_source

def render(self, tools: tuple[TOOL_GROUPS, ...] | None) -> str:
def render(self, tools: set[str] | None) -> str:
"""
Render system prompt with tool configuration.

Args:
tools: Normalized tuple of tool groups to enable (already normalized by caller)
tools: Normalized set of tool groups to enable (already normalized by caller)

Returns:
Fully rendered system prompt string
Expand Down
2 changes: 1 addition & 1 deletion pkg-py/src/querychat/_viz_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .__version import __version__


def has_viz_tool(tools: tuple[str, ...] | None) -> bool:
def has_viz_tool(tools: set[str] | None) -> bool:
"""Check if visualize is among the configured tools."""
return tools is not None and "visualize" in tools

Expand Down
14 changes: 0 additions & 14 deletions pkg-py/tests/playwright/test_10_viz_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,17 +158,3 @@ def test_fullscreen_footer_does_not_stretch(self) -> None:
assert footer_box is not None
assert footer_box["height"] < 100

Comment on lines 158 to 160
def test_non_viz_tool_results_have_no_fullscreen(self) -> None:
"""VIZ-NO-FS: Non-visualization tool results don't have fullscreen."""
self.chat.set_user_input("Show me passengers who survived")
self.chat.send_user_input(method="click")

# Wait for a tool result (any)
tool_result = self.page.locator(".shiny-tool-result").first
expect(tool_result).to_be_visible(timeout=90000)

# Non-viz tool results should NOT have fullscreen toggle
fs_results = self.page.locator(
".shiny-tool-result:has(.tool-fullscreen-toggle)"
)
expect(fs_results).to_have_count(0)
Loading
Loading