Skip to content
Draft
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
7 changes: 6 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ Imports:
tibble,
utils,
withr,
xml2
xml2,
promises
Suggests:
bslib (>= 0.7.0),
chromote,
gh,
mirai,
pandoc,
shiny,
shinychat (>= 0.2.0),
Expand All @@ -69,6 +71,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'
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
100 changes: 74 additions & 26 deletions R/btw_client.R
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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")
client <- default$clone()
}
}

# 3a. Check for usage of deprecated btw.md fields
if (!is.null(config$provider)) {
lifecycle::deprecate_stop(
when = "0.0.3",
Expand All @@ -202,33 +207,72 @@ 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 %||% list(),
config_agent$client %||% list()
)

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)
}
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())
Expand Down Expand Up @@ -270,6 +314,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")
Expand Down
68 changes: 68 additions & 0 deletions R/tool-agent-researcher-r-task.R
Original file line number Diff line number Diff line change
@@ -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.)---",
)
}
105 changes: 105 additions & 0 deletions R/tool-agent-summarize-chat.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#' 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 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.
#' 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(
turns,
client = NULL,
additional_guidance = "",
start_turn = 0
) {
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."
)
}
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)))

# 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)

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) {
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
}
Loading
Loading