Skip to content

Commit 9a1b6a0

Browse files
committed
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
1 parent 1d87120 commit 9a1b6a0

File tree

6 files changed

+272
-5
lines changed

6 files changed

+272
-5
lines changed

pkg-py/src/querychat/_utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import os
4+
import warnings
45
from contextlib import contextmanager
56
from typing import TYPE_CHECKING, Optional
67

@@ -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[str]:
58+
"""
59+
Get the tool details setting from environment variable.
60+
61+
Returns
62+
-------
63+
Optional[str]
64+
The value of QUERYCHAT_TOOL_DETAILS environment variable, or None if not set
65+
66+
"""
67+
return os.environ.get("QUERYCHAT_TOOL_DETAILS")
68+
69+
70+
def resolve_tool_open_state(action: str, is_error: bool) -> bool:
71+
"""
72+
Determine whether a tool card should be open based on action and setting.
73+
74+
Parameters
75+
----------
76+
action : str
77+
The action type ('update', 'query', or 'reset')
78+
is_error : bool
79+
Whether an error occurred
80+
81+
Returns
82+
-------
83+
bool
84+
True if the tool card should be open, False otherwise
85+
86+
"""
87+
# If there's an error, always show collapsed
88+
if is_error:
89+
return False
90+
91+
# Get the tool details setting
92+
setting = get_tool_details_setting()
93+
94+
# If no setting, use default behavior
95+
if setting is None:
96+
return action != "reset"
97+
98+
# Validate and apply the setting
99+
setting_lower = setting.lower()
100+
if setting_lower == "expanded":
101+
return True
102+
elif setting_lower == "collapsed":
103+
return False
104+
elif setting_lower == "default":
105+
return action != "reset"
106+
else:
107+
warnings.warn(
108+
f"Invalid value for QUERYCHAT_TOOL_DETAILS: {setting!r}. "
109+
"Must be one of: 'expanded', 'collapsed', or 'default'",
110+
UserWarning,
111+
stacklevel=2,
112+
)
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, resolve_tool_open_state
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=resolve_tool_open_state("update", False),
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=resolve_tool_open_state("reset", False),
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=resolve_tool_open_state("query", False),
212212
icon=bs_icon("table"),
213213
),
214214
},

pkg-py/tests/test_tools.py

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

pkg-r/R/querychat_tools.R

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

111+
resolve_tool_open_state <- function(action, is_error) {
112+
# If there's an error, always show collapsed
113+
if (is_error) {
114+
return(FALSE)
115+
}
116+
117+
# Get the tool details setting
118+
setting <- querychat_tool_details_option()
119+
120+
# If no setting, use default behavior
121+
if (is.null(setting)) {
122+
return(action != "reset")
123+
}
124+
125+
# Validate and apply the setting
126+
setting <- tolower(setting)
127+
switch(
128+
setting,
129+
"expanded" = TRUE,
130+
"collapsed" = FALSE,
131+
"default" = action != "reset",
132+
{
133+
cli::cli_warn(c(
134+
"Invalid value for {.code querychat.tool_details} or {.envvar QUERYCHAT_TOOL_DETAILS}: {.val {setting}}",
135+
"i" = "Must be one of: {.val expanded}, {.val collapsed}, or {.val default}"
136+
))
137+
action != "reset"
138+
}
139+
)
140+
}
141+
111142
querychat_tool_result <- function(
112143
data_source,
113144
query,
@@ -182,7 +213,7 @@ querychat_tool_result <- function(
182213
title = if (action == "update" && !is.null(title)) title,
183214
show_request = is_error,
184215
markdown = display_md,
185-
open = !is_error && action != "reset"
216+
open = resolve_tool_open_state(action, is_error)
186217
)
187218
)
188219
)

