Skip to content

Commit 39dce1f

Browse files
committed
feat(pkg-py): Add .app() method, enable bookmarking by default
1 parent 6d7c59e commit 39dce1f

File tree

5 files changed

+163
-14
lines changed

5 files changed

+163
-14
lines changed

pkg-py/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [UNRELEASED]
9+
10+
### New features
11+
12+
* New `QueryChat.app()` method enables quicker/easier chatting with a dataset. (#xx)
13+
* 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)
14+
15+
816
## [UNRELEASED]
917

1018
### Changes

pkg-py/src/querychat/_icons.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import Literal
2+
3+
from shiny import ui
4+
5+
ICON_NAMES = Literal["funnel-fill", "terminal-fill", "table"]
6+
7+
8+
def bs_icon(name: ICON_NAMES) -> ui.HTML:
9+
"""Get Bootstrap icon SVG by name."""
10+
if name not in BS_ICONS:
11+
raise ValueError(f"Unknown Bootstrap icon: {name}")
12+
return ui.HTML(BS_ICONS[name])
13+
14+
15+
BS_ICONS = {
16+
"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>',
17+
"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>',
18+
"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>',
19+
}

pkg-py/src/querychat/querychat.py

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
import chevron
1111
import shinychat
1212
import sqlalchemy
13-
from shiny import Inputs, Outputs, Session, module, reactive, ui
13+
from shiny import App, Inputs, Outputs, Session, module, reactive, render, req, ui
14+
from shinychat import output_markdown_stream
1415

16+
from ._icons import bs_icon
1517
from ._utils import normalize_client
1618
from .datasource import DataFrameSource, DataSource, SQLAlchemySource
1719
from .tools import tool_query, tool_reset_dashboard, tool_update_dashboard
@@ -20,6 +22,7 @@
2022
import chatlas
2123
import pandas as pd
2224
from narwhals.stable.v1.typing import IntoFrame
25+
from shiny.bookmark import BookmarkState, RestoreState
2326

2427

