Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Suggests:
bslib (>= 0.7.0),
chromote,
DBI,
devtools,
duckdb,
evaluate,
fansi,
Expand All @@ -60,6 +61,7 @@ Suggests:
htmltools,
pandoc,
ragg,
roxygen2,
shiny,
shinychat (>= 0.2.0),
testthat (>= 3.0.0),
Expand Down Expand Up @@ -95,6 +97,7 @@ Collate:
'tool-files.R'
'tool-git.R'
'tool-github.R'
'tool-pkg.R'
'tool-rstudioapi.R'
'tool-run.R'
'tool-search-packages.R'
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export(btw_tool_git_log)
export(btw_tool_git_status)
export(btw_tool_github)
export(btw_tool_ide_read_current_editor)
export(btw_tool_pkg_check)
export(btw_tool_pkg_document)
export(btw_tool_pkg_test)
export(btw_tool_run_r)
export(btw_tool_search_package_info)
export(btw_tool_search_packages)
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# btw (development version)

* New "pkg" tool group with package development tools: `btw_tool_pkg_document()`, `btw_tool_pkg_check()`, and `btw_tool_pkg_test()` provide LLMs with the ability to document, check, and test R packages during development (#133).

* New `btw_tool_run_r()` tool allows LLMs to run R code and to see the output, including of plots. Because this tool lets LLMs run R arbitrary R code in the global environment (which can be great but can also have security implications), it is opt-in and disabled by default. See `?btw_tool_run_r` for more details (#126).

* `btw_tool_docs_help_page()` now uses markdown headings and sections for argument descriptions, rather than a table. This is considerably more token efficient when the argument descriptions have more than one paragraph and can't be converted into a markdown table (@jeanchristophe13v, #123).
Expand Down
1 change: 1 addition & 0 deletions R/btw_client_app.R
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ app_tool_group_choice_input <- function(
"git" = shiny::span(label_icon, "Git"),
"github" = shiny::span(label_icon, "GitHub"),
"ide" = shiny::span(label_icon, "IDE"),
"pkg" = shiny::span(label_icon, "Package Tools"),
"run" = shiny::span(label_icon, "Run Code"),
"search" = shiny::span(label_icon, "Search"),
"session" = shiny::span(label_icon, "Session Info"),
Expand Down
4 changes: 3 additions & 1 deletion R/btw_this.R
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,9 @@ btw_this_file_path <- function(x) {
path <- "."
}
if (fs::is_file(path)) {
return(btw_tool_files_read_text_file_impl(path, check_within_wd = FALSE)@value)
return(
btw_tool_files_read_text_file_impl(path, check_within_wd = FALSE)@value
)
} else {
return(btw_tool_files_list_files_impl(path, check_within_wd = FALSE)@value)
}
Expand Down
223 changes: 223 additions & 0 deletions R/tool-pkg.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
#' @include tool-result.R
NULL

has_devtools <- function() {
is_installed("devtools")
}

has_roxygen2 <- function() {
is_installed("roxygen2")
}

# btw_tool_pkg_document --------------------------------------------------------

#' Tool: Generate package documentation
#'
#' Generate package documentation using [devtools::document()]. This runs
#' \pkg{roxygen2} on the package to create/update man pages and `NAMESPACE`.
#'
#' @param pkg Path to package directory. Defaults to `"."`. Must be within
#' current working directory.
#' @inheritParams btw_tool_docs_package_news
#'
#' @returns The output from [devtools::document()].
#'
#' @seealso [btw_tools()]
#' @family Tools
#' @export
btw_tool_pkg_document <- function(pkg = ".", `_intent`) {}

btw_tool_pkg_document_impl <- function(pkg = ".") {
check_string(pkg)
check_path_within_current_wd(pkg)

code <- sprintf(
'devtools::document(pkg = "%s", roclets = NULL, quiet = FALSE)',
pkg
)

btw_tool_run_r_impl(code)
}

.btw_add_to_tools(
name = "btw_tool_pkg_document",
group = "pkg",
tool = function() {
ellmer::tool(
btw_tool_pkg_document_impl,
name = "btw_tool_pkg_document",
description = "Generate package documentation.

Runs `devtools::document()` which processes roxygen2 tags to:
- Create/update .Rd help files in man/ directory
- Update NAMESPACE with exports and imports
- Update Collate field in DESCRIPTION if needed

Use this after adding or modifying roxygen2 comments in your R code. The tool modifies files but changes are safe and can be committed to version control. Returns a summary of files created or updated.",
annotations = ellmer::tool_annotations(
title = "Package Document",
read_only_hint = FALSE,
idempotent_hint = TRUE,
btw_can_register = function() has_devtools() && has_roxygen2()
),
arguments = list(
pkg = ellmer::type_string(
"Path to package directory. Defaults to '.'. Must be within current working directory.",
required = FALSE
)
)
)
}
)

# btw_tool_pkg_check -----------------------------------------------------------

#' Tool: Run R CMD check on a package
#'
#' Run R CMD check on a package using [devtools::check()]. This performs
#' comprehensive checks on the package structure, code, and documentation.
#'
#' The check runs with `remote = TRUE`, `cran = TRUE`, `manual = FALSE`, and
#' `error_on = "never"` to provide comprehensive feedback without failing.
#'
#' @param pkg Path to package directory. Defaults to '.'. Must be within
#' current working directory.
#' @inheritParams btw_tool_docs_package_news
#'
#' @returns The output from [devtools::check()].
#'
#' @seealso [btw_tools()]
#' @family Tools
#' @export
btw_tool_pkg_check <- function(pkg = ".", `_intent`) {}

btw_tool_pkg_check_impl <- function(pkg = ".") {
check_string(pkg)
check_path_within_current_wd(pkg)

code <- sprintf(
'devtools::check(pkg = "%s", remote = TRUE, cran = TRUE, manual = FALSE, quiet = FALSE, error_on = "never")',
pkg
)

btw_tool_run_r_impl(code)
}

.btw_add_to_tools(
name = "btw_tool_pkg_check",
group = "pkg",
tool = function() {
ellmer::tool(
btw_tool_pkg_check_impl,
name = "btw_tool_pkg_check",
description = "Run comprehensive package checks.

Use this tool to verify the package is ready for release or CRAN submission.

Runs devtools::check() which builds the package and performs ~50 checks including:
- Documentation completeness and validity
- Code syntax and best practices
- Example code execution
- Test suite execution
- Vignette building (if present)
- CRAN policy compliance

This tool runs with CRAN-like settings and takes several minutes to complete. It always completes and reports findings even if errors are found.

For iterative development, use the `btw_tool_pkg_test` if available or `devtools::test()` to run only the test suite for faster feedback.",
annotations = ellmer::tool_annotations(
title = "Package Check",
read_only_hint = FALSE,
idempotent_hint = TRUE,
btw_can_register = function() has_devtools()
),
arguments = list(
pkg = ellmer::type_string(
"Path to package directory. Defaults to '.'. Must be within current working directory.",
required = FALSE
)
)
)
}
)

# btw_tool_pkg_test ------------------------------------------------------------

#' Tool: Run package tests
#'
#' Run package tests using [devtools::test()]. Optionally filter tests by name
#' pattern.
#'
#' @param pkg Path to package directory. Defaults to '.'. Must be within
#' current working directory.
#' @param filter Optional regex to filter test files. Example: 'helper' matches
#' 'test-helper.R'.
#' @inheritParams btw_tool_docs_package_news
#'
#' @returns The output from [devtools::test()].
#'
#' @seealso [btw_tools()]
#' @family Tools
#' @export
btw_tool_pkg_test <- function(pkg = ".", filter = NULL, `_intent`) {}

btw_tool_pkg_test_impl <- function(pkg = ".", filter = NULL) {
check_string(pkg)
check_path_within_current_wd(pkg)

filter_arg <- if (!is.null(filter)) {
check_string(filter)
sprintf(', filter = "%s"', filter)
} else {
""
}

code <- sprintf(
'devtools::test(pkg = "%s"%s, stop_on_failure = FALSE, export_all = TRUE, reporter = "check")',
pkg,
filter_arg
)

btw_tool_run_r_impl(code)
}

.btw_add_to_tools(
name = "btw_tool_pkg_test",
group = "pkg",
tool = function() {
ellmer::tool(
btw_tool_pkg_test_impl,
name = "btw_tool_pkg_test",
description = "Run testthat tests for an R package.

Runs `devtools::test()` which executes the test suite in tests/testthat/ and reports:
- Number of tests passed, failed, warned, and skipped
- Detailed failure messages with file locations
- Test execution time

The filter parameter accepts a regular expression matched against test file names after stripping the 'test-' prefix and '.R' extension. For example:
- filter = 'helper' runs test-helper.R
- filter = 'tool-.*' runs test-tool-docs.R, test-tool-files.R, etc.
- No filter runs all tests
- It is common to pair `test-{name}.R` with a source `{name}.R` file. To test this file, you can generally use filter = '{name}'.

Use `filter` when working on specific functionality to get faster feedback. The tool always runs all matching tests to completion regardless of failures.",
annotations = ellmer::tool_annotations(
title = "Package Test",
read_only_hint = FALSE,
idempotent_hint = TRUE,
btw_can_register = function() has_devtools()
),
arguments = list(
pkg = ellmer::type_string(
"Path to package directory. Defaults to '.'. Must be within current working directory.",
required = FALSE
),
filter = ellmer::type_string(
"Optional regex to filter test files. Example: 'helper' matches 'test-helper.R'.",
required = FALSE
)
)
)
}
)
11 changes: 8 additions & 3 deletions R/tool-run.R
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,13 @@ btw_tool_run_r_impl <- function(code, .envir = global_env()) {
last_plot <<- NULL
}

local_reproducible_output(disable_ansi_features = !is_installed("fansi"))

# Ensure working directory, options, envvar are restored after execution
withr::local_dir(getwd())
withr::local_options()
withr::local_envvar()

local_reproducible_output(disable_ansi_features = !is_installed("fansi"))

# Create output handler that converts to Content types as outputs are generated
handler <- evaluate::new_output_handler(
source = function(src, expr) {
Expand Down Expand Up @@ -520,13 +520,17 @@ S7::method(contents_shinychat, BtwRunToolResult) <- function(content) {
request_id <- NULL
tool_title <- NULL

display <- content@extra$display %||% list()
annotations <- list()

if (!is.null(content@request)) {
request_id <- content@request@id

tool_title <- NULL
tool <- content@request@tool
annotations <- tool@annotations
if (!is.null(tool)) {
tool_title <- tool@annotations$title
tool_title <- annotations$title
}
}

Expand All @@ -537,6 +541,7 @@ S7::method(contents_shinychat, BtwRunToolResult) <- function(content) {
code = code,
status = status,
`tool-title` = tool_title,
icon = display$icon %||% annotations$icon,
htmltools::HTML(output_html),
btw_run_tool_card_dep()
)
Expand Down
1 change: 1 addition & 0 deletions R/tools.R
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ tool_group_icon <- function(group, default = NULL) {
"git" = tool_icon("git"),
"github" = tool_icon("github"),
"ide" = tool_icon("code-blocks"),
"pkg" = tool_icon("package"),
"search" = tool_icon("search"),
"session" = tool_icon("screen-search-desktop"),
"web" = tool_icon("globe-book"),
Expand Down
11 changes: 7 additions & 4 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,16 @@ local_reproducible_output <- function(
) {
# Replicating testthat::local_reproducible_output()
withr::local_options(width = width, cli.width = width, .local_envir = .env)
withr::local_envvar(RSTUDIO_CONSOLE_WIDTH = width, .local_envir = .env)
withr::local_envvar(
RSTUDIO_CONSOLE_WIDTH = width,
R_CLI_DYNAMIC = "false",
.local_envir = .env
)

if (disable_ansi_features) {
withr::local_envvar(list(NO_COLOR = "true"), .local_envir = .env)
withr::local_envvar(NO_COLOR = "true", .local_envir = .env)
withr::local_options(
crayon.enabled = FALSE,
cli.dynamic = FALSE,
cli.unicode = FALSE,
cli.condition_width = Inf,
cli.num_colors = 1L,
Expand All @@ -219,7 +222,7 @@ local_reproducible_output <- function(
}

withr::local_options(
cil.dynamic = FALSE,
cli.dynamic = FALSE,
cli.spinner = FALSE,
cli.hyperlink = FALSE,
cli.hyperlink_run = FALSE,
Expand Down
1 change: 1 addition & 0 deletions inst/icons/package.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion inst/js/run-r/btw-run-r.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class BtwRunRResult extends HTMLElement {
super()

this.toolTitle = this.getAttribute("tool-title") || "Run R Code"
this.icon = this.getAttribute("icon") || ICONS.playCircle
}

connectedCallback() {
Expand All @@ -57,7 +58,6 @@ class BtwRunRResult extends HTMLElement {
this.titleTemplate = "{title} failed"
} else {
this.classStatus = ""
this.icon = ICONS.playCircle
this.titleTemplate = "{title}"
}

Expand Down
3 changes: 3 additions & 0 deletions man/btw_tool_docs_package_news.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading