diff --git a/NAMESPACE b/NAMESPACE index d5fb98a..ebc6c29 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -24,6 +24,9 @@ export(chat_perplexity) export(content_image_file) export(content_image_plot) export(content_image_url) +export(contents_html) +export(contents_markdown) +export(contents_text) export(cortex_credentials) export(create_tool_def) export(interpolate) diff --git a/R/chat.R b/R/chat.R index 754c950..ea23619 100644 --- a/R/chat.R +++ b/R/chat.R @@ -452,3 +452,21 @@ print.Chat <- function(x, ...) { invisible(x) } + +method(contents_markdown, new_S3_class("Chat")) <- function(content, heading_level = 2) { + turns <- content$get_turns() + if (length(turns) == 0) { + return("") + } + + hh <- strrep("#", heading_level) + + res <- vector("character", length(turns)) + for (i in seq_along(res)) { + role <- turns[[i]]@role + substr(role, 0, 1) <- toupper(substr(role, 0, 1)) + res[i] <- glue::glue("{hh} {role}\n\n{contents_markdown(turns[[i]])}") + } + + paste(res, collapse="\n\n") +} diff --git a/R/content.R b/R/content.R index 93f9061..bd83092 100644 --- a/R/content.R +++ b/R/content.R @@ -1,6 +1,54 @@ #' @include utils-S7.R NULL +#' Format contents into a textual representation +#' +#' @description +#' These generic functions can be use to convert [Turn] contents or [Content] +#' objects into textual representations. +#' +#' * `contents_text()` is the most minimal and only includes [ContentText] +#' objects in the output. +#' * `contents_markdown()` returns the text content (which it assumes to be +#' markdown and does not convert it) plus markdown representations of images +#' and other content types. +#' * `contents_html()` returns the text content, converted from markdown to +#' HTML with [commonmark::markdown_html()], plus HTML representations of +#' images and other content types. +#' +#' @examples +#' turns <- list( +#' Turn("user", contents = list( +#' ContentText("What's this image?"), +#' content_image_url("https://placehold.co/200x200") +#' )), +#' Turn("assistant", "It's a placeholder image.") +#' ) +#' +#' lapply(turns, contents_text) +#' lapply(turns, contents_markdown) +#' if (rlang::is_installed("commonmark")) { +#' contents_html(turns[[1]]) +#' } +#' +#' @param content The [Turn] or [Content] object to be converted into text. +#' `contents_markdown()` also accepts [Chat] instances to turn the entire +#' conversation history into markdown text. +#' @param ... Additional arguments passed to methods. +#' +#' @return A string of text, markdown or HTML. +#' @export +contents_text <- new_generic("contents_text", "content") + +#' @rdname contents_text +#' @export +contents_html <- new_generic("contents_html", "content") + +#' @rdname contents_text +#' @export +contents_markdown <- new_generic("contents_markdown", "content") + + #' Content types received from and sent to a chatbot #' #' @description @@ -24,6 +72,19 @@ NULL #' @export Content <- new_class("Content") +method(contents_text, Content) <- function(content) { + NULL +} + +method(contents_markdown, Content) <- function(content) { + # Fall back to text representation in markdown + contents_text(content) +} + +method(contents_html, Content) <- function(content) { + NULL +} + #' @rdname Content #' @export #' @param text A single string. @@ -36,14 +97,16 @@ method(format, ContentText) <- function(x, ...) { paste0(unlist(strwrap(x@text, width = getOption("width"))), collapse = "\n") } -# Internal generic for content that has a textual representation. -contents_text <- new_generic("contents_text", "content") +method(contents_text, ContentText) <- function(content) { + content@text +} -method(contents_text, Content) <- function(content) { - NULL +method(contents_html, ContentText) <- function(content) { + check_installed("commonmark") + commonmark::markdown_html(content@text) } -method(contents_text, ContentText) <- function(content) { +method(contents_markdown, ContentText) <- function(content) { content@text } @@ -71,6 +134,12 @@ ContentImageRemote <- new_class( method(format, ContentImageRemote) <- function(x, ...) { cli::format_inline("[{.strong remote image}]: {.url {x@url}}") } +method(contents_html, ContentImageRemote) <- function(content) { + sprintf('', content@url) +} +method(contents_markdown, ContentImageRemote) <- function(content) { + sprintf('![](%s)', content@url) +} #' @rdname Content #' @export @@ -87,6 +156,12 @@ ContentImageInline <- new_class( method(format, ContentImageInline) <- function(x, ...) { cli::format_inline("[{.strong inline image}]") } +method(contents_html, ContentImageInline) <- function(content) { + sprintf('', content@type, content@data) +} +method(contents_markdown, ContentImageInline) <- function(content) { + sprintf('![](data:%s;base64,%s)', content@type, content@data) +} # Tools ------------------------------------------------------------------ @@ -156,6 +231,12 @@ method(format, ContentJson) <- function(x, ...) { pretty_json(x@value) ) } +method(contents_html, ContentJson) <- function(content) { + sprintf('
%s
\n', pretty_json(content@value)) +} +method(contents_markdown, ContentJson) <- function(content) { + sprintf('```json\n%s\n```\n', pretty_json(content@value)) +} # Helpers ---------------------------------------------------------------------- diff --git a/R/provider-cortex.R b/R/provider-cortex.R index 367381e..b4db2f7 100644 --- a/R/provider-cortex.R +++ b/R/provider-cortex.R @@ -335,9 +335,17 @@ method(as_json, list(ProviderCortex, ContentSql)) <- function(provider, x) { method(contents_text, ContentSql) <- function(content) { # Emit a Markdown-formatted SQL code block as the textual representation. + contents_markdown(content) +} + +method(contents_markdown, ContentSql) <- function(content) { paste0("\n\n```sql\n", content@statement, "\n```") } +method(contents_html, ContentSql) <- function(content) { + sprintf('
%s
\n', content@statement) +} + method(format, ContentSql) <- function(x, ...) { cli::format_inline("{.strong SQL:} {.code {x@statement}}") } diff --git a/R/shiny.R b/R/shiny.R index c24f1d2..a4d53e4 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -78,7 +78,7 @@ live_browser <- function(chat, quiet = FALSE) { for (turn in chat$get_turns()) { shinychat::chat_append_message("chat", list( role = turn@role, - content = turn@text + content = contents_markdown(turn) )) } diff --git a/R/turns.R b/R/turns.R index 93de9ad..f39d3c7 100644 --- a/R/turns.R +++ b/R/turns.R @@ -44,9 +44,7 @@ Turn <- new_class( ), text = new_property( class = class_character, - getter = function(self) { - paste0(unlist(lapply(self@contents, contents_text)), collapse = "") - } + getter = function(self) contents_text(self) ) ), constructor = function(role, @@ -70,6 +68,15 @@ method(format, Turn) <- function(x, ...) { contents <- map_chr(x@contents, format, ...) paste0(contents, "\n", collapse = "") } +method(contents_text, Turn) <- function(content) { + paste0(unlist(lapply(content@contents, contents_text)), collapse = "") +} +method(contents_html, Turn) <- function(content) { + paste0(unlist(lapply(content@contents, contents_html)), collapse = "\n") +} +method(contents_markdown, Turn) <- function(content) { + paste0(unlist(lapply(content@contents, contents_markdown)), collapse = "\n\n") +} user_turn <- function(..., .error_call = caller_env()) { if (...length() == 0) { diff --git a/_pkgdown.yml b/_pkgdown.yml index 8c1a796..a2b2ab5 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -38,3 +38,7 @@ reference: - Provider - Content - Chat + +- title: Utilities + contents: + - contents_text diff --git a/man/contents_text.Rd b/man/contents_text.Rd new file mode 100644 index 0000000..c0e4814 --- /dev/null +++ b/man/contents_text.Rd @@ -0,0 +1,54 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/content.R +\name{contents_text} +\alias{contents_text} +\alias{contents_html} +\alias{contents_markdown} +\title{Format contents into a textual representation} +\usage{ +contents_text(content, ...) + +contents_html(content, ...) + +contents_markdown(content, ...) +} +\arguments{ +\item{content}{The \link{Turn} or \link{Content} object to be converted into text. +\code{contents_markdown()} also accepts \link{Chat} instances to turn the entire +conversation history into markdown text.} + +\item{...}{Additional arguments passed to methods.} +} +\value{ +A string of text, markdown or HTML. +} +\description{ +These generic functions can be use to convert \link{Turn} contents or \link{Content} +objects into textual representations. +\itemize{ +\item \code{contents_text()} is the most minimal and only includes \link{ContentText} +objects in the output. +\item \code{contents_markdown()} returns the text content (which it assumes to be +markdown and does not convert it) plus markdown representations of images +and other content types. +\item \code{contents_html()} returns the text content, converted from markdown to +HTML with \code{\link[commonmark:commonmark]{commonmark::markdown_html()}}, plus HTML representations of +images and other content types. +} +} +\examples{ +turns <- list( + Turn("user", contents = list( + ContentText("What's this image?"), + content_image_url("https://placehold.co/200x200") + )), + Turn("assistant", "It's a placeholder image.") +) + +lapply(turns, contents_text) +lapply(turns, contents_markdown) +if (rlang::is_installed("commonmark")) { + contents_html(turns[[1]]) +} + +} diff --git a/tests/testthat/_snaps/content.md b/tests/testthat/_snaps/content.md index 70f7bda..5c51ce9 100644 --- a/tests/testthat/_snaps/content.md +++ b/tests/testthat/_snaps/content.md @@ -13,3 +13,108 @@ Error in `FUN()`: ! `...` must be made up strings or objects, not `TRUE`. +# turn contents can be converted to text, markdown and HTML + + Code + cat(contents_text(turn)) + Output + User input. + + ```sql + SELECT * FROM mtcars + ``` + + #### Suggestions + + - What is the total quantity sold for each product last quarter? + - What is the average discount percentage for orders from the United States? + - What is the average price of products in the 'electronics' category? + +--- + + Code + cat(contents_markdown(turn)) + Output + User input. + + ![](data:image/png;base64,abcd123) + + ![](https://example.com/image.jpg) + + ```json + { + "a": [1, 2], + "b": "apple" + } + ``` + + + + + ```sql + SELECT * FROM mtcars + ``` + + + + #### Suggestions + + - What is the total quantity sold for each product last quarter? + - What is the average discount percentage for orders from the United States? + - What is the average price of products in the 'electronics' category? + +--- + + Code + cat(contents_markdown(chat)) + Output + ## User + + User input. + + ![](data:image/png;base64,abcd123) + + ![](https://example.com/image.jpg) + + ```json + { + "a": [1, 2], + "b": "apple" + } + ``` + + + + + ```sql + SELECT * FROM mtcars + ``` + + + + #### Suggestions + + - What is the total quantity sold for each product last quarter? + - What is the average discount percentage for orders from the United States? + - What is the average price of products in the 'electronics' category? + + ## Assistant + + Here's your answer. + +--- + + Code + cat(contents_html(turn)) + Output +