pkg-r/R/utils-ellmer.R

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,17 @@ querychat_client_option <- function() {
5757

5858
NULL
5959
}
60+
61+
querychat_tool_details_option <- function() {
62+
opt <- getOption("querychat.tool_details", NULL)
63+
if (!is.null(opt)) {
64+
return(opt)
65+
}
66+
67+
env <- Sys.getenv("QUERYCHAT_TOOL_DETAILS", "")
68+
if (nzchar(env)) {
69+
return(env)
70+
}
71+
72+
NULL
73+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
test_that("resolve_tool_open_state respects default behavior", {
2+
withr::local_options(querychat.tool_details = NULL)
3+
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = NA)
4+
5+
expect_true(resolve_tool_open_state("query", FALSE))
6+
expect_true(resolve_tool_open_state("update", FALSE))
7+
expect_false(resolve_tool_open_state("reset", FALSE))
8+
expect_false(resolve_tool_open_state("query", TRUE))
9+
})
10+
11+
test_that("resolve_tool_open_state respects 'expanded' setting via option", {
12+
withr::local_options(querychat.tool_details = "expanded")
13+
14+
expect_true(resolve_tool_open_state("query", FALSE))
15+
expect_true(resolve_tool_open_state("update", FALSE))
16+
expect_true(resolve_tool_open_state("reset", FALSE))
17+
expect_false(resolve_tool_open_state("query", TRUE))
18+
})
19+
20+
test_that("resolve_tool_open_state respects 'collapsed' setting via option", {
21+
withr::local_options(querychat.tool_details = "collapsed")
22+
23+
expect_false(resolve_tool_open_state("query", FALSE))
24+
expect_false(resolve_tool_open_state("update", FALSE))
25+
expect_false(resolve_tool_open_state("reset", FALSE))
26+
expect_false(resolve_tool_open_state("query", TRUE))
27+
})
28+
29+
test_that("resolve_tool_open_state respects 'default' setting via option", {
30+
withr::local_options(querychat.tool_details = "default")
31+
32+
expect_true(resolve_tool_open_state("query", FALSE))
33+
expect_true(resolve_tool_open_state("update", FALSE))
34+
expect_false(resolve_tool_open_state("reset", FALSE))
35+
expect_false(resolve_tool_open_state("query", TRUE))
36+
})
37+
38+
test_that("resolve_tool_open_state respects 'expanded' setting via envvar", {
39+
withr::local_options(querychat.tool_details = NULL)
40+
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "expanded")
41+
42+
expect_true(resolve_tool_open_state("query", FALSE))
43+
expect_true(resolve_tool_open_state("update", FALSE))
44+
expect_true(resolve_tool_open_state("reset", FALSE))
45+
})
46+
47+
test_that("resolve_tool_open_state respects 'collapsed' setting via envvar", {
48+
withr::local_options(querychat.tool_details = NULL)
49+
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "collapsed")
50+
51+
expect_false(resolve_tool_open_state("query", FALSE))
52+
expect_false(resolve_tool_open_state("update", FALSE))
53+
expect_false(resolve_tool_open_state("reset", FALSE))
54+
})
55+
56+
test_that("resolve_tool_open_state is case-insensitive", {
57+
withr::local_options(querychat.tool_details = "EXPANDED")
58+
expect_true(resolve_tool_open_state("query", FALSE))
59+
60+
withr::local_options(querychat.tool_details = "Collapsed")
61+
expect_false(resolve_tool_open_state("query", FALSE))
62+
})
63+
64+
test_that("resolve_tool_open_state warns on invalid setting", {
65+
withr::local_options(querychat.tool_details = "invalid")
66+
67+
expect_warning(
68+
resolve_tool_open_state("query", FALSE),
69+
"Invalid value for"
70+
)
71+
})
72+
73+
test_that("option takes precedence over environment variable", {
74+
withr::local_options(querychat.tool_details = "collapsed")
75+
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "expanded")
76+
77+
expect_false(resolve_tool_open_state("query", FALSE))
78+
})

0 commit comments

Comments
 (0)