Skip to content
Draft
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>',
}
100 changes: 98 additions & 2 deletions pkg-py/src/querychat/_querychat.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import chatlas
import chevron
import sqlalchemy
from shiny import ui
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui
from shiny.session import get_current_session
from shinychat import output_markdown_stream

from ._icons import bs_icon
from ._querychat_module import ModServerResult, mod_server, mod_ui
from .datasource import DataFrameSource, DataSource, SQLAlchemySource

Expand Down Expand Up @@ -134,6 +136,99 @@ def __init__(
# Populated when ._server() gets called (in an active session)
self._server_values: ModServerResult | None = None

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(),
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):
self._server(enable_bookmarking=enable_bookmarking)

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

@render.ui
def ui_reset():
req(self.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 _():
self.sql("")
self.title(None)

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

@render.ui
def sql_output():
sql = self.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,
*,
Expand Down Expand Up @@ -184,7 +279,7 @@ def ui(self, **kwargs):
"""
return mod_ui(self.id, **kwargs)

def _server(self):
def _server(self, *, enable_bookmarking: bool = True) -> None:
"""
Initialize the server module.

Expand Down Expand Up @@ -212,6 +307,7 @@ def _server(self):
system_prompt=self.system_prompt,
greeting=self.greeting,
client=self.client,
enable_bookmarking=enable_bookmarking,
)

return
Expand Down
30 changes: 30 additions & 0 deletions pkg-py/src/querychat/_querychat_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import chatlas
import pandas as pd
from shiny import Inputs, Outputs, Session
from shiny.bookmark import BookmarkState, RestoreState

from .datasource import DataSource

Expand Down Expand Up @@ -60,10 +61,12 @@ def mod_server(
system_prompt: str,
greeting: str | None,
client: chatlas.Chat,
enable_bookmarking: bool = False,
):
# Reactive values to store state
sql = ReactiveString("")
title = ReactiveStringOrNone(None)
has_greeted = reactive.value[bool](False) # noqa: FBT003

# Set up the chat object for this session
chat = copy.deepcopy(client)
Expand Down Expand Up @@ -99,6 +102,9 @@ async def _(user_input: str):

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

if greeting:
await chat_ui.append_message(greeting)
elif greeting is None:
Expand All @@ -108,6 +114,8 @@ async def greet_on_startup():
)
await chat_ui.append_message_stream(stream)

has_greeted.set(True)

# Handle update button clicks
@reactive.effect
@reactive.event(input.chat_update)
Expand All @@ -125,4 +133,26 @@ def _():
if new_title is not None:
title.set(new_title)

if enable_bookmarking:
chat_ui.enable_bookmarking(client)

def _on_bookmark(x: BookmarkState) -> None:
vals = x.values # noqa: PD011
vals["querychat_sql"] = sql.get()
vals["querychat_title"] = 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_sql" in vals:
sql.set(vals["querychat_sql"])
if "querychat_title" in vals:
title.set(vals["querychat_title"])
if "querychat_has_greeted" in vals:
has_greeted.set(vals["querychat_has_greeted"])

session.bookmark.on_restore(_on_restore)

return ModServerResult(df=filtered_df, sql=sql, title=title, client=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",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would require a release

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

"shinywidgets",
"htmltools",
"chatlas>=0.13.2",
Expand Down