From 68a0917c8c2d6a1c8243ea1339ad991468bffe8d Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 23 Jun 2025 13:28:26 -0400 Subject: [PATCH 1/7] feat: btw Agents --- DESCRIPTION | 4 + NAMESPACE | 3 + R/btw_client.R | 96 ++++++++++---- R/tool-agent-researcher-r-task.R | 68 ++++++++++ R/tool-agent-summarize-chat.R | 86 +++++++++++++ R/tool-agent.R | 197 +++++++++++++++++++++++++++++ inst/prompts/chat-summary.md | 30 +++++ man/btw_agent_researcher_r_task.Rd | 34 +++++ man/btw_task_summarize_chat.Rd | 49 +++++++ man/btw_tool_agent.Rd | 66 ++++++++++ 10 files changed, 607 insertions(+), 26 deletions(-) create mode 100644 R/tool-agent-researcher-r-task.R create mode 100644 R/tool-agent-summarize-chat.R create mode 100644 R/tool-agent.R create mode 100644 inst/prompts/chat-summary.md create mode 100644 man/btw_agent_researcher_r_task.Rd create mode 100644 man/btw_task_summarize_chat.Rd create mode 100644 man/btw_tool_agent.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 5d5ef12e..6214e36e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -45,6 +45,7 @@ Suggests: bslib (>= 0.7.0), chromote, gh, + mirai, pandoc, shiny, shinychat (>= 0.2.0), @@ -69,6 +70,9 @@ Collate: 'import-standalone-purrr.R' 'import-standalone-types-check.R' 'mcp.R' + 'tool-agent-researcher-r-task.R' + 'tool-agent-summarize-chat.R' + 'tool-agent.R' 'tool-data-frame.R' 'tool-result.R' 'tool-docs-news.R' diff --git a/NAMESPACE b/NAMESPACE index f820fb44..80072f53 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -20,11 +20,14 @@ S3method(btw_this,pkg_search_result) S3method(btw_this,tbl) S3method(btw_this,vignette) export(btw) +export(btw_agent_researcher_r_task) export(btw_app) export(btw_client) export(btw_mcp_server) export(btw_mcp_session) +export(btw_task_summarize_chat) export(btw_this) +export(btw_tool_agent) export(btw_tool_docs_available_vignettes) export(btw_tool_docs_help_page) export(btw_tool_docs_package_help_topics) diff --git a/R/btw_client.R b/R/btw_client.R index a405bc9c..f49e8d3e 100644 --- a/R/btw_client.R +++ b/R/btw_client.R @@ -123,6 +123,11 @@ btw_client <- function( client <- config$client + withr::local_options( + btw.__agent_client__ = client, + btw.__agent_turns__ = client + ) + sys_prompt <- client$get_system_prompt() sys_prompt <- c( "# System and Session Context", @@ -173,19 +178,19 @@ btw_client_config <- function(client = NULL, tools = NULL, config = list()) { config$tools <- flatten_and_check_tools(config$tools) + # 1. Client was provided explicitly if (!is.null(client)) { check_inherits(client, "Chat") - config$client <- client - return(config) - } - - default <- getOption("btw.client") - if (!is.null(default)) { - check_inherits(default, "Chat") - config$client <- default$clone() - return(config) + } else { + # 2. Client wasn't provided, check the `btw.client` R option + default <- getOption("btw.client") + if (!is.null(default)) { + check_inherits(default, "Chat") + config$client <- default$clone() + } } + # 3a. Check for usage of deprecated btw.md fields if (!is.null(config$provider)) { lifecycle::deprecate_stop( when = "0.0.3", @@ -202,33 +207,68 @@ btw_client_config <- function(client = NULL, tools = NULL, config = list()) { ) } - if (!is.null(config$client)) { - chat_args <- utils::modifyList( - list(echo = "output"), # defaults - config$client - ) + # 3b. Use the `btw.md` file to configure the client + if (is.null(client) && !is.null(config$client)) { + client <- btw_config_client(config$client) + } - chat_fn <- gsub(" ", "_", tolower(chat_args$provider)) - if (!grepl("^chat_", chat_fn)) { - chat_fn <- paste0("chat_", chat_fn) - } - chat_args$provider <- NULL + # 4. Default to Claude from Anthropic + if (is.null(client)) { + client <- ellmer::chat_anthropic(echo = "output") + } - chat_client <- call2(.ns = "ellmer", chat_fn, !!!chat_args) - config$client <- eval(chat_client) + withr::local_options( + btw.__agent_client__ = client, + btw.__agent_turns__ = client + ) - if (!is.null(chat_args$model)) { - cli::cli_inform( - "Using {.field {chat_args$model}} from {.strong {config$client$get_provider()@name}}." + # ---- Agents ---- + if (!is.null(config$agents)) { + for (config_agent in config$agents) { + # The agent inherits from the base btw client + config_agent$client <- utils::modifyList( + config$client, + config_agent$client %||% list() ) + + config_agent$client <- btw_config_client( + config_agent$client, + quiet = TRUE + ) + + agent_tool <- do.call(btw_tool_agent, config_agent) + client$register_tool(agent_tool) } - return(config) } - config$client <- ellmer::chat_anthropic(echo = "output") + config$client <- client config } +btw_config_client <- function(config_client, quiet = FALSE) { + chat_args <- utils::modifyList( + list(echo = "output"), # defaults + config_client + ) + + chat_fn <- gsub(" ", "_", tolower(chat_args$provider)) + if (!grepl("^chat_", chat_fn)) { + chat_fn <- paste0("chat_", chat_fn) + } + chat_args$provider <- NULL + + chat_client <- call2(.ns = "ellmer", chat_fn, !!!chat_args) + client <- eval(chat_client) + + if (isFALSE(quiet) && !is.null(chat_args$model)) { + cli::cli_inform( + "Using {.field {chat_args$model}} from {.strong {client$get_provider()@name}}." + ) + } + + client +} + flatten_and_check_tools <- function(tools) { if (isFALSE(tools)) { return(list()) @@ -270,6 +310,10 @@ flatten_and_check_tools <- function(tools) { } read_btw_file <- function(path = NULL) { + if (isFALSE(path)) { + return(list()) + } + must_find <- !is.null(path) path <- path %||% path_find_in_project("btw.md") %||% path_find_user("btw.md") diff --git a/R/tool-agent-researcher-r-task.R b/R/tool-agent-researcher-r-task.R new file mode 100644 index 00000000..9a056b53 --- /dev/null +++ b/R/tool-agent-researcher-r-task.R @@ -0,0 +1,68 @@ +#' Agent: A Tool for Researching R Tasks +#' +#' An agent that researches R packages and provides example code for specific R +#' tasks. This agent is designed to help users find the best R packages for +#' specific tasks, list key functions and arguments, and return runnable +#' example code. +#' +#' @param client An [ellmer::Chat] client, defaults to [btw_client()]. +#' @inheritParams btw_tool_agent +#' +#' @returns Returns an aync [ellmer::tool()] that can be used to invoke the +#' R task research agent. +#' +#' @family agents +#' @export +btw_agent_researcher_r_task <- function( + client = btw_client(tools = FALSE), + tools = c("docs", "search", "session") +) { + btw_tool_agent( + client = client, + tools = tools, + turns = FALSE, # Don't use initial turns, this agent is stateless + name = "btw_agent_researcher_r_task", + title = "R Task Research Agent", + description = r"(Research an R programming task. + +Rapidly researches a single, narrowly-scoped R-programming task. Finds the best CRAN (or locally installed) package(s), lists key functions/arguments, and returns runnable example code. + +INPUT +A unique, non-overlapping question such as "Draw a clustered heat-map", "Fit a beta-regression", or "Read a parquet file". + +PARALLEL TASKS +Prefer batching distinct research tasks only when they are clearly orthogonal. + +GUIDELINES +* Break the user's goal into the small, independent R tasks. +* Avoid issuing more than one call for the same or highly similar question. +* Limit parallel calls to reduce duplication and API load. +* Do not use this tool for broad or overlapping questions. +* Only use this tool when you do not already know the answer. + )", + system_prompt = r"---( +MISSION +Treat the input as an explicit research question about R packages. +Search CRAN docs, vignettes, help pages, and other provided tools to answer that question. +Return the answer in a compact format that explains your findings and provides example code that can be run in R. + +RESPONSE FORMAT +Your response should include (one block per topic or package) + +* Task +* Package(s): pkg1, pkg2, ... +* Why these packages? 1-sentence rationale. +* Key functions & arguments related to the task as a bullet list. +* Example code in a fenced R snippet that can be run as-is. +* Citations: minimal list of URLs or help files consulted. + +GUIDELINES +* Be factual; no speculation. +* Prefer base CRAN packages unless the question implies otherwise. +* Keep prose succinct; let code illustrate usage. +* If multiple packages solve the task, list the best 2-3 and show code for the top choice. +* If no suitable package exists, say so and suggest an alternative strategy. + +Return only the summarized research.)---", + ) +} diff --git a/R/tool-agent-summarize-chat.R b/R/tool-agent-summarize-chat.R new file mode 100644 index 00000000..fb82e266 --- /dev/null +++ b/R/tool-agent-summarize-chat.R @@ -0,0 +1,86 @@ +#' Summarize Chat Conversation +#' +#' Creates a concise summary of a chat conversation, removing false starts and +#' focusing on key outcomes and findings. This is a direct function that can be +#' called without the agent wrapper. +#' +#' @param client An [ellmer::Chat] client that will be used to summarize the +#' chat. +#' @param turns A list of [ellmer::Turn]s or an [ellmer::Chat] object that +#' provides the conversation history to summarize. If not provided, uses the +#' chat history from `client`. +#' @param additional_guidance Optional prompt to guide the summarization focus. +#' If not provided, creates a general comprehensive summary. +#' @param start_turn Integer specifying which turn to start summarizing from. +#' Default is 0, meaning summarize the entire conversation history. +#' +#' @returns A character string containing the conversation summary. +#' +#' @examples +#' \dontrun{ +#' # Summarize an entire chat +#' summary <- btw_task_summarize_chat(my_chat) +#' +#' # Summarize with specific focus +#' summary <- btw_task_summarize_chat(my_chat, "Focus on the R programming solutions") +#' +#' # Summarize starting from turn 5 +#' summary <- btw_task_summarize_chat(my_chat, start_turn = 5) +#' } +#' +#' @family tasks +#' @export +btw_task_summarize_chat <- function( + client, + turns = client, + additional_guidance = "", + start_turn = 0 +) { + check_inherits(client, "Chat") + check_string(additional_guidance) + turns <- turns_simplify(turns) + check_number_whole(start_turn, min = 0, max = as.numeric(length(turns))) + + # Create clone of the client to avoid modifying the original client + summary_client <- client$clone() + # No tools required + summary_client$set_tools(list()) + + if (start_turn > 0) { + turns <- turns[-seq_len(start_turn - 1)] + } + + summary_client$set_turns(turns) + + summary_client$set_system_prompt(NULL) + + summary_client$chat( + ellmer::interpolate_file( + system.file("prompts", "chat-summary.md", package = "btw"), + additional_guidance = additional_guidance + ) + ) +} + +turns_simplify <- function(turns) { + if (inherits(turns, "Chat")) { + turns <- turns$get_turns() + } + + # Simplify contents to flatten tool requests/results, avoiding API errors that + # happen when there are tool requests/results in the chat history, but no tools. + for (i in seq_along(turns)) { + turns[[i]]@contents <- map(turns[[i]]@contents, function(content) { + is_tool_request <- S7::S7_inherits(content, ellmer::ContentToolRequest) + is_tool_result <- S7::S7_inherits(content, ellmer::ContentToolResult) + + if (is_tool_request || is_tool_result) { + content <- ellmer::ContentText(format(content)) + } + + content + }) + } + + turns +} diff --git a/R/tool-agent.R b/R/tool-agent.R new file mode 100644 index 00000000..c5a7ed70 --- /dev/null +++ b/R/tool-agent.R @@ -0,0 +1,197 @@ +#' Tool: Create An Agent +#' +#' A btw agent is simply an [ellmer::Chat] client wrapped into a tool call. +#' +#' @param name The name of the agent tool. This is used to identify the tool +#' in the chat client or in the tool registry. +#' @param ... Ignored. +#' @param description A description of the agent tool, describing how and when +#' it should be used. This provides context to the chat client that will +#' invoke the agent. +#' @param title The title of the agent tool, used when displaying the tool +#' in a tool listing or when showing the agent tool's response. +#' @param system_prompt Additional instructions added to the system prompt of +#' `client` that describe the agent's task and how it should operate. +#' @param client An [ellmer::Chat] client. If not provided, uses `btw_client()` +#' by default. +#' @param turns A list of [ellmer::Turn]s or an [ellmer::Chat] object that +#' provides the initial turns used by the `client`. For example, you can use +#' this argument to pass a chat client with a long chat history to ask the +#' agent to summarize the chat history. +#' @param tools A list of tools to use with the agent. See [btw_client()] for +#' details on how to specify tools or [btw_tools()] for the list of built-in +#' tools. If provided, `tools` replaces the set of tools in `client`; if +#' `NULL` the `client` retains its existing registered tools. +#' @param mirai_profile The name of the \pkg{mirai} `.compute` profile to use +#' for this agent, see [mirai::mirai()] for details. +#' @param setup_code Additional code to run first before invoking the `$chat()` +#' method of `client`. Use this argument to load required packages, for +#' example if `client` uses tools from custom packages. +#' +#' @return Returns an agent as an [ellmer::tool()]. +#' +#' @family agents +#' @export +btw_tool_agent <- function( + name, + ..., + description = "", + title = NULL, + system_prompt = NULL, + client = NULL, + turns = NULL, + tools = NULL, + mirai_profile = "btw_agent", + setup_code = NULL +) { + rlang::check_installed("mirai") + check_dots_empty() + check_character(system_prompt, allow_null = TRUE) + check_string(mirai_profile) + check_string(setup_code, allow_null = TRUE, allow_empty = TRUE) + + client <- client %||% getOption("btw.__agent_client__", btw_client()) + check_inherits(client, "Chat") + + # Clone the client to avoid modifying the original client. It would be + # surprising if this function modified the client in place (e.g. by adding + # tools or updating the system prompt). + client <- client$clone() + + if (isFALSE(turns) || is_na(turns) || identical(turns, "")) { + turns <- NULL + } else if (isTRUE(turns)) { + turns <- getOption("btw.__agent_turns__", NULL) + } + + if (!is.null(tools)) { + tools <- flatten_and_check_tools(tools) + client$set_tools(tools) + } + + if (!is.null(turns)) { + ok_turns <- is_list(turns) || is_function(turns) || inherits(turns, "Chat") + if (!ok_turns) { + cli::cli_abort( + "{.var turns} must be a list of {.fn ellmer::Turn}s, an {.code ellmer::Chat}, or a function, not {.obj_type_friendly {turns}}." + ) + } + } + + if (!is.null(system_prompt)) { + if (inherits(system_prompt, "AsIs")) { + client$set_system_prompt(system_prompt) + } else { + client$set_system_prompt(c(client$get_system_prompt(), system_prompt)) + } + } + + agent_fn <- function(prompt) { + the_client <- client$clone() + + if (!is.null(turns)) { + # Set turns as late as possible, `turns` could be a function or a ref to + # a chat that may have changed since the tool definition was created. + if (inherits(turns, "Chat")) { + turns <- turns$get_turns() + } + if (is_function(turns)) { + turns <- turns() + } + the_client$set_turns(turns) + } + + m <- mirai::mirai( + { + library(btw) + + # Evaluate the setup code if provided + if (!is.null(setup_code) && nzchar(setup_code)) { + eval(parse(text = setup_code)) + } + + tool_result <- function(x) { + force(x) + + tokens <- the_client$get_tokens() + + tokens_input <- sum(tokens$tokens_total[tokens$role == "user"]) + tokens_output <- sum(tokens$tokens_total[tokens$role == "assistant"]) + + asNamespace("btw")[["BtwAgentToolResult"]]( + value = if (!inherits(x, "error")) x, + error = if (inherits(x, "error")) x, + extra = list( + turns = the_client$get_turns(), + tokens = list( + input = tokens_input, + output = tokens_output, + all = tokens + ), + cost = the_client$get_cost() + ) + ) + } + + tryCatch( + tool_result(the_client$chat(prompt)), + error = tool_result + ) + }, + the_client = the_client, + prompt = prompt, + setup_code = expr_text(setup_code), + .compute = mirai_profile + ) + + promises::as.promise(m) + } + + ellmer::tool( + agent_fn, + .description = description, + prompt = ellmer::type_string( + "The prompt to send to the agent. This should be a specific task or question." + ), + .name = name, + .annotations = ellmer::tool_annotations( + title = title %||% glue_("btw Agent ({{name}})") + ), + .convert = TRUE + ) +} + +BtwAgentToolResult <- S7::new_class( + "BtwAgentToolResult", + parent = ellmer::ContentToolResult +) + +S7::method(print, BtwAgentToolResult) <- function(x, ...) { + title <- x@request@tool@annotations$title %||% + glue_("Agent {{x@request@tool@name}}") + prompt <- x@request@arguments$prompt + + input <- cli::col_green(paste0(x@extra$tokens$input, "\u2191")) + output <- cli::col_red(paste0(x@extra$tokens$output, "\u2193")) + + cli::cli_rule( + left = title, + right = cli::format_inline( + "tokens={.strong {input}}/{.strong {output}}" + ) + ) + cli::cli_text( + "{.strong Task:} {.emph {prompt}}" + ) + if (!is.null(x@value)) { + cli::cli_text("{.strong Response \u2500\u2500\u2500\u2500}") + cli::cli_verbatim(x@value) + } else if (!is.null(x@error)) { + cli::cli_text( + "{.strong Error:} {conditionMessage(x@error)}" + ) + } + cli::cli_text("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500") + + invisible(x) +} diff --git a/inst/prompts/chat-summary.md b/inst/prompts/chat-summary.md new file mode 100644 index 00000000..723973db --- /dev/null +++ b/inst/prompts/chat-summary.md @@ -0,0 +1,30 @@ +You are now a conversation summarization specialist. Your task is to create a concise, accurate summary of the provided chat conversation that captures the essential outcomes and value while eliminating noise. + +CORE PRINCIPLES +- COMPRESS: Remove false starts, abandoned approaches, tangents, and repetitive content +- PRESERVE: Keep all successful solutions, key findings, important context, and final outcomes +- FOCUS: Prioritize what someone reading this summary would need to understand the conversation's value +- ACCURACY: Never fabricate or speculate - only summarize what actually happened + +SUGGESTED STRUCTURE (adapt as needed) +Consider organizing your summary around themes like: +- **Objective/Goal**: What was the user trying to accomplish? +- **Key Findings**: Important discoveries, solutions, or insights +- **Final Approach**: The successful method or solution that worked +- **Important Context**: Critical background information or constraints +- **Outcomes**: What was ultimately achieved or decided + +GUIDELINES +- Be ruthlessly concise while maintaining accuracy +- Use clear, direct language +- Focus on actionable insights and concrete outcomes +- Eliminate conversational filler and process details unless they're crucial +- If code was involved, include final working solutions but not debugging iterations +- If research was conducted, focus on conclusions rather than the search process + +Remember: Your summary should allow someone else (human or AI) to quickly understand what was accomplished and learned without needing to read the full conversation. + +--- + +Now, create a comprehensive summary of this conversation. +{{ additional_guidance }} diff --git a/man/btw_agent_researcher_r_task.Rd b/man/btw_agent_researcher_r_task.Rd new file mode 100644 index 00000000..6c63c1bd --- /dev/null +++ b/man/btw_agent_researcher_r_task.Rd @@ -0,0 +1,34 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tool-agent-researcher-r-task.R +\name{btw_agent_researcher_r_task} +\alias{btw_agent_researcher_r_task} +\title{Agent: A Tool for Researching R Tasks} +\usage{ +btw_agent_researcher_r_task( + client = btw_client(tools = FALSE), + tools = c("docs", "search", "session") +) +} +\arguments{ +\item{client}{An \link[ellmer:Chat]{ellmer::Chat} client, defaults to \code{\link[=btw_client]{btw_client()}}.} + +\item{tools}{A list of tools to use with the agent. See \code{\link[=btw_client]{btw_client()}} for +details on how to specify tools or \code{\link[=btw_tools]{btw_tools()}} for the list of built-in +tools. If provided, \code{tools} replaces the set of tools in \code{client}; if +\code{NULL} the \code{client} retains its existing registered tools.} +} +\value{ +Returns an aync \code{\link[ellmer:tool]{ellmer::tool()}} that can be used to invoke the +R task research agent. +} +\description{ +An agent that researches R packages and provides example code for specific R +tasks. This agent is designed to help users find the best R packages for +specific tasks, list key functions and arguments, and return runnable +example code. +} +\seealso{ +Other agents: +\code{\link{btw_tool_agent}()} +} +\concept{agents} diff --git a/man/btw_task_summarize_chat.Rd b/man/btw_task_summarize_chat.Rd new file mode 100644 index 00000000..247d5d6b --- /dev/null +++ b/man/btw_task_summarize_chat.Rd @@ -0,0 +1,49 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tool-agent-summarize-chat.R +\name{btw_task_summarize_chat} +\alias{btw_task_summarize_chat} +\title{Summarize Chat Conversation} +\usage{ +btw_task_summarize_chat( + client, + turns = client, + additional_guidance = "", + start_turn = 0 +) +} +\arguments{ +\item{client}{An \link[ellmer:Chat]{ellmer::Chat} client that will be used to summarize the +chat.} + +\item{turns}{A list of \link[ellmer:Turn]{ellmer::Turn}s or an \link[ellmer:Chat]{ellmer::Chat} object that +provides the conversation history to summarize. If not provided, uses the +chat history from \code{client}.} + +\item{additional_guidance}{Optional prompt to guide the summarization focus. +If not provided, creates a general comprehensive summary.} + +\item{start_turn}{Integer specifying which turn to start summarizing from. +Default is 0, meaning summarize the entire conversation history.} +} +\value{ +A character string containing the conversation summary. +} +\description{ +Creates a concise summary of a chat conversation, removing false starts and +focusing on key outcomes and findings. This is a direct function that can be +called without the agent wrapper. +} +\examples{ +\dontrun{ +# Summarize an entire chat +summary <- btw_task_summarize_chat(my_chat) + +# Summarize with specific focus +summary <- btw_task_summarize_chat(my_chat, "Focus on the R programming solutions") + +# Summarize starting from turn 5 +summary <- btw_task_summarize_chat(my_chat, start_turn = 5) +} + +} +\concept{tasks} diff --git a/man/btw_tool_agent.Rd b/man/btw_tool_agent.Rd new file mode 100644 index 00000000..6736a444 --- /dev/null +++ b/man/btw_tool_agent.Rd @@ -0,0 +1,66 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tool-agent.R +\name{btw_tool_agent} +\alias{btw_tool_agent} +\title{Tool: Create An Agent} +\usage{ +btw_tool_agent( + name, + ..., + description = "", + title = NULL, + system_prompt = NULL, + client = NULL, + turns = NULL, + tools = NULL, + mirai_profile = "btw_agent", + setup_code = NULL +) +} +\arguments{ +\item{name}{The name of the agent tool. This is used to identify the tool +in the chat client or in the tool registry.} + +\item{...}{Ignored.} + +\item{description}{A description of the agent tool, describing how and when +it should be used. This provides context to the chat client that will +invoke the agent.} + +\item{title}{The title of the agent tool, used when displaying the tool +in a tool listing or when showing the agent tool's response.} + +\item{system_prompt}{Additional instructions added to the system prompt of +\code{client} that describe the agent's task and how it should operate.} + +\item{client}{An \link[ellmer:Chat]{ellmer::Chat} client. If not provided, uses \code{btw_client()} +by default.} + +\item{turns}{A list of \link[ellmer:Turn]{ellmer::Turn}s or an \link[ellmer:Chat]{ellmer::Chat} object that +provides the initial turns used by the \code{client}. For example, you can use +this argument to pass a chat client with a long chat history to ask the +agent to summarize the chat history.} + +\item{tools}{A list of tools to use with the agent. See \code{\link[=btw_client]{btw_client()}} for +details on how to specify tools or \code{\link[=btw_tools]{btw_tools()}} for the list of built-in +tools. If provided, \code{tools} replaces the set of tools in \code{client}; if +\code{NULL} the \code{client} retains its existing registered tools.} + +\item{mirai_profile}{The name of the \pkg{mirai} \code{.compute} profile to use +for this agent, see \code{\link[mirai:mirai]{mirai::mirai()}} for details.} + +\item{setup_code}{Additional code to run first before invoking the \verb{$chat()} +method of \code{client}. Use this argument to load required packages, for +example if \code{client} uses tools from custom packages.} +} +\value{ +Returns an agent as an \code{\link[ellmer:tool]{ellmer::tool()}}. +} +\description{ +A btw agent is simply an \link[ellmer:Chat]{ellmer::Chat} client wrapped into a tool call. +} +\seealso{ +Other agents: +\code{\link{btw_agent_researcher_r_task}()} +} +\concept{agents} From 695648100ee671b8f34ae4a024dbd0f83b00acb4 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 2 Jul 2025 16:46:49 -0400 Subject: [PATCH 2/7] fix(btw_client_config): Typo setting wrong `client` --- R/btw_client.R | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/R/btw_client.R b/R/btw_client.R index f49e8d3e..d301b0b1 100644 --- a/R/btw_client.R +++ b/R/btw_client.R @@ -186,7 +186,7 @@ btw_client_config <- function(client = NULL, tools = NULL, config = list()) { default <- getOption("btw.client") if (!is.null(default)) { check_inherits(default, "Chat") - config$client <- default$clone() + client <- default$clone() } } @@ -227,14 +227,18 @@ btw_client_config <- function(client = NULL, tools = NULL, config = list()) { for (config_agent in config$agents) { # The agent inherits from the base btw client config_agent$client <- utils::modifyList( - config$client, + config$client %||% list(), config_agent$client %||% list() ) - config_agent$client <- btw_config_client( - config_agent$client, - quiet = TRUE - ) + if (length(config_agent$client) == 0) { + config_agent$client <- client + } else { + config_agent$client <- btw_config_client( + config_agent$client, + quiet = TRUE + ) + } agent_tool <- do.call(btw_tool_agent, config_agent) client$register_tool(agent_tool) From 3849a1b3d5541b23ac8b701d70df3a573c425178 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Wed, 2 Jul 2025 16:53:49 -0400 Subject: [PATCH 3/7] chore: use_package("promises") --- DESCRIPTION | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 6214e36e..b8cdc338 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -40,7 +40,8 @@ Imports: tibble, utils, withr, - xml2 + xml2, + promises Suggests: bslib (>= 0.7.0), chromote, From 758442efd3274a5b56714c28132b7b3ff9909c8a Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 3 Jul 2025 11:01:49 -0400 Subject: [PATCH 4/7] chore: tweak summary prompt --- inst/prompts/chat-summary.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inst/prompts/chat-summary.md b/inst/prompts/chat-summary.md index 723973db..9c2cf78a 100644 --- a/inst/prompts/chat-summary.md +++ b/inst/prompts/chat-summary.md @@ -17,10 +17,11 @@ Consider organizing your summary around themes like: GUIDELINES - Be ruthlessly concise while maintaining accuracy - Use clear, direct language -- Focus on actionable insights and concrete outcomes +- Focus on actionable insights, concrete outcomes, and high-quality context - Eliminate conversational filler and process details unless they're crucial - If code was involved, include final working solutions but not debugging iterations - If research was conducted, focus on conclusions rather than the search process +- If coding research was involved include example code snippets and summarize documentation in a way that teaches the key points discovered in the research. Remember: Your summary should allow someone else (human or AI) to quickly understand what was accomplished and learned without needing to read the full conversation. From 9f2fa43b026238b93fb3b9a23047a02f445b9824 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 3 Jul 2025 11:02:06 -0400 Subject: [PATCH 5/7] chore: Return summary with client attached --- R/tool-agent-summarize-chat.R | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/R/tool-agent-summarize-chat.R b/R/tool-agent-summarize-chat.R index fb82e266..f0975d67 100644 --- a/R/tool-agent-summarize-chat.R +++ b/R/tool-agent-summarize-chat.R @@ -54,12 +54,15 @@ btw_task_summarize_chat <- function( summary_client$set_system_prompt(NULL) - summary_client$chat( + res <- summary_client$chat( ellmer::interpolate_file( system.file("prompts", "chat-summary.md", package = "btw"), additional_guidance = additional_guidance ) ) + + attr(res, "client") <- summary_client + invisible(res) } turns_simplify <- function(turns) { From 9120c845976693efbfc77e2217af5fb4fdd56d63 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Thu, 3 Jul 2025 11:25:40 -0400 Subject: [PATCH 6/7] feat(summarize): Take `turns` first, `client` second --- R/tool-agent-summarize-chat.R | 19 ++++-- man/btw_task_summarize_chat.Rd | 10 +-- .../testthat/test-tool-agent-summarize-chat.R | 63 +++++++++++++++++++ 3 files changed, 82 insertions(+), 10 deletions(-) create mode 100644 tests/testthat/test-tool-agent-summarize-chat.R diff --git a/R/tool-agent-summarize-chat.R b/R/tool-agent-summarize-chat.R index f0975d67..8fb9b0d6 100644 --- a/R/tool-agent-summarize-chat.R +++ b/R/tool-agent-summarize-chat.R @@ -4,11 +4,11 @@ #' focusing on key outcomes and findings. This is a direct function that can be #' called without the agent wrapper. #' -#' @param client An [ellmer::Chat] client that will be used to summarize the -#' chat. #' @param turns A list of [ellmer::Turn]s or an [ellmer::Chat] object that #' provides the conversation history to summarize. If not provided, uses the #' chat history from `client`. +#' @param client An [ellmer::Chat] client that will be used to summarize the +#' chat. Must be provided if `turns` is not an [ellmer::Chat] object. #' @param additional_guidance Optional prompt to guide the summarization focus. #' If not provided, creates a general comprehensive summary. #' @param start_turn Integer specifying which turn to start summarizing from. @@ -31,12 +31,21 @@ #' @family tasks #' @export btw_task_summarize_chat <- function( - client, - turns = client, + turns, + client = NULL, additional_guidance = "", start_turn = 0 ) { - check_inherits(client, "Chat") + if (inherits(turns, "Chat")) { + if (is.null(client)) { + client <- turns + } else { + check_inherits(client, "Chat") + } + } else if (is.null(client)) { + stop("If 'turns' is not a Chat object, 'client' must be provided.") + } + check_string(additional_guidance) turns <- turns_simplify(turns) check_number_whole(start_turn, min = 0, max = as.numeric(length(turns))) diff --git a/man/btw_task_summarize_chat.Rd b/man/btw_task_summarize_chat.Rd index 247d5d6b..d85c0670 100644 --- a/man/btw_task_summarize_chat.Rd +++ b/man/btw_task_summarize_chat.Rd @@ -5,20 +5,20 @@ \title{Summarize Chat Conversation} \usage{ btw_task_summarize_chat( - client, - turns = client, + turns, + client = NULL, additional_guidance = "", start_turn = 0 ) } \arguments{ -\item{client}{An \link[ellmer:Chat]{ellmer::Chat} client that will be used to summarize the -chat.} - \item{turns}{A list of \link[ellmer:Turn]{ellmer::Turn}s or an \link[ellmer:Chat]{ellmer::Chat} object that provides the conversation history to summarize. If not provided, uses the chat history from \code{client}.} +\item{client}{An \link[ellmer:Chat]{ellmer::Chat} client that will be used to summarize the +chat. Must be provided if \code{turns} is not an \link[ellmer:Chat]{ellmer::Chat} object.} + \item{additional_guidance}{Optional prompt to guide the summarization focus. If not provided, creates a general comprehensive summary.} diff --git a/tests/testthat/test-tool-agent-summarize-chat.R b/tests/testthat/test-tool-agent-summarize-chat.R new file mode 100644 index 00000000..4540be31 --- /dev/null +++ b/tests/testthat/test-tool-agent-summarize-chat.R @@ -0,0 +1,63 @@ +test_that("btw_task_summarize_chat() throws for invalid input", { + withr::local_envvar(list(OPENAI_API_KEY = "beep")) + + chat <- ellmer::chat_openai(model = "gpt-4.1-nano") + + expect_error( + btw_task_summarize_chat(list()) + ) + + expect_error( + btw_task_summarize_chat(chat, additional_guidance = 123) + ) + + expect_error( + btw_task_summarize_chat(list(), chat, start_turn = -1) + ) + + # Start turn can't be greater than the number of turns + expect_error( + btw_task_summarize_chat(list(list()), chat, start_turn = 2) + ) +}) + +test_that("turns_simplify()", { + withr::local_envvar(list(OPENAI_API_KEY = "beep")) + chat <- ellmer::chat_openai(model = "gpt-4.1-nano") + + turns <- list( + ellmer::Turn(role = "user", contents = "Hello"), + ellmer::Turn(role = "assistant", contents = "Hi there!"), + ellmer::Turn(role = "user", contents = "How are you?"), + ellmer::Turn( + role = "assistant", + contents = list(ellmer::ContentToolRequest(id = "tool1", name = "tool_1")) + ), + ellmer::Turn( + role = "assistant", + contents = list( + ellmer::ContentToolResult( + value = "Done", + request = ellmer::ContentToolRequest(id = "tool1", name = "tool_1") + ) + ) + ) + ) + + expect_equal(turns_simplify(chat), list()) + expect_equal(turns_simplify(turns[1:3]), turns[1:3]) + + chat$set_turns(turns[1:3]) + expect_equal(turns_simplify(chat), turns[1:3]) + + turns_expected <- turns + for (i in 4:5) { + turns_expected[[i]]@contents[[1]] <- ellmer::ContentText( + format(turns[[i]]@contents[[1]]) + ) + } + expect_equal(turns_simplify(turns), turns_expected) + + chat$set_turns(turns) + expect_equal(turns_simplify(chat), turns_expected) +}) From 81059f168f88e88c281cea026b81f15aa58f95f9 Mon Sep 17 00:00:00 2001 From: Garrick Aden-Buie Date: Mon, 28 Jul 2025 11:55:51 -0400 Subject: [PATCH 7/7] draft: btw.client.summarizer option --- R/tool-agent-summarize-chat.R | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/R/tool-agent-summarize-chat.R b/R/tool-agent-summarize-chat.R index 8fb9b0d6..796a334b 100644 --- a/R/tool-agent-summarize-chat.R +++ b/R/tool-agent-summarize-chat.R @@ -36,16 +36,23 @@ btw_task_summarize_chat <- function( additional_guidance = "", start_turn = 0 ) { - if (inherits(turns, "Chat")) { - if (is.null(client)) { - client <- turns - } else { - check_inherits(client, "Chat") + if (is.null(client)) { + summarizer_client <- getOption("btw.client.summarizer", NULL) + if (!is.null(summarizer_client)) { + client <- summarizer_client + } + } + + if (is.null(client)) { + if (!inherits(turns, "Chat")) { + stop( + "If 'turns' is not a Chat object, 'client' must be provided or set via the `btw.client.summarizer` option." + ) } - } else if (is.null(client)) { - stop("If 'turns' is not a Chat object, 'client' must be provided.") + client <- turns } + check_inherits(client, "Chat") check_string(additional_guidance) turns <- turns_simplify(turns) check_number_whole(start_turn, min = 0, max = as.numeric(length(turns)))