Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
4 changes: 4 additions & 0 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ 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)

### 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)
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
16 changes: 11 additions & 5 deletions pkg-py/src/querychat/_querychat_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
import re
from pathlib import Path
from typing import TYPE_CHECKING, Generic, Literal, Optional
from typing import TYPE_CHECKING, Generic, Literal, Optional, cast

import chatlas
import narwhals.stable.v1 as nw
Expand Down 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 @@ -320,6 +321,11 @@ def normalize_tools(
result = tools
else:
result = tuple(tools)
if result is not None:
result = cast(
"tuple[TOOL_GROUPS, ...]",
tuple(dict.fromkeys("update" if t == "filter" else t for t in result)),
)
if not check_deps:
return result
if has_viz_tool(result) and not has_viz_deps():
Expand Down
16 changes: 9 additions & 7 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,16 +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
Set to `"filter"` to prevent the LLM from accessing data values, only
allowing dashboard filtering without answering questions.

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.
data_description
Expand Down Expand Up @@ -142,7 +144,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 +622,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
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
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)
22 changes: 17 additions & 5 deletions pkg-py/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,27 +141,39 @@ def test_with_empty_tuple_returns_none(self):

def test_with_missing_returns_default(self):
result = normalize_tools(MISSING, default=("update", "query"))
assert result == ("update", "query")
assert set(result) == {"update", "query"}

def test_with_string_returns_tuple(self):
result = normalize_tools("update", default=None)
assert result == ("update",)
assert set(result) == {"update"}

def test_with_tuple_returns_same(self):
result = normalize_tools(("update", "query"), default=None)
assert result == ("update", "query")
assert set(result) == {"update", "query"}

def test_with_list_returns_tuple(self):
tools_list: Any = ["update", "query"]
result = normalize_tools(tools_list, default=None)
assert result == ("update", "query")
assert set(result) == {"update", "query"}

def test_filter_normalized_to_update(self):
result = normalize_tools("filter", default=None)
assert set(result) == {"update"}

def test_filter_query_normalized(self):
result = normalize_tools(("filter", "query"), default=None)
assert set(result) == {"update", "query"}

def test_filter_and_update_deduplicated(self):
result = normalize_tools(("filter", "update", "query"), default=None)
assert set(result) == {"update", "query"}


class TestQueryChatBase:
def test_init_with_dataframe(self, sample_df):
qc = QueryChatBase(sample_df, "test_table")
assert isinstance(qc.data_source, DataFrameSource)
assert qc.tools == ("update", "query")
assert set(qc.tools) == {"update", "query"}

def test_init_with_custom_greeting(self, sample_df):
qc = QueryChatBase(sample_df, "test_table", greeting="Hello!")
Expand Down
4 changes: 2 additions & 2 deletions pkg-py/tests/test_client_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ def test_default_tools_maintain_current_behavior(self, sample_df):
# Without tools parameter, should include both tools (like before)
qc = QueryChat(sample_df, "test_table", greeting="Hello!")

assert qc.tools == ("update", "query")
assert set(qc.tools) == {"update", "query"}

prompt = qc.system_prompt
assert "Filtering and Sorting Data" in prompt
Expand All @@ -292,4 +292,4 @@ def test_existing_initialization_still_works(self, sample_df):

assert qc is not None
assert qc.id == "querychat_test_table"
assert qc.tools == ("update", "query")
assert set(qc.tools) == {"update", "query"}
2 changes: 1 addition & 1 deletion pkg-r/DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ Suggests:
withr
VignetteBuilder:
knitr
Config/roxygen2/version: 8.0.0
Config/testthat/edition: 3
Config/testthat/parallel: true
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
Config/roxygen2/version: 8.0.0
2 changes: 2 additions & 0 deletions pkg-r/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# querychat (development version)

* The `tools` parameter now uses `"filter"` as the preferred name (instead of `"update"`) for the dashboard-filtering tool group. The default is now `c("filter", "query")`. The legacy name `"update"` is still accepted everywhere. (#222)

* `QueryChat$server()` now accepts a `client` parameter for session-scoped chat client overrides. This enables Posit Connect managed OAuth workflows where API credentials are only available inside the Shiny server function. The client spec is stored lazily at construction time and resolved only when needed, so `QueryChat$new(NULL, "table")` no longer requires an API key. (#205)

* 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)
Expand Down
Loading
Loading