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
8 changes: 8 additions & 0 deletions pkg-py/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [UNRELEASED]

### New features

* New `QueryChat.app()` method enables quicker/easier chatting with a dataset. (#xx)
* Enabled bookmarking by default in both `.app()` and `.server()` methods. In latter case, you'll need to also specify the `bookmark_store` (either in `shiny.App()` or `shiny.express.app_opts()`) for it to take effect. (#xx)


## [UNRELEASED]

### Changes
Expand Down
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>',
}
134 changes: 131 additions & 3 deletions pkg-py/src/querychat/querychat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import chevron
import shinychat
import sqlalchemy
from shiny import Inputs, Outputs, Session, module, reactive, ui
from shiny import App, Inputs, Outputs, Session, module, reactive, render, req, ui
from shinychat import output_markdown_stream

from ._icons import bs_icon
from ._utils import normalize_client
from .datasource import DataFrameSource, DataSource, SQLAlchemySource
from .tools import tool_query, tool_reset_dashboard, tool_update_dashboard
Expand All @@ -20,6 +22,7 @@
import chatlas
import pandas as pd
from narwhals.stable.v1.typing import IntoFrame
from shiny.bookmark import BookmarkState, RestoreState


@dataclass
Expand Down Expand Up @@ -127,6 +130,97 @@ def __init__(
self.greeting = config.greeting
self.client = config.client

def app(self, bookmark_store: Literal["url", "server", "disable"] = "url") -> 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
----------
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.

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

"""
enable_bookmarking = bookmark_store != "disable"
table_name = self.data_source.table_name

def app_ui(request):
return ui.page_sidebar(
self.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(table_name)),
class_="bslib-page-dashboard",
fillable=True,
)

def app_server(input: Inputs, output: Outputs, session: Session):
qc = self.server("chat", 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 {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)

def sidebar(
self,
id: str,
Expand Down Expand Up @@ -187,14 +281,17 @@ def _ui_wrapper(**ui_kwargs):

return _ui_wrapper(id, **kwargs)

def server(self, id: str):
def server(self, id: str, *, enable_bookmarking: bool = True) -> QueriedValues:
"""
Initialize the querychat server logic.

Parameters
----------
id
An ID corresponding to the UI component.
enable_bookmarking
Whether to enable bookmarking for this chat session. For this to take
effect, the Shiny app must also have a `bookmark_store` configured.

Returns
-------
Expand All @@ -214,13 +311,14 @@ def server(self, id: str):
def mod_server_wrapper(
input: Inputs,
output: Outputs,
session: Session,
session: Session
):
return _server_impl(
input,
output,
session,
querychat_config=config,
enable_bookmarking=enable_bookmarking,
)

return mod_server_wrapper(id)
Expand Down Expand Up @@ -644,6 +742,8 @@ def _server_impl(
output: Outputs,
session: Session,
querychat_config: QueryChatConfig,
*,
enable_bookmarking: bool = True,
) -> QueriedValues:
data_source = querychat_config.data_source
system_prompt = querychat_config.system_prompt
Expand All @@ -653,6 +753,7 @@ def _server_impl(
# 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 @@ -710,6 +811,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 @@ -719,5 +823,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 QueriedValues(filtered_df, current_query, current_title, chat)
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",
"shinywidgets",
"htmltools",
"chatlas>=0.12.0",
Expand Down