Shiny

Shiny is a web framework for building interactive data applications. It provides a reactive programming model that’s a natural fit for querychat.

In this guide, you’ll learn how to build Shiny apps with querychat to enable rich data exploration experiences where data views update based on natural language filters.

Screenshot of querychat running in a custom Shiny app.

Initialize QueryChat

The “main” QueryChat class is available directly from the top-level module, and it designed to work with Shiny (Core):

from querychat import QueryChat

For Shiny Express, import from querychat.express instead (similar to how you import from shiny.express):

from querychat.express import QueryChat

Once imported, initialize it with your data source:

from querychat.data import titanic

qc = QueryChat(titanic(), "titanic")

Remember, the simplest way to get started is with .app(), which gives a “pre-baked” Shiny app:

shiny-app.py
from querychat import QueryChat
from querychat.data import titanic

qc = QueryChat(titanic(), "titanic")
app = qc.app()

Run with:

shiny run shiny-app.py

Relevant methods

After initializing QueryChat, use .sidebar() or .ui() to place the chat interface in your app. As users interact with the chat, .df(), .sql(), and .title() automatically update to reflect the current query. Any Shiny outputs that depend on these reactive values will re-render automatically.

Method Description
.sidebar() Place the chat interface in a sidebar
.ui() Returns just the chat component for custom placement
.df() Current filtered/sorted DataFrame
.sql() Current SQL query (str | None)
.title() Short description of current filter (str | None)
Note

Shiny has two modes: Express (simple, script-based) and Core (explicit UI/server separation). With Core, call qc.server() in your server function and access reactives via the returned object (e.g., qc_vals.df()). With Express, access them directly on the QueryChat instance (e.g., qc.df()).

Basic sidebar

The most common pattern places chat in the sidebar with your custom filtered views in the main area:

from pathlib import Path

from shiny.express import render, ui
from querychat.express import QueryChat
from querychat.data import titanic

greeting = Path(__file__).parent / "greeting.md"

# 1. Provide data source to QueryChat
qc = QueryChat(titanic(), "titanic", greeting=greeting)

# 2. Add sidebar chat control
qc.sidebar()

# 3. Add a card with reactive title and data frame
with ui.card():
    with ui.card_header():
        @render.text
        def title():
            return qc.title() or "Titanic Dataset"

    @render.data_frame
    def data_table():
        return qc.df()
    
# 4. Set some page options (optional)
ui.page_opts(
    fillable=True,
    title="Titanic Dataset Explorer"
)
Deferred data sources

Some data sources, like database connections or reactive calculations, may need to be created within an active Shiny session. To help support this, QueryChat allows you to initialize without a data source and provide it later, like this:

deferred-app.py
from shiny.express import render, session, ui
from querychat.express import QueryChat

# Don't create connection until we have an actual session
if session.is_stub_session():
    conn = None
else:
    conn = get_user_connection(session)

qc = QueryChat(conn, "users")
qc.sidebar()

@render.data_frame
def table():
    return qc.df()
from pathlib import Path

from shiny import App, render, ui
from querychat import QueryChat
from querychat.data import titanic

greeting = Path(__file__).parent / "greeting.md"

# 1. Provide data source to QueryChat
qc = QueryChat(titanic(), "titanic", greeting=greeting)

app_ui = ui.page_sidebar(
    # 2. Create sidebar chat control
    qc.sidebar(),
    ui.card(
        ui.card_header(ui.output_text("title")),
        ui.output_data_frame("data_table"),
        fill=True,
    ),
    fillable=True
)


def server(input, output, session):
    # 3. Add server logic (to get reactive data frame and title)
    qc_vals = qc.server()

    # 4. Use the filtered/sorted data frame reactively
    @render.data_frame
    def data_table():
        return qc_vals.df()

    @render.text
    def title():
        return qc_vals.title() or "Titanic Dataset"


app = App(app_ui, server)
Deferred data sources

Some data sources, like database connections or reactive calculations, may need to be created within an active Shiny session. To help support this, QueryChat allows you to initialize without a data source and provide it later, like this:

deferred-app.py
from shiny import App, ui
from querychat import QueryChat

# Global scope - create QueryChat without data source
qc = QueryChat(None, "users")

app_ui = ui.page_sidebar(
    qc.sidebar(),
    ui.output_data_frame("table"),
)

def server(input, output, session):
    # Server scope - create connection with session credentials
    conn = get_user_connection(session)
    qc_vals = qc.server(data_source=conn)

    @render.data_frame
    def table():
        return qc_vals.df()

app = App(app_ui, server)

Custom chat UI

Use .ui() to place the chat anywhere in your layout. Here we use it simply to place custom content in the sidebar alongside the chat (like a reset button):

from shiny.express import ui
from querychat.express import QueryChat

qc = QueryChat(data, "my_data")

with ui.sidebar():
    qc.ui()  # Chat component
    ui.hr()
    ui.input_action_button("reset", "Reset Filters", class_="w-100")
from shiny import ui
from querychat import QueryChat

qc = QueryChat(data, "my_data")

app_ui = ui.page_sidebar(
    ui.sidebar(
        qc.ui(),  # Chat component
        ui.hr(),
        ui.input_action_button("reset", "Reset Filters", class_="w-100"),
    ),
    # Main content here
)
Custom Shiny chat UIs

