Skip to content

Commit 68a0917

Browse files
committed
feat: btw Agents
1 parent e328484 commit 68a0917

File tree

10 files changed

+607
-26
lines changed

10 files changed

+607
-26
lines changed

DESCRIPTION

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Suggests:
4545
bslib (>= 0.7.0),
4646
chromote,
4747
gh,
48+
mirai,
4849
pandoc,
4950
shiny,
5051
shinychat (>= 0.2.0),
@@ -69,6 +70,9 @@ Collate:
6970
'import-standalone-purrr.R'
7071
'import-standalone-types-check.R'
7172
'mcp.R'
73+
'tool-agent-researcher-r-task.R'
74+
'tool-agent-summarize-chat.R'
75+
'tool-agent.R'
7276
'tool-data-frame.R'
7377
'tool-result.R'
7478
'tool-docs-news.R'

NAMESPACE

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ S3method(btw_this,pkg_search_result)
2020
S3method(btw_this,tbl)
2121
S3method(btw_this,vignette)
2222
export(btw)
23+
export(btw_agent_researcher_r_task)
2324
export(btw_app)
2425
export(btw_client)
2526
export(btw_mcp_server)
2627
export(btw_mcp_session)
28+
export(btw_task_summarize_chat)
2729
export(btw_this)
30+
export(btw_tool_agent)
2831
export(btw_tool_docs_available_vignettes)
2932
export(btw_tool_docs_help_page)
3033
export(btw_tool_docs_package_help_topics)

R/btw_client.R