2528
@dataclass
@@ -127,6 +130,97 @@ def __init__(
127130
self.greeting = config.greeting
128131
self.client = config.client
129132

133+
def app(self, bookmark_store: Literal["url", "server", "disable"] = "url") -> App:
134+
"""
135+
Quickly chat with a dataset.
136+
137+
Creates a Shiny app with a chat sidebar and data table view -- providing a
138+
quick-and-easy way to start chatting with your data.
139+
140+
Parameters
141+
----------
142+
bookmark_store
143+
The bookmarking store to use for the Shiny app. Options are:
144+
- `"url"`: Store bookmarks in the URL (default).
145+
- `"server"`: Store bookmarks on the server.
146+
- `"disable"`: Disable bookmarking.
147+
148+
Returns
149+
-------
150+
:
151+
A Shiny App object that can be run with `app.run()` or served with `shiny run`.
152+
153+
"""
154+
enable_bookmarking = bookmark_store != "disable"
155+
table_name = self.data_source.table_name
156+
157+
def app_ui(request):
158+
return ui.page_sidebar(
159+
self.sidebar("chat"),
160+
ui.card(
161+
ui.card_header(
162+
ui.div(
163+
ui.div(
164+
bs_icon("terminal-fill"),
165+
ui.output_text("query_title", inline=True),
166+
class_="d-flex align-items-center gap-2",
167+
),
168+
ui.output_ui("ui_reset", inline=True),
169+
class_="hstack gap-3",
170+
),
171+
),
172+
ui.output_ui("sql_output"),
173+
fill=False,
174+
style="max-height: 33%;",
175+
),
176+
ui.card(
177+
ui.card_header(bs_icon("table"), " Data"),
178+
ui.output_data_frame("dt"),
179+
),
180+
title=ui.span("querychat with ", ui.code(table_name)),
181+
class_="bslib-page-dashboard",
182+
fillable=True,
183+
)
184+
185+
def app_server(input: Inputs, output: Outputs, session: Session):
186+
qc = self.server("chat", enable_bookmarking=enable_bookmarking)
187+
188+
@render.text
189+
def query_title():
190+
return qc.title() or "SQL Query"
191+
192+
@render.ui
193+
def ui_reset():
194+
req(qc.sql())
195+
return ui.input_action_button(
196+
"reset_query",
197+
"Reset Query",
198+
class_="btn btn-outline-danger btn-sm lh-1 ms-auto",
199+
)
200+
201+
@reactive.effect
202+
@reactive.event(input.reset_query)
203+
def _():
204+
qc.sql("")
205+
qc.title(None)
206+
207+
@render.data_frame
208+
def dt():
209+
return qc.df()
210+
211+
@render.ui
212+
def sql_output():
213+
sql = qc.sql() or f"SELECT * FROM {table_name}"
214+
sql_code = f"```sql\n{sql}\n```"
215+
return output_markdown_stream(
216+
"sql_code",
217+
content=sql_code,
218+
auto_scroll=False,
219+
width="100%",
220+
)
221+
222+
return App(app_ui, app_server, bookmark_store=bookmark_store)
223+
130224
def sidebar(
131225
self,
132226
id: str,
@@ -187,14 +281,17 @@ def _ui_wrapper(**ui_kwargs):
187281

188282
return _ui_wrapper(id, **kwargs)
189283

190-
def server(self, id: str):
284+
def server(self, id: str, *, enable_bookmarking: bool = True) -> QueriedValues:
191285
"""
192286
Initialize the querychat server logic.
193287
194288
Parameters
195289
----------
196290
id
197291
An ID corresponding to the UI component.
292+
enable_bookmarking
293+
Whether to enable bookmarking for this chat session. For this to take
294+
effect, the Shiny app must also have a `bookmark_store` configured.
198295
199296
Returns
200297
-------
@@ -214,13 +311,14 @@ def server(self, id: str):
214311
def mod_server_wrapper(
215312
input: Inputs,
216313
output: Outputs,
217-
session: Session,
314+
session: Session
218315
):
219316
return _server_impl(
220317
input,
221318
output,
222319
session,
223320
querychat_config=config,
321+
enable_bookmarking=enable_bookmarking,
224322
)
225323

226324
return mod_server_wrapper(id)
@@ -644,6 +742,8 @@ def _server_impl(
644742
output: Outputs,
645743
session: Session,
646744
querychat_config: QueryChatConfig,
745+
*,
746+
enable_bookmarking: bool = True,
647747
) -> QueriedValues:
648748
data_source = querychat_config.data_source
649749
system_prompt = querychat_config.system_prompt
@@ -653,6 +753,7 @@ def _server_impl(
653753
# Reactive values to store state
654754
current_title = ReactiveStringOrNone(None)
655755
current_query = ReactiveString("")
756+
has_greeted = reactive.value[bool](False) # noqa: FBT003
656757

657758
@reactive.calc
658759
def filtered_df():
@@ -710,6 +811,9 @@ def _():
710811

711812
@reactive.effect
712813
async def greet_on_startup():
814+
if has_greeted():
815+
return
816+
713817
if querychat_config.greeting:
714818
await chat_ui.append_message(greeting)
715819
elif querychat_config.greeting is None:
@@ -719,5 +823,29 @@ async def greet_on_startup():
719823
)
720824
await chat_ui.append_message_stream(stream)
721825

826+
has_greeted.set(True)
827+
828+
if enable_bookmarking:
829+
chat_ui.enable_bookmarking(client)
830+
831+
def _on_bookmark(x: BookmarkState) -> None:
832+
vals = x.values # noqa: PD011
833+
vals["querychat_current_query"] = current_query.get()
834+
vals["querychat_current_title"] = current_title.get()
835+
vals["querychat_has_greeted"] = has_greeted.get()
836+
837+
session.bookmark.on_bookmark(_on_bookmark)
838+
839+
def _on_restore(x: RestoreState) -> None:
840+
vals = x.values # noqa: PD011
841+
if "querychat_current_query" in vals:
842+
current_query.set(vals["querychat_current_query"])
843+
if "querychat_current_title" in vals:
844+
current_title.set(vals["querychat_current_title"])
845+
if "querychat_has_greeted" in vals:
846+
has_greeted.set(vals["querychat_has_greeted"])
847+
848+
session.bookmark.on_restore(_on_restore)
849+
722850
# Return the interface for other components to use
723851
return QueriedValues(filtered_df, current_query, current_title, chat)

pkg-py/src/querychat/tools.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
import chevron
77
from chatlas import ContentToolResult, Tool
8-
from htmltools import HTML
98
from shinychat.types import ToolResultDisplay
109

10+
from ._icons import bs_icon
1111
from ._utils import df_to_html
1212

1313
if TYPE_CHECKING:
@@ -66,9 +66,7 @@ def update_dashboard(query: str, title: str) -> ContentToolResult:
6666
title=title,
6767
show_request=False,
6868
open=True,
69-
icon=HTML(
70-
'<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>',
71-
),
69+
icon=bs_icon("funnel-fill"),
7270
),
7371
},
7472
)
@@ -142,9 +140,7 @@ def reset_dashboard() -> ContentToolResult:
142140
title=None,
143141
show_request=False,
144142
open=False,
145-
icon=HTML(
146-
'<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>',
147-
),
143+
icon=bs_icon("terminal-fill"),
148144
),
149145
},
150146
)
@@ -213,9 +209,7 @@ def query(query: str, _intent: str = "") -> ContentToolResult:
213209
markdown=markdown,
214210
show_request=False,
215211
open=True,
216-
icon=HTML(
217-
'<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>',
218-
),
212+
icon=bs_icon("table"),
219213
),
220214
},
221215
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ maintainers = [
2121
dependencies = [
2222
"duckdb",
2323
"pandas",
24-
"shiny",
24+
"shiny @ git+https://github.com/posit-dev/py-shiny.git@fix/bookmark-missing-input-error",
2525
"shinywidgets",
2626
"htmltools",
2727
"chatlas>=0.12.0",

0 commit comments

Comments
 (0)