From a4b261d885e886e0b945915a643fc600ae2e1fc3 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 23 Feb 2026 09:06:46 -0500 Subject: [PATCH 1/7] feat: btw_task() --- .gitignore | 1 + DESCRIPTION | 1 + NAMESPACE | 1 + R/btw_task.R | 268 +++++++++++++++++++++++++++++++++ inst/tasks/README.md | 95 ++++++++++++ inst/tasks/analyze-package.md | 17 +++ inst/tasks/code-review.md | 36 +++++ inst/tasks/data-summary.md | 27 ++++ man/btw_agent_tool.Rd | 2 +- man/btw_task.Rd | 112 ++++++++++++++ man/btw_task_create_btw_md.Rd | 1 + man/btw_task_create_readme.Rd | 1 + tests/testthat/test-btw_task.R | 208 +++++++++++++++++++++++++ 13 files changed, 769 insertions(+), 1 deletion(-) create mode 100644 R/btw_task.R create mode 100644 inst/tasks/README.md create mode 100644 inst/tasks/analyze-package.md create mode 100644 inst/tasks/code-review.md create mode 100644 inst/tasks/data-summary.md create mode 100644 man/btw_task.Rd create mode 100644 tests/testthat/test-btw_task.R diff --git a/.gitignore b/.gitignore index 372e6a0f..18965bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .quarto docs .claude/settings.local.json +_dev diff --git a/DESCRIPTION b/DESCRIPTION index b8b29507..e9d3ff5f 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -88,6 +88,7 @@ Collate: 'btw.R' 'btw_client.R' 'btw_client_app.R' + 'btw_task.R' 'btw_this.R' 'clipboard.R' 'deprecated.R' diff --git a/NAMESPACE b/NAMESPACE index 746fa91c..948d1884 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -26,6 +26,7 @@ export(btw_app) export(btw_client) export(btw_mcp_server) export(btw_mcp_session) +export(btw_task) export(btw_task_create_btw_md) export(btw_task_create_readme) export(btw_this) diff --git a/R/btw_task.R b/R/btw_task.R new file mode 100644 index 00000000..49097090 --- /dev/null +++ b/R/btw_task.R @@ -0,0 +1,268 @@ +#' Run a pre-formatted btw task +#' +#' @description +#' Runs a btw task defined in a file with YAML frontmatter configuration and +#' a markdown body containing the task prompt. The task file format is similar +#' to `btw.md` files, with client and tool configuration in the frontmatter and +#' the task instructions in the body. +#' +#' ## Task File Format +#' +#' Task files use the same format as `btw.md` files: +#' +#' ```yaml +#' --- +#' client: +#' provider: anthropic +#' model: claude-sonnet-4 +#' tools: [docs, files] +#' --- +#' +#' Your task prompt here with {{ variable }} interpolation... +#' ``` +#' +#' ## Template Variables +#' +#' The task prompt body supports template variable interpolation using +#' `{{ variable }}` syntax via [ellmer::interpolate()]. Pass named arguments +#' to provide values for template variables: +#' +#' ```r +#' btw_task("my-task.md", package_name = "dplyr", version = "1.1.0") +#' ``` +#' +#' ## Additional Context +#' +#' Unnamed arguments are treated as additional context and converted to text +#' using [btw()]. This context is appended to the system prompt: +#' +#' ```r +#' btw_task("analyze.md", dataset_name = "mtcars", mtcars, my_function) +#' # ^-- template var ^-- additional context +#' ``` +#' +#' @param path Path to the task file containing YAML configuration and prompt. +#' @param ... Named arguments become template variables for interpolation in the +#' task prompt. Unnamed arguments are treated as additional context objects +#' and converted to text via [btw()]. +#' @param client An [ellmer::Chat] client to override the task file's client +#' configuration. If `NULL`, uses the client specified in the task file's +#' YAML frontmatter, falling back to the default client resolution of +#' [btw_client()]. +#' @param mode The execution mode for the task: +#' - `"app"`: Launch interactive Shiny app (default) +#' - `"console"`: Interactive console chat with [ellmer::live_console()] +#' - `"client"`: Return configured [ellmer::Chat] client without running +#' - `"tool"`: Return an [ellmer::tool()] object for programmatic use +#' +#' @return Depending on `mode`: +#' - `"app"`: Returns the chat client invisibly after launching the app +#' - `"console"`: Returns the chat client after console interaction +#' - `"client"`: Returns the configured chat client +#' - `"tool"`: Returns an [ellmer::tool()] object +#' +#' @examples +#' # Create a simple task file +#' tmp_task_file <- tempfile(fileext = ".md") +#' +#' cat(file = tmp_task_file, '--- +#' client: anthropic/claude-sonnet-4-6 +#' tools: [docs, files] +#' --- +#' +#' Analyze the {{ package_name }} package and create a summary. +#' ') +#' +#' # Task with template interpolation +#' btw_task(tmp_task_file, package_name = "dplyr", mode = "tool") +#' +#' # Include additional context +#' btw_task( +#' tmp_task_file, +#' package_name = "ggplot2", +#' mtcars, # Additional context +#' mode = "tool" +#' ) +#' +#' @family task and agent functions +#' @export +btw_task <- function( + path, + ..., + client = NULL, + mode = c("app", "console", "client", "tool") +) { + check_string(path) + mode <- arg_match(mode) + + # Read and parse the task file using existing utility + if (!fs::file_exists(path)) { + cli::cli_abort("Task file not found: {.path {path}}") + } + + # Use the existing read_single_btw_file function + task_config <- read_single_btw_file(path) + + # Extract the task prompt from btw_system_prompt field + task_prompt <- task_config$btw_system_prompt + + if (is.null(task_prompt) || !nzchar(task_prompt)) { + cli::cli_abort( + "Task file must contain a prompt in the body: {.path {path}}" + ) + } + + # Remove btw_system_prompt from config to avoid confusion with other fields + task_config$btw_system_prompt <- NULL + + # Separate named (template vars) and unnamed (context) arguments + dots <- dots_list(...) + dot_names <- names2(dots) + + # Named args are template variables + template_vars <- dots[nzchar(dot_names)] + + # Unnamed args are additional context + context_objects <- dots[!nzchar(dot_names)] + + # Interpolate template variables in the task prompt + if (length(template_vars) > 0) { + task_prompt <- ellmer::interpolate(task_prompt, !!!template_vars) + } + + # Create the client with configuration from task file + # Use provided client, or task file config, or btw defaults + if (is.null(client)) { + # Build client from task config + if (!is.null(task_config$client)) { + client <- task_config$client + } + } + + # Get tools from task config + tools <- task_config$tools %||% btw_tools() + + # Create btw client with resolved configuration + chat_client <- btw_client( + client = client, + tools = tools + ) + + # Build system prompt + base_system_prompt <- chat_client$get_system_prompt() + + # Add task prompt to system + sys_prompt_parts <- c( + base_system_prompt, + "---", + "# Task Instructions", + "", + task_prompt + ) + + # Add additional context if provided + if (length(context_objects) > 0) { + user_context <- do.call(btw, c(context_objects, list(clipboard = FALSE))) + if (nzchar(user_context)) { + sys_prompt_parts <- c( + sys_prompt_parts, + "", + "---", + "# Additional Context", + "", + user_context + ) + } + } + + final_system_prompt <- paste(sys_prompt_parts, collapse = "\n") + chat_client$set_system_prompt(final_system_prompt) + + # Mode dispatch + if (mode == "client") { + return(chat_client) + } + + if (mode == "tool") { + # Create a tool wrapper function (simplified without ... to avoid ellmer argument mismatch) + task_tool_fn <- function(prompt = "") { + # Clone to avoid state pollution + this_client <- chat_client$clone() + + # Append tool mode instructions + sys_prompt <- paste0( + this_client$get_system_prompt(), + "\n\n---\n\n", + "YOU ARE NOW OPERATING IN TOOL MODE. ", + "The user cannot respond directly to you. ", + "Because you cannot talk to the user, you will need to make ", + "your own decisions using the information available to you ", + "and the best of your abilities. ", + "You may do additional exploration if needed." + ) + + this_client$set_system_prompt(sys_prompt) + + # Add any additional prompt instructions + if (nzchar(prompt)) { + initial_message <- paste0( + "Additional instructions: ", + prompt + ) + } else { + initial_message <- "Please complete the task as instructed." + } + + this_client$chat(initial_message) + } + + # Create the tool definition + tool <- ellmer::tool( + task_tool_fn, + name = paste0("btw_task_", fs::path_ext_remove(basename(path))), + description = paste0( + "Run the task defined in ", + basename(path), + ". ", + substr(task_prompt, 1, 100), + if (nchar(task_prompt) > 100) "..." + ), + arguments = list( + prompt = ellmer::type_string( + "Additional instructions for the task. Leave empty to proceed with default instructions.", + required = FALSE + ) + ) + ) + + return(tool) + } + + # Console or app mode + if (mode == "console") { + task_name <- fs::path_ext_remove(basename(path)) + cli::cli_text( + "Starting {.strong btw_task()} for {.file {task_name}} in live console mode." + ) + cli::cli_text( + "{cli::col_yellow(cli::symbol$play)} ", + "Say \"{.strong {cli::col_magenta('Let\\'s get started.')}}\" to begin the task." + ) + ellmer::live_console(chat_client) + } else { + # App mode + task_name <- fs::path_ext_remove(basename(path)) + btw_app_from_client( + client = chat_client, + messages = list(list( + role = "assistant", + content = paste0( + "\U1F4CB Hi! I'm ready to help with the ", + htmltools::htmlEscape(task_name), + " task.

