Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
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)
Comment thread
cpsievert marked this conversation as resolved.
Outdated
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