Lines changed: 70 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ btw_client <- function(
123123

124124
client <- config$client
125125

126+
withr::local_options(
127+
btw.__agent_client__ = client,
128+
btw.__agent_turns__ = client
129+
)
130+
126131
sys_prompt <- client$get_system_prompt()
127132
sys_prompt <- c(
128133
"# System and Session Context",
@@ -173,19 +178,19 @@ btw_client_config <- function(client = NULL, tools = NULL, config = list()) {
173178

174179
config$tools <- flatten_and_check_tools(config$tools)
175180

181+
# 1. Client was provided explicitly
176182
if (!is.null(client)) {
177183
check_inherits(client, "Chat")
178-
config$client <- client
179-
return(config)
180-
}
181-
182-
default <- getOption("btw.client")
183-
if (!is.null(default)) {
184-
check_inherits(default, "Chat")
185-
config$client <- default$clone()
186-
return(config)
184+
} else {
185+
# 2. Client wasn't provided, check the `btw.client` R option
186+
default <- getOption("btw.client")
187+
if (!is.null(default)) {
188+
check_inherits(default, "Chat")
189+
config$client <- default$clone()
190+
}
187191
}
188192

193+
# 3a. Check for usage of deprecated btw.md fields
189194
if (!is.null(config$provider)) {
190195
lifecycle::deprecate_stop(
191196
when = "0.0.3",
@@ -202,33 +207,68 @@ btw_client_config <- function(client = NULL, tools = NULL, config = list()) {
202207
)
203208
}
204209

205-
if (!is.null(config$client)) {
206-
chat_args <- utils::modifyList(
207-
list(echo = "output"), # defaults
208-
config$client
209-
)
210+
# 3b. Use the `btw.md` file to configure the client
211+
if (is.null(client) && !is.null(config$client)) {
212+
client <- btw_config_client(config$client)
213+
}
210214

211-
chat_fn <- gsub(" ", "_", tolower(chat_args$provider))
212-
if (!grepl("^chat_", chat_fn)) {
213-
chat_fn <- paste0("chat_", chat_fn)
214-
}
215-
chat_args$provider <- NULL
215+
# 4. Default to Claude from Anthropic
216+
if (is.null(client)) {
217+
client <- ellmer::chat_anthropic(echo = "output")
218+
}
216219

217-
chat_client <- call2(.ns = "ellmer", chat_fn, !!!chat_args)
218-
config$client <- eval(chat_client)
220+
withr::local_options(
221+
btw.__agent_client__ = client,
222+
btw.__agent_turns__ = client
223+
)
219224

220-
if (!is.null(chat_args$model)) {
221-
cli::cli_inform(
222-
"Using {.field {chat_args$model}} from {.strong {config$client$get_provider()@name}}."
225+
# ---- Agents ----
226+
if (!is.null(config$agents)) {
227+
for (config_agent in config$agents) {
228+
# The agent inherits from the base btw client
229+
config_agent$client <- utils::modifyList(
230+
config$client,
231+
config_agent$client %||% list()
223232
)
233+
234+
config_agent$client <- btw_config_client(
235+
config_agent$client,
236+
quiet = TRUE
237+
)
238+
239+
agent_tool <- do.call(btw_tool_agent, config_agent)
240+
client$register_tool(agent_tool)
224241
}
225-
return(config)
226242
}
227243

228-
config$client <- ellmer::chat_anthropic(echo = "output")
244+
config$client <- client
229245
config
230246
}
231247

248+
btw_config_client <- function(config_client, quiet = FALSE) {
249+
chat_args <- utils::modifyList(
250+
list(echo = "output"), # defaults
251+
config_client
252+
)
253+
254+
chat_fn <- gsub(" ", "_", tolower(chat_args$provider))
255+
if (!grepl("^chat_", chat_fn)) {
256+
chat_fn <- paste0("chat_", chat_fn)
257+
}
258+
chat_args$provider <- NULL
259+
260+
chat_client <- call2(.ns = "ellmer", chat_fn, !!!chat_args)
261+
client <- eval(chat_client)
262+
263+
if (isFALSE(quiet) && !is.null(chat_args$model)) {
264+
cli::cli_inform(
265+
"Using {.field {chat_args$model}} from {.strong {client$get_provider()@name}}."
266+
)
267+
}
268+
269+
client
270+
}
271+
232272
flatten_and_check_tools <- function(tools) {
233273
if (isFALSE(tools)) {
234274
return(list())
@@ -270,6 +310,10 @@ flatten_and_check_tools <- function(tools) {
270310
}
271311

272312
read_btw_file <- function(path = NULL) {
313+
if (isFALSE(path)) {
314+
return(list())
315+
}
316+
273317
must_find <- !is.null(path)
274318

275319
path <- path %||% path_find_in_project("btw.md") %||% path_find_user("btw.md")

R/tool-agent-researcher-r-task.R

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#' Agent: A Tool for Researching R Tasks
2+
#'
3+
#' An agent that researches R packages and provides example code for specific R
4+
#' tasks. This agent is designed to help users find the best R packages for
5+
#' specific tasks, list key functions and arguments, and return runnable
6+
#' example code.
7+
#'
8+
#' @param client An [ellmer::Chat] client, defaults to [btw_client()].
9+
#' @inheritParams btw_tool_agent
10+
#'
11+
#' @returns Returns an aync [ellmer::tool()] that can be used to invoke the
12+
#' R task research agent.
13+
#'
14+
#' @family agents
15+
#' @export
16+
btw_agent_researcher_r_task <- function(
17+
client = btw_client(tools = FALSE),
18+
tools = c("docs", "search", "session")
19+
) {
20+
btw_tool_agent(
21+
client = client,
22+
tools = tools,
23+
turns = FALSE, # Don't use initial turns, this agent is stateless
24+
name = "btw_agent_researcher_r_task",
25+
title = "R Task Research Agent",
26+
description = r"(Research an R programming task.
27+
28+
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.
29+
30+
INPUT
31+
A unique, non-overlapping question such as "Draw a clustered heat-map", "Fit a beta-regression", or "Read a parquet file".
32+
33+
PARALLEL TASKS
34+
Prefer batching distinct research tasks only when they are clearly orthogonal.
35+
36+
GUIDELINES
37+
* Break the user's goal into the small, independent R tasks.
38+
* Avoid issuing more than one call for the same or highly similar question.
39+
* Limit parallel calls to reduce duplication and API load.
40+
* Do not use this tool for broad or overlapping questions.
41+
* Only use this tool when you do not already know the answer.
42+
)",
43+
system_prompt = r"---(
44+
MISSION
45+
Treat the input as an explicit research question about R packages.
46+
Search CRAN docs, vignettes, help pages, and other provided tools to answer that question.
47+
Return the answer in a compact format that explains your findings and provides example code that can be run in R.
48+
49+
RESPONSE FORMAT
50+
Your response should include (one block per topic or package)
51+
52+
* Task
53+
* Package(s): pkg1, pkg2, ...
54+
* Why these packages? 1-sentence rationale.
55+
* Key functions & arguments related to the task as a bullet list.
56+
* Example code in a fenced R snippet that can be run as-is.
57+
* Citations: minimal list of URLs or help files consulted.
58+
59+
GUIDELINES
60+
* Be factual; no speculation.
61+
* Prefer base CRAN packages unless the question implies otherwise.
62+
* Keep prose succinct; let code illustrate usage.
63+
* If multiple packages solve the task, list the best 2-3 and show code for the top choice.
64+
* If no suitable package exists, say so and suggest an alternative strategy.
65+
66+
Return only the summarized research.)---",
67+
)
68+
}

R/tool-agent-summarize-chat.R

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#' Summarize Chat Conversation
2+
#'
3+
#' Creates a concise summary of a chat conversation, removing false starts and
4+
#' focusing on key outcomes and findings. This is a direct function that can be
5+
#' called without the agent wrapper.
6+
#'
7+
#' @param client An [ellmer::Chat] client that will be used to summarize the
8+
#' chat.
9+
#' @param turns A list of [ellmer::Turn]s or an [ellmer::Chat] object that
10+
#' provides the conversation history to summarize. If not provided, uses the
11+
#' chat history from `client`.
12+
#' @param additional_guidance Optional prompt to guide the summarization focus.
13+
#' If not provided, creates a general comprehensive summary.
14+
#' @param start_turn Integer specifying which turn to start summarizing from.
15+
#' Default is 0, meaning summarize the entire conversation history.
16+
#'
17+
#' @returns A character string containing the conversation summary.
18+
#'
19+
#' @examples
20+
#' \dontrun{
21+
#' # Summarize an entire chat
22+
#' summary <- btw_task_summarize_chat(my_chat)
23+
#'
24+
#' # Summarize with specific focus
25+
#' summary <- btw_task_summarize_chat(my_chat, "Focus on the R programming solutions")
26+
#'
27+
#' # Summarize starting from turn 5
28+
#' summary <- btw_task_summarize_chat(my_chat, start_turn = 5)
29+
#' }
30+
#'
31+
#' @family tasks
32+
#' @export
33+
btw_task_summarize_chat <- function(
34+
client,
35+
turns = client,
36+
additional_guidance = "",
37+
start_turn = 0
38+
) {
39+
check_inherits(client, "Chat")
40+
check_string(additional_guidance)
41+
turns <- turns_simplify(turns)
42+
check_number_whole(start_turn, min = 0, max = as.numeric(length(turns)))
43+
44+
# Create clone of the client to avoid modifying the original client
45+
summary_client <- client$clone()
46+
# No tools required
47+
summary_client$set_tools(list())
48+
49+
if (start_turn > 0) {
50+
turns <- turns[-seq_len(start_turn - 1)]
51+
}
52+
53+
summary_client$set_turns(turns)
54+
55+
summary_client$set_system_prompt(NULL)
56+
57+
summary_client$chat(
58+
ellmer::interpolate_file(
59+
system.file("prompts", "chat-summary.md", package = "btw"),
60+
additional_guidance = additional_guidance
61+
)
62+
)
63+
}
64+
65+
turns_simplify <- function(turns) {
66+
if (inherits(turns, "Chat")) {
67+
turns <- turns$get_turns()
68+
}
69+
70+
# Simplify contents to flatten tool requests/results, avoiding API errors that
71+
# happen when there are tool requests/results in the chat history, but no tools.
72+
for (i in seq_along(turns)) {
73+
turns[[i]]@contents <- map(turns[[i]]@contents, function(content) {
74+
is_tool_request <- S7::S7_inherits(content, ellmer::ContentToolRequest)
75+
is_tool_result <- S7::S7_inherits(content, ellmer::ContentToolResult)
76+
77+
if (is_tool_request || is_tool_result) {
78+
content <- ellmer::ContentText(format(content))
79+
}
80+
81+
content
82+
})
83+
}
84+
85+
turns
86+
}

0 commit comments

Comments
 (0)