Skip to content
Merged
54 changes: 54 additions & 0 deletions pkg-py/src/querychat/_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import warnings
from contextlib import contextmanager
from typing import TYPE_CHECKING, Optional

Expand Down Expand Up @@ -53,6 +54,59 @@ def temp_env_vars(env_vars: dict[str, Optional[str]]):
os.environ[key] = original_value


def get_tool_details_setting() -> Optional[str]:
"""
Get the tool details setting from environment variable.

Returns
-------
Optional[str]
The value of QUERYCHAT_TOOL_DETAILS environment variable, or None if not set

"""
return os.environ.get("QUERYCHAT_TOOL_DETAILS")


def querychat_tool_starts_open(action: str) -> bool:
"""
Determine whether a tool card should be open based on action and setting.

Parameters
----------
action : str
The action type ('update', 'query', or 'reset')

Returns
-------
bool
True if the tool card should be open, False otherwise

"""
# Get the tool details setting
setting = get_tool_details_setting()

# If no setting, use default behavior
if setting is None:
return action != "reset"

# Validate and apply the setting
setting_lower = setting.lower()
if setting_lower == "expanded":
return True
elif setting_lower == "collapsed":
return False
elif setting_lower == "default":
return action != "reset"
else:
warnings.warn(
f"Invalid value for QUERYCHAT_TOOL_DETAILS: {setting!r}. "
"Must be one of: 'expanded', 'collapsed', or 'default'",
UserWarning,
stacklevel=2,
)
return action != "reset"


def df_to_html(df: IntoFrame, maxrows: int = 5) -> str:
"""
Convert a DataFrame to an HTML table for display in chat.
Expand Down
8 changes: 4 additions & 4 deletions pkg-py/src/querychat/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from shinychat.types import ToolResultDisplay

from ._icons import bs_icon
from ._utils import df_to_html
from ._utils import df_to_html, querychat_tool_starts_open

if TYPE_CHECKING:
from ._datasource import DataSource
Expand Down Expand Up @@ -65,7 +65,7 @@ def update_dashboard(query: str, title: str) -> ContentToolResult:
markdown=markdown + f"\n\n{button_html}",
title=title,
show_request=False,
open=True,
open=querychat_tool_starts_open("update"),
icon=bs_icon("funnel-fill"),
),
},
Expand Down Expand Up @@ -139,7 +139,7 @@ def reset_dashboard() -> ContentToolResult:
markdown=button_html,
title=None,
show_request=False,
open=False,
open=querychat_tool_starts_open("reset"),
icon=bs_icon("arrow-counterclockwise"),
),
},
Expand Down Expand Up @@ -208,7 +208,7 @@ def query(query: str, _intent: str = "") -> ContentToolResult:
"display": ToolResultDisplay(
markdown=markdown,
show_request=False,
open=True,
open=querychat_tool_starts_open("query"),
icon=bs_icon("table"),
),
},
Expand Down
66 changes: 66 additions & 0 deletions pkg-py/tests/test_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""Tests for tool functions and utilities."""

import warnings

from querychat._utils import querychat_tool_starts_open


def test_querychat_tool_starts_open_default_behavior(monkeypatch):
"""Test default behavior when no setting is provided."""
monkeypatch.delenv("QUERYCHAT_TOOL_DETAILS", raising=False)

assert querychat_tool_starts_open("query") is True
assert querychat_tool_starts_open("update") is True
assert querychat_tool_starts_open("reset") is False


def test_querychat_tool_starts_open_expanded(monkeypatch):
"""Test 'expanded' setting."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "expanded")

assert querychat_tool_starts_open("query") is True
assert querychat_tool_starts_open("update") is True
assert querychat_tool_starts_open("reset") is True


def test_querychat_tool_starts_open_collapsed(monkeypatch):
"""Test 'collapsed' setting."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "collapsed")

assert querychat_tool_starts_open("query") is False
assert querychat_tool_starts_open("update") is False
assert querychat_tool_starts_open("reset") is False


def test_querychat_tool_starts_open_default_setting(monkeypatch):
"""Test 'default' setting."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "default")

assert querychat_tool_starts_open("query") is True
assert querychat_tool_starts_open("update") is True
assert querychat_tool_starts_open("reset") is False


def test_querychat_tool_starts_open_case_insensitive(monkeypatch):
"""Test that setting is case-insensitive."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "EXPANDED")
assert querychat_tool_starts_open("query") is True

monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "Collapsed")
assert querychat_tool_starts_open("query") is False

monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "DeFaUlT")
assert querychat_tool_starts_open("query") is True


def test_querychat_tool_starts_open_invalid_setting(monkeypatch):
"""Test warning on invalid setting."""
monkeypatch.setenv("QUERYCHAT_TOOL_DETAILS", "invalid")

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = querychat_tool_starts_open("query")

assert len(w) == 1
assert "Invalid value" in str(w[0].message)
assert result is True # Falls back to default behavior
28 changes: 27 additions & 1 deletion pkg-r/R/querychat_tools.R
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,32 @@ tool_query <- function(data_source) {
)
}

querychat_tool_starts_open <- function(action) {
# Get the tool details setting
setting <- querychat_tool_details_option()

# If no setting, use default behavior
if (is.null(setting)) {
return(action != "reset")
}

# Validate and apply the setting
setting <- tolower(setting)
switch(
setting,
"expanded" = TRUE,
"collapsed" = FALSE,
"default" = action != "reset",
{
cli::cli_warn(c(
"Invalid value for {.code querychat.tool_details} or {.envvar QUERYCHAT_TOOL_DETAILS}: {.val {setting}}",
"i" = "Must be one of: {.val expanded}, {.val collapsed}, or {.val default}"
))
action != "reset"
}
)
}

querychat_tool_result <- function(
data_source,
query,
Expand Down Expand Up @@ -182,7 +208,7 @@ querychat_tool_result <- function(
title = if (action == "update" && !is.null(title)) title,
show_request = is_error,
markdown = display_md,
open = !is_error && action != "reset"
open = querychat_tool_starts_open(action)
)
)
)
Expand Down
14 changes: 14 additions & 0 deletions pkg-r/R/utils-ellmer.R
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,17 @@ querychat_client_option <- function() {

NULL
}

querychat_tool_details_option <- function() {
opt <- getOption("querychat.tool_details", NULL)
if (!is.null(opt)) {
return(opt)
}

env <- Sys.getenv("QUERYCHAT_TOOL_DETAILS", "")
if (nzchar(env)) {
return(env)
}

NULL
}
74 changes: 74 additions & 0 deletions pkg-r/tests/testthat/test-querychat_tools.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
test_that("querychat_tool_starts_open respects default behavior", {
withr::local_options(querychat.tool_details = NULL)
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = NA)

expect_true(querychat_tool_starts_open("query"))
expect_true(querychat_tool_starts_open("update"))
expect_false(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'expanded' setting via option", {
withr::local_options(querychat.tool_details = "expanded")

expect_true(querychat_tool_starts_open("query"))
expect_true(querychat_tool_starts_open("update"))
expect_true(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'collapsed' setting via option", {
withr::local_options(querychat.tool_details = "collapsed")

expect_false(querychat_tool_starts_open("query"))
expect_false(querychat_tool_starts_open("update"))
expect_false(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'default' setting via option", {
withr::local_options(querychat.tool_details = "default")

expect_true(querychat_tool_starts_open("query"))
expect_true(querychat_tool_starts_open("update"))
expect_false(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'expanded' setting via envvar", {
withr::local_options(querychat.tool_details = NULL)
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "expanded")

expect_true(querychat_tool_starts_open("query"))
expect_true(querychat_tool_starts_open("update"))
expect_true(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open respects 'collapsed' setting via envvar", {
withr::local_options(querychat.tool_details = NULL)
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "collapsed")

expect_false(querychat_tool_starts_open("query"))
expect_false(querychat_tool_starts_open("update"))
expect_false(querychat_tool_starts_open("reset"))
})

test_that("querychat_tool_starts_open is case-insensitive", {
withr::local_options(querychat.tool_details = "EXPANDED")
expect_true(querychat_tool_starts_open("query"))

withr::local_options(querychat.tool_details = "Collapsed")
expect_false(querychat_tool_starts_open("query"))
})

test_that("querychat_tool_starts_open warns on invalid setting", {
withr::local_options(querychat.tool_details = "invalid")

expect_warning(
querychat_tool_starts_open("query"),
"Invalid value for"
)
})

test_that("option takes precedence over environment variable", {
withr::local_options(querychat.tool_details = "collapsed")
withr::local_envvar(QUERYCHAT_TOOL_DETAILS = "expanded")

expect_false(querychat_tool_starts_open("query"))
})