", + "Say Let's get started. to begin." + ) + )) + ) + } +} diff --git a/inst/tasks/README.md b/inst/tasks/README.md new file mode 100644 index 00000000..9b2d41ca --- /dev/null +++ b/inst/tasks/README.md @@ -0,0 +1,95 @@ +# btw Task Files + +This directory contains pre-defined task files for use with `btw_task()`. + +## Usage + +Tasks are defined in markdown files with YAML frontmatter for configuration and a markdown body containing the task instructions: + +```r +# Run a task with template variables +chat <- btw_task( + system.file("tasks/analyze-package.md", package = "btw"), + package_name = "dplyr", + mode = "console" +) + +# Include additional context (unnamed arguments) +chat <- btw_task( + system.file("tasks/data-summary.md", package = "btw"), + dataset_name = "mtcars", # Named - template variable + mtcars, # Unnamed - additional context + mode = "app" +) +``` + +## Available Tasks + +### analyze-package.md +Analyzes an R package and provides a comprehensive summary including purpose, key functions, dependencies, use cases, and comparisons to similar packages. + +**Template variables:** +- `package_name`: Name of the package to analyze + +### code-review.md +Reviews code files and provides detailed feedback on code quality, best practices, performance, documentation, and testing considerations. + +**Template variables:** +- `file_path`: Path to the file to review + +### data-summary.md +Creates comprehensive summaries of datasets including basic statistics, data quality assessment, and insights. + +**Template variables:** +- `dataset_name`: Name of the dataset to summarize + +## Task File Format + +```yaml +--- +client: # Optional: LLM client configuration + provider: anthropic + model: claude-sonnet-4 +tools: [docs, files] # Optional: btw tools to include +--- + +# Your task prompt here + +Use {{ variable_name }} syntax for template variables that can be +interpolated when running the task. +``` + +## Execution Modes + +Tasks can be run in four different modes: + +- **app**: Launch interactive Shiny app (default) +- **console**: Interactive console chat +- **client**: Return configured chat client +- **tool**: Return an ellmer tool object for programmatic use + +## Creating Custom Tasks + +1. Create a markdown file with YAML frontmatter and task instructions +2. Use `{{ variable }}` syntax for template placeholders +3. Run with `btw_task()` providing the path and template values + +Example custom task: + +```markdown +--- +tools: [files, git] +--- + +Review the recent changes in {{ file_path }} and suggest improvements. +Focus on {{ focus_area }} aspects of the code. +``` + +```r +chat <- btw_task( + "my-review-task.md", + file_path = "R/my-function.R", + focus_area = "performance", + mode = "console" +) +``` \ No newline at end of file diff --git a/inst/tasks/analyze-package.md b/inst/tasks/analyze-package.md new file mode 100644 index 00000000..9f0180f2 --- /dev/null +++ b/inst/tasks/analyze-package.md @@ -0,0 +1,17 @@ +--- +tools: [docs, sessioninfo] +--- + +# Package Analysis Task + +Analyze the R package **{{ package_name }}** and provide a comprehensive summary. + +## Your analysis should include: + +1. **Purpose**: What is the main purpose of this package? +2. **Key Functions**: Identify the 5-10 most important functions +3. **Dependencies**: What packages does it depend on? +4. **Use Cases**: Provide 2-3 example use cases +5. **Comparison**: How does it compare to similar packages (if any)? + +Please be thorough but concise in your analysis. \ No newline at end of file diff --git a/inst/tasks/code-review.md b/inst/tasks/code-review.md new file mode 100644 index 00000000..17cf32a5 --- /dev/null +++ b/inst/tasks/code-review.md @@ -0,0 +1,36 @@ +--- +tools: [files, git] +--- + +# Code Review Task + +Review the code in **{{ file_path }}** and provide detailed feedback. + +## Review Criteria + +Please evaluate the code based on: + +1. **Code Quality** + - Is the code readable and well-structured? + - Are variable and function names descriptive? + - Is the code DRY (Don't Repeat Yourself)? + +2. **Best Practices** + - Does it follow R/tidyverse style conventions? + - Are there appropriate comments where needed? + - Is error handling adequate? + +3. **Performance** + - Are there any obvious performance bottlenecks? + - Could any operations be vectorized? + - Are there more efficient alternatives? + +4. **Documentation** + - Are functions properly documented with roxygen2? + - Are complex logic sections explained? + +5. **Testing Considerations** + - What edge cases should be tested? + - Are there potential bugs or issues? + +Provide actionable suggestions for improvement with specific code examples where applicable. \ No newline at end of file diff --git a/inst/tasks/data-summary.md b/inst/tasks/data-summary.md new file mode 100644 index 00000000..31d0dbba --- /dev/null +++ b/inst/tasks/data-summary.md @@ -0,0 +1,27 @@ +--- +tools: [env] +--- + +# Data Summary Task + +Create a comprehensive summary of the dataset **{{ dataset_name }}**. + +## Analysis Requirements + +1. **Basic Statistics** + - Number of observations and variables + - Data types of each column + - Summary statistics for numeric variables + - Frequency tables for categorical variables + +2. **Data Quality** + - Missing values analysis + - Potential outliers + - Data validation issues + +3. **Insights** + - Key patterns or trends + - Interesting relationships between variables + - Recommendations for further analysis + +Format your response with clear sections and use markdown tables where appropriate. \ No newline at end of file diff --git a/man/btw_agent_tool.Rd b/man/btw_agent_tool.Rd index e2b3c600..0e3aa89b 100644 --- a/man/btw_agent_tool.Rd +++ b/man/btw_agent_tool.Rd @@ -65,7 +65,7 @@ from other icon packages. Supported packages:\tabular{lll}{ bsicons \tab \code{bsicons::house} \tab \code{\link[bsicons:bs_icon]{bsicons::bs_icon()}} \cr phosphoricons \tab \code{phosphoricons::house} \tab \code{\link[phosphoricons:ph]{phosphoricons::ph()}} \cr rheroicons \tab \code{rheroicons::home} \tab \code{\link[rheroicons:rheroicon]{rheroicons::rheroicon()}} \cr - tabler \tab \code{tabler::home} \tab \code{\link[tabler:tabler-components]{tabler::icon()}} \cr + tabler \tab \code{tabler::home} \tab \code{\link[tabler:icon]{tabler::icon()}} \cr shiny \tab \code{shiny::home} \tab \code{\link[shiny:icon]{shiny::icon()}} \cr } diff --git a/man/btw_task.Rd b/man/btw_task.Rd new file mode 100644 index 00000000..a173d23d --- /dev/null +++ b/man/btw_task.Rd @@ -0,0 +1,112 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/btw_task.R +\name{btw_task} +\alias{btw_task} +\title{Run a pre-formatted btw task} +\usage{ +btw_task( + path, + ..., + client = NULL, + mode = c("app", "console", "client", "tool") +) +} +\arguments{ +\item{path}{Path to the task file containing YAML configuration and prompt.} + +\item{...}{Named arguments become template variables for interpolation in the +task prompt. Unnamed arguments are treated as additional context objects +and converted to text via \code{\link[=btw]{btw()}}.} + +\item{client}{An \link[ellmer:Chat]{ellmer::Chat} client to override the task file's client +configuration. If \code{NULL}, uses the client specified in the task file's +YAML frontmatter, falling back to the default client resolution of +\code{\link[=btw_client]{btw_client()}}.} + +\item{mode}{The execution mode for the task: +\itemize{ +\item \code{"app"}: Launch interactive Shiny app (default) +\item \code{"console"}: Interactive console chat with \code{\link[ellmer:live_console]{ellmer::live_console()}} +\item \code{"client"}: Return configured \link[ellmer:Chat]{ellmer::Chat} client without running +\item \code{"tool"}: Return an \code{\link[ellmer:tool]{ellmer::tool()}} object for programmatic use +}} +} +\value{ +Depending on \code{mode}: +\itemize{ +\item \code{"app"}: Returns the chat client invisibly after launching the app +\item \code{"console"}: Returns the chat client after console interaction +\item \code{"client"}: Returns the configured chat client +\item \code{"tool"}: Returns an \code{\link[ellmer:tool]{ellmer::tool()}} object +} +} +\description{ +Runs a btw task defined in a file with YAML frontmatter configuration and +a markdown body containing the task prompt. The task file format is similar +to \code{btw.md} files, with client and tool configuration in the frontmatter and +the task instructions in the body. +\subsection{Task File Format}{ + +Task files use the same format as \code{btw.md} files: + +\if{html}{\out{
}}\preformatted{--- +client: + provider: anthropic + model: claude-sonnet-4 +tools: [docs, files] +--- + +Your task prompt here with \{\{ variable \}\} interpolation... +}\if{html}{\out{
}} +} + +\subsection{Template Variables}{ + +The task prompt body supports template variable interpolation using +\code{{{ variable }}} syntax via \code{\link[ellmer:interpolate]{ellmer::interpolate()}}. Pass named arguments +to provide values for template variables: + +\if{html}{\out{
}}\preformatted{btw_task("my-task.md", package_name = "dplyr", version = "1.1.0") +}\if{html}{\out{
}} +} + +\subsection{Additional Context}{ + +Unnamed arguments are treated as additional context and converted to text +using \code{\link[=btw]{btw()}}. This context is appended to the system prompt: + +\if{html}{\out{
}}\preformatted{btw_task("analyze.md", dataset_name = "mtcars", mtcars, my_function) +# ^-- template var ^-- additional context +}\if{html}{\out{
}} +} +} +\examples{ +# Create a simple task file +tmp_task_file <- tempfile(fileext = ".md") + +cat(file = tmp_task_file, '--- +client: anthropic/claude-sonnet-4-6 +tools: [docs, files] +--- + +Analyze the {{ package_name }} package and create a summary. +') + +# Task with template interpolation +btw_task(tmp_task_file, package_name = "dplyr", mode = "tool") + +# Include additional context +btw_task( + tmp_task_file, + package_name = "ggplot2", + mtcars, # Additional context + mode = "tool" +) + +} +\seealso{ +Other task and agent functions: +\code{\link{btw_task_create_btw_md}()}, +\code{\link{btw_task_create_readme}()} +} +\concept{task and agent functions} diff --git a/man/btw_task_create_btw_md.Rd b/man/btw_task_create_btw_md.Rd index 3b13bae2..e9ef2c90 100644 --- a/man/btw_task_create_btw_md.Rd +++ b/man/btw_task_create_btw_md.Rd @@ -59,6 +59,7 @@ withr::with_envvar(list(ANTHROPIC_API_KEY = "example"), { } \seealso{ Other task and agent functions: +\code{\link{btw_task}()}, \code{\link{btw_task_create_readme}()} } \concept{task and agent functions} diff --git a/man/btw_task_create_readme.Rd b/man/btw_task_create_readme.Rd index 37d3eada..696afdb8 100644 --- a/man/btw_task_create_readme.Rd +++ b/man/btw_task_create_readme.Rd @@ -58,6 +58,7 @@ withr::with_envvar(list(ANTHROPIC_API_KEY = "example"), { } \seealso{ Other task and agent functions: +\code{\link{btw_task}()}, \code{\link{btw_task_create_btw_md}()} } \concept{task and agent functions} diff --git a/tests/testthat/test-btw_task.R b/tests/testthat/test-btw_task.R new file mode 100644 index 00000000..319ad964 --- /dev/null +++ b/tests/testthat/test-btw_task.R @@ -0,0 +1,208 @@ +local_enable_tools() +withr::local_options(btw.client.quiet = TRUE) + +describe("btw_task()", { + withr::local_envvar(list(ANTHROPIC_API_KEY = "test-key")) + + it("reads and parses task files correctly", { + # Create a temporary task file + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "---", + "client:", + " provider: anthropic", + " model: claude-sonnet-4", + "tools: [docs, files]", + "---", + "", + "Analyze the {{ package_name }} package.", + "Focus on {{ focus_area }}." + ) + ) + + # Test client mode + chat <- btw_task( + task_file, + package_name = "dplyr", + focus_area = "data manipulation", + mode = "client" + ) + + expect_s3_class(chat, "Chat") + + # Check that interpolation worked + sys_prompt <- chat$get_system_prompt() + expect_match(sys_prompt, "Analyze the dplyr package", fixed = TRUE) + expect_match(sys_prompt, "Focus on data manipulation", fixed = TRUE) + + # Should not contain template markers + expect_no_match(sys_prompt, "{{", fixed = TRUE) + expect_no_match(sys_prompt, "}}", fixed = TRUE) + }) + + it("handles mixed named and unnamed arguments", { + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "---", + "tools: [env]", + "---", + "", + "Analyze {{ dataset_name }}." + ) + ) + + # Just test that named args work for template interpolation + # Testing unnamed context would require complex mocking + chat <- btw_task( + task_file, + dataset_name = "mtcars", # Named - template var + mode = "client" + ) + + sys_prompt <- chat$get_system_prompt() + expect_match(sys_prompt, "Analyze mtcars", fixed = TRUE) + }) + + it("creates a working tool", { + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "---", + "tools: false", + "---", + "", + "Simple task: {{ action }}" + ) + ) + + tool <- btw_task( + task_file, + action = "test", + mode = "tool" + ) + + expect_s3_class(tool, "ellmer::ToolDef") + + # Check tool properties + expect_match(tool@name, "btw_task_") + expect_type(tool@description, "character") + + # Tool arguments are stored as an ArgumentSchema object + # Just check that the tool has arguments defined + expect_false(is.null(tool@arguments)) + }) + + it("errors on missing file", { + expect_error( + btw_task("nonexistent.md", mode = "client"), + "Task file not found" + ) + }) + + it("errors on empty task prompt", { + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "---", + "tools: [docs]", + "---", + "" # Empty body + ) + ) + + expect_error( + btw_task(task_file, mode = "client"), + "must contain a prompt" + ) + }) + + it("handles task files without frontmatter", { + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "Simple task without config.", + "Analyze {{ target }}." + ) + ) + + # Should work with defaults + chat <- btw_task( + task_file, + target = "the data", + mode = "client" + ) + + expect_s3_class(chat, "Chat") + sys_prompt <- chat$get_system_prompt() + expect_match(sys_prompt, "Analyze the data", fixed = TRUE) + }) + + it("respects client override", { + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "---", + "client:", + " provider: openai", + " model: gpt-4", + "---", + "", + "Task content" + ) + ) + + # Override with different client + custom_client <- ellmer::chat_anthropic( + system_prompt = "Custom prompt" + ) + + chat <- btw_task( + task_file, + client = custom_client, + mode = "client" + ) + + # Should use the provided client + expect_identical(chat$get_provider()@name, "Anthropic") + }) + + it("handles conditional template sections", { + # Skip this test - Mustache-style sections don't work with glue/ellmer::interpolate + # They use {{ }} delimiters which conflicts with the conditional section syntax + skip("Conditional sections not supported with current interpolation") + }) +}) + +test_that("btw_task() example files are valid", { + withr::local_envvar(list(ANTHROPIC_API_KEY = "test-key")) + + tasks_dir <- system.file("tasks", package = "btw") + + if (dir.exists(tasks_dir)) { + task_files <- list.files(tasks_dir, pattern = "\\.md$", full.names = TRUE) + # Exclude README.md as it's documentation, not a task file + task_files <- task_files[!grepl("README\\.md$", task_files)] + + for (file in task_files) { + # Test that each task file can be loaded + expect_no_error( + chat <- btw_task( + file, + # Provide template variables that examples might need + package_name = "base", + file_path = "R/test.R", + dataset_name = "mtcars", + mode = "client" + ) + ) + } + } +}) \ No newline at end of file From 8c41f44916aa05ed8d3f1e0953afa4ede9ae6a46 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 23 Feb 2026 09:45:07 -0500 Subject: [PATCH 2/7] test(btw_task): add coverage for context args and clean up test suite - Add test for unnamed args as additional context (previously untested despite being a documented feature) - Delete the skipped conditional-sections test, which covered no assertions and signalled an abandoned attempt rather than a known gap - Wrap the example-files test in a describe() block for consistency with the rest of the file --- tests/testthat/test-btw_task.R | 75 +++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/tests/testthat/test-btw_task.R b/tests/testthat/test-btw_task.R index 319ad964..bfa36f05 100644 --- a/tests/testthat/test-btw_task.R +++ b/tests/testthat/test-btw_task.R @@ -55,8 +55,6 @@ describe("btw_task()", { ) ) - # Just test that named args work for template interpolation - # Testing unnamed context would require complex mocking chat <- btw_task( task_file, dataset_name = "mtcars", # Named - template var @@ -67,6 +65,29 @@ describe("btw_task()", { expect_match(sys_prompt, "Analyze mtcars", fixed = TRUE) }) + it("appends unnamed arguments as additional context", { + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "---", + "tools: false", + "---", + "", + "Analyze the data." + ) + ) + + chat <- btw_task( + task_file, + mtcars, # Unnamed - additional context + mode = "client" + ) + + sys_prompt <- chat$get_system_prompt() + expect_match(sys_prompt, "# Additional Context", fixed = TRUE) + }) + it("creates a working tool", { task_file <- withr::local_tempfile(fileext = ".md") writeLines( @@ -173,36 +194,32 @@ describe("btw_task()", { # Should use the provided client expect_identical(chat$get_provider()@name, "Anthropic") }) - - it("handles conditional template sections", { - # Skip this test - Mustache-style sections don't work with glue/ellmer::interpolate - # They use {{ }} delimiters which conflicts with the conditional section syntax - skip("Conditional sections not supported with current interpolation") - }) }) -test_that("btw_task() example files are valid", { +describe("btw_task() example files", { withr::local_envvar(list(ANTHROPIC_API_KEY = "test-key")) - tasks_dir <- system.file("tasks", package = "btw") - - if (dir.exists(tasks_dir)) { - task_files <- list.files(tasks_dir, pattern = "\\.md$", full.names = TRUE) - # Exclude README.md as it's documentation, not a task file - task_files <- task_files[!grepl("README\\.md$", task_files)] - - for (file in task_files) { - # Test that each task file can be loaded - expect_no_error( - chat <- btw_task( - file, - # Provide template variables that examples might need - package_name = "base", - file_path = "R/test.R", - dataset_name = "mtcars", - mode = "client" + it("included task files are valid", { + tasks_dir <- system.file("tasks", package = "btw") + + if (dir.exists(tasks_dir)) { + task_files <- list.files(tasks_dir, pattern = "\\.md$", full.names = TRUE) + # Exclude README.md as it's documentation, not a task file + task_files <- task_files[!grepl("README\\.md$", task_files)] + + for (file in task_files) { + # Test that each task file can be loaded + expect_no_error( + chat <- btw_task( + file, + # Provide template variables that examples might need + package_name = "base", + file_path = "R/test.R", + dataset_name = "mtcars", + mode = "client" + ) ) - ) + } } - } -}) \ No newline at end of file + }) +}) From 6088de284943d05b0631f96fc70ccc1ab216f49b Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 23 Feb 2026 09:46:54 -0500 Subject: [PATCH 3/7] fix(btw_task): address review feedback on system prompt and tool metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit System prompt: - Replace the system prompt with the task-specific content rather than appending to btw's standard session/tool/project prompts, matching the pattern used by btw_task_create_readme() and btw_task_create_btw_md() Context args: - Switch from do.call(btw, ...) to enquos() + eval_tidy() + btw_this(), which preserves the original expressions so btw can derive object names (do.call passed evaluated values, losing the expression context that btw()'s NSE relies on) Tool metadata from YAML frontmatter: - Read optional name, title, description, icon fields from task file YAML - Derive tool name from name field or file basename (hyphens → underscores) - Derive display name from title field or title-cased name - Add ellmer::tool_annotations() with title/read_only_hint/open_world_hint - Apply custom_icon() when icon field is present in YAML, matching the btw_agent_tool() pattern Minor cleanup: - Remove redundant comments that restate what the next line does - Compute display_name once before mode dispatch instead of recomputing task_name separately inside the console and app branches - Derive tool description from description YAML field when present, otherwise take the first non-empty line of the task body (stripping markdown header markers) rather than blindly substr()-ing at 100 chars --- R/btw_task.R | 139 ++++++++++++++++++++++----------------------------- 1 file changed, 61 insertions(+), 78 deletions(-) diff --git a/R/btw_task.R b/R/btw_task.R index 49097090..ae4ab6b5 100644 --- a/R/btw_task.R +++ b/R/btw_task.R @@ -95,16 +95,13 @@ btw_task <- function( check_string(path) mode <- arg_match(mode) - # Read and parse the task file using existing utility if (!fs::file_exists(path)) { cli::cli_abort("Task file not found: {.path {path}}") } - # Use the existing read_single_btw_file function task_config <- read_single_btw_file(path) - - # Extract the task prompt from btw_system_prompt field task_prompt <- task_config$btw_system_prompt + task_config$btw_system_prompt <- NULL if (is.null(task_prompt) || !nzchar(task_prompt)) { cli::cli_abort( @@ -112,84 +109,68 @@ btw_task <- function( ) } - # Remove btw_system_prompt from config to avoid confusion with other fields - task_config$btw_system_prompt <- NULL - - # Separate named (template vars) and unnamed (context) arguments - dots <- dots_list(...) - dot_names <- names2(dots) - - # Named args are template variables - template_vars <- dots[nzchar(dot_names)] + # Capture dots as quosures to preserve expressions (needed for btw() naming) + all_quos <- enquos(...) + quo_names <- names2(all_quos) - # Unnamed args are additional context - context_objects <- dots[!nzchar(dot_names)] + # Named quos → template variables (evaluated); unnamed quos → context objects + template_vars <- lapply(all_quos[nzchar(quo_names)], eval_tidy) + context_quos <- all_quos[!nzchar(quo_names)] # Interpolate template variables in the task prompt if (length(template_vars) > 0) { task_prompt <- ellmer::interpolate(task_prompt, !!!template_vars) } - # Create the client with configuration from task file - # Use provided client, or task file config, or btw defaults - if (is.null(client)) { - # Build client from task config - if (!is.null(task_config$client)) { - client <- task_config$client + # Build final system prompt: task body + optional additional context + final_system_prompt <- task_prompt + + if (length(context_quos) > 0) { + context_values <- lapply(context_quos, eval_tidy) + context_names <- vapply(context_quos, as_label, character(1)) + names(context_values) <- context_names + context_env <- new_environment(context_values, parent = parent.frame()) + user_context_text <- trimws(paste( + btw_this(context_env, items = context_names), + collapse = "\n" + )) + if (nzchar(user_context_text)) { + final_system_prompt <- paste( + c(final_system_prompt, "", "---", "# Additional Context", "", user_context_text), + collapse = "\n" + ) } } - # Get tools from task config + # Resolve client: explicit arg > task file YAML > btw defaults + if (is.null(client) && !is.null(task_config$client)) { + client <- task_config$client + } + tools <- task_config$tools %||% btw_tools() - # Create btw client with resolved configuration + # Call btw_client() to register tools, then replace the system prompt + # with the task-specific content (matches btw_task_create_* pattern) chat_client <- btw_client( client = client, tools = tools ) - - # Build system prompt - base_system_prompt <- chat_client$get_system_prompt() - - # Add task prompt to system - sys_prompt_parts <- c( - base_system_prompt, - "---", - "# Task Instructions", - "", - task_prompt - ) - - # Add additional context if provided - if (length(context_objects) > 0) { - user_context <- do.call(btw, c(context_objects, list(clipboard = FALSE))) - if (nzchar(user_context)) { - sys_prompt_parts <- c( - sys_prompt_parts, - "", - "---", - "# Additional Context", - "", - user_context - ) - } - } - - final_system_prompt <- paste(sys_prompt_parts, collapse = "\n") chat_client$set_system_prompt(final_system_prompt) - # Mode dispatch + # Derive identity fields from task config or file basename + tool_name_raw <- task_config$name %||% fs::path_ext_remove(basename(path)) + tool_name_normalized <- gsub("-", "_", tool_name_raw) + display_name <- task_config$title %||% + to_title_case(gsub("[_-]", " ", tool_name_raw)) + if (mode == "client") { return(chat_client) } if (mode == "tool") { - # Create a tool wrapper function (simplified without ... to avoid ellmer argument mismatch) task_tool_fn <- function(prompt = "") { - # Clone to avoid state pollution this_client <- chat_client$clone() - # Append tool mode instructions sys_prompt <- paste0( this_client$get_system_prompt(), "\n\n---\n\n", @@ -203,29 +184,31 @@ btw_task <- function( this_client$set_system_prompt(sys_prompt) - # Add any additional prompt instructions if (nzchar(prompt)) { - initial_message <- paste0( - "Additional instructions: ", - prompt - ) + this_client$chat(paste0("Additional instructions: ", prompt)) } else { - initial_message <- "Please complete the task as instructed." + this_client$chat("Please complete the task as instructed.") } + } - this_client$chat(initial_message) + description <- task_config$description %||% { + lines <- strsplit(task_prompt, "\n")[[1]] + first_nonempty <- trimws(lines[nzchar(trimws(lines))]) + if (length(first_nonempty) > 0) { + gsub("^#+\\s*", "", first_nonempty[1]) + } else { + paste0("Run the task defined in ", basename(path)) + } } - # Create the tool definition tool <- ellmer::tool( task_tool_fn, - name = paste0("btw_task_", fs::path_ext_remove(basename(path))), - description = paste0( - "Run the task defined in ", - basename(path), - ". ", - substr(task_prompt, 1, 100), - if (nchar(task_prompt) > 100) "..." + name = paste0("btw_task_", tool_name_normalized), + description = description, + annotations = ellmer::tool_annotations( + title = display_name, + read_only_hint = FALSE, + open_world_hint = TRUE ), arguments = list( prompt = ellmer::type_string( @@ -235,30 +218,30 @@ btw_task <- function( ) ) + if (!is.null(task_config$icon)) { + tool@annotations$icon <- custom_icon(task_config$icon) + } + return(tool) } - # Console or app mode if (mode == "console") { - task_name <- fs::path_ext_remove(basename(path)) cli::cli_text( - "Starting {.strong btw_task()} for {.file {task_name}} in live console mode." + "Starting {.strong btw_task()} for {.file {display_name}} in live console mode." ) cli::cli_text( "{cli::col_yellow(cli::symbol$play)} ", - "Say \"{.strong {cli::col_magenta('Let\\'s get started.')}}\" to begin the task." + "Say \"{.strong {cli::col_magenta('Let\\'s get started.')}}\" to begin." ) ellmer::live_console(chat_client) } else { - # App mode - task_name <- fs::path_ext_remove(basename(path)) btw_app_from_client( client = chat_client, messages = list(list( role = "assistant", content = paste0( "\U1F4CB Hi! I'm ready to help with the ", - htmltools::htmlEscape(task_name), + htmltools::htmlEscape(display_name), " task.

", "Say Let's get started. to begin." ) From eb0a1f933114ed3d03fe1a26607a8500fe6e3349 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 23 Feb 2026 09:47:08 -0500 Subject: [PATCH 4/7] chore: add NEWS entry for btw_task() --- NEWS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NEWS.md b/NEWS.md index fff0de9e..7e03896c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,13 @@ # btw (development version) +* New `btw_task()` function runs pre-formatted LLM tasks defined in markdown + files with YAML frontmatter. Task files support template variable + interpolation via `{{ variable }}` syntax, optional client and tool + configuration, and all four execution modes (`"app"`, `"console"`, + `"client"`, `"tool"`). Task files can also specify `title`, `icon`, + `description`, and `name` fields in their YAML frontmatter to customize + the tool definition when used in `"tool"` mode (#42). + * New `btw_tool_files_edit()` tool makes targeted, validated line-based edits to files using `replace`, `insert_after`, and `replace_range` actions. Edits are anchored to content hashes, so stale edits are rejected if the file has From 55d2b9c6fce724affa34e7b222eed35ea2392173 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 23 Feb 2026 13:10:45 -0500 Subject: [PATCH 5/7] docs: Update pkgdown reference index --- pkgdown/_pkgdown.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgdown/_pkgdown.yml b/pkgdown/_pkgdown.yml index 953bcdd8..23afbb48 100644 --- a/pkgdown/_pkgdown.yml +++ b/pkgdown/_pkgdown.yml @@ -90,6 +90,8 @@ reference: Tasks are typically designed for interactive use (e.g. to collaboratively create a project context file), but they can also be used programmatically. contents: + - btw_task + - btw_agent_tool - starts_with("btw_task_") # TODO update docs: Agents are similar, but are designed to be used primarily as tools by other LLMs. - starts_with("btw_agent_") From 3fcc3a980574fc09cec6ba81ca9ba821dace5295 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 23 Feb 2026 14:09:58 -0500 Subject: [PATCH 6/7] chore: validate task tool name and icon --- R/btw_task.R | 32 ++++++++++++++++-- tests/testthat/test-btw_task.R | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/R/btw_task.R b/R/btw_task.R index ae4ab6b5..aee7db7e 100644 --- a/R/btw_task.R +++ b/R/btw_task.R @@ -159,7 +159,7 @@ btw_task <- function( # Derive identity fields from task config or file basename tool_name_raw <- task_config$name %||% fs::path_ext_remove(basename(path)) - tool_name_normalized <- gsub("-", "_", tool_name_raw) + tool_name_normalized <- validate_task_tool_name(tool_name_raw, path) display_name <- task_config$title %||% to_title_case(gsub("[_-]", " ", tool_name_raw)) @@ -219,7 +219,14 @@ btw_task <- function( ) if (!is.null(task_config$icon)) { - tool@annotations$icon <- custom_icon(task_config$icon) + icon <- custom_icon(task_config$icon) + if (is.null(icon)) { + cli::cli_warn(c( + "Invalid icon in task file {.path {path}}", + "i" = "Ignoring {.field icon}: {.val {task_config$icon}}" + )) + } + tool@annotations$icon <- icon } return(tool) @@ -249,3 +256,24 @@ btw_task <- function( ) } } + +validate_task_tool_name <- function(name, path) { + check_string(name) + + name <- trimws(name) + if (!nzchar(name)) { + cli::cli_abort(c( + "Task name cannot be empty: {.path {path}}", + "i" = "Provide a non-empty {.field name} in YAML frontmatter or use a file name with letters." + )) + } + + if (!grepl("^[a-zA-Z0-9_-]+$", name)) { + cli::cli_abort(c( + "Invalid task name {.val {name}} in {.path {path}}", + "i" = "{.field name} must contain only letters, numbers, - and _." + )) + } + + name +} diff --git a/tests/testthat/test-btw_task.R b/tests/testthat/test-btw_task.R index bfa36f05..f577722b 100644 --- a/tests/testthat/test-btw_task.R +++ b/tests/testthat/test-btw_task.R @@ -118,6 +118,66 @@ describe("btw_task()", { expect_false(is.null(tool@arguments)) }) + it("uses valid task file names for tool identifiers", { + task_dir <- withr::local_tempdir() + task_file <- file.path(task_dir, "my-task-file.md") + writeLines( + con = task_file, + c( + "---", + "tools: false", + "---", + "", + "Simple task" + ) + ) + + tool <- btw_task(task_file, mode = "tool") + expect_identical(tool@name, "btw_task_my-task-file") + }) + + it("errors on invalid task names in frontmatter", { + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "---", + "name: task.with.dot", + "tools: false", + "---", + "", + "Simple task" + ) + ) + + expect_error( + btw_task(task_file, mode = "tool"), + "Invalid task name" + ) + }) + + it("warns on invalid icon and falls back to NULL", { + task_file <- withr::local_tempfile(fileext = ".md") + writeLines( + con = task_file, + c( + "---", + "tools: false", + "icon: ''", + "---", + "", + "Simple task" + ) + ) + + expect_warning( + tool <- btw_task(task_file, mode = "tool"), + "Invalid icon in task file" + ) + expect_s3_class(tool, "ellmer::ToolDef") + expect_null(tool@annotations$icon) + }) + it("errors on missing file", { expect_error( btw_task("nonexistent.md", mode = "client"), From 7c5b20340d965c492d0934ec5d9bd93e46bcd5bb Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 23 Feb 2026 15:01:54 -0500 Subject: [PATCH 7/7] chore: remove inst/tasks example files, add test fixture The bundled example task files weren't ready to ship. Deleted inst/tasks/ entirely and replaced the test coverage with a minimal fixture at tests/testthat/fixtures/test-analyze-package.md. --- inst/tasks/README.md | 95 ------------------- inst/tasks/analyze-package.md | 17 ---- inst/tasks/code-review.md | 36 ------- inst/tasks/data-summary.md | 27 ------ .../testthat/fixtures/test-analyze-package.md | 5 + tests/testthat/test-btw_task.R | 29 ++---- 6 files changed, 11 insertions(+), 198 deletions(-) delete mode 100644 inst/tasks/README.md delete mode 100644 inst/tasks/analyze-package.md delete mode 100644 inst/tasks/code-review.md delete mode 100644 inst/tasks/data-summary.md create mode 100644 tests/testthat/fixtures/test-analyze-package.md diff --git a/inst/tasks/README.md b/inst/tasks/README.md deleted file mode 100644 index 9b2d41ca..00000000 --- a/inst/tasks/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# btw Task Files - -This directory contains pre-defined task files for use with `btw_task()`. - -## Usage - -Tasks are defined in markdown files with YAML frontmatter for configuration and a markdown body containing the task instructions: - -```r -# Run a task with template variables -chat <- btw_task( - system.file("tasks/analyze-package.md", package = "btw"), - package_name = "dplyr", - mode = "console" -) - -# Include additional context (unnamed arguments) -chat <- btw_task( - system.file("tasks/data-summary.md", package = "btw"), - dataset_name = "mtcars", # Named - template variable - mtcars, # Unnamed - additional context - mode = "app" -) -``` - -## Available Tasks - -### analyze-package.md -Analyzes an R package and provides a comprehensive summary including purpose, key functions, dependencies, use cases, and comparisons to similar packages. - -**Template variables:** -- `package_name`: Name of the package to analyze - -### code-review.md -Reviews code files and provides detailed feedback on code quality, best practices, performance, documentation, and testing considerations. - -**Template variables:** -- `file_path`: Path to the file to review - -### data-summary.md -Creates comprehensive summaries of datasets including basic statistics, data quality assessment, and insights. - -**Template variables:** -- `dataset_name`: Name of the dataset to summarize - -## Task File Format - -```yaml ---- -client: # Optional: LLM client configuration - provider: anthropic - model: claude-sonnet-4 -tools: [docs, files] # Optional: btw tools to include ---- - -# Your task prompt here - -Use {{ variable_name }} syntax for template variables that can be -interpolated when running the task. -``` - -## Execution Modes - -Tasks can be run in four different modes: - -- **app**: Launch interactive Shiny app (default) -- **console**: Interactive console chat -- **client**: Return configured chat client -- **tool**: Return an ellmer tool object for programmatic use - -## Creating Custom Tasks - -1. Create a markdown file with YAML frontmatter and task instructions -2. Use `{{ variable }}` syntax for template placeholders -3. Run with `btw_task()` providing the path and template values - -Example custom task: - -```markdown ---- -tools: [files, git] ---- - -Review the recent changes in {{ file_path }} and suggest improvements. -Focus on {{ focus_area }} aspects of the code. -``` - -```r -chat <- btw_task( - "my-review-task.md", - file_path = "R/my-function.R", - focus_area = "performance", - mode = "console" -) -``` \ No newline at end of file diff --git a/inst/tasks/analyze-package.md b/inst/tasks/analyze-package.md deleted file mode 100644 index 9f0180f2..00000000 --- a/inst/tasks/analyze-package.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -tools: [docs, sessioninfo] ---- - -# Package Analysis Task - -Analyze the R package **{{ package_name }}** and provide a comprehensive summary. - -## Your analysis should include: - -1. **Purpose**: What is the main purpose of this package? -2. **Key Functions**: Identify the 5-10 most important functions -3. **Dependencies**: What packages does it depend on? -4. **Use Cases**: Provide 2-3 example use cases -5. **Comparison**: How does it compare to similar packages (if any)? - -Please be thorough but concise in your analysis. \ No newline at end of file diff --git a/inst/tasks/code-review.md b/inst/tasks/code-review.md deleted file mode 100644 index 17cf32a5..00000000 --- a/inst/tasks/code-review.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -tools: [files, git] ---- - -# Code Review Task - -Review the code in **{{ file_path }}** and provide detailed feedback. - -## Review Criteria - -Please evaluate the code based on: - -1. **Code Quality** - - Is the code readable and well-structured? - - Are variable and function names descriptive? - - Is the code DRY (Don't Repeat Yourself)? - -2. **Best Practices** - - Does it follow R/tidyverse style conventions? - - Are there appropriate comments where needed? - - Is error handling adequate? - -3. **Performance** - - Are there any obvious performance bottlenecks? - - Could any operations be vectorized? - - Are there more efficient alternatives? - -4. **Documentation** - - Are functions properly documented with roxygen2? - - Are complex logic sections explained? - -5. **Testing Considerations** - - What edge cases should be tested? - - Are there potential bugs or issues? - -Provide actionable suggestions for improvement with specific code examples where applicable. \ No newline at end of file diff --git a/inst/tasks/data-summary.md b/inst/tasks/data-summary.md deleted file mode 100644 index 31d0dbba..00000000 --- a/inst/tasks/data-summary.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -tools: [env] ---- - -# Data Summary Task - -Create a comprehensive summary of the dataset **{{ dataset_name }}**. - -## Analysis Requirements - -1. **Basic Statistics** - - Number of observations and variables - - Data types of each column - - Summary statistics for numeric variables - - Frequency tables for categorical variables - -2. **Data Quality** - - Missing values analysis - - Potential outliers - - Data validation issues - -3. **Insights** - - Key patterns or trends - - Interesting relationships between variables - - Recommendations for further analysis - -Format your response with clear sections and use markdown tables where appropriate. \ No newline at end of file diff --git a/tests/testthat/fixtures/test-analyze-package.md b/tests/testthat/fixtures/test-analyze-package.md new file mode 100644 index 00000000..f5c12ea1 --- /dev/null +++ b/tests/testthat/fixtures/test-analyze-package.md @@ -0,0 +1,5 @@ +--- +tools: [docs] +--- + +Analyze the R package **{{ package_name }}**. diff --git a/tests/testthat/test-btw_task.R b/tests/testthat/test-btw_task.R index f577722b..05dec845 100644 --- a/tests/testthat/test-btw_task.R +++ b/tests/testthat/test-btw_task.R @@ -256,30 +256,13 @@ describe("btw_task()", { }) }) -describe("btw_task() example files", { +describe("btw_task() task files", { withr::local_envvar(list(ANTHROPIC_API_KEY = "test-key")) - it("included task files are valid", { - tasks_dir <- system.file("tasks", package = "btw") - - if (dir.exists(tasks_dir)) { - task_files <- list.files(tasks_dir, pattern = "\\.md$", full.names = TRUE) - # Exclude README.md as it's documentation, not a task file - task_files <- task_files[!grepl("README\\.md$", task_files)] - - for (file in task_files) { - # Test that each task file can be loaded - expect_no_error( - chat <- btw_task( - file, - # Provide template variables that examples might need - package_name = "base", - file_path = "R/test.R", - dataset_name = "mtcars", - mode = "client" - ) - ) - } - } + it("can load a task file with template variables", { + task_file <- testthat::test_path("fixtures/test-analyze-package.md") + expect_no_error( + btw_task(task_file, package_name = "base", mode = "client") + ) }) })