Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### New features

* New `querychat.app()` function lets you quickly launch an app to chat with your data. (#99)

* The `.sql` query and `.title` returned from `querychat.server()` are now reactive values, meaning you can now `.set()` their value, and `.df()` will update accordingly. (#98)

* Added `querychat.greeting()` to help you create a greeting message for your querychat bot. (#87)
Expand All @@ -19,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Added rich tool UI support using shinychat development version and chatlas >= 0.11.1. (#67)

* Enabled bookmarking by default in both `querychat.app()` and `querychat.server()`. In latter case, you'll need to also specify the `bookmark_store` (either in `shiny.App()` or `shiny.express.app_opts()`) for it to work. (#99)

* querychat's system prompt and tool descriptions were rewritten for clarity and future extensibility. (#90)

## [0.2.2] - 2025-09-04
Expand Down
3 changes: 2 additions & 1 deletion pkg-py/src/querychat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from querychat._greeting import greeting
from querychat._app import app
from querychat.querychat import (
init,
sidebar,
Expand All @@ -11,4 +12,4 @@
mod_ui as ui,
)

__all__ = ["greeting", "init", "server", "sidebar", "system_prompt", "ui"]
__all__ = ["app", "greeting", "init", "server", "sidebar", "system_prompt", "ui"]
144 changes: 144 additions & 0 deletions pkg-py/src/querychat/_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
"""Convenience function for creating a simple querychat app."""

from __future__ import annotations

from typing import TYPE_CHECKING, Literal, Optional

import chatlas
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui
from shinychat import output_markdown_stream

from ._icons import bs_icon
from .datasource import DataSource
from .querychat import QueryChatConfig, _resolve_querychat_client, init
from .querychat import mod_server as server
from .querychat import sidebar

if TYPE_CHECKING:
import sqlalchemy
from narwhals.stable.v1.typing import IntoFrame


def app(
x: IntoFrame | sqlalchemy.Engine | QueryChatConfig | DataSource,
*,
name: Optional[str] = None,
client: Optional[str | chatlas.Chat] = None,
bookmark_store: Literal["url", "server", "disable"] = "url",
**kwargs,
) -> App:
"""
Quickly chat with a dataset.

Creates a Shiny app with a chat sidebar and data table view -- providing a
quick-and-easy way to start chatting with your data.

Parameters
----------
x
The dataset to chat with. Can be any one of the following:
- A Narwhals-compatible data frame (e.g., Polars or Pandas)
- A SQLAlchemy engine containing the table to query against.
- The result of `querychat.init()` (for more advanced configuration).
- A `DataSource` object.
name
The name of the dataset.
client
A `chatlas.Chat` object or a string to be passed to `chatlas.ChatAuto()`
describing the model to use (e.g. `"openai/gpt-4.1"`). If no client is
provided, querychat will look for the `QUERYCHAT_CLIENT` environment
variable. If that variable is not set, it will default to
`chatlas.ChatOpenAI()`.
bookmark_store
The bookmarking store to use for the Shiny app. Options are:
- `"url"`: Store bookmarks in the URL (default).
- `"server"`: Store bookmarks on the server.
- `"disable"`: Disable bookmarking.
**kwargs
Additional keyword arguments to pass to `querychat.init()` if `x`
is not already a QueryChatConfig object.

Returns
-------
App
A Shiny App object that can be run with `app.run()` or served with `shiny run`.

"""
if not isinstance(x, QueryChatConfig):
data_source = x.get_data() if isinstance(x, DataSource) else x
x = init(data_source, table_name=name or "data", **kwargs)

if client is not None:
x.client = _resolve_querychat_client(client)

enable_bookmarking = bookmark_store != "disable"

def app_ui(request):
return ui.page_sidebar(
sidebar("chat"),
ui.card(
ui.card_header(
ui.div(
ui.div(
bs_icon("terminal-fill"),
ui.output_text("query_title", inline=True),
class_="d-flex align-items-center gap-2",
),
ui.output_ui("ui_reset", inline=True),
class_="hstack gap-3",
),
),
ui.output_ui("sql_output"),
fill=False,
style="max-height: 33%;",
),
ui.card(
ui.card_header(bs_icon("table"), " Data"),
ui.output_data_frame("dt"),
),
title=ui.span(
"querychat with ",
ui.code(x.data_source.table_name),
),
class_="bslib-page-dashboard",
fillable=True,
)

def app_server(input: Inputs, output: Outputs, session: Session):
qc = server("chat", x, enable_bookmarking=enable_bookmarking)

@render.text
def query_title():
return qc.title() or "SQL Query"

@render.ui
def ui_reset():
req(qc.sql())
return ui.input_action_button(
"reset_query",
"Reset Query",
class_="btn btn-outline-danger btn-sm lh-1 ms-auto",
)

@reactive.effect
@reactive.event(input.reset_query)
def _():
qc.sql("")
qc.title(None)

@render.data_frame
def dt():
return qc.df()

@render.ui
def sql_output():
sql = qc.sql() or f"SELECT * FROM {x.data_source.table_name}"
sql_code = f"```sql\n{sql}\n```"
return output_markdown_stream(
"sql_code",
content=sql_code,
auto_scroll=False,
width="100%",
)

return App(app_ui, app_server, bookmark_store=bookmark_store)
19 changes: 19 additions & 0 deletions pkg-py/src/querychat/_icons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from typing import Literal

from shiny import ui

ICON_NAMES = Literal["funnel-fill", "terminal-fill", "table"]


def bs_icon(name: ICON_NAMES) -> 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])


BS_ICONS = {
"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>',
"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>',
}
33 changes: 33 additions & 0 deletions pkg-py/src/querychat/querychat.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
if TYPE_CHECKING:
import pandas as pd
from narwhals.stable.v1.typing import IntoFrame
from shiny.bookmark import BookmarkState, RestoreState

from .datasource import DataFrameSource, DataSource, SQLAlchemySource

Expand Down Expand Up @@ -521,6 +522,8 @@ def mod_server( # noqa: D417
output: Outputs,
session: Session,
querychat_config: QueryChatConfig,
*,
enable_bookmarking: bool = True,
) -> QueryChat:
"""
Initialize the querychat server.
Expand All @@ -529,6 +532,8 @@ def mod_server( # noqa: D417
----------
querychat_config
Configuration object from init().
enable_bookmarking
Whether to enable bookmarking support for the chat session.

Returns
-------
Expand All @@ -546,6 +551,7 @@ def mod_server( # noqa: D417
# Reactive values to store state
current_title = ReactiveStringOrNone(None)
current_query = ReactiveString("")
has_greeted = reactive.value[bool](False) # noqa: FBT003

@reactive.calc
def filtered_df():
Expand Down Expand Up @@ -603,6 +609,9 @@ def _():

@reactive.effect
async def greet_on_startup():
if has_greeted():
return

if querychat_config.greeting:
await chat_ui.append_message(greeting)
elif querychat_config.greeting is None:
Expand All @@ -612,5 +621,29 @@ async def greet_on_startup():
)
await chat_ui.append_message_stream(stream)

has_greeted.set(True)

if enable_bookmarking:
chat_ui.enable_bookmarking(client)

def _on_bookmark(x: BookmarkState) -> None:
vals = x.values # noqa: PD011
vals["querychat_current_query"] = current_query.get()
vals["querychat_current_title"] = current_title.get()
vals["querychat_has_greeted"] = has_greeted.get()

session.bookmark.on_bookmark(_on_bookmark)

def _on_restore(x: RestoreState) -> None:
vals = x.values # noqa: PD011
if "querychat_current_query" in vals:
current_query.set(vals["querychat_current_query"])
if "querychat_current_title" in vals:
current_title.set(vals["querychat_current_title"])
if "querychat_has_greeted" in vals:
has_greeted.set(vals["querychat_has_greeted"])

session.bookmark.on_restore(_on_restore)

# Return the interface for other components to use
return QueryChat(chat, current_query, current_title, filtered_df)
14 changes: 4 additions & 10 deletions pkg-py/src/querychat/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

import chevron
from chatlas import ContentToolResult, Tool
from htmltools import HTML
from shinychat.types import ToolResultDisplay

from ._icons import bs_icon
from ._utils import df_to_html

if TYPE_CHECKING:
Expand Down Expand Up @@ -66,9 +66,7 @@ def update_dashboard(query: str, title: str) -> ContentToolResult:
title=title,
show_request=False,
open=True,
icon=HTML(
'<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>',
),
icon=bs_icon("funnel-fill"),
),
},
)
Expand Down Expand Up @@ -142,9 +140,7 @@ def reset_dashboard() -> ContentToolResult:
title=None,
show_request=False,
open=False,
icon=HTML(
'<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>',
),
icon=bs_icon("terminal-fill"),
),
},
)
Expand Down Expand Up @@ -213,9 +209,7 @@ def query(query: str, _intent: str = "") -> ContentToolResult:
markdown=markdown,
show_request=False,
open=True,
icon=HTML(
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-table" viewBox="0 0 16 16"><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-2zm15 2h-4v3h4zm0 4h-4v3h4zm0 4h-4v3h3a1 1 0 0 0 1-1zm-5 3v-3H6v3zm-5 0v-3H1v2a1 1 0 0 0 1 1zm-4-4h4V8H1zm0-4h4V4H1zm5-3v3h4V4zm4 4H6v3h4z"/></svg>',
),
icon=bs_icon("table"),
),
},
)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ maintainers = [
dependencies = [
"duckdb",
"pandas",
"shiny",
"shiny @ git+https://github.com/posit-dev/py-shiny.git@fix/bookmark-missing-input-error",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once posit-dev/py-shiny#2117 gets merged/released

Suggested change
"shiny @ git+https://github.com/posit-dev/py-shiny.git@fix/bookmark-missing-input-error",
"shiny>1.5.0",

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't want to make release a requirement, should probably make bookmarking an opt-in for now

"shinywidgets",
"htmltools",
"chatlas>=0.12.0",
Expand Down
Loading