diff --git a/.github/workflows/py-release.yml b/.github/workflows/py-release.yml
index 769ca515..e7330c7e 100644
--- a/.github/workflows/py-release.yml
+++ b/.github/workflows/py-release.yml
@@ -31,7 +31,7 @@ jobs:
run: uv python install ${{ env.PYTHON_VERSION }}
- name: ๐ฆ Install the project
- run: uv sync --python ${{ env.PYTHON_VERSION }} --all-extras
+ run: uv sync --python ${{ env.PYTHON_VERSION }} --all-extras --all-groups
# - name: ๐งช Check tests
# run: make py-check-tests
diff --git a/.github/workflows/py-test.yml b/.github/workflows/py-test.yml
index 0e534c6c..4486a805 100644
--- a/.github/workflows/py-test.yml
+++ b/.github/workflows/py-test.yml
@@ -35,7 +35,7 @@ jobs:
run: uv python install ${{matrix.config.python-version }}
- name: ๐ฆ Install the project
- run: uv sync --python ${{matrix.config.python-version }} --all-extras
+ run: uv sync --python ${{matrix.config.python-version }} --all-extras --all-groups
# - name: ๐งช Check tests
# run: make py-check-tests
diff --git a/.gitignore b/.gitignore
index f64bb8d6..67ecb914 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,12 @@ README.html
.DS_Store
python-package/examples/titanic.db
.quarto
+*.db
+
+docs/r
+docs/py
+
+!pkg-py/docs
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -237,7 +243,7 @@ vignettes/*.pdf
.Renviron
# pkgdown site
-docs/
+#docs/
# translation temp files
po/*~
@@ -247,3 +253,5 @@ rsconnect/
uv.lock
_dev
+
+/.quarto/
diff --git a/Makefile b/Makefile
index aaa021f2..18444881 100644
--- a/Makefile
+++ b/Makefile
@@ -111,7 +111,7 @@ r-docs-preview: ## [r] Build R docs
.PHONY: py-setup
py-setup: ## [py] Setup python environment
- uv sync --all-extras
+ uv sync --all-extras --all-groups
.PHONY: py-check
# py-check: py-check-format py-check-types py-check-tests ## [py] Run python checks
@@ -165,39 +165,39 @@ py-format: ## [py] Format python code
# @echo "๐ธ Updating pytest snapshots"
# uv run pytest --snapshot-update
-# .PHONY: py-docs
-# py-docs: py-docs-api py-docs-render ## [py] Build python docs
-
-# .PHONY: py-docs-render
-# py-docs-render: ## [py] Render python docs
-# @echo "๐ Rendering python docs with quarto"
-# @$(eval export IN_QUARTODOC=true)
-# ${QUARTO_PATH} render pkg-py/docs
-
-# .PHONY: py-docs-preview
-# py-docs-preview: ## [py] Preview python docs
-# @echo "๐ Rendering python docs with quarto"
-# @$(eval export IN_QUARTODOC=true)
-# ${QUARTO_PATH} preview pkg-py/docs
-
-# .PHONY: py-docs-api
-# py-docs-api: ## [py] Update python API docs
-# @echo "๐ Generating python docs with quartodoc"
-# @$(eval export IN_QUARTODOC=true)
-# cd pkg-py/docs && uv run quartodoc build
-# cd pkg-py/docs && uv run quartodoc interlinks
-
-# .PHONY: py-docs-api-watch
-# py-docs-api-watch: ## [py] Update python docs
-# @echo "๐ Generating python docs with quartodoc"
-# @$(eval export IN_QUARTODOC=true)
-# uv run quartodoc build --config pkg-py/docs/_quarto.yml --watch
-
-# .PHONY: py-docs-clean
-# py-docs-clean: ## [py] Clean python docs
-# @echo "๐งน Cleaning python docs"
-# rm -r pkg-py/docs/api
-# find pkg-py/docs/py -name '*.quarto_ipynb' -delete
+.PHONY: py-docs
+py-docs: py-docs-api py-docs-render ## [py] Build python docs
+
+.PHONY: py-docs-render
+py-docs-render: ## [py] Render python docs
+ @echo "๐ Rendering python docs with quarto"
+ @$(eval export IN_QUARTODOC=true)
+ ${QUARTO_PATH} render pkg-py/docs
+
+.PHONY: py-docs-preview
+py-docs-preview: ## [py] Preview python docs
+ @echo "๐ Rendering python docs with quarto"
+ @$(eval export IN_QUARTODOC=true)
+ ${QUARTO_PATH} preview pkg-py/docs
+
+.PHONY: py-docs-api
+py-docs-api: ## [py] Update python API docs
+ @echo "๐ Generating python docs with quartodoc"
+ @$(eval export IN_QUARTODOC=true)
+ cd pkg-py/docs && uv run quartodoc build
+ cd pkg-py/docs && uv run quartodoc interlinks
+
+.PHONY: py-docs-api-watch
+py-docs-api-watch: ## [py] Update python docs
+ @echo "๐ Generating python docs with quartodoc"
+ @$(eval export IN_QUARTODOC=true)
+ uv run quartodoc build --config pkg-py/docs/_quarto.yml --watch
+
+.PHONY: py-docs-clean
+py-docs-clean: ## [py] Clean python docs
+ @echo "๐งน Cleaning python docs"
+ rm -r pkg-py/docs/api
+ find pkg-py/docs/py -name '*.quarto_ipynb' -delete
.PHONY: py-build
py-build: ## [py] Build python package
diff --git a/pkg-py/README.md b/pkg-py/README.md
index 81401feb..d0d49e14 100644
--- a/pkg-py/README.md
+++ b/pkg-py/README.md
@@ -1,242 +1,8 @@
-# querychat: Chat with Shiny apps (Python)
+# querychat for Python
-Imagine typing questions like these directly into your Shiny dashboard, and seeing the results in realtime:
+Please see the package documentation for installation, setup, and usage.
-* "Show only penguins that are not species Gentoo and have a bill length greater than 50mm."
-* "Show only blue states with an incidence rate greater than 100 per 100,000 people."
-* "What is the average mpg of cars with 6 cylinders?"
+
-querychat is a drop-in component for Shiny that allows users to query a data frame using natural language. The results are available as a reactive data frame, so they can be easily used from Shiny outputs, reactive expressions, downloads, etc.
-
-**This is not as terrible an idea as you might think!** We need to be very careful when bringing LLMs into data analysis, as we all know that they are prone to hallucinations and other classes of errors. querychat is designed to excel in reliability, transparency, and reproducibility by using this one technique: denying it raw access to the data, and forcing it to write SQL queries instead. See the section below on ["How it works"](#how-it-works) for more.
-
-## Installation
-
-```bash
-pip install "querychat @ git+https://github.com/posit-dev/querychat#subdirectory=pkg-py"
-```
-
-## How to use
-
-First, you'll need access to an LLM that supports tools/function calling. querychat uses [chatlas](https://github.com/posit-dev/chatlas) to interface with various providers.
-
-Here's a very minimal example that shows the three function calls you need to make:
-
-```python
-from pathlib import Path
-
-from seaborn import load_dataset
-from shiny import App, render, ui
-
-import querychat
-
-# 1. Configure querychat. This is where you specify the dataset and can also
-# override options like the greeting message, system prompt, model, etc.
-titanic = load_dataset("titanic")
-querychat_config = querychat.init(titanic, "titanic")
-
-# Create UI
-app_ui = ui.page_sidebar(
- # 2. Use querychat.sidebar(id) in a ui.page_sidebar.
- # Alternatively, use querychat.ui(id) elsewhere if you don't want your
- # chat interface to live in a sidebar.
- querychat.sidebar("chat"),
- ui.output_data_frame("data_table"),
- title="querychat with Python",
- fillable=True,
-)
-
-
-# Define server logic
-def server(input, output, session):
- # 3. Create a querychat object using the config from step 1.
- chat = querychat.server("chat", querychat_config)
-
- # 4. Use the filtered/sorted data frame anywhere you wish, via the
- # chat["df"]() reactive.
- @render.data_frame
- def data_table():
- return chat.df()
-
-
-# Create Shiny app
-app = App(app_ui, server)
-```
-
-## How it works
-
-### Powered by LLMs
-
-querychat's natural language chat experience is powered by LLMs. You may use any model that [chatlas](https://github.com/posit-dev/chatlas) supports that has the ability to do tool calls, but we currently recommend (as of March 2025):
-
-* GPT-4o
-* Claude 3.5 Sonnet
-* Claude 3.7 Sonnet
-
-In our testing, we've found that those models strike a good balance between accuracy and latency. Smaller models like GPT-4o-mini are fine for simple queries but make surprising mistakes with moderately complex ones; and reasoning models like o3-mini slow down responses without providing meaningfully better results.
-
-The small open source models (8B and below) we've tested have fared extremely poorly. Sorry. ๐คท
-
-### Powered by SQL
-
-querychat does not have direct access to the raw data; it can _only_ read or filter the data by writing SQL `SELECT` statements. This is crucial for ensuring relability, transparency, and reproducibility:
-
-- **Reliability:** Today's LLMs are excellent at writing SQL, but bad at direct calculation.
-- **Transparency:** querychat always displays the SQL to the user, so it can be vetted instead of blindly trusted.
-- **Reproducibility:** The SQL query can be easily copied and reused.
-
-Currently, querychat uses DuckDB for its SQL engine. It's extremely fast and has a surprising number of [statistical functions](https://duckdb.org/docs/stable/sql/functions/aggregates.html#statistical-aggregates).
-
-## Customizing querychat
-
-### Provide a greeting (recommended)
-
-When the querychat UI first appears, you will usually want it to greet the user with some basic instructions. By default, these instructions are auto-generated every time a user arrives; this is slow, wasteful, and unpredictable. Instead, you should create a file called `greeting.md`, and when calling `querychat.init`, pass `greeting=Path("greeting.md").read_text()`.
-
-You can provide suggestions to the user by using the ` ` tag.
-
-For example:
-
-```markdown
-* **Filter and sort the data:**
- * Show only survivors
- * Filter to first class passengers under 30
- * Sort by fare from highest to lowest
-
-* **Answer questions about the data:**
- * What was the survival rate by gender?
- * What's the average age of children who survived?
- * How many passengers were traveling alone?
-```
-
-These suggestions appear in the greeting and automatically populate the chat text box when clicked.
-This gives the user a few ideas to explore on their own.
-You can see this behavior in our [`querychat template`](https://shiny.posit.co/py/templates/querychat/).
-
-If you need help coming up with a greeting, your own app can help you! Just launch it and paste this into the chat interface:
-
-> Help me create a greeting for your future users. Include some example questions. Format your suggested greeting as Markdown, in a code block.
-
-And keep giving it feedback until you're happy with the result, which will then be ready to be pasted into `greeting.md`.
-
-Alternatively, you can completely suppress the greeting by passing `greeting=""`.
-
-### Augment the system prompt (recommended)
-
-In LLM parlance, the _system prompt_ is the set of instructions and specific knowledge you want the model to use during a conversation. querychat automatically creates a system prompt which is comprised of:
-
-1. The basic set of behaviors the LLM must follow in order for querychat to work properly. (See `querychat/prompt/prompt.md` if you're curious what this looks like.)
-2. The SQL schema of the data frame you provided.
-3. (Optional) Any additional description of the data you choose to provide.
-4. (Optional) Any additional instructions you want to use to guide querychat's behavior.
-
-#### Data description
-
-If you give querychat your dataset and nothing else, it will provide the LLM with the basic schema of your data:
-
-- Column names
-- DuckDB data type (integer, float, boolean, datetime, text)
-- For text columns with less than 10 unique values, we assume they are categorical variables and include the list of values. This threshold is configurable.
-- For integer and float columns, we include the range
-
-And that's all the LLM will know about your data.
-The actual data does not get passed into the LLM.
-We calculate these values before we pass the schema information into the LLM.
-
-If the column names are usefully descriptive, it may be able to make a surprising amount of sense out of the data. But if your data frame's columns are `x`, `V1`, `value`, etc., then the model will need to be given more background info--just like a human would.
-
-To provide this information, use the `data_description` argument. For example, if you're using the `titanic` dataset, you might create a `data_description.md` like this:
-
-```markdown
-This dataset contains information about Titanic passengers, collected for predicting survival.
-
-- survived: Survival (0 = No, 1 = Yes)
-- pclass: Ticket class (1 = 1st, 2 = 2nd, 3 = 3rd)
-- sex: Sex of passenger
-- age: Age in years
-- sibsp: Number of siblings/spouses aboard
-- parch: Number of parents/children aboard
-- fare: Passenger fare
-- embarked: Port of embarkation (C = Cherbourg, Q = Queenstown, S = Southampton)
-- class: Same as pclass but as text
-- who: Man, woman, or child
-- adult_male: Boolean for adult males
-- deck: Deck of the ship
-- embark_town: Town of embarkation
-- alive: Survival status as text
-- alone: Whether the passenger was alone
-```
-
-which you can then pass via:
-
-```python
-querychat_config = querychat.init(
- titanic,
- "titanic",
- data_description=Path("data_description.md").read_text()
-)
-```
-
-querychat doesn't need this information in any particular format; just put whatever information, in whatever format, you think a human would find helpful.
-
-#### Additional instructions
-
-You can add additional instructions of your own to the end of the system prompt, by passing `extra_instructions` into `querychat.init`.
-
-```python
-querychat_config = querychat.init(
- titanic,
- "titanic",
- extra_instructions=[
- "You're speaking to a British audience--please use appropriate spelling conventions.",
- "Use lots of emojis! ๐ Emojis everywhere, ๐ emojis forever. โพ๏ธ",
- "Stay on topic, only talk about the data dashboard and refuse to answer other questions."
- ]
-)
-```
-
-You can also put these instructions in a separate file and use `Path("instructions.md").read_text()` to load them, as we did for `data_description` above.
-
-**Warning:** It is not 100% guaranteed that the LLM will alwaysโor in many cases, everโobey your instructions, and it can be difficult to predict which instructions will be a problem. So be sure to test extensively each time you change your instructions, and especially, if you change the model you use.
-
-### Use a different LLM provider
-
-By default, querychat uses GPT-4o via the OpenAI API. If you want to use a different model, you can provide a `create_chat_callback` function that takes a `system_prompt` parameter, and returns a chatlas Chat object:
-
-```python
-import chatlas
-from functools import partial
-
-# Option 1: Define a function
-def my_chat_func(system_prompt: str) -> chatlas.Chat:
- return chatlas.ChatAnthropic(
- model="claude-3-7-sonnet-latest",
- system_prompt=system_prompt
- )
-
-# Option 2: Use partial
-my_chat_func = partial(chatlas.ChatAnthropic, model="claude-3-7-sonnet-latest")
-
-querychat_config = querychat.init(
- titanic,
- "titanic",
- create_chat_callback=my_chat_func
-)
-```
-
-This would use Claude 3.7 Sonnet instead, which would require you to provide an API key. See the [chatlas documentation](https://github.com/posit-dev/chatlas) for more information on how to authenticate with different providers.
-
-## Complete example
-
-For a complete working example, see the [examples/app.py](examples/app.py) file in the repository. This example includes:
-
-- Loading a dataset
-- Reading greeting and data description from files
-- Setting up the querychat configuration
-- Creating a Shiny UI with the chat sidebar
-- Displaying the filtered data in the main panel
-
-If you have Shiny installed, and want to get started right away, you can use our
-[querychat template](https://shiny.posit.co/py/templates/querychat/)
-or
-[sidebot template](https://shiny.posit.co/py/templates/sidebot/).
+If you are looking for querychat python examples,
+you can find them in the `examples/` directory.
diff --git a/pkg-py/docs/.gitignore b/pkg-py/docs/.gitignore
new file mode 100644
index 00000000..e0c5635d
--- /dev/null
+++ b/pkg-py/docs/.gitignore
@@ -0,0 +1,11 @@
+/.quarto/
+/_site
+/_inv
+*.quarto_ipynb
+objects.txt
+objects.json
+
+# Ignore quartodoc artifacts, these are built in CI
+_sidebar-python.yml
+api/
+reference/
diff --git a/pkg-py/docs/_brand.yml b/pkg-py/docs/_brand.yml
new file mode 100644
index 00000000..0393f067
--- /dev/null
+++ b/pkg-py/docs/_brand.yml
@@ -0,0 +1,48 @@
+
+color:
+ palette:
+ blue: "#007bc2"
+ indigo: "#4b00c1"
+ purple: "#74149c"
+ pink: "#bf007f"
+ red: "#c10000"
+ orange: "#f45100"
+ yellow: "#f9b928"
+ green: "#00891a"
+ teal: "#00bf7f"
+ cyan: "#03c7e8"
+ white: "#ffffff"
+ black: "#1D1F21"
+
+ foreground: black
+ background: white
+ primary: blue
+ secondary: gray
+ success: green
+ info: cyan
+ warning: yellow
+ danger: red
+ light: "#f8f8f8"
+ dark: "#212529"
+
+typography:
+ fonts:
+ - family: Open Sans
+ source: bunny
+ - family: Source Code Pro
+ source: bunny
+
+ headings:
+ family: Open Sans
+ weight: 400
+ monospace: Source Code Pro
+ monospace-inline:
+ color: pink
+ background-color: transparent
+ size: 0.95em
+
+defaults:
+ bootstrap:
+ defaults:
+ navbar-bg: $brand-blue
+ code-color-dark: "#fa88d4"
diff --git a/pkg-py/docs/_quarto.yml b/pkg-py/docs/_quarto.yml
new file mode 100644
index 00000000..73659c1a
--- /dev/null
+++ b/pkg-py/docs/_quarto.yml
@@ -0,0 +1,102 @@
+project:
+ type: website
+ output-dir: ../../docs/py
+
+website:
+ title: "querychat"
+ site-url: https://posit-dev.github.io/querychat/py
+ description: Chat with your data in Shiny apps
+
+ bread-crumbs: true
+ open-graph: true
+ twitter-card: true
+
+ repo-url: https://github.com/posit-dev/querychat/
+ repo-actions: [issue, edit]
+ repo-subdir: pkg-py/docs
+
+ page-footer:
+ left: |
+ Proudly supported by
+ [{fig-alt="Posit" width=65px}](https://posit.co)
+
+ navbar:
+ left:
+ - text: Get Started
+ href: index.qmd
+ - text: "Examples"
+ href: examples/index.qmd
+ - text: API Reference
+ href: reference/index.qmd
+
+ tools:
+ - icon: github
+ menu:
+ - text: Source code
+ href: https://github.com/posit-dev/querychat/tree/main/pkg-py
+ - text: Report a bug
+ href: https://github.com/posit-dev/querychat/issues/new
+
+
+ sidebar:
+ - id: examples
+ title: "Examples"
+ style: docked
+ type: light
+ background: light
+ foreground: dark
+ contents:
+ - href: examples/index.qmd
+ - section: "DataFrames"
+ contents:
+ - href: examples/pandas.qmd
+ - section: "Databases"
+ contents:
+ - href: examples/sqlite.qmd
+
+format:
+ html:
+ theme: [brand]
+ highlight-style: github
+ toc: true
+
+lightbox: auto
+
+metadata-files:
+ - reference/_sidebar.yml
+
+quartodoc:
+ package: querychat
+
+ sidebar: reference/_sidebar.yml
+ css: reference/_styles-quartodoc.css
+
+ sections:
+ - title: Shiny Core
+ options:
+ signature_name: relative
+ include_imports: false
+ include_inherited: false
+ include_attributes: true
+ include_classes: true
+ include_functions: true
+ contents:
+ - init
+ - sidebar
+ - server
+ - system_prompt
+ - ui
+
+interlinks:
+ fast: true
+ sources:
+ pydantic:
+ url: https://docs.pydantic.dev/latest/
+ python:
+ url: https://docs.python.org/3/
+
+editor:
+ render-on-save: true
+ markdown:
+ canonical: true
+ wrap: sentence
diff --git a/pkg-py/docs/examples/index.qmd b/pkg-py/docs/examples/index.qmd
new file mode 100644
index 00000000..06f82737
--- /dev/null
+++ b/pkg-py/docs/examples/index.qmd
@@ -0,0 +1,11 @@
+---
+title: "Basic Example"
+---
+
+Here's the basic example that uses the `titanic` dataset.
+
+{{< include /includes/github_models-callout.qmd >}}
+
+```python
+{{< include /../examples/app.py >}}
+```
diff --git a/pkg-py/docs/examples/pandas.qmd b/pkg-py/docs/examples/pandas.qmd
new file mode 100644
index 00000000..8e90f3ba
--- /dev/null
+++ b/pkg-py/docs/examples/pandas.qmd
@@ -0,0 +1,43 @@
+---
+title: Pandas
+---
+
+This example and walkthrough has the following features:
+
+- querychat interaction with a pandas dataframe
+- Reads in a data description file
+- Reads in a greeting file
+
+## Data
+
+This examples uses the `seaborn` library to load the `titanic` dataset.
+
+## Greeting file
+
+Save this file as `greeting.md`:
+
+```markdown
+{{< include /../examples/greeting.md >}}
+```
+
+## Data description file
+
+Save this file as `data_description.md`:
+
+```markdown
+{{< include /../examples/data_description.md >}}
+```
+
+
+## The application
+
+Our application will read the the `greeting.md` and `data_description.md` files
+and pass them along to the `querychat.init()` function.
+
+Here is our pandas example app, save the contents to `app.py`.
+
+{{< include /includes/github_models-callout.qmd >}}
+
+```python
+{{< include /../examples/app-dataframe-pandas.py >}}
+```
diff --git a/pkg-py/docs/examples/sqlite.qmd b/pkg-py/docs/examples/sqlite.qmd
new file mode 100644
index 00000000..2d180831
--- /dev/null
+++ b/pkg-py/docs/examples/sqlite.qmd
@@ -0,0 +1,49 @@
+---
+title: "SQLite"
+---
+
+This example and walkthrough has the following features:
+
+- querychat interaction with a SQLite database using SQLAlchemy
+- Reads in a data description file
+- Reads in a greeting file
+
+## Data
+
+This example uses the `seaborn` library to load up the `titanic` dataset,
+and then write the dataframe into a SQLite database, `titanic.db`.
+It then uses SQLAlchemy to connect to the SQLite database.
+
+If the `titanic.db` file does not exist in the same directory as the `app.py` file,
+it will create the SQLite database file.
+
+
+## Greeting file
+
+Save this file as `greeting.md`:
+
+```markdown
+{{< include /../examples/greeting.md >}}
+```
+
+## Data description file
+
+Save this file as `data_description.md`:
+
+```markdown
+{{< include /../examples/data_description.md >}}
+```
+
+## The application
+
+Our application will read the the `greeting.md` and `data_description.md` files
+and pass them along to the `querychat.init()` function.
+Also, instead of passing in a dataframe object to the `data_source` parameter in `querychat.init()`, we pass in the database connection, along with the table in the database as `table_name`.
+
+Here is our SQLite example app, save the contents to `app.py`.
+
+{{< include /includes/github_models-callout.qmd >}}
+
+```python
+{{< include /../examples/app-database-sqlite.py >}}
+```
diff --git a/pkg-py/docs/includes/github_models-callout.qmd b/pkg-py/docs/includes/github_models-callout.qmd
new file mode 100644
index 00000000..223cc1f0
--- /dev/null
+++ b/pkg-py/docs/includes/github_models-callout.qmd
@@ -0,0 +1,5 @@
+:::{.callout-note}
+## GitHub Models and GitHub Personal Access Tokens
+
+{{< include /includes/github_models.qmd >}}
+:::
diff --git a/pkg-py/docs/includes/github_models.qmd b/pkg-py/docs/includes/github_models.qmd
new file mode 100644
index 00000000..887ed82f
--- /dev/null
+++ b/pkg-py/docs/includes/github_models.qmd
@@ -0,0 +1,11 @@
+This example does not use the default OpenAI model directly from OpenAI,
+which would require you to create an OpenAI API key and save it as an environment variable named `OPENAI_API_KEY`.
+Instead we are using [GitHub Models](https://github.com/marketplace/models)
+as a free way to access the latest LLMs, with a [rate-limit](https://docs.github.com/en/github-models/use-github-models/prototyping-with-ai-models#rate-limits).
+You can follow the instructions on the
+[GitHub Docs](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
+or
+[Axure AI Demo](https://github.com/Azure-Samples/python-ai-agent-frameworks-demos/tree/main?tab=readme-ov-file#configuring-github-models)
+on creating a PAT.
+
+We suggest you save your PAT into 2 environment variables: `GITHUB_TOKEN`, and `GITHUB_PAT`.
diff --git a/pkg-py/docs/index.qmd b/pkg-py/docs/index.qmd
new file mode 100644
index 00000000..72b69adf
--- /dev/null
+++ b/pkg-py/docs/index.qmd
@@ -0,0 +1,211 @@
+---
+pagetitle: "Get Started"
+title: "querychat: Chat with Shiny apps (Python)"
+---
+
+Imagine typing questions like these directly into your Shiny dashboard, and seeing the results in realtime:
+
+* "Show only penguins that are not species Gentoo and have a bill length greater than 50mm."
+* "Show only blue states with an incidence rate greater than 100 per 100,000 people."
+* "What is the average mpg of cars with 6 cylinders?"
+
+querychat is a drop-in component for Shiny that allows users to query a data frame using natural language. The results are available as a reactive data frame, so they can be easily used from Shiny outputs, reactive expressions, downloads, etc.
+
+**This is not as terrible an idea as you might think!** We need to be very careful when bringing LLMs into data analysis, as we all know that they are prone to hallucinations and other classes of errors. querychat is designed to excel in reliability, transparency, and reproducibility by using this one technique: denying it raw access to the data, and forcing it to write SQL queries instead. See the section below on ["How it works"](#how-it-works) for more.
+
+## Installation
+
+```bash
+pip install querychat
+```
+
+## How to use
+
+First, you'll need access to an LLM that supports tools/function calling. querychat uses [chatlas](https://github.com/posit-dev/chatlas) to interface with various providers.
+
+Here's a very minimal example that shows the three function calls you need to make:
+
+```python
+{{< include /../examples/app.py >}}
+```
+
+{{< include /includes/github_models-callout.qmd >}}
+
+## How it works
+
+### Powered by LLMs
+
+querychat's natural language chat experience is powered by LLMs. You may use any model that [chatlas](https://github.com/posit-dev/chatlas) supports that has the ability to do tool calls, but we currently recommend (as of March 2025):
+
+* GPT-4o
+* Claude 3.5 Sonnet
+* Claude 3.7 Sonnet
+
+In our testing, we've found that those models strike a good balance between accuracy and latency. Smaller models like GPT-4o-mini are fine for simple queries but make surprising mistakes with moderately complex ones; and reasoning models like o3-mini slow down responses without providing meaningfully better results.
+
+The small open source models (8B and below) we've tested have fared extremely poorly. Sorry. ๐คท
+
+### Powered by SQL
+
+querychat does not have direct access to the raw data; it can _only_ read or filter the data by writing SQL `SELECT` statements. This is crucial for ensuring relability, transparency, and reproducibility:
+
+- **Reliability:** Today's LLMs are excellent at writing SQL, but bad at direct calculation.
+- **Transparency:** querychat always displays the SQL to the user, so it can be vetted instead of blindly trusted.
+- **Reproducibility:** The SQL query can be easily copied and reused.
+
+Currently, querychat uses DuckDB for its SQL engine. It's extremely fast and has a surprising number of [statistical functions](https://duckdb.org/docs/stable/sql/functions/aggregates.html#statistical-aggregates).
+
+## Customizing querychat
+
+### Provide a greeting (recommended)
+
+When the querychat UI first appears, you will usually want it to greet the user with some basic instructions. By default, these instructions are auto-generated every time a user arrives; this is slow, wasteful, and unpredictable. Instead, you should create a file called `greeting.md`, and when calling `querychat.init`, pass `greeting=Path("greeting.md").read_text()`.
+
+You can provide suggestions to the user by using the ` ` tag.
+
+For example:
+
+```markdown
+* **Filter and sort the data:**
+ * Show only survivors
+ * Filter to first class passengers under 30
+ * Sort by fare from highest to lowest
+
+* **Answer questions about the data:**
+ * What was the survival rate by gender?
+ * What's the average age of children who survived?
+ * How many passengers were traveling alone?
+```
+
+These suggestions appear in the greeting and automatically populate the chat text box when clicked.
+This gives the user a few ideas to explore on their own.
+You can see this behavior in our [`querychat template`](https://shiny.posit.co/py/templates/querychat/).
+
+If you need help coming up with a greeting, your own app can help you! Just launch it and paste this into the chat interface:
+
+> Help me create a greeting for your future users. Include some example questions. Format your suggested greeting as Markdown, in a code block.
+
+And keep giving it feedback until you're happy with the result, which will then be ready to be pasted into `greeting.md`.
+
+Alternatively, you can completely suppress the greeting by passing `greeting=""`.
+
+### Augment the system prompt (recommended)
+
+In LLM parlance, the _system prompt_ is the set of instructions and specific knowledge you want the model to use during a conversation. querychat automatically creates a system prompt which is comprised of:
+
+1. The basic set of behaviors the LLM must follow in order for querychat to work properly. (See `querychat/prompt/prompt.md` if you're curious what this looks like.)
+2. The SQL schema of the data frame you provided.
+3. (Optional) Any additional description of the data you choose to provide.
+4. (Optional) Any additional instructions you want to use to guide querychat's behavior.
+
+#### Data description
+
+If you give querychat your dataset and nothing else, it will provide the LLM with the basic schema of your data:
+
+- Column names
+- DuckDB data type (integer, float, boolean, datetime, text)
+- For text columns with less than 10 unique values, we assume they are categorical variables and include the list of values. This threshold is configurable.
+- For integer and float columns, we include the range
+
+And that's all the LLM will know about your data.
+The actual data does not get passed into the LLM.
+We calculate these values before we pass the schema information into the LLM.
+
+If the column names are usefully descriptive, it may be able to make a surprising amount of sense out of the data. But if your data frame's columns are `x`, `V1`, `value`, etc., then the model will need to be given more background info--just like a human would.
+
+To provide this information, use the `data_description` argument. For example, if you're using the `titanic` dataset, you might create a `data_description.md` like this:
+
+```markdown
+This dataset contains information about Titanic passengers, collected for predicting survival.
+
+- survived: Survival (0 = No, 1 = Yes)
+- pclass: Ticket class (1 = 1st, 2 = 2nd, 3 = 3rd)
+- sex: Sex of passenger
+- age: Age in years
+- sibsp: Number of siblings/spouses aboard
+- parch: Number of parents/children aboard
+- fare: Passenger fare
+- embarked: Port of embarkation (C = Cherbourg, Q = Queenstown, S = Southampton)
+- class: Same as pclass but as text
+- who: Man, woman, or child
+- adult_male: Boolean for adult males
+- deck: Deck of the ship
+- embark_town: Town of embarkation
+- alive: Survival status as text
+- alone: Whether the passenger was alone
+```
+
+which you can then pass via:
+
+```python
+querychat_config = querychat.init(
+ titanic,
+ "titanic",
+ data_description=Path("data_description.md").read_text()
+)
+```
+
+querychat doesn't need this information in any particular format; just put whatever information, in whatever format, you think a human would find helpful.
+
+#### Additional instructions
+
+You can add additional instructions of your own to the end of the system prompt, by passing `extra_instructions` into `querychat.init`.
+
+```python
+querychat_config = querychat.init(
+ titanic,
+ "titanic",
+ extra_instructions=[
+ "You're speaking to a British audience--please use appropriate spelling conventions.",
+ "Use lots of emojis! ๐ Emojis everywhere, ๐ emojis forever. โพ๏ธ",
+ "Stay on topic, only talk about the data dashboard and refuse to answer other questions."
+ ]
+)
+```
+
+You can also put these instructions in a separate file and use `Path("instructions.md").read_text()` to load them, as we did for `data_description` above.
+
+**Warning:** It is not 100% guaranteed that the LLM will alwaysโor in many cases, everโobey your instructions, and it can be difficult to predict which instructions will be a problem. So be sure to test extensively each time you change your instructions, and especially, if you change the model you use.
+
+### Use a different LLM provider
+
+By default, querychat uses GPT-4o via the OpenAI API. If you want to use a different model, you can provide a `create_chat_callback` function that takes a `system_prompt` parameter, and returns a chatlas Chat object:
+
+```python
+import chatlas
+from functools import partial
+
+# Option 1: Define a function
+def my_chat_func(system_prompt: str) -> chatlas.Chat:
+ return chatlas.ChatAnthropic(
+ model="claude-3-7-sonnet-latest",
+ system_prompt=system_prompt
+ )
+
+# Option 2: Use partial
+my_chat_func = partial(chatlas.ChatAnthropic, model="claude-3-7-sonnet-latest")
+
+querychat_config = querychat.init(
+ titanic,
+ "titanic",
+ create_chat_callback=my_chat_func
+)
+```
+
+This would use Claude 3.7 Sonnet instead, which would require you to provide an API key. See the [chatlas documentation](https://github.com/posit-dev/chatlas) for more information on how to authenticate with different providers.
+
+## Complete example
+
+For a complete working example, see the [examples/app-dataframe.py](examples/app-dataframe.py) file in the repository.
+This example includes:
+
+- Loading a dataset
+- Reading greeting and data description from files
+- Setting up the querychat configuration
+- Creating a Shiny UI with the chat sidebar
+- Displaying the filtered data in the main panel
+
+If you have Shiny installed, and want to get started right away, you can use our
+[querychat template](https://shiny.posit.co/py/templates/querychat/)
+or
+[sidebot template](https://shiny.posit.co/py/templates/sidebot/).
diff --git a/pkg-py/examples/app-database.py b/pkg-py/examples/app-database-sqlite.py
similarity index 74%
rename from pkg-py/examples/app-database.py
rename to pkg-py/examples/app-database-sqlite.py
index ac7066d9..d451a900 100644
--- a/pkg-py/examples/app-database.py
+++ b/pkg-py/examples/app-database-sqlite.py
@@ -1,10 +1,11 @@
from pathlib import Path
+import chatlas
from seaborn import load_dataset
from shiny import App, render, ui
from sqlalchemy import create_engine
-import querychat
+import querychat as qc
# Load titanic data and create SQLite database
db_path = Path(__file__).parent / "titanic.db"
@@ -20,17 +21,27 @@
data_desc = (Path(__file__).parent / "data_description.md").read_text()
# 1. Configure querychat
-querychat_config = querychat.init(
+
+def use_github_models(system_prompt: str) -> chatlas.Chat:
+ # GitHub models give us free rate-limited access to the latest LLMs
+ # you will need to have GITHUB_PAT defined in your environment
+ return chatlas.ChatGithub(
+ model="gpt-4.1",
+ system_prompt=system_prompt,
+ )
+
+querychat_config = qc.init(
engine,
"titanic",
greeting=greeting,
data_description=data_desc,
+ create_chat_callback=use_github_models,
)
# Create UI
app_ui = ui.page_sidebar(
# 2. Place the chat component in the sidebar
- querychat.sidebar("chat"),
+ qc.sidebar("chat"),
# Main panel with data viewer
ui.card(
ui.output_data_frame("data_table"),
@@ -44,7 +55,7 @@
# Define server logic
def server(input, output, session):
# 3. Initialize querychat server with the config from step 1
- chat = querychat.server("chat", querychat_config)
+ chat = qc.server("chat", querychat_config)
# 4. Display the filtered dataframe
@render.data_frame
diff --git a/pkg-py/examples/app-dataframe.py b/pkg-py/examples/app-dataframe-pandas.py
similarity index 66%
rename from pkg-py/examples/app-dataframe.py
rename to pkg-py/examples/app-dataframe-pandas.py
index 1966900f..e860e393 100644
--- a/pkg-py/examples/app-dataframe.py
+++ b/pkg-py/examples/app-dataframe-pandas.py
@@ -1,9 +1,10 @@
from pathlib import Path
+import chatlas
from seaborn import load_dataset
from shiny import App, render, ui
-import querychat
+import querychat as qc
titanic = load_dataset("titanic")
@@ -11,17 +12,27 @@
data_desc = (Path(__file__).parent / "data_description.md").read_text()
# 1. Configure querychat
-querychat_config = querychat.init(
+
+def use_github_models(system_prompt: str) -> chatlas.Chat:
+ # GitHub models give us free rate-limited access to the latest LLMs
+ # you will need to have GITHUB_PAT defined in your environment
+ return chatlas.ChatGithub(
+ model="gpt-4.1",
+ system_prompt=system_prompt,
+ )
+
+querychat_config = qc.init(
titanic,
"titanic",
greeting=greeting,
data_description=data_desc,
+ create_chat_callback=use_github_models,
)
# Create UI
app_ui = ui.page_sidebar(
# 2. Place the chat component in the sidebar
- querychat.sidebar("chat"),
+ qc.sidebar("chat"),
# Main panel with data viewer
ui.card(
ui.output_data_frame("data_table"),
@@ -35,7 +46,7 @@
# Define server logic
def server(input, output, session):
# 3. Initialize querychat server with the config from step 1
- chat = querychat.server("chat", querychat_config)
+ chat = qc.server("chat", querychat_config)
# 4. Display the filtered dataframe
@render.data_frame
diff --git a/pkg-py/examples/app.py b/pkg-py/examples/app.py
new file mode 100644
index 00000000..b1477790
--- /dev/null
+++ b/pkg-py/examples/app.py
@@ -0,0 +1,52 @@
+import chatlas
+from seaborn import load_dataset
+from shiny import App, render, ui
+
+import querychat as qc
+
+titanic = load_dataset("titanic")
+
+# 1. Configure querychat.
+# This is where you specify the dataset and can also
+# override options like the greeting message, system prompt, model, etc.
+
+
+def use_github_models(system_prompt: str) -> chatlas.Chat:
+ # GitHub models give us free rate-limited access to the latest LLMs
+ # you will need to have GITHUB_PAT defined in your environment
+ return chatlas.ChatGithub(
+ model="gpt-4.1",
+ system_prompt=system_prompt,
+ )
+
+
+querychat_config = qc.init(
+ data_source=titanic,
+ table_name="titanic",
+ create_chat_callback=use_github_models,
+)
+
+# Create UI
+app_ui = ui.page_sidebar(
+ # 2. Use qc.sidebar(id) in a ui.page_sidebar.
+ # Alternatively, use qc.ui(id) elsewhere if you don't want your
+ # chat interface to live in a sidebar.
+ qc.sidebar("chat"),
+ ui.output_data_frame("data_table"),
+)
+
+
+# Define server logic
+def server(input, output, session):
+ # 3. Create a querychat object using the config from step 1.
+ chat = qc.server("chat", querychat_config)
+
+ # 4. Use the filtered/sorted data frame anywhere you wish, via the
+ # chat.df() reactive.
+ @render.data_frame
+ def data_table():
+ return chat.df()
+
+
+# Create Shiny app
+app = App(app_ui, server)
diff --git a/pkg-py/examples/greeting.md b/pkg-py/examples/greeting.md
index 8227f654..4d73d2e7 100644
--- a/pkg-py/examples/greeting.md
+++ b/pkg-py/examples/greeting.md
@@ -1,4 +1,5 @@
-Hello! I'm here to assist you with analyzing the Titanic dataset. Here are some examples of what you can ask me to do:
+Hello! I'm here to assist you with analyzing the Titanic dataset.
+Here are some examples of what you can ask me to do:
- **Filtering and Sorting:**
- Show only passengers who boarded in Cherbourg.
@@ -10,4 +11,4 @@ Hello! I'm here to assist you with analyzing the Titanic dataset. Here are some
- **General Statistics:**
- Calculate the average age of female passengers.
- - Find the total fare collected from passengers who did not survive.
\ No newline at end of file
+ - Find the total fare collected from passengers who did not survive.
diff --git a/pkg-py/src/querychat/__init__.py b/pkg-py/src/querychat/__init__.py
index 3aa1ef83..985d24f5 100644
--- a/pkg-py/src/querychat/__init__.py
+++ b/pkg-py/src/querychat/__init__.py
@@ -1,3 +1,3 @@
-from querychat.querychat import init, server, sidebar, ui
+from querychat.querychat import init, mod_server as server, sidebar, system_prompt, mod_ui as ui
-__all__ = ["init", "server", "sidebar", "ui"]
+__all__ = ["init", "server", "sidebar", "ui", "system_prompt"]
diff --git a/pkg-py/src/querychat/querychat.py b/pkg-py/src/querychat/querychat.py
index 9eba2c47..d52fcb7c 100644
--- a/pkg-py/src/querychat/querychat.py
+++ b/pkg-py/src/querychat/querychat.py
@@ -362,7 +362,7 @@ def sidebar(id: str, width: int = 400, height: str = "100%", **kwargs) -> ui.Sid
@module.server
-def server( # noqa: D417
+def mod_server( # noqa: D417
input: Inputs,
output: Outputs,
session: Session,
diff --git a/pyproject.toml b/pyproject.toml
index 3ce33dc4..daec9962 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -43,8 +43,10 @@ packages = ["pkg-py/src/querychat"]
[tool.hatch.build.targets.sdist]
include = ["pkg-py/src/querychat", "pkg-py/LICENSE", "pkg-py/README.md"]
-[tool.uv]
-dev-dependencies = ["ruff>=0.6.5", "pyright>=1.1.401", "tox-uv>=1.11.4"]
+[dependency-groups]
+dev = ["ruff>=0.6.5", "pyright>=1.1.401", "tox-uv>=1.11.4"]
+docs = ["quartodoc>=0.11.1"]
+examples = ["seaborn", "openai"]
[tool.ruff]
src = ["pkg-py/src/querychat"]