Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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 @@ -37,10 +37,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* File attachments are now enabled by default in the Shiny chat UI. Users can attach images, PDFs, and text files to their messages and the LLM will receive them. Disable with `allow_attachments=False` in `mod_ui()` or `QueryChat.ui()`. (#253)

* Conversation history is now persisted by default. `QueryChat`/`QueryChatExpress` keep a user's chat around across page reloads and browser sessions, backed by shinychat's history support. The default `restore_mode="browser"` stores the active conversation in the browser's localStorage, but you can pass `history=shinychat.types.HistoryOptions(restore_mode="url")` to restore via a plain, shareable URL instead, or `restore_mode="bookmark"` to fold the conversation into a full Shiny bookmark. Disable with `history=False`.

### Breaking Changes

* The `data_source` property has been removed. Use `qc.table("name").data_source` to read a table's data source, and `qc.add_table(df, "name", replace=True)` to replace it. The `data_source` parameter to `server()` (Shiny) has also been removed; call `add_table()` before `server()` instead. (#195)

* `.app()`'s `bookmark_store` parameter has been removed. Pass `history=shinychat.types.HistoryOptions(restore_mode="bookmark")` to get the same shareable-bookmark behavior; any other `history` value disables Shiny-level bookmarking for the generated app. `.app()` defaults to `restore_mode="bookmark"` when no `history` is set anywhere, so existing `.app()` callers keep working without changes. Note this default is a storage-mechanism change, not just a rename: the old default (`bookmark_store="url"`) encoded the entire bookmark state in the URL itself, requiring no server storage; the new default requires server-side bookmark storage (`bookmark_store="server"`), with just a short state ID in the URL. Deployments that relied on `.app()` being fully stateless should pass `history=False` or a non-bookmark `HistoryOptions()`.

### Deprecated

* `.server()`'s and `QueryChatExpress`'s `enable_bookmarking` parameter is deprecated in favor of `history`. Pass `history=shinychat.types.HistoryOptions(restore_mode="bookmark")` instead of `enable_bookmarking=True` for the equivalent behavior.

### Improvements

* Chat greetings now use shinychat's greeting API (requires shinychat >= 0.4.0). A provided `greeting` renders instantly when the app loads, and when no `greeting` is given one is generated on demand — now **schema-aware**, so it can describe the data it's about to help you explore — without being added to the conversation history. Generated greetings are preserved across bookmark/restore. Tables passed to `QueryChat()` are described in the greeting automatically; opt additional tables in with `include_in_greeting=True` on `add_table()`/`add_tables()`, or fine-tune which tables and which template the greeting uses via `qc.greeter`. (#249, #261)
Expand Down
7 changes: 2 additions & 5 deletions pkg-py/examples/10-viz-app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from pathlib import Path

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

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

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

Expand All @@ -18,5 +17,3 @@
qc.ui()

ui.page_opts(fillable=True, title="QueryChat Visualization Demo")

app_opts(bookmark_store="url")
3 changes: 3 additions & 0 deletions pkg-py/src/querychat/_querychat_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from ibis.backends.sql import SQLBackend
from narwhals.stable.v1.typing import IntoFrame
from pins.boards import BaseBoard
from shinychat.types import HistoryOptions

from ._data_dict import DataDict
from ._viz_tools import VisualizeData
Expand Down Expand Up @@ -91,6 +92,7 @@ def __init__(
prompt_template: Optional[str | Path] = None,
categorical_threshold: int = 20,
data_description: Optional[str | Path] = None,
history: Optional[bool | HistoryOptions] = None,
):
self._data_dicts: list[DataDict] = _normalize_data_dicts(data_dict)

Expand All @@ -103,6 +105,7 @@ def __init__(

self.tools = normalize_tools(tools, default=DEFAULT_TOOLS)
self.greeting = greeting.read_text() if isinstance(greeting, Path) else greeting
self.history = history

# Store init parameters for deferred system prompt building
self._prompt_template = prompt_template
Expand Down
20 changes: 3 additions & 17 deletions pkg-py/src/querychat/_querychat_greeter.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@

import chatlas

from shiny import reactive


class QueryChatGreeter:
"""Controls greeting generation for a QueryChat instance. Access via ``qc.greeter``."""
Expand Down Expand Up @@ -66,19 +64,7 @@ def generate(
"""Generate a greeting using the greeting system prompt."""
return str(self.build_client(base).chat(GREETING_PROMPT, echo=echo))

async def generate_stream(
self,
*,
chat_ui,
current_greeting: reactive.Value[str | None],
base: chatlas.Chat | None = None,
) -> None:
"""Stream a greeting into the Shiny chat UI and capture the result."""
async def generate_async(self, *, base: chatlas.Chat | None = None):
"""Stream a greeting response from the greeting client."""
client = self.build_client(base)
stream = await client.stream_async(GREETING_PROMPT, echo="none")
from ._utils_shinychat import chat_greeting_persistent

await chat_ui.set_greeting(chat_greeting_persistent(stream)) # pyright: ignore[reportArgumentType]
last_turn = client.get_last_turn(role="assistant")
if last_turn is not None:
current_greeting.set(last_turn.text)
return await client.stream_async(GREETING_PROMPT, echo="none")
Loading
Loading