Skip to content

Commit 35f6932

Browse files
authored
feat: Add querychat_greeting() (#87)
* feat(pkg-r): Add `querychat_greeting()` * Gives an easy way to generate a greeting once * feat(pkg-py): Add `querychat_greeting()` * update docs * pkg-py: hint about querychat greeting in init() * ide: hook up testing pane * add changelog items * fix docs * tests: fix test * r build ignore things
1 parent 7c0aeeb commit 35f6932

File tree

19 files changed

+448
-28
lines changed

19 files changed

+448
-28
lines changed

.vscode/settings.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,12 @@
88
},
99
"editor.defaultFormatter": "charliermarsh.ruff",
1010
},
11-
"flake8.args": ["--max-line-length=120"]
11+
"flake8.args": [
12+
"--max-line-length=120"
13+
],
14+
"python.testing.pytestArgs": [
15+
"pkg-py"
16+
],
17+
"python.testing.unittestEnabled": false,
18+
"python.testing.pytestEnabled": true
1219
}

pkg-py/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
* Added `querychat_reset_dashboard()` tool for easily resetting the dashboard filters when asked by the user. (#81)
1313

14+
* Added `querychat.greeting()` to help you create a greeting message for your querychat bot. (#87)
15+
1416
## [0.2.2] - 2025-09-04
1517

1618
* Fixed another issue with data sources that aren't already narwhals DataFrames (#83)

pkg-py/docs/index.qmd

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,39 @@ These suggestions appear in the greeting and automatically populate the chat tex
8181
This gives the user a few ideas to explore on their own.
8282
You can see this behavior in our [`querychat template`](https://shiny.posit.co/py/templates/querychat/).
8383

84-
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:
84+
### Generate a greeting
8585

86-
> Help me create a greeting for your future users. Include some example questions. Format your suggested greeting as Markdown, in a code block.
86+
If you need help coming up with a greeting, you can use the `querychat.greeting()` function to generate one:
8787

88-
And keep giving it feedback until you're happy with the result, which will then be ready to be pasted into `greeting.md`.
88+
```python
89+
import querychat
90+
from palmerpenguins import load_penguins
91+
92+
# Create config with your dataset
93+
penguins = load_penguins()
94+
penguins_config = querychat.init(penguins, "penguins")
95+
96+
# Generate a greeting
97+
querychat.greeting(penguins_config)
98+
#> Hello! I'm here to help you explore and analyze the penguins dataset.
99+
#> Here are some example prompts you can try:
100+
#> ...
101+
102+
# Update the config with the generated greeting
103+
penguins_config = querychat.init(
104+
penguins,
105+
"penguins",
106+
greeting="Hello! I'm here to help you explore and analyze the penguins dataset..."
107+
)
108+
```
89109

90-
Alternatively, you can completely suppress the greeting by passing `greeting=""`.
110+
This will use the LLM to generate a friendly greeting message with sample prompts.
111+
In Shiny apps, you could also generate the greeting once when the app starts up so that it's shared among all users:
112+
113+
```python
114+
penguins_config = querychat.init(penguins, "penguins")
115+
penguins_config.greeting = querychat.greeting(penguins_config, echo="none")
116+
```
91117

92118
### Augment the system prompt (recommended)
93119

pkg-py/src/querychat/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from querychat._greeting import greeting
12
from querychat.querychat import (
23
init,
34
sidebar,
@@ -10,4 +11,4 @@
1011
mod_ui as ui,
1112
)
1213

13-
__all__ = ["init", "server", "sidebar", "system_prompt", "ui"]
14+
__all__ = ["greeting", "init", "server", "sidebar", "system_prompt", "ui"]

pkg-py/src/querychat/_greeting.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from __future__ import annotations
2+
3+
from copy import deepcopy
4+
5+
6+
def greeting(
7+
querychat_config,
8+
*,
9+
generate: bool = True,
10+
stream: bool = False,
11+
**kwargs,
12+
) -> str | None:
13+
"""
14+
Generate or retrieve a greeting message.
15+
16+
Use this function to generate a friendly greeting message using the chat
17+
client and data source specified in the `querychat_config` object. You can
18+
pass this greeting to `init()` to set an initial greeting for users for
19+
faster startup times and lower costs. If you don't provide a greeting in
20+
`init()`, one will be generated at the start of every new conversation.
21+
22+
Parameters
23+
----------
24+
querychat_config
25+
A QueryChatConfig object from `init()`.
26+
generate
27+
If `True` and if `querychat_config` does not include a `greeting`, a new
28+
greeting is generated. If `False`, returns the existing greeting from
29+
the configuration (if any).
30+
stream
31+
If `True`, returns a streaming response suitable for use in a Shiny app
32+
with `chat_ui.append_message_stream()`. If `False` (default), returns
33+
the full greeting at once. Only relevant when `generate = True`.
34+
**kwargs
35+
Additional arguments passed to the chat client's `chat()` or `stream_async()` method.
36+
37+
Returns
38+
-------
39+
str | None
40+
- When `generate = False`: Returns the existing greeting as a string or
41+
`None` if no greeting exists.
42+
- When `generate = True`: Returns the chat response containing a greeting and
43+
sample prompts.
44+
45+
Examples
46+
--------
47+
```python
48+
import pandas as pd
49+
from querychat import init, greeting
50+
51+
# Create config with mtcars dataset
52+
mtcars = pd.read_csv(
53+
"https://gist.githubusercontent.com/seankross/a412dfbd88b3db70b74b/raw/5f23f993cd87c283ce766e7ac6b329ee7cc2e1d1/mtcars.csv"
54+
)
55+
mtcars_config = init(mtcars, "mtcars")
56+
57+
# Generate a new greeting
58+
greeting_text = greeting(mtcars_config)
59+
60+
# Update the config with the generated greeting
61+
mtcars_config = init(
62+
mtcars,
63+
"mtcars",
64+
greeting="Hello! I'm here to help you explore and analyze the mtcars...",
65+
)
66+
```
67+
68+
"""
69+
not_querychat_config = (
70+
not hasattr(querychat_config, "client")
71+
and not hasattr(querychat_config, "greeting")
72+
and not hasattr(querychat_config, "system_prompt")
73+
)
74+
75+
if not_querychat_config:
76+
raise TypeError("`querychat_config` must be a QueryChatConfig object.")
77+
78+
greeting_text = querychat_config.greeting
79+
80+
if not generate:
81+
has_greeting = greeting_text is not None and len(greeting_text.strip()) > 0
82+
return greeting_text if has_greeting else None
83+
84+
chat = deepcopy(querychat_config.client)
85+
chat.system_prompt = querychat_config.system_prompt
86+
87+
prompt = "Please give me a friendly greeting. Include a few sample prompts in a two-level bulleted list."
88+
89+
if stream:
90+
return chat.stream_async(prompt, **kwargs)
91+
else:
92+
return chat.chat(prompt, **kwargs)

pkg-py/src/querychat/querychat.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,12 @@ def init(
295295
have to be). If a data_source is a SQLAlchemy engine, the table_name is
296296
the name of the table in the database to query against.
297297
greeting : str | Path, optional
298-
A string in Markdown format, containing the initial message.
299-
If a pathlib.Path object is passed,
300-
querychat will read the contents of the path into a string with `.read_text()`.
298+
A string in Markdown format, containing the initial message. If a
299+
pathlib.Path object is passed, querychat will read the contents of the
300+
path into a string with `.read_text()`. You can use
301+
`querychat.greeting()` to help generate a greeting from a querychat
302+
configuration. If no greeting is provided, one will be generated at the
303+
start of every new conversation.
301304
data_description : str | Path, optional
302305
Description of the data in plain text or Markdown.
303306
If a pathlib.Path object is passed,
@@ -375,6 +378,7 @@ def init(
375378
print(
376379
"Warning: No greeting provided; the LLM will be invoked at conversation start to generate one. "
377380
"For faster startup, lower cost, and determinism, please save a greeting and pass it to init().",
381+
"You can also use `querychat.greeting()` to help generate a greeting.",
378382
file=sys.stderr,
379383
)
380384

pkg-py/tests/test_greeting.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import os
2+
3+
import pandas as pd
4+
import pytest
5+
6+
from querychat import greeting, init
7+
8+
9+
@pytest.fixture(autouse=True)
10+
def set_dummy_api_key():
11+
"""Set a dummy OpenAI API key for testing."""
12+
old_api_key = os.environ.get("OPENAI_API_KEY")
13+
os.environ["OPENAI_API_KEY"] = "sk-dummy-api-key-for-testing"
14+
yield
15+
if old_api_key is not None:
16+
os.environ["OPENAI_API_KEY"] = old_api_key
17+
else:
18+
del os.environ["OPENAI_API_KEY"]
19+
20+
21+
@pytest.fixture
22+
def querychat_config():
23+
"""Create a test querychat configuration."""
24+
# Create a simple pandas DataFrame
25+
df = pd.DataFrame(
26+
{
27+
"id": [1, 2, 3],
28+
"name": ["Alice", "Bob", "Charlie"],
29+
"age": [25, 30, 35],
30+
},
31+
)
32+
33+
# Create a config with a greeting
34+
return init(
35+
data_source=df,
36+
table_name="test_table",
37+
greeting="Hello! This is a test greeting.",
38+
)
39+
40+
41+
@pytest.fixture
42+
def querychat_config_no_greeting():
43+
"""Create a test querychat configuration without a greeting."""
44+
# Create a simple pandas DataFrame
45+
df = pd.DataFrame(
46+
{
47+
"id": [1, 2, 3],
48+
"name": ["Alice", "Bob", "Charlie"],
49+
"age": [25, 30, 35],
50+
},
51+
)
52+
53+
# Create a config without a greeting
54+
return init(
55+
data_source=df,
56+
table_name="test_table",
57+
greeting=None,
58+
)
59+
60+
61+
def test_greeting_retrieval(querychat_config):
62+
"""
63+
Test that greeting() returns the existing greeting when generate=False.
64+
"""
65+
result = greeting(querychat_config, generate=False)
66+
assert result == "Hello! This is a test greeting."
67+
68+
69+
def test_greeting_retrieval_none(querychat_config_no_greeting):
70+
"""
71+
Test that greeting() returns None when there's no existing greeting and
72+
generate=False.
73+
"""
74+
result = greeting(querychat_config_no_greeting, generate=False)
75+
assert result is None
76+
77+
78+
def test_greeting_retrieval_empty(querychat_config):
79+
"""
80+
Test that greeting() returns None when the existing greeting is empty and
81+
generate=False.
82+
"""
83+
querychat_config.greeting = ""
84+
85+
result = greeting(querychat_config, generate=False)
86+
assert result is None
87+
88+
89+
def test_greeting_invalid_config():
90+
"""Test that greeting() raises TypeError when given an invalid config."""
91+
with pytest.raises(TypeError):
92+
greeting("not a config")

pkg-r/.Rbuildignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
^pkgdown$
2+
^LICENSE\.md$

pkg-r/LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
YEAR: 2025
2-
COPYRIGHT HOLDER: Posit Software, PBC
2+
COPYRIGHT HOLDER: querychat authors

pkg-r/LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# MIT License
2+
3+
Copyright (c) 2025 querychat authors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)