Skip to content

Commit 4ef8a28

Browse files
gadenbuiecpsievert
andauthored
feat: add option to control tool detail card collapsed/expanded state (#137)
* feat: add option to control tool detail card collapsed/expanded state Adds support for controlling whether tool detail cards (SQL query and results) are shown expanded or collapsed by default. **R package:** - Option: `querychat.tool_details` - Environment variable: `QUERYCHAT_TOOL_DETAILS` **Python package:** - Environment variable: `QUERYCHAT_TOOL_DETAILS` **Valid values:** - `"expanded"` - Tool cards are always shown expanded - `"collapsed"` - Tool cards are always shown collapsed - `"default"` - Uses tool-specific behavior (some expanded, some collapsed based on tool type) **Usage:** ```r # Set for the session options(querychat.tool_details = "collapsed") # Or set via environment variable in .Renviron QUERYCHAT_TOOL_DETAILS=collapsed ``` The option takes precedence over the environment variable in R. Closes #120 * refactor: simplify tool open state logic Remove special handling for error states. The tool details setting now applies uniformly regardless of whether an error occurred. This simplifies the implementation and gives users full control over the display state in all cases. * chore: py-format * refactor: rename resolve_tool_open_state to querychat_tool_starts_open Rename function in both R and Python packages for better clarity. The new name more directly describes what the function returns. * refactor: move validation logic and reorganize functions - R: Move querychat_tool_details_option() to querychat_tools.R adjacent to querychat_tool_starts_open() - R: Move validation logic into querychat_tool_details_option() - Python: Move validation logic into get_tool_details_setting() - Simplify querychat_tool_starts_open() in both packages to just apply validated settings Addresses review comments in PR #137 * style: remove unnecessary comments from new functions Remove self-explanatory comments that don't add value beyond what the code clearly expresses. * chore: small edits * chore(py): Better typing Co-authored-by: Carson Sievert <[email protected]> * docs: add NEWS/CHANGELOG entries for tool details feature Add entries to R NEWS.md and Python CHANGELOG.md documenting the new querychat.tool_details option and QUERYCHAT_TOOL_DETAILS environment variable introduced in PR #137. * chore(pkg-py): fix types * chore: fix changelog * chore: remove changes to duplicate changelog --------- Co-authored-by: Carson Sievert <[email protected]>
1 parent 0fc654b commit 4ef8a28

File tree

7 files changed

+252
-6
lines changed

7 files changed

+252
-6
lines changed

pkg-py/CHANGELOG.md

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

3232
* querychat's system prompt and tool descriptions were rewritten for clarity and future extensibility. (#90)
3333

34+
* Tool detail cards can now be expanded or collapsed by default when querychat runs a query or updates the dashboard via the `QUERYCHAT_TOOL_DETAILS` environment variable. Valid values are `"expanded"`, `"collapsed"`, or `"default"`. (#137)
35+
3436
## [0.2.2] - 2025-09-04
3537

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

pkg-py/src/querychat/_utils.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
22

33
import os
4+
import warnings
45
from contextlib import contextmanager
5-
from typing import TYPE_CHECKING, Optional
6+
from typing import TYPE_CHECKING, Literal, Optional
67

78
import narwhals.stable.v1 as nw
89

@@ -53,6 +54,65 @@ def temp_env_vars(env_vars: dict[str, Optional[str]]):
5354
os.environ[key] = original_value
5455

5556

57+
def get_tool_details_setting() -> Optional[Literal["expanded", "collapsed", "default"]]:
58+
"""
59+
Get and validate the tool details setting from environment variable.
60+
61+
Returns
62+
-------
63+
Optional[str]
64+
The validated value of QUERYCHAT_TOOL_DETAILS environment variable
65+
(one of 'expanded', 'collapsed', or 'default'), or None if not set
66+
or invalid
67+
68+
"""
69+
setting = os.environ.get("QUERYCHAT_TOOL_DETAILS")
70+
if setting is None:
71+
return None
72+
73+
setting_lower = setting.lower()
74+
valid_settings = ("expanded", "collapsed", "default")
75+
76+
if setting_lower not in valid_settings:
77+
warnings.warn(
78+
f"Invalid value for QUERYCHAT_TOOL_DETAILS: {setting!r}. "
79+
"Must be one of: 'expanded', 'collapsed', or 'default'",
80+
UserWarning,
81+
stacklevel=2,
82+
)
83+
return None
84+
85+
return setting_lower
86+
87+
88+
def querychat_tool_starts_open(action: Literal["update", "query", "reset"]) -> bool:
89+
"""
90+
Determine whether a tool card should be open based on action and setting.
91+
92+
Parameters
93+
----------
94+
action : str
95+
The action type ('update', 'query', or 'reset')
96+
97+
Returns
98+
-------
99+
bool
100+
True if the tool card should be open, False otherwise
101+
102+
"""
103+
setting = get_tool_details_setting()
104+
105+
if setting is None:
106+
return action != "reset"
107+
108+
if setting == "expanded":
109+
return True
110+
elif setting == "collapsed":
111+
return False
112+
else: # setting == "default"
113+
return action != "reset"
114+
115+
56116
def df_to_html(df: IntoFrame, maxrows: int = 5) -> str:
57117
"""
58118
Convert a DataFrame to an HTML table for display in chat.

pkg-py/src/querychat/tools.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from shinychat.types import ToolResultDisplay
99

1010
from ._icons import bs_icon
11-
from ._utils import df_to_html
11+
from ._utils import df_to_html, querychat_tool_starts_open
1212

1313
if TYPE_CHECKING:
1414
from ._datasource import DataSource
@@ -65,7 +65,7 @@ def update_dashboard(query: str, title: str) -> ContentToolResult:
6565
markdown=markdown + f"\n\n{button_html}",
6666
title=title,
6767
show_request=False,
68-
open=True,
68+
open=querychat_tool_starts_open("update"),
6969
icon=bs_icon("funnel-fill"),
7070
),
7171
},
@@ -139,7 +139,7 @@ def reset_dashboard() -> ContentToolResult:
139139
markdown=button_html,
140140
title=None,
141141
show_request=False,
142-
open=False,
142+
open=querychat_tool_starts_open("reset"),
143143
icon=bs_icon("arrow-counterclockwise"),
144144
),
145145
},
@@ -208,7 +208,7 @@ def query(query: str, _intent: str = "") -> ContentToolResult:
208208
"display": ToolResultDisplay(
209209
markdown=markdown,
210210
show_request=False,
211-
open=True,
211+
open=querychat_tool_starts_open("query"),
212212
icon=bs_icon("table"),
213213
),
214214
},

pkg-py/tests/test_tools.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Tests for tool functions and utilities."""
2+
3+
import warnings
4+
5+
from querychat._utils import querychat_tool_starts_open
6+
7+
8+
def test_querychat_tool_starts_open_default_behavior(monkeypatch):
9+
"""Test default behavior when no setting is provided."""
10+
monkeypatch.delenv("QUERYCHAT_TOOL_DETAILS", raising=False)
11+
12+
assert querychat_tool_starts_open("query") is True
13+
assert querychat_tool_starts_open("update") is True
14+
assert querychat_tool_starts_open("reset") is False
15+
16+
17+
def test_querychat_tool_starts_open_expanded(monkeypatch):
18+
"""Test 'expanded' setting."""
19+
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "expanded")
20+
21+
assert querychat_tool_starts_open("query") is True
22+
assert querychat_tool_starts_open("update") is True
23+
assert querychat_tool_starts_open("reset") is True
24+
25+
26+
def test_querychat_tool_starts_open_collapsed(monkeypatch):
27+
"""Test 'collapsed' setting."""
28+
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "collapsed")
29+
30+
assert querychat_tool_starts_open("query") is False
31+
assert querychat_tool_starts_open("update") is False
32+
assert querychat_tool_starts_open("reset") is False
33+
34+
35+
def test_querychat_tool_starts_open_default_setting(monkeypatch):
36+
"""Test 'default' setting."""
37+
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "default")
38+
39+
assert querychat_tool_starts_open("query") is True
40+
assert querychat_tool_starts_open("update") is True
41+
assert querychat_tool_starts_open("reset") is False
42+
43+
44+
def test_querychat_tool_starts_open_case_insensitive(monkeypatch):
45+
"""Test that setting is case-insensitive."""
46+
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "EXPANDED")
47+
assert querychat_tool_starts_open("query") is True
48+
49+
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "Collapsed")
50+
assert querychat_tool_starts_open("query") is False
51+
52+
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "DeFaUlT")
53+
assert querychat_tool_starts_open("query") is True
54+
55+
56+
def test_querychat_tool_starts_open_invalid_setting(monkeypatch):
57+
"""Test warning on invalid setting."""
58+
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "invalid")
59+
60+
with warnings.catch_warnings(record=True) as w:
61+
warnings.simplefilter("always")
62+
result = querychat_tool_starts_open("query")
63+
64+
assert len(w) == 1
65+
assert "Invalid value" in str(w[0].message)
66+
assert result is True # Falls back to default behavior