User input.

+ + + +
{
+        "a": [1, 2],
+        "b": "apple"
+      }
+ +
SELECT * FROM mtcars
+ diff --git a/tests/testthat/test-content.R b/tests/testthat/test-content.R index e9b4317..40bc861 100644 --- a/tests/testthat/test-content.R +++ b/tests/testthat/test-content.R @@ -13,3 +13,36 @@ test_that("can create content from a vector", { ContentText("a\n\nb") ) }) + +test_that("turn contents can be converted to text, markdown and HTML", { + turn <- Turn( + "user", + contents = list( + ContentText("User input."), + ContentImageInline("image/png", "abcd123"), + ContentImageRemote("https://example.com/image.jpg", detail = ""), + ContentJson(list(a = 1:2, b = "apple")), + ContentSql("SELECT * FROM mtcars"), + ContentSuggestions( + c( + "What is the total quantity sold for each product last quarter?", + "What is the average discount percentage for orders from the United States?", + "What is the average price of products in the 'electronics' category?" + ) + ) + ) + ) + + expect_snapshot(cat(contents_text(turn))) + expect_snapshot(cat(contents_markdown(turn))) + + turns <- list( + turn, + Turn("assistant", list(ContentText("Here's your answer."))) + ) + chat <- Chat$new(Provider("https://example.com/api"), turns = turns) + expect_snapshot(cat(contents_markdown(chat))) + + skip_if_not_installed("commonmark") + expect_snapshot(cat(contents_html(turn))) +})