Skip to content

Commit cfdee4e

Browse files
authored
Add RPCs for package documentation tools (#868)
* tools for package help & vignettes * cleanup pandoc path & return types * fix string interpolation
1 parent 74d3f1d commit cfdee4e

File tree

3 files changed

+627
-1
lines changed

3 files changed

+627
-1
lines changed

crates/ark/src/modules/positron/help.R

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#
22
# help.R
33
#
4-
# Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
4+
# Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
55
#
66
#
77

@@ -427,3 +427,70 @@ rewrite_help_links <- function(line, package, package_root) {
427427

428428
line
429429
}
430+
431+
#' Convert a help page to an Rd object.
432+
#'
433+
#' Ported from btw:::help_to_rd()
434+
help_to_rd <- function(help_page) {
435+
if (inherits(help_page, "dev_topic")) {
436+
rd_path <- help_page$path
437+
return(tools::parse_Rd(rd_path))
438+
}
439+
440+
help_path <- as.character(help_page)
441+
rd_name <- basename(help_path)
442+
rd_package <- basename(dirname(dirname(help_path)))
443+
tools::Rd_db(rd_package)[[paste0(rd_name, ".Rd")]]
444+
}
445+
446+
447+
#' Format a help page as Markdown.
448+
#'
449+
#' Ported from btw:::format_help_page_markdown()
450+
format_help_page_markdown <- function(
451+
help_page,
452+
...,
453+
to = "markdown_strict+pipe_tables+backtick_code_blocks"
454+
) {
455+
rd_obj <- help_to_rd(help_page)
456+
tmp_rd_file <- tempfile(fileext = ".html")
457+
458+
tools::Rd2HTML(rd_obj, out = tmp_rd_file)
459+
460+
pandoc_convert(
461+
tmp_rd_file,
462+
to = to,
463+
...
464+
)
465+
}
466+
467+
#' Get the help topic and package from a help page.
468+
#'
469+
#' Ported from btw:::help_package_topic()
470+
help_package_topic <- function(help_page) {
471+
if (inherits(help_page, "dev_topic")) {
472+
return(list(
473+
topic = help_page$topic,
474+
resolved = help_page$path,
475+
package = help_page$pkg
476+
))
477+
}
478+
479+
# help() mainly returns a path to the un-aliased help topic
480+
# help("promise"): .../library/promises/help/promise
481+
# help("mutate_if", "dplyr"): .../library/dplyr/help/mutate_all
482+
topic <- attr(help_page, "topic", exact = TRUE)
483+
484+
help_path <- as.character(help_page)
485+
486+
# In the case where there are multiple matches, sort them so that the
487+
# raised error is deterministic
488+
package <- basename(dirname(dirname(help_path)))
489+
sort_indices <- rank(package, ties.method = "first")
490+
491+
list(
492+
topic = rep(topic, length(help_path)),
493+
resolved = basename(help_path)[sort_indices],
494+
package = if (length(package)) package[sort_indices]
495+
)
496+
}
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#
2+
# llm_tools.R
3+
#
4+
# Copyright (C) 2025 Posit Software, PBC. All rights reserved.
5+
#
6+
#
7+
8+
#' Get the help topics for a package
9+
#'
10+
#' This function retrieves the help topics for a specified package in R.
11+
#' It returns a data frame with the topic ID, title, and aliases for each help
12+
#' topic in the package.
13+
#'
14+
#' Adapted from btw::btw_tool_docs_package_help_topics
15+
#'
16+
#' @param package_name Name of the package to get help topics for
17+
#' @return A list of help topics for the package, each with a topic ID,
18+
#' title, and aliases.
19+
#'
20+
#' @export
21+
.ps.rpc.list_package_help_topics <- function(package_name) {
22+
# Check if the package is installed
23+
if (!requireNamespace(package_name, quietly = TRUE)) {
24+
return(paste("Package", package_name, "is not installed."))
25+
}
26+
27+
# Search for help topics in the package
28+
help_db <- utils::help.search(
29+
"",
30+
package = package_name,
31+
fields = c("alias", "title"),
32+
ignore.case = TRUE
33+
)
34+
res <- help_db$matches
35+
36+
# Did we get any matches?
37+
if (nrow(res) == 0) {
38+
return(paste("No help topics found for package", package_name, "."))
39+
}
40+
41+
res_split <- split(res, res$Name)
42+
res_list <- lapply(res_split, function(group) {
43+
list(
44+
topic_id = group$Name[1],
45+
title = group$Entry[group$Field == "Title"][1],
46+
aliases = paste(
47+
group$Entry[group$Field == "alias"],
48+
collapse = ", "
49+
)
50+
)
51+
})
52+
names(res_list) <- NULL
53+
res_list
54+
}
55+
56+
#' Get available vignettes for a package
57+
#'
58+
#' This function retrieves the vignettes available for a specified package in R.
59+
#' It returns a list of vignettes, each with a title and topic.
60+
#'
61+
#' Adapted from btw::btw_tool_docs_available_vignettes.
62+
#'
63+
#' @param package_name Name of the package to get vignettes for
64+
#' @return A list of vignettes for the package, each with a title and topic.
65+
#'
66+
#' @export
67+
.ps.rpc.list_available_vignettes <- function(package_name) {
68+
# Check if the package is installed
69+
if (!requireNamespace(package_name, quietly = TRUE)) {
70+
return(paste("Package", package_name, "is not installed."))
71+
}
72+
73+
# Get vignettes for the package
74+
vignettes <- tools::getVignetteInfo(package = package_name)
75+
if (length(vignettes) == 0) {
76+
return(paste("Package", package_name, "has no vignettes."))
77+
}
78+
79+
# Convert the matrix to a list of lists
80+
vignette_list <- lapply(seq_len(nrow(vignettes)), function(i) {
81+
list(
82+
title = vignettes[i, "Title"],
83+
topic = vignettes[i, "Topic"]
84+
)
85+
})
86+
vignette_list
87+
}
88+
89+
#' Get a specific vignette for a package
90+
#'
91+
#' This function retrieves a specific vignette available for a specified package in R.
92+
#' It returns the vignette content as a Markdown character string.
93+
#'
94+
#' Adapted from btw::btw_tool_docs_vignette.
95+
#'
96+
#' @param package_name Name of the package to get vignettes for
97+
#' @return A list of vignettes for the package, each with a title and topic.
98+
#'
99+
#' @export
100+
.ps.rpc.get_package_vignette <- function(package_name, vignette) {
101+
vignettes <- as.data.frame(tools::getVignetteInfo(package = package_name))
102+
if (nrow(vignettes) == 0) {
103+
return(paste("Package", package_name, "has no vignettes."))
104+
}
105+
vignette_info <- vignettes[vignettes$Topic == vignette, , drop = FALSE]
106+
if (nrow(vignette_info) == 0) {
107+
return(
108+
paste(
109+
"No vignette",
110+
vignette,
111+
"for package",
112+
package_name,
113+
"found."
114+
)
115+
)
116+
}
117+
118+
# Use Pandoc (bundled with Positron) to convert rendered vignette (PDF or
119+
# HTML) to Markdown
120+
output_file <- tempfile(fileext = ".md")
121+
tryCatch(
122+
{
123+
pandoc_convert(
124+
input = file.path(vignette_info$Dir, "doc", vignette_info$PDF),
125+
to = "markdown",
126+
output = output_file,
127+
verbose = FALSE
128+
)
129+
# read the converted Markdown file
130+
vignette_md <- readLines(output_file, warn = FALSE)
131+
132+
# remove the first line which is the title
133+
vignette_md <- vignette_md[-1]
134+
vignette_md <- paste(vignette_md, collapse = "\n")
135+
vignette_md
136+
},
137+
error = function(e) {
138+
paste("Error converting vignette:", e$message)
139+
}
140+
)
141+
}
142+
143+
144+
#' Get a specific help page
145+
#'
146+
#' This function retrieves a specific help page available for a specified package in R.
147+
#' It returns the help page content as a Markdown character string.
148+
#'
149+
#' Adapted from btw::btw_tool_docs_help_page.
150+
#'
151+
#' @param topic The topic to get help for
152+
#' @param package_name The name of the package to get help for. If empty,
153+
#' searches all installed packages.
154+
#' @return A list of help pages for the package, each with a title and topic.
155+
#'
156+
#' @export
157+
.ps.rpc.get_help_page <- function(topic, package_name = "") {
158+
if (identical(package_name, "")) {
159+
package_name <- NULL
160+
}
161+
162+
if (!is.null(package_name)) {
163+
if (!requireNamespace(package_name, quietly = TRUE)) {
164+
return(paste("Package", package_name, "is not installed."))
165+
}
166+
}
167+
168+
# Temporarily disable menu graphics
169+
old.menu.graphics <- getOption("menu.graphics", default = TRUE)
170+
options(menu.graphics = FALSE)
171+
on.exit(options(menu.graphics = old.menu.graphics), add = TRUE)
172+
173+
# Read the help page
174+
help_page <- utils::help(
175+
package = (package_name),
176+
topic = (topic),
177+
help_type = "text",
178+
try.all.packages = (is.null(package_name))
179+
)
180+
181+
if (!length(help_page)) {
182+
return(
183+
paste0(
184+
"No help page found for topic ",
185+
topic,
186+
if (!is.null(package_name)) {
187+
paste(" in package", package_name)
188+
} else {
189+
" in all installed packages"
190+
},
191+
"."
192+
)
193+
)
194+
}
195+
196+
# Resolve the help page to a specific topic and package
197+
resolved <- help_package_topic(help_page)
198+
199+
if (length(resolved$resolved) > 1) {
200+
calls <- sprintf(
201+
'{"topic":"%s", "package_name":"%s"}',
202+
resolved$resolved,
203+
resolved$package
204+
)
205+
calls <- stats::setNames(calls, "*")
206+
return(
207+
paste(
208+
"Topic",
209+
topic,
210+
"matched",
211+
length(resolved$resolved),
212+
"different topics. Choose one or submit individual tool calls for each topic.",
213+
)
214+
)
215+
}
216+
217+
# Convert the help page to Markdown using Pandoc
218+
md_file <- tempfile(fileext = ".md")
219+
format_help_page_markdown(
220+
help_page,
221+
output = md_file,
222+
options = c("--shift-heading-level-by=1")
223+
)
224+
md <- readLines(md_file, warn = FALSE)
225+
226+
# Remove up to the first empty line
227+
first_empty <- match(TRUE, !nzchar(md), nomatch = 1) - 1
228+
if (first_empty > 0) {
229+
md <- md[-seq_len(first_empty)]
230+
}
231+
232+
# Add a heading for the help page
233+
heading <- sprintf(
234+
"## `help(package = \"%s\", \"%s\")`",
235+
resolved$package,
236+
topic
237+
)
238+
239+
# Return the help page as a list
240+
list(
241+
help_text = paste0(md, collapse = "\n"),
242+
topic = basename(resolved$topic),
243+
package = resolved$package
244+
)
245+
}

0 commit comments

Comments
 (0)