Learn more about customizing Shiny chat UIs in the Shiny Chat documentation.

Data views

The real power of querychat comes from connecting it to visualizations. Here’s an example showing both filtered data and a chart:

import plotly.express as px

from shiny.express import render, ui
from shinywidgets import render_plotly

from querychat.express import QueryChat
from querychat.data import titanic

qc = QueryChat(titanic(), "titanic")
qc.sidebar()

with ui.layout_columns():
    with ui.card():
        ui.card_header("Data Table")

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

    with ui.card():
        ui.card_header("Survival by Class")

        @render_plotly
        def survival_plot():
            d = qc.df().to_native()  # Convert for pandas groupby()
            summary = d.groupby('pclass')['survived'].mean().reset_index()
            return px.bar(summary, x='pclass', y='survived')

When users filter data through the chat (e.g., “show only children”), both charts update automatically.

Screenshot of a querychat app showing both a data table and a bar chart of survival by class.

A more complete example adds metrics, tabs, and multiple views:

titanic-dashboard.py
import plotly.express as px
from faicons import icon_svg
from querychat.data import titanic
from querychat.express import QueryChat
from shiny.express import render, ui
from shinywidgets import render_plotly

qc = QueryChat(titanic(), "titanic")
qc.sidebar()

with ui.layout_column_wrap(fill=False):
    with ui.value_box(showcase=icon_svg("users")):
        "Passengers"

        @render.text
        def count():
            return str(len(qc.df()))

    with ui.value_box(showcase=icon_svg("heart")):
        "Survival Rate"

        @render.text
        def survival():
            rate = qc.df()["survived"].mean() * 100
            return f"{rate:.1f}%"

    with ui.value_box(showcase=icon_svg("coins")):
        "Avg Fare"

        @render.text
        def fare():
            avg = qc.df()["fare"].mean()
            return f"${avg:.2f}"


with ui.layout_columns():
    with ui.card():
        with ui.card_header():
            "Data Table"

            @render.text
            def table_title():
                return f" - {qc.title()}" if qc.title() else ""

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

    with ui.card():
        ui.card_header("Survival by Class")

        @render_plotly
        def survival_by_class():
            df = qc.df().to_pandas()
            summary = df.groupby("pclass")["survived"].mean().reset_index()
            return px.bar(
                summary,
                x="pclass",
                y="survived",
                labels={"pclass": "Class", "survived": "Survival Rate"},
            )


with ui.layout_columns():
    with ui.card():
        ui.card_header("Age Distribution")

        @render_plotly
        def age_dist():
            df = qc.df()
            return px.histogram(df, x="age", nbins=30)

    with ui.card():
        ui.card_header("Fare by Class")

        @render_plotly
        def fare_by_class():
            df = qc.df()
            return px.box(df, x="pclass", y="fare", color="survived")


ui.page_opts(
    title="Titanic Survival Analysis",
    fillable=True,
    class_="bslib-page-dashboard",
)

Screenshot of a querychat app showing value boxes, a data table, and multiple plots.

Resetting filters

Add a reset button to clear filters and show all data:

ui.input_action_button("reset", "Reset Filters")

@reactive.effect
@reactive.event(input.reset)
def _():
    qc.sql("")
    qc.title(None)
ui.input_action_button("reset", "Reset Filters")

qc_vals = qc.server()

@reactive.effect
@reactive.event(input.reset)
def _():
    qc_vals.sql.set("")
    qc_vals.title.set(None)
Tip

Users can also ask the LLM to “reset” or “show all data” to clear filters through the chat interface.

Advanced patterns

Programmatic updates

You can update the query state programmatically using .sql() and .title() as setters. This is useful for adding preset filter buttons or linking filters to other UI controls.

Multiple datasets

To explore multiple datasets, use separate QueryChat instances (i.e., separate chat interfaces).

Multiple tables in one chat?

In some cases, you might be able to “pre-join” datasets into a single table and use one QueryChat instance to explore them together. In the future, we may support multiple filtered tables in one chat interface, but this is not currently available. Please upvote the relevant issue if this is a feature you’d like to see!

multiple-datasets.py
from querychat.data import titanic
from querychat.express import QueryChat
from seaborn import load_dataset
from shiny.express import render, ui

penguins = load_dataset("penguins")

qc_titanic = QueryChat(titanic(), "titanic")
qc_penguins = QueryChat(penguins, "penguins")

with ui.sidebar():
    with ui.panel_conditional("input.navbar == 'Titanic'"):
        qc_titanic.ui()
    with ui.panel_conditional("input.navbar == 'Penguins'"):
        qc_penguins.ui()

with ui.nav_panel("Titanic"):

    @render.data_frame
    def titanic_table():
        return qc_titanic.df()


with ui.nav_panel("Penguins"):

    @render.data_frame
    def penguins_table():
        return qc_penguins.df()


ui.page_opts(
    id="navbar",
    title="Multiple Datasets with querychat",
    fillable=True,
)

Screenshot of a querychat app with two datasets: titanic and penguins.

Note

Each dataset gets its own chat interface and maintains separate state.

See also

  • Greet users - Create welcoming onboarding experiences
  • Provide context - Help the LLM understand your data better
  • Tools - Understand what querychat can do under the hood