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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: ChatRBox
Title: Your Chatbot Development Toolkit
Version: 0.2.7
Version: 0.3.0
Authors@R:
c(
person("Abigail", "Barnett", email = "Abigail.Barnett@pfizer.com", role = c("aut", "cre"), comment = c(ORCID = "0009-0009-1441-4929")),
Expand Down Expand Up @@ -32,6 +32,7 @@ Imports:
S7,
stats,
stringr,
tools,
yaml
Suggests:
config,
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export(add_http_status_guidance)
export(apply_httr2_config)
export(build_api_client)
export(build_api_client_env)
export(build_tool_docs)
export(check_duplicate_names)
export(check_names)
export(code_extract)
Expand Down Expand Up @@ -111,6 +112,8 @@ importFrom(stats,setNames)
importFrom(stringr,str_detect)
importFrom(stringr,str_extract)
importFrom(stringr,str_remove_all)
importFrom(tools,Rd2txt)
importFrom(tools,Rd_db)
importFrom(utils,capture.output)
importFrom(utils,modifyList)
importFrom(utils,str)
Expand Down
6 changes: 5 additions & 1 deletion NEWS
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
Version: 0.2.7
- First opensource deployment
- First opensource deployment

Version: 0.3.0
- Added ability to interpolate tool function package documentation into system prompt
- New user-facing arguments: tool_docs, include_examples
14 changes: 14 additions & 0 deletions R/chatrbox_ai_services.R
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ ChatRBox <- R6::R6Class(
#' Environment to which data frames are added. Defaults to \code{NULL} (creates a new environment).
#' @param object_env (`environment()`)\cr
#' Environment in which past API service/tool outputs are stored as named R objects of any type, used to resolve chained API calls and to inform the AI prompt via \code{past_outputs}. Defaults to \code{NULL} (creates a new environment).
#' @param tool_docs (`logical(1)`)\cr
#' Whether to interpolate rendered tool documentation into the prompt via \code{\link{build_tool_docs}}. Defaults to \code{FALSE}.
#' @param include_examples (`logical(1)`)\cr
#' Whether retained tool documentation includes the \code{\\examples} section. Only relevant when \code{tool_docs = TRUE}. Defaults to \code{FALSE}.
#' @param prompt_template (`character(1)`)\cr
#' Prompt template for the AI, for parameter extraction and user interaction. Defaults to the ChatRBox conversational template.
#' @param summary_list (`list()`)\cr
Expand Down Expand Up @@ -84,6 +88,8 @@ ChatRBox <- R6::R6Class(
tools_env = NULL,
data_env = NULL,
object_env = NULL,
tool_docs = FALSE,
include_examples = FALSE,
prompt_template = ChatRBox::load_prompt_template(),
summary_list = list(),
final_summary_prompt = "",
Expand All @@ -104,6 +110,8 @@ ChatRBox <- R6::R6Class(
tools_env = tools_env,
data_env = data_env,
object_env = object_env,
tool_docs = tool_docs,
include_examples = include_examples,
prompt_template = prompt_template,
summary_list = summary_list,
final_summary_prompt = final_summary_prompt,
Expand Down Expand Up @@ -285,6 +293,8 @@ ChatRBox <- R6::R6Class(
#' @param tools_env Environment. Tool functions are added to this environment. Defaults to current \code{S7} object tools environment.
#' @param data_env Environment. Data frames are added to this environment. Defaults to current \code{S7} object data environment.
#' @param object_env Environment. Past API service/tool outputs are stored in this environment as named R objects of any type, used to resolve chained API calls and to inform the AI prompt via \code{past_outputs}. Defaults to current \code{S7} object object environment.
#' @param tool_docs Logical. Whether to interpolate rendered tool documentation into the prompt via \code{\link{build_tool_docs}}. Defaults to \code{S7} object current state to preserve original user intent during update.
#' @param include_examples Logical. Whether retained tool documentation includes the \code{\\examples} section. Only relevant when \code{tool_docs = TRUE}. Defaults to \code{FALSE}.
#' @param prompt_template Character. General AI prompt for outputting parameter key-value pairs for available API services/tools. Defaults to current \code{S7} object prompt, likely interpolating function paths and arguments.
#' @param summary_list List. Optional named list of instructions for AI-enabled summaries, assigned to specific services via name matching. Prompting may be provided via character strings or a Markdown file. Defaults to empty list.
#' @param final_summary_prompt Character. Single synthesis instruction steering \code{summarize = "final"}, as opposed to the per-service \code{summary_list} used by \code{summarize = TRUE}. Supplied as a literal string or the path to a Markdown file, resolved identically to \code{prompt_template}. Updated additively; defaults to the current object value.
Expand Down Expand Up @@ -312,6 +322,8 @@ ChatRBox_update <- function(object,
tools_env = object$chat_object@tools_env,
data_env = object$chat_object@data_env,
object_env = object$chat_object@object_env,
tool_docs = isTRUE(nzchar(object$chat_object@tool_docs)),
include_examples = FALSE,
prompt_template = object$chat_object@prompt_template,
summary_list = list(),
final_summary_prompt = object$chat_object@final_summary_prompt,
Expand All @@ -331,6 +343,8 @@ ChatRBox_update <- function(object,
tools_env = tools_env,
data_env = data_env,
object_env = object_env,
tool_docs = tool_docs,
include_examples = include_examples,
prompt_template = prompt_template,
summary_list = summary_list,
final_summary_prompt = final_summary_prompt,
Expand Down
123 changes: 123 additions & 0 deletions R/documentation_interpolation.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#' Builds Tool Documentation for AI Prompt Interpolation
#'
#' This function renders the help documentation of every tool function contained within a tools environment into a single Markdown-formatted character string. The result is intended for interpolation into the AI prompt via \code{\link{object_generate}}, providing the LLM with detailed reference documentation for each available tool alongside the service paths and arguments already supplied by \code{\link{get_function_path}} and \code{\link{get_function_args}}.
#'
#' For each tool function, the package documentation (Rd) is located and rendered to plain text. Where a function has no associated Rd documentation, or is not defined within a package namespace, the function falls back to its argument signature so that the LLM is never left without context.
#' @details
#' Documentation is extracted from the installed package Rd database using \code{tools::Rd_db} and rendered with \code{tools::Rd2txt}, so it reuses the tool author's own maintained documentation rather than duplicating it. The \code{\\examples} section is dropped by default to keep the interpolated prompt concise, and may be retained via \code{include_examples}. All curly braces in the assembled documentation are escaped exactly once so the returned string is safe to pass through \code{glue::glue()}, consistent with how \code{paths_string}, \code{args_string} and \code{past_outputs} are interpolated into \code{prompt_template} within \code{\link{object_generate}}. This function underpins the optional \code{tool_docs} argument of \code{\link{ChatRBox}}, \code{\link{object_generate}} and \code{\link{ChatRBox_update}}. When \code{tool_docs = TRUE}, the returned string is interpolated into the \code{{tool_docs}} placeholder of the default prompt template. Otherwise, the default prompt remains which only provides LLMs with service names and arguments. Notably, this function locates tool functions by searching the global environment, such that it was intended for package functions supplied to chatbots in R using \code{pkg::fn()} notation. Written functions and API endpoints do not have documentation suitable for rendering via this method.
#' @param tools_env Environment. Environment containing tool functions, as stored in the \code{tools_env} property of a \code{\link{ChatRBox_obj}} \code{S7} object. Defaults to \code{NULL}, which returns an empty string.
#' @param width Integer. Wrapping width passed to \code{tools::Rd2txt}. Defaults to \code{1000}.
#' @param include_examples Logical. Whether to retain the \code{\\examples} section of each tool's Rd documentation. Defaults to \code{FALSE} to keep the interpolated prompt concise.
#' @return A brace-escaped character string documenting each tool function, separated by horizontal rules. Returns \code{""} when \code{tools_env} is \code{NULL}, is not an environment, or contains no functions.
#' @seealso \code{\link{object_generate}}, \code{\link{get_function_path}}, \code{\link{get_function_args}}, \code{\link{load_prompt_template}}
#' @importFrom tools Rd_db Rd2txt
#' @importFrom utils capture.output
#' @example man/examples/examples_tool_docs.R
#' @export
build_tool_docs <- function(tools_env = NULL,
width = 1000L,
include_examples = FALSE) {

if (is.null(tools_env) || !is.environment(tools_env)) return("")

nms <- ls(envir = tools_env)
nms <- nms[vapply(nms, function(n) is.function(get(n, envir = tools_env)),
logical(1))]
if (length(nms) == 0L) return("")

blocks <- vapply(nms, function(service_name) {
fun <- get(service_name, envir = tools_env)
docs <- .tool_documentation(fun, width = width,
include_examples = include_examples)
if (any_is_empty(docs)) {
docs <- paste0("(No documentation available. Arguments: ",
paste(names(formals(fun)), collapse = ", "), ")")
}
paste0("### Service: `", service_name, "`\n\n", docs)
}, character(1))

.escape_braces(paste(blocks, collapse = "\n\n---\n\n"))
}

#' @noRd
.escape_braces <- function(text) {
if (any_is_empty(text)) return("")
text <- gsub("{", "{{", text, fixed = TRUE)
text <- gsub("}", "}}", text, fixed = TRUE)
text
}

#' Deliberately does NOT escape braces; that is done once in build_tool_docs().
#' @noRd
.clean_tool_docs <- function(text = NULL) {
if (any_is_empty(text)) return("")
text <- gsub("_\b", "", text, fixed = TRUE)
text <- gsub("\b", "", text, fixed = TRUE)
text <- gsub("\n{3,}", "\n\n", text)
trimws(text)
}

#' @noRd
.tool_documentation <- function(fun,
width = 1000L,
include_examples = FALSE) {
if (!is.function(fun)) return(NULL)

ns <- environment(fun)
if (is.null(ns) || !isNamespace(ns)) {
args <- paste(names(formals(fun)), collapse = ", ")
return(.clean_tool_docs(paste0(
"Arguments: ", if (nzchar(args)) args else "none",
". (No package documentation available.)")))
}

pkg <- getNamespaceName(ns)
rd <- tryCatch(.find_rd(pkg, fun), error = function(e) NULL)
if (is.null(rd)) {
args <- paste(names(formals(fun)), collapse = ", ")
return(.clean_tool_docs(paste0(
"Arguments: ", if (nzchar(args)) args else "none",
". (No Rd documentation in package ", pkg, ".)")))
}

if (!isTRUE(include_examples)) {
keep <- vapply(rd, function(node) {
tag <- attr(node, "Rd_tag", exact = TRUE)
is.null(tag) || tag != "\\examples"
}, logical(1))
rd <- structure(rd[keep], class = class(rd), Rd_tag = attr(rd, "Rd_tag"))
}

txt <- utils::capture.output(
tools::Rd2txt(rd, options = list(underline_titles = FALSE,
width = width, code_quote = FALSE))
)
.clean_tool_docs(paste(txt, collapse = "\n"))
}

#' @noRd
.find_rd <- function(pkg, fun) {
fun_name <- NULL
ns <- asNamespace(pkg)
for (nm in getNamespaceExports(pkg)) {
obj <- tryCatch(get(nm, envir = ns), error = function(e) NULL)
if (!is.null(obj) && identical(obj, fun)) {
fun_name <- nm
break
}
}
if (is.null(fun_name)) return(NULL)
db <- tryCatch(tools::Rd_db(pkg), error = function(e) NULL)
if (is.null(db) || length(db) == 0L) return(NULL)
aliases <- lapply(db, function(rd) {
tags <- vapply(rd, function(node) {
tag <- attr(node, "Rd_tag", exact = TRUE)
if (is.null(tag)) "" else tag
}, character(1))
unlist(lapply(rd[tags == "\\alias"],
function(a) trimws(paste(unlist(a), collapse = ""))))
})
hit <- which(vapply(aliases, function(a) fun_name %in% a, logical(1)))
if (length(hit) == 0L) return(NULL)
db[[hit[1]]]
}
41 changes: 31 additions & 10 deletions R/generate_ai_response.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
# Supported code block languages for extraction/removal
# Extend this vector to support additional structured output formats
# See CONTRIBUTING.md ('Adding a New Structured Output Language')
.SUPPORTED_CODE_LANGUAGES <- c("json", "display")
# Supported code block languages for extraction/removal.
# Extend this vector to support additional structured output formats.
# See CONTRIBUTING.md ('Adding a New Structured Output Language').
.SUPPORTED_CODE_LANGUAGES <- c("json", "display", "yaml", "yml", "tsv", "csv", "md")

#' @noRd
.code_block <- function(string = NULL,
language = NULL,
action = c("extract", "remove")) {

if (any_is_empty(string)) stop("string is empty")
if (any_is_empty(language)) stop("language is empty")

action <- match.arg(action)

if (identical(action, "extract")) {
stringr::str_extract(
string,
glue::glue("(?s)(?<=```{language}\n)(.+?)(?=\n?```)"))
} else {
stringr::str_remove_all(
string,
glue::glue("(?s)```{language}(?=\\s)\\s?.+?```"))
}
}

#' Redacts Sensitive Values from an Argument List for Safe Display
#'
Expand Down Expand Up @@ -53,7 +74,7 @@ redact_sensitive <- function(x,
#' This function extracts the first code block of a specified language from a Markdown-formatted string. The extracted block excludes language tags and code fences.
#'
#' @param string Character. The input string containing Markdown code block(s). Required.
#' @param language Character. The language tag of the code block to extract. Defaults to "json".
#' @param language Character. The language tag of the code block. One of the supported tags in \code{.SUPPORTED_CODE_LANGUAGES} (\code{"json"}, \code{"display"}, \code{"yaml"}, \code{"yml"}, \code{"tsv"}, \code{"csv"}, \code{"md"}). This may be extended by adding to the vector assignment. See CONTRIBUTING.md for further detail. Defaults to "json".
#' @return The contents of the first extracted code block.
#' @details
#' This function is used in \code{\link{llm_api_result}} to parse the LLM response. The ChatRBox AI response given the default prompt should contain both a JSON object and text output, if an API service/tool has been used. The JSON object is used to populate the chosen API service/tool function with parameters from the input question, whereas the explanation is for user interaction. Hence, these are employed differently in downstream workflows.
Expand All @@ -64,7 +85,7 @@ redact_sensitive <- function(x,
#' @importFrom glue glue
#' @example man/examples/examples_parse.R
#' @export
code_extract <- function(string = NULL,
code_extract <- function(string = NULL,
language = "json") {

if (any_is_empty(string)) stop("string is empty")
Expand All @@ -73,15 +94,15 @@ code_extract <- function(string = NULL,

language <- match.arg(arg = language, choices = .SUPPORTED_CODE_LANGUAGES)

stringr::str_extract(string, glue::glue("(?s)(?<=```{language}\n)(.+?)(?=\n?```)"))
.code_block(string = string, language = language, action = "extract")
}

#' Removes Code Block from Markdown String
#'
#' This function removes all code blocks of a specified language from a Markdown-formatted string. The string is returned excluding code blocks and code fences.
#'
#' @param string Character. The input string containing Markdown code block(s). Required.
#' @param language Character. The language tag of the code block to remove. Defaults to "json".
#' @param language Character. The language tag of the code block. One of the supported tags in \code{.SUPPORTED_CODE_LANGUAGES} (\code{"json"}, \code{"display"}, \code{"yaml"}, \code{"yml"}, \code{"tsv"}, \code{"csv"}, \code{"md"}). This may be extended by adding to the vector assignment. See CONTRIBUTING.md for further detail. Defaults to "json".
#' @return A character string with specified code blocks and fences removed.
#' @details
#' This function is used in \code{\link{llm_api_result}} to parse text and code components from the LLM response. The ChatRBox AI response given the default prompt should contain both a JSON object and plain text output, if an API service/tool has been used. The JSON object is used to populate the chosen API service/tool function with parameters from the input question, whereas the explanation is for user interaction. Hence, these are employed differently in downstream workflows.
Expand All @@ -92,7 +113,7 @@ code_extract <- function(string = NULL,
#' @importFrom glue glue
#' @example man/examples/examples_parse.R
#' @export
code_remove <- function(string = NULL,
code_remove <- function(string = NULL,
language = "json") {

if (any_is_empty(string)) stop("string is empty")
Expand All @@ -101,7 +122,7 @@ code_remove <- function(string = NULL,

language <- match.arg(arg = language, choices = .SUPPORTED_CODE_LANGUAGES)

stringr::str_remove_all(string, glue::glue("(?s)```{language}\\n?.+?```"))
.code_block(string = string, language = language, action = "remove")
}

#' Returns Second Value from JSON Object
Expand Down
Loading
Loading