pkg-r/NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# querychat (development version)
22

3+
* Tool detail cards can now be expanded or collapsed by default when querychat runs a query or updates the dashboard via the `querychat.tool_details` R option or the `QUERYCHAT_TOOL_DETAILS` environment variable. Valid values are `"expanded"`, `"collapsed"`, or `"default"`. (#137)
4+
35
* Added bookmarking support to `QueryChat$server()` and `querychat_app()`. When bookmarking is enabled (via `bookmark_store = "url"` or `"server"` in `querychat_app()` or `$app_obj()`, or via `enable_bookmarking = TRUE` in `$server()`), the chat state (including current query, title, and chat history) will be saved and restored with Shiny bookmarks. (#107)
46

57
* Nearly the entire functional API (i.e., `querychat_init()`, `querychat_sidebar()`, `querychat_server()`, etc) has been hard deprecated in favor of a simpler OOP-based API. Namely, the new `QueryChat$new()` class is now the main entry point (instead of `querychat_init()`) and has methods to replace old functions (e.g., `$sidebar()`, `$server()`, etc). (#109)

pkg-r/R/querychat_tools.R

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,48 @@ tool_query <- function(data_source) {
108108
)
109109
}
110110

111+
querychat_tool_details_option <- function() {
112+
opt <- getOption("querychat.tool_details", NULL)
113+
if (!is.null(opt)) {
114+
setting <- opt
115+
} else {
116+
env <- Sys.getenv("QUERYCHAT_TOOL_DETAILS", "")
117+
if (nzchar(env)) {
118+
setting <- env
119+
} else {
120+
return(NULL)
121+
}
122+
}
123+
124+
setting <- tolower(setting)
125+
valid_settings <- c("expanded", "collapsed", "default")
126+
127+
if (!setting %in% valid_settings) {
128+
cli::cli_warn(c(
129+
"Invalid value for {.code querychat.tool_details} or {.envvar QUERYCHAT_TOOL_DETAILS}: {.val {setting}}",
130+
"i" = "Must be one of: {.or {.val {valid_settings}}}"
131+
))
132+
return(NULL)
133+
}
134+
135+
setting
136+
}
137+
138+
querychat_tool_starts_open <- function(action) {
139+
setting <- querychat_tool_details_option()
140+
141+
if (is.null(setting)) {
142+
return(action != "reset")
143+
}
144+
145+
switch(
146+
setting,
147+
"expanded" = TRUE,
148+
"collapsed" = FALSE,
149+
action != "reset"
150+
)
151+
}
152+
111153
querychat_tool_result <- function(
112154
data_source,
113155
query,
@@ -182,7 +224,7 @@ querychat_tool_result <- function(
182224
title = if (action == "update" && !is.null(title)) title,
183225
show_request = is_error,
184226
markdown = display_md,
185-
open = !is_error && action != "reset"
227+
open = querychat_tool_starts_open(action)
186228
)
187229
)
188230
)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
test_that("querychat_tool_starts_open respects default behavior", {
2+
withr::local_options(querychat.tool_details = NULL)
3+
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = NA)
4+
5+
expect_true(querychat_tool_starts_open("query"))
6+
expect_true(querychat_tool_starts_open("update"))
7+
expect_false(querychat_tool_starts_open("reset"))
8+
})
9+
10+
test_that("querychat_tool_starts_open respects 'expanded' setting via option", {
11+
withr::local_options(querychat.tool_details = "expanded")
12+
13+
expect_true(querychat_tool_starts_open("query"))
14+
expect_true(querychat_tool_starts_open("update"))
15+
expect_true(querychat_tool_starts_open("reset"))
16+
})
17+
18+
test_that("querychat_tool_starts_open respects 'collapsed' setting via option", {
19+
withr::local_options(querychat.tool_details = "collapsed")
20+
21+
expect_false(querychat_tool_starts_open("query"))
22+
expect_false(querychat_tool_starts_open("update"))
23+
expect_false(querychat_tool_starts_open("reset"))
24+
})
25+
26+
test_that("querychat_tool_starts_open respects 'default' setting via option", {
27+
withr::local_options(querychat.tool_details = "default")
28+
29+
expect_true(querychat_tool_starts_open("query"))
30+
expect_true(querychat_tool_starts_open("update"))
31+
expect_false(querychat_tool_starts_open("reset"))
32+
})
33+
34+
test_that("querychat_tool_starts_open respects 'expanded' setting via envvar", {
35+
withr::local_options(querychat.tool_details = NULL)
36+
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "expanded")
37+
38+
expect_true(querychat_tool_starts_open("query"))
39+
expect_true(querychat_tool_starts_open("update"))
40+
expect_true(querychat_tool_starts_open("reset"))
41+
})
42+
43+
test_that("querychat_tool_starts_open respects 'collapsed' setting via envvar", {
44+
withr::local_options(querychat.tool_details = NULL)
45+
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "collapsed")
46+
47+
expect_false(querychat_tool_starts_open("query"))
48+
expect_false(querychat_tool_starts_open("update"))
49+
expect_false(querychat_tool_starts_open("reset"))
50+
})
51+
52+
test_that("querychat_tool_starts_open is case-insensitive", {
53+
withr::local_options(querychat.tool_details = "EXPANDED")
54+
expect_true(querychat_tool_starts_open("query"))
55+
56+
withr::local_options(querychat.tool_details = "Collapsed")
57+
expect_false(querychat_tool_starts_open("query"))
58+
})
59+
60+
test_that("querychat_tool_starts_open warns on invalid setting", {
61+
withr::local_options(querychat.tool_details = "invalid")
62+
63+
expect_warning(
64+
querychat_tool_starts_open("query"),
65+
"Invalid value for"
66+
)
67+
})
68+
69+
test_that("option takes precedence over environment variable", {
70+
withr::local_options(querychat.tool_details = "collapsed")
71+
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "expanded")
72+
73+
expect_false(querychat_tool_starts_open("query"))
74+
})

0 commit comments

Comments
 (0)