From fe36933265e6d7aafadbc6200106fa9bd844cdfd Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Wed, 10 May 2023 09:35:16 -0500 Subject: [PATCH 01/21] base for request in httr2 --- NAMESPACE | 1 + R/openai_api_calls.R | 40 +++++++++++++++++++++++++++++++++- man/get_available_endpoints.Rd | 17 +++++++++++++++ man/request_base.Rd | 22 +++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 man/get_available_endpoints.Rd create mode 100644 man/request_base.Rd diff --git a/NAMESPACE b/NAMESPACE index 51d21b38..65ce7a9f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -8,6 +8,7 @@ export(addin_spelling_grammar) export(check_api) export(check_api_connection) export(check_api_key) +export(get_available_endpoints) export(get_ide_theme_info) export(gpt_chat) export(gpt_chat_in_source) diff --git a/R/openai_api_calls.R b/R/openai_api_calls.R index fa7e5cde..828ec948 100644 --- a/R/openai_api_calls.R +++ b/R/openai_api_calls.R @@ -71,6 +71,8 @@ openai_create_edit <- function(model, #' @param task The task that specifies the API url to use, defaults to #' "completions" and "chat/completions" is required for ChatGPT model. #' +#' @importFrom assertthat assert_that +#' #' @return A list with the generated completions and other information returned #' by the API. #' @examples @@ -162,7 +164,7 @@ openai_create_chat_completion <- } query_openai_api <- function(body, openai_api_key, task) { - arg_match(task, c("completions", "chat/completions", "edits", "embeddings")) + base_url <- glue("https://api.openai.com/v1/{task}") @@ -208,3 +210,39 @@ get_available_models <- function() { jsonlite::fromJSON(flatten = TRUE) %>% purrr::pluck("data", "root") } + + +#' Base for a request to the OPENAI API +#' +#' This function sends a request to a specific OpenAI API \code{task} endpoint at the base URL \code{https://api.openai.com/v1}, and authenticates with an API key using a Bearer token. +#' +#' @param task character string specifying an OpenAI API endpoint task +#' @param token character string containing an API Bearer token; defaults to the OPENAI_API_KEY environmental variable if not specified. +#' @keywords openai, api, authentication +#' @return An httr2 request object +request_base <- function(task, token = Sys.getenv("OPENAI_API_KEY")) { + + if (! task %in% get_available_endpoints()) { + cli::cli_abort(message = c( + "{.var task} must be a supported endpoint", + "i" = "Run {.run gptstudio::get_available_endpoints()} to get a list of supported endpoints" + )) + } + + httr2::request("https://api.openai.com/v1") |> + httr2::req_url_path_append(task) |> + httr2::req_auth_bearer_token(token = token) +} + +#' List supported endpoints +#' +#' Get a list of the endpoints supported by gptstudio. +#' +#' @return A character vector +#' @export +#' +#' @examples +#' get_available_endpoints() +get_available_endpoints <- function() { + c("completions", "chat/completions", "edits", "embeddings") +} diff --git a/man/get_available_endpoints.Rd b/man/get_available_endpoints.Rd new file mode 100644 index 00000000..937a32ed --- /dev/null +++ b/man/get_available_endpoints.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/openai_api_calls.R +\name{get_available_endpoints} +\alias{get_available_endpoints} +\title{List supported endpoints} +\usage{ +get_available_endpoints() +} +\value{ +A character vector +} +\description{ +Get a list of the endpoints supported by gptstudio. +} +\examples{ +get_available_endpoints() +} diff --git a/man/request_base.Rd b/man/request_base.Rd new file mode 100644 index 00000000..5a5ebae2 --- /dev/null +++ b/man/request_base.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/openai_api_calls.R +\name{request_base} +\alias{request_base} +\title{Base for a request to the OPENAI API} +\usage{ +request_base(task, token = Sys.getenv("OPENAI_API_KEY")) +} +\arguments{ +\item{task}{character string specifying an OpenAI API endpoint task} + +\item{token}{character string containing an API Bearer token; defaults to the OPENAI_API_KEY environmental variable if not specified.} +} +\value{ +An httr2 request object +} +\description{ +This function sends a request to a specific OpenAI API \code{task} endpoint at the base URL \code{https://api.openai.com/v1}, and authenticates with an API key using a Bearer token. +} +\keyword{api,} +\keyword{authentication} +\keyword{openai,} From e638465da8611d39c46a69cc95070c66dfa4f6d5 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Wed, 10 May 2023 11:23:40 -0500 Subject: [PATCH 02/21] complete migration to httr2 and fix #72 --- DESCRIPTION | 3 +- NAMESPACE | 1 + R/check_api.R | 13 +++--- R/openai_api_calls.R | 79 ++++++++++++++++++++++--------------- man/get_available_models.Rd | 17 ++++++++ man/query_openai_api.Rd | 19 +++++++++ 6 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 man/get_available_models.Rd create mode 100644 man/query_openai_api.Rd diff --git a/DESCRIPTION b/DESCRIPTION index a41d146f..7c29b0c9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -27,8 +27,7 @@ Imports: glue, grDevices, htmltools, - httr, - jsonlite, + httr2, magrittr, methods, purrr, diff --git a/NAMESPACE b/NAMESPACE index 65ce7a9f..9f87c1c3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(check_api) export(check_api_connection) export(check_api_key) export(get_available_endpoints) +export(get_available_models) export(get_ide_theme_info) export(gpt_chat) export(gpt_chat_in_source) diff --git a/R/check_api.R b/R/check_api.R index d24fe7b2..11e6cda7 100644 --- a/R/check_api.R +++ b/R/check_api.R @@ -25,7 +25,7 @@ check_api_connection <- function(api_key, update_api = TRUE, verbose = FALSE) { if (!check_api_key(api_key, update_api)) { invisible() } else { - status_code <- simple_api_check(api_key) + status_code <- simple_api_check() if (status_code == 200) { if (verbose) { cli_alert_success("API key is valid and a simple API call worked.") @@ -130,12 +130,11 @@ check_api <- function() { } } -simple_api_check <- function(api_key = Sys.getenv("OPENAI_API_KEY")) { - response <- httr::GET( - "https://api.openai.com/v1/models", - httr::add_headers(Authorization = paste0("Bearer ", api_key)) - ) - httr::status_code(response) +simple_api_check <- function() { + request_base(task = "models") |> + httr2::req_error(is_error = \(resp) FALSE) |> + httr2::req_perform() |> + httr2::resp_status() } set_openai_api_key <- function() { diff --git a/R/openai_api_calls.R b/R/openai_api_calls.R index 828ec948..51b080b5 100644 --- a/R/openai_api_calls.R +++ b/R/openai_api_calls.R @@ -163,52 +163,65 @@ openai_create_chat_completion <- query_openai_api(body, openai_api_key, task = task) } -query_openai_api <- function(body, openai_api_key, task) { +# Make a request to the OpenAI API - base_url <- glue("https://api.openai.com/v1/{task}") - - headers <- c( - "Authorization" = glue("Bearer {openai_api_key}"), - "Content-Type" = "application/json" - ) +#' A function that sends a request to the OpenAI API and returns the response. +#' +#' @param task A character string that specifies the task to send to the API. +#' @param request_body A list that contains the parameters for the task. +#' +#' @return The response from the API. +#' +query_openai_api <- function(task, request_body) { - response <- - httr::RETRY("POST", - url = base_url, - httr::add_headers(headers), body = body, - encode = "json", - quiet = TRUE - ) + response <- request_base(task) |> + httr2::req_body_json(data = request_body) |> + httr2::req_retry(max_tries = 3) |> + httr2::req_error(is_error = \(resp) FALSE) |> + httr2::req_perform() - parsed <- response %>% - httr::content(as = "text", encoding = "UTF-8") %>% - jsonlite::fromJSON(flatten = TRUE) + # error handling + if (httr2::resp_is_error(response)) { + status <- httr2::resp_status(response) + description <- httr2::resp_status_desc(response) - if (httr::http_error(response)) { - cli_alert_warning(c( - "x" = glue("OpenAI API request failed [{httr::status_code(response)}]."), - "i" = glue("Error message: {parsed$error$message}") + cli::cli_abort(message = c( + "x" = "OpenAI API request failed. Error {status} - {description}", + "i" = "Visit the {.href [OpenAi Error code guidance](https://help.openai.com/en/articles/6891839-api-error-code-guidance)} for more details", + "i" = "You can also visit the {.href [API documentation](https://platform.openai.com/docs/guides/error-codes/api-errors)}" )) } - parsed + + response |> + httr2::resp_body_json() } + + value_between <- function(x, lower, upper) { x >= lower && x <= upper } + +#' List supported models +#' +#' Get a list of the models supported by the OpenAI API. +#' +#' @return A character vector +#' @export +#' +#' @examples +#' get_available_endpoints() get_available_models <- function() { + check_api() - httr::GET( - "https://api.openai.com/v1/models", - httr::add_headers( - "Authorization" = glue("Bearer {Sys.getenv(\"OPENAI_API_KEY\")}") - ) - ) |> - httr::content(as = "text", encoding = "UTF-8") %>% - jsonlite::fromJSON(flatten = TRUE) %>% - purrr::pluck("data", "root") + + request_base("models") |> + httr2::req_perform() |> + httr2::resp_body_json() |> + purrr::pluck("data") |> + purrr::map_chr("root") } @@ -234,6 +247,8 @@ request_base <- function(task, token = Sys.getenv("OPENAI_API_KEY")) { httr2::req_auth_bearer_token(token = token) } + + #' List supported endpoints #' #' Get a list of the endpoints supported by gptstudio. @@ -244,5 +259,5 @@ request_base <- function(task, token = Sys.getenv("OPENAI_API_KEY")) { #' @examples #' get_available_endpoints() get_available_endpoints <- function() { - c("completions", "chat/completions", "edits", "embeddings") + c("completions", "chat/completions", "edits", "embeddings", "models") } diff --git a/man/get_available_models.Rd b/man/get_available_models.Rd new file mode 100644 index 00000000..14c24971 --- /dev/null +++ b/man/get_available_models.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/openai_api_calls.R +\name{get_available_models} +\alias{get_available_models} +\title{List supported models} +\usage{ +get_available_models() +} +\value{ +A character vector +} +\description{ +Get a list of the models supported by the OpenAI API. +} +\examples{ +get_available_endpoints() +} diff --git a/man/query_openai_api.Rd b/man/query_openai_api.Rd new file mode 100644 index 00000000..6af09961 --- /dev/null +++ b/man/query_openai_api.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/openai_api_calls.R +\name{query_openai_api} +\alias{query_openai_api} +\title{A function that sends a request to the OpenAI API and returns the response.} +\usage{ +query_openai_api(task, request_body) +} +\arguments{ +\item{task}{A character string that specifies the task to send to the API.} + +\item{request_body}{A list that contains the parameters for the task.} +} +\value{ +The response from the API. +} +\description{ +A function that sends a request to the OpenAI API and returns the response. +} From d28a0d7fcb046586d5f6bbad0617944b73db10e8 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Wed, 10 May 2023 11:53:31 -0500 Subject: [PATCH 03/21] use expect_error() for failure expectations instead of expect_snapshot() --- R/check_api.R | 6 +- R/openai_api_calls.R | 13 ++-- tests/testthat/_snaps/gpt_api_calls.md | 93 -------------------------- tests/testthat/test-gpt_api_calls.R | 8 +-- 4 files changed, 14 insertions(+), 106 deletions(-) delete mode 100644 tests/testthat/_snaps/gpt_api_calls.md diff --git a/R/check_api.R b/R/check_api.R index 11e6cda7..443f4a30 100644 --- a/R/check_api.R +++ b/R/check_api.R @@ -25,7 +25,7 @@ check_api_connection <- function(api_key, update_api = TRUE, verbose = FALSE) { if (!check_api_key(api_key, update_api)) { invisible() } else { - status_code <- simple_api_check() + status_code <- simple_api_check(api_key) if (status_code == 200) { if (verbose) { cli_alert_success("API key is valid and a simple API call worked.") @@ -130,8 +130,8 @@ check_api <- function() { } } -simple_api_check <- function() { - request_base(task = "models") |> +simple_api_check <- function(api_key = Sys.getenv("OPENAI_API_KEY")) { + request_base(task = "models", token = api_key) |> httr2::req_error(is_error = \(resp) FALSE) |> httr2::req_perform() |> httr2::resp_status() diff --git a/R/openai_api_calls.R b/R/openai_api_calls.R index 51b080b5..6db98cda 100644 --- a/R/openai_api_calls.R +++ b/R/openai_api_calls.R @@ -49,7 +49,7 @@ openai_create_edit <- function(model, top_p = top_p ) - query_openai_api(body, openai_api_key, task = "edits") + query_openai_api(task = "edits", request_body = body, openai_api_key = openai_api_key) } @@ -114,7 +114,7 @@ openai_create_completion <- temperature = temperature ) - query_openai_api(body, openai_api_key, task = task) + query_openai_api(task = task, request_body = body, openai_api_key = openai_api_key) } #' Generate text completions using OpenAI's API for Chat @@ -160,7 +160,7 @@ openai_create_chat_completion <- messages = prompt ) - query_openai_api(body, openai_api_key, task = task) + query_openai_api(task = task, request_body = body, openai_api_key = openai_api_key) } @@ -170,12 +170,13 @@ openai_create_chat_completion <- #' #' @param task A character string that specifies the task to send to the API. #' @param request_body A list that contains the parameters for the task. +#' @param openai_api_key String containing an OpenAI API key. Defaults to the OPENAI_API_KEY environmental variable if not specified. #' #' @return The response from the API. #' -query_openai_api <- function(task, request_body) { +query_openai_api <- function(task, request_body, openai_api_key = Sys.getenv("OPENAI_API_KEY")) { - response <- request_base(task) |> + response <- request_base(task, token = openai_api_key) |> httr2::req_body_json(data = request_body) |> httr2::req_retry(max_tries = 3) |> httr2::req_error(is_error = \(resp) FALSE) |> @@ -230,7 +231,7 @@ get_available_models <- function() { #' This function sends a request to a specific OpenAI API \code{task} endpoint at the base URL \code{https://api.openai.com/v1}, and authenticates with an API key using a Bearer token. #' #' @param task character string specifying an OpenAI API endpoint task -#' @param token character string containing an API Bearer token; defaults to the OPENAI_API_KEY environmental variable if not specified. +#' @param token String containing an OpenAI API key. Defaults to the OPENAI_API_KEY environmental variable if not specified. #' @keywords openai, api, authentication #' @return An httr2 request object request_base <- function(task, token = Sys.getenv("OPENAI_API_KEY")) { diff --git a/tests/testthat/_snaps/gpt_api_calls.md b/tests/testthat/_snaps/gpt_api_calls.md deleted file mode 100644 index 4783a560..00000000 --- a/tests/testthat/_snaps/gpt_api_calls.md +++ /dev/null @@ -1,93 +0,0 @@ -# OpenAI create completion fails with bad key - - Code - openai_create_completion(model = "text-davinci-003", prompt = "a test prompt", - openai_api_key = sample_key) - Message - ! OpenAI API request failed [401].Error message: Incorrect API key provided: 4f9bb533************************cc24. You can find your API key at https://platform.openai.com/account/api-keys. - Output - $error - $error$message - [1] "Incorrect API key provided: 4f9bb533************************cc24. You can find your API key at https://platform.openai.com/account/api-keys." - - $error$type - [1] "invalid_request_error" - - $error$param - NULL - - $error$code - [1] "invalid_api_key" - - - -# OpenAI create edit fails with bad key - - Code - openai_create_edit(model = "text-davinci-edit-001", input = "I is a human.", - temperature = 1, instruction = "fix the grammar", openai_api_key = sample_key) - Message - ! OpenAI API request failed [401].Error message: Incorrect API key provided: 4f9bb533************************cc24. You can find your API key at https://platform.openai.com/account/api-keys. - Output - $error - $error$message - [1] "Incorrect API key provided: 4f9bb533************************cc24. You can find your API key at https://platform.openai.com/account/api-keys." - - $error$type - [1] "invalid_request_error" - - $error$param - NULL - - $error$code - [1] "invalid_api_key" - - - ---- - - Code - openai_create_edit(model = "text-davinci-edit-001", input = "I is a human.", - temperature = 1, instruction = "fix the grammar", top_p = 1, openai_api_key = sample_key) - Warning - Specify either temperature or top_p, not both. - Message - ! OpenAI API request failed [401].Error message: Incorrect API key provided: 4f9bb533************************cc24. You can find your API key at https://platform.openai.com/account/api-keys. - Output - $error - $error$message - [1] "Incorrect API key provided: 4f9bb533************************cc24. You can find your API key at https://platform.openai.com/account/api-keys." - - $error$type - [1] "invalid_request_error" - - $error$param - NULL - - $error$code - [1] "invalid_api_key" - - - -# OpenAI create chat completion fails with bad key - - Code - openai_create_chat_completion(prompt = "What is your name?", openai_api_key = sample_key) - Message - ! OpenAI API request failed [401].Error message: Incorrect API key provided: 4f9bb533************************cc24. You can find your API key at https://platform.openai.com/account/api-keys. - Output - $error - $error$message - [1] "Incorrect API key provided: 4f9bb533************************cc24. You can find your API key at https://platform.openai.com/account/api-keys." - - $error$type - [1] "invalid_request_error" - - $error$param - NULL - - $error$code - [1] "invalid_api_key" - - - diff --git a/tests/testthat/test-gpt_api_calls.R b/tests/testthat/test-gpt_api_calls.R index 11137c82..1ee8e4e7 100644 --- a/tests/testthat/test-gpt_api_calls.R +++ b/tests/testthat/test-gpt_api_calls.R @@ -1,7 +1,7 @@ sample_key <- "4f9bb533-c0ac-4fef-9f7a-9eabe1afcc24" test_that("OpenAI create completion fails with bad key", { - expect_snapshot(openai_create_completion( + expect_error(openai_create_completion( model = "text-davinci-003", prompt = "a test prompt", openai_api_key = sample_key @@ -9,7 +9,7 @@ test_that("OpenAI create completion fails with bad key", { }) test_that("OpenAI create edit fails with bad key", { - expect_snapshot(openai_create_edit( + expect_error(openai_create_edit( model = "text-davinci-edit-001", input = "I is a human.", temperature = 1, @@ -17,7 +17,7 @@ test_that("OpenAI create edit fails with bad key", { openai_api_key = sample_key )) - expect_snapshot(openai_create_edit( + expect_error(openai_create_edit( model = "text-davinci-edit-001", input = "I is a human.", temperature = 1, @@ -28,7 +28,7 @@ test_that("OpenAI create edit fails with bad key", { }) test_that("OpenAI create chat completion fails with bad key", { - expect_snapshot( + expect_error( openai_create_chat_completion( prompt = "What is your name?", openai_api_key = sample_key From 02aea204410deb83387bd735bb325034fb9bf461 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Wed, 10 May 2023 17:00:52 -0500 Subject: [PATCH 04/21] change param from token to openai_api_key --- man/query_openai_api.Rd | 8 +++++++- man/request_base.Rd | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/man/query_openai_api.Rd b/man/query_openai_api.Rd index 6af09961..b0aa6eb1 100644 --- a/man/query_openai_api.Rd +++ b/man/query_openai_api.Rd @@ -4,12 +4,18 @@ \alias{query_openai_api} \title{A function that sends a request to the OpenAI API and returns the response.} \usage{ -query_openai_api(task, request_body) +query_openai_api( + task, + request_body, + openai_api_key = Sys.getenv("OPENAI_API_KEY") +) } \arguments{ \item{task}{A character string that specifies the task to send to the API.} \item{request_body}{A list that contains the parameters for the task.} + +\item{openai_api_key}{String containing an OpenAI API key. Defaults to the OPENAI_API_KEY environmental variable if not specified.} } \value{ The response from the API. diff --git a/man/request_base.Rd b/man/request_base.Rd index 5a5ebae2..2d8aee90 100644 --- a/man/request_base.Rd +++ b/man/request_base.Rd @@ -9,7 +9,7 @@ request_base(task, token = Sys.getenv("OPENAI_API_KEY")) \arguments{ \item{task}{character string specifying an OpenAI API endpoint task} -\item{token}{character string containing an API Bearer token; defaults to the OPENAI_API_KEY environmental variable if not specified.} +\item{token}{String containing an OpenAI API key. Defaults to the OPENAI_API_KEY environmental variable if not specified.} } \value{ An httr2 request object From 6c0d3d8bd62a7b146080c15394cd4193b36a264a Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Wed, 10 May 2023 17:03:09 -0500 Subject: [PATCH 05/21] first steps into #85 --- inst/StreamText.R | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 inst/StreamText.R diff --git a/inst/StreamText.R b/inst/StreamText.R new file mode 100644 index 00000000..505aac5d --- /dev/null +++ b/inst/StreamText.R @@ -0,0 +1,44 @@ +StreamText <- R6::R6Class( + classname = "StreamText", + public = list( + value = NULL, + initialize = function(value = "") { + self$value <- value + invisible(self) + }, + append_text = function(text) { + self$value <- paste0(self$value, text) + invisible(self) + }, + print = function() { + print(self$value) + invisible(self) + }, + get_value = function() { + self$value + } + ) +) + +tempchar <- StreamText$new() + +handle_stream <- function(x, char) { + parsed <- rawToChar(x) + + char$append_text(parsed) + cat(char$get_value()) + + return(TRUE) +} + +request_base("chat/completions") |> + httr2::req_body_json(data = list( + model = "gpt-3.5-turbo", + messages = list( + list( + role = "user", + content = "Count from 1 to 20" + ) + ) + )) |> + httr2::req_stream(callback = \(x) handle_stream(x, tempchar), buffer_kb = 0.010) From 3e3117721e3362f1f3df467c62389a056081084c Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Thu, 11 May 2023 11:47:11 -0500 Subject: [PATCH 06/21] handle first chunk values in private --- inst/StreamText.R | 187 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 163 insertions(+), 24 deletions(-) diff --git a/inst/StreamText.R b/inst/StreamText.R index 505aac5d..503fc809 100644 --- a/inst/StreamText.R +++ b/inst/StreamText.R @@ -1,44 +1,183 @@ StreamText <- R6::R6Class( classname = "StreamText", public = list( - value = NULL, - initialize = function(value = "") { - self$value <- value + + initialize = function() { + private$value <- "" invisible(self) }, + append_text = function(text) { - self$value <- paste0(self$value, text) + private$value <- paste0(private$value, text) + + # never change this order unless the API itself changes + private$handle_chunk_id() + private$handle_chunk_object() + private$handle_chunk_created() + private$handle_chunk_model() + invisible(self) }, + print = function() { - print(self$value) + print(private$value) invisible(self) }, + get_value = function() { - self$value + private$value + }, + + get_chunk_base = function() { + private$chunk + } + + ), + + private = list( + value = NULL, + chunk = list( + choices = NULL, + created = NULL, + id = NULL, + model = NULL, + object = NULL + ), + + handle_chunk_id = function() { + is_set <- !is.null(private$chunk$id) + + if (is_set) return(NULL) + + if (stringr::str_detect(private$value, "^\\{\"id\":\"chatcmpl-[a-zA-Z0-9]+\",")) { + + private$chunk$id <- stringr::str_extract(private$value, "chatcmpl-[a-zA-Z0-9]+") + + private$value <- stringr::str_replace(private$value, "^(\\{)\"id\":\"chatcmpl-[a-zA-Z0-9]+\",", "\\1") + } + }, + + handle_chunk_object = function() { + is_set <- !is.null(private$chunk$object) + + if (is_set) return(NULL) + + if (stringr::str_detect(private$value, "^\\{\"object\":\"[\\w\\.]+\",")) { + + # private$chunk$object <- stringr::str_replace(private$value, "^\\{\"object\":\"([\\w\\.]+).*", "\\1") + private$chunk$object <- "chat.completion.chunk" + + private$value <- stringr::str_replace(private$value, "^(\\{)\"object\":\"[\\w\\.]+\",", "\\1") + } + }, + + handle_chunk_created = function() { + is_set <- !is.null(private$chunk$created) + + if (is_set) return(NULL) + + if (stringr::str_detect(private$value, "^\\{\"created\":[\\d]+,")) { + + private$chunk$created <- stringr::str_replace(private$value, "^\\{\"created\":([\\d]+).*", "\\1") |> as.integer() + + private$value <- stringr::str_replace(private$value, "^(\\{)\"created\":[\\d]+,", "\\1") + } + }, + + handle_chunk_model = function() { + is_set <- !is.null(private$chunk$model) + + if (is_set) return(NULL) + + if (stringr::str_detect(private$value, "^\\{\"model\":\"[\\w\\d\\.\\-]+\",")) { + + private$chunk$model <- stringr::str_replace(private$value, "^\\{\"model\":\"([\\w\\d\\.\\-]+).*", "\\1") + + private$value <- stringr::str_replace(private$value, "^(\\{)\"model\":\"[\\w\\d\\.\\-]+\",", "\\1") + } + } ) ) -tempchar <- StreamText$new() +full_stream <- "{\"id\":\"chatcmpl-7F2EGwP25x19nSqqewmxKHzbsNCeQ\",\"object\":\"chat.completion\",\"created\":1683817844,\"model\":\"gpt-3.5-turbo-0301\",\"usage\":{\"prompt_tokens\":15,\"completion_tokens\":59,\"total_tokens\":74},\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20.\"},\"finish_reason\":\"stop\",\"index\":0}]}\n" + +full_stream |> + stringr::str_replace("^(\\{)\"id\":\"chatcmpl-[a-zA-Z0-9]+\",", "\\1") |> + stringr::str_replace("^(\\{)\"object\":\"[\\w\\.]+\",", "\\1") |> + stringr::str_replace("^(\\{)\"created\":[\\d]+,", "\\1") |> + stringr::str_replace("^(\\{)\"model\":\"[\\w\\d\\.\\-]+\",", "\\1") + +# tempchar <- StreamText$new() +# +# handle_stream <- function(x, char) { +# parsed <- rawToChar(x) +# +# # print(parsed) +# +# char$append_text(parsed) +# +# return(TRUE) +# } +# +# request_base("chat/completions") |> +# httr2::req_body_json(data = list( +# model = "gpt-3.5-turbo", +# messages = list( +# list( +# role = "user", +# content = "Count from 1 to 20" +# ) +# ) +# )) |> +# httr2::req_stream(callback = \(x) handle_stream(x, tempchar), buffer_kb = 10/1024) + + + +example_stream <- c( + "{\"id\":\"cha", + "tcmpl-7F2E", + "GwP25x19nS", + "qqewmxKHzb", + "sNCeQ\",\"ob", + "ject\":\"cha", + "t.completi", + "on\",\"creat", + "ed\":168381", + "7844,\"mode", + "l\":\"gpt-3.", + "5-turbo-03", + "01\",\"usage", + "\":{\"prompt", + "_tokens\":1", + "5,\"complet", + "ion_tokens", + "\":59,\"tota", + "l_tokens\":", + "74},\"choic", + "es\":[{\"mes", + "sage\":{\"ro", + "le\":\"assis", + "tant\",\"con", + "tent\":\"1, ", + "2, 3, 4, 5", + ", 6, 7, 8,", + " 9, 10, 11", + ", 12, 13, ", + "14, 15, 16", + ", 17, 18, ", + "19, 20.\"},", + "\"finish_re", + "ason\":\"sto", + "p\",\"index\"", + ":0}]}\n" +) -handle_stream <- function(x, char) { - parsed <- rawToChar(x) - char$append_text(parsed) - cat(char$get_value()) +tempchar_example <- StreamText$new() - return(TRUE) -} +example_stream |> + purrr::walk(\(x) tempchar_example$append_text(x)) -request_base("chat/completions") |> - httr2::req_body_json(data = list( - model = "gpt-3.5-turbo", - messages = list( - list( - role = "user", - content = "Count from 1 to 20" - ) - ) - )) |> - httr2::req_stream(callback = \(x) handle_stream(x, tempchar), buffer_kb = 0.010) +tempchar_example$get_chunk_base() +tempchar_example$get_value() From 0e84a59b968fc86b35f9a05b51e4cd15d6b0a40e Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Thu, 11 May 2023 17:01:23 -0500 Subject: [PATCH 07/21] add methods to handle base chunk and generate chunk list --- inst/StreamText.R | 355 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 300 insertions(+), 55 deletions(-) diff --git a/inst/StreamText.R b/inst/StreamText.R index 503fc809..cbf4558c 100644 --- a/inst/StreamText.R +++ b/inst/StreamText.R @@ -3,18 +3,17 @@ StreamText <- R6::R6Class( public = list( initialize = function() { - private$value <- "" + private$value <- "" # this will hold the stream and mutate it with inner methods + private$value_buffer <- private$value # this will hold previous and current value + private$full_response <- private$value # this will hold the full response invisible(self) }, - append_text = function(text) { + handle_text_stream = function(text) { private$value <- paste0(private$value, text) + private$full_response <- paste0(private$full_response, text) - # never change this order unless the API itself changes - private$handle_chunk_id() - private$handle_chunk_object() - private$handle_chunk_created() - private$handle_chunk_model() + private$generate_chunk_list() invisible(self) }, @@ -28,15 +27,42 @@ StreamText <- R6::R6Class( private$value }, - get_chunk_base = function() { - private$chunk + get_full_response = function() { + private$full_response + }, + + get_base_chunk = function() { + private$base_chunk + }, + + get_chunk_list = function() { + private$chunk_list + }, + + reset_chunk_setup = function() { + private$value = NULL + + private$base_chunk <- list( + choices = NULL, + created = NULL, + id = NULL, + model = NULL, + object = NULL + ) + + private$chunk_list <- NULL + + invisible(self) } ), private = list( value = NULL, - chunk = list( + value_buffer = NULL, + full_response = NULL, + + base_chunk = list( choices = NULL, created = NULL, id = NULL, @@ -44,59 +70,143 @@ StreamText <- R6::R6Class( object = NULL ), - handle_chunk_id = function() { - is_set <- !is.null(private$chunk$id) + chunk_list = NULL, + + handle_base_chunk_id = function() { + is_set <- !is.null(private$base_chunk$id) if (is_set) return(NULL) + # matches strings that start with the exact sequence: `{"id":"chatcmpl-` followed by one or more alphanumeric characters, and ending with a comma. if (stringr::str_detect(private$value, "^\\{\"id\":\"chatcmpl-[a-zA-Z0-9]+\",")) { - private$chunk$id <- stringr::str_extract(private$value, "chatcmpl-[a-zA-Z0-9]+") + private$base_chunk$id <- stringr::str_extract(private$value, "chatcmpl-[a-zA-Z0-9]+") private$value <- stringr::str_replace(private$value, "^(\\{)\"id\":\"chatcmpl-[a-zA-Z0-9]+\",", "\\1") } }, - handle_chunk_object = function() { - is_set <- !is.null(private$chunk$object) + handle_base_chunk_object = function() { + is_set <- !is.null(private$base_chunk$object) if (is_set) return(NULL) + # matches strings that start with the exact sequence: {"object":" followed by one or more word characters or dots, and ending with a comma. if (stringr::str_detect(private$value, "^\\{\"object\":\"[\\w\\.]+\",")) { - # private$chunk$object <- stringr::str_replace(private$value, "^\\{\"object\":\"([\\w\\.]+).*", "\\1") - private$chunk$object <- "chat.completion.chunk" + # private$base_chunk$object <- stringr::str_replace(private$value, "^\\{\"object\":\"([\\w\\.]+).*", "\\1") + private$base_chunk$object <- "chat.completion.chunk" private$value <- stringr::str_replace(private$value, "^(\\{)\"object\":\"[\\w\\.]+\",", "\\1") } }, - handle_chunk_created = function() { - is_set <- !is.null(private$chunk$created) + handle_base_chunk_created = function() { + is_set <- !is.null(private$base_chunk$created) if (is_set) return(NULL) + # matches strings that start with the exact sequence: {"created": followed by one or more digits, and ending with a comma. if (stringr::str_detect(private$value, "^\\{\"created\":[\\d]+,")) { - private$chunk$created <- stringr::str_replace(private$value, "^\\{\"created\":([\\d]+).*", "\\1") |> as.integer() + private$base_chunk$created <- stringr::str_replace(private$value, "^\\{\"created\":([\\d]+).*", "\\1") |> as.integer() private$value <- stringr::str_replace(private$value, "^(\\{)\"created\":[\\d]+,", "\\1") } }, - handle_chunk_model = function() { - is_set <- !is.null(private$chunk$model) + handle_base_chunk_model = function() { + is_set <- !is.null(private$base_chunk$model) if (is_set) return(NULL) + # matches strings that start with the exact sequence: {"model":" followed by one or more word characters, digits, dots, or hyphens, and ending with a comma. if (stringr::str_detect(private$value, "^\\{\"model\":\"[\\w\\d\\.\\-]+\",")) { - private$chunk$model <- stringr::str_replace(private$value, "^\\{\"model\":\"([\\w\\d\\.\\-]+).*", "\\1") + private$base_chunk$model <- stringr::str_replace(private$value, "^\\{\"model\":\"([\\w\\d\\.\\-]+).*", "\\1") private$value <- stringr::str_replace(private$value, "^(\\{)\"model\":\"[\\w\\d\\.\\-]+\",", "\\1") } + }, + + initialize_chunk_list = function() { + base_chunk_tracks_choices <- !is.null(private$base_chunk$choices) + + if (base_chunk_tracks_choices) return(NULL) + + usage_regex <- "^\\{\"usage\":\\{\"prompt_tokens\":\\d+,\"completion_tokens\":\\d+,\"total_tokens\":\\d+\\}," + choices_regex <- "\"choices\":\\[\\{\"message\":\\{\"role\":\"assistant\",\"content\":\"" + + full_regex <- paste0(usage_regex, choices_regex) + + if (stringr::str_detect(private$value, full_regex)) { + private$value <- stringr::str_replace(private$value, full_regex, "") + + private$base_chunk$choices <- list( + list( + delta = list(), + finish_reason = NULL, + index = 0L + ) + ) + + role_chunk <- private$generate_single_chunk( + delta_name = "role", + delta_value = "assistant" + ) + + private$chunk_list <- list(role_chunk) + } + + }, + + generate_chunk_list = function() { + # never change this order unless the API itself changes + private$handle_base_chunk_id() + private$handle_base_chunk_object() + private$handle_base_chunk_created() + private$handle_base_chunk_model() + + private$initialize_chunk_list() + + private$append_new_chunk() + }, + + generate_single_chunk = function(delta_name, delta_value) { + match.arg(delta_name, c("role", "content")) + + copied_chunk <- private$base_chunk + + delta_value_has_new_line <- stringr::str_detect(delta_value, "\\n") # basically detecs the last stream + + if (delta_value_has_new_line) { + copied_chunk$choices[[1]]$finish_reason <- "stop" + return(copied_chunk) + } + + copied_chunk$choices[[1]]$delta[[delta_name]] <- delta_value + + copied_chunk + + }, + + append_new_chunk = function() { + chunk_list_is_null <- is.null(private$chunk_list) + + if (chunk_list_is_null) return(NULL) + + new_chunk <- private$generate_single_chunk( + delta_name = "content", + delta_value = private$value + ) + + private$chunk_list <- c(private$chunk_list, list(new_chunk)) + + # reset value + private$value <- "" } + ) ) @@ -106,35 +216,168 @@ full_stream |> stringr::str_replace("^(\\{)\"id\":\"chatcmpl-[a-zA-Z0-9]+\",", "\\1") |> stringr::str_replace("^(\\{)\"object\":\"[\\w\\.]+\",", "\\1") |> stringr::str_replace("^(\\{)\"created\":[\\d]+,", "\\1") |> - stringr::str_replace("^(\\{)\"model\":\"[\\w\\d\\.\\-]+\",", "\\1") - -# tempchar <- StreamText$new() -# -# handle_stream <- function(x, char) { -# parsed <- rawToChar(x) -# -# # print(parsed) -# -# char$append_text(parsed) -# -# return(TRUE) -# } -# -# request_base("chat/completions") |> -# httr2::req_body_json(data = list( -# model = "gpt-3.5-turbo", -# messages = list( -# list( -# role = "user", -# content = "Count from 1 to 20" -# ) -# ) -# )) |> -# httr2::req_stream(callback = \(x) handle_stream(x, tempchar), buffer_kb = 10/1024) - - - -example_stream <- c( + stringr::str_replace("^(\\{)\"model\":\"[\\w\\d\\.\\-]+\",", "\\1") |> + stringr::str_replace(full_regex, "") + +tempchar <- character() + +handle_stream <- function(x, char) { + parsed <- rawToChar(x) + + # print(parsed) + + # char$handle_text_stream(parsed) + + tempchar <<- c(tempchar, parsed) + + return(TRUE) +} + +request_base("chat/completions") |> + httr2::req_body_json(data = list( + model = "gpt-3.5-turbo", + messages = list( + list( + role = "user", + content = "Generate a JSON text to store customer data. return a single object for 3 customers." + ) + ) + )) |> + httr2::req_stream(callback = \(x) handle_stream(x, tempchar), buffer_kb = 10/1024) + +example_stream2 <- + c( + "{\"id\":\"cha", + "tcmpl-7F7B", + "sNWFht4pWw", + "rPPSE3vJAu", + "B9efW\",\"ob", + "ject\":\"cha", + "t.completi", + "on\",\"creat", + "ed\":168383", + "6916,\"mode", + "l\":\"gpt-3.", + "5-turbo-03", + "01\",\"usage", + "\":{\"prompt", + "_tokens\":2", + "6,\"complet", + "ion_tokens", + "\":251,\"tot", + "al_tokens\"", + ":277},\"cho", + "ices\":[{\"m", + "essage\":{\"", + "role\":\"ass", + "istant\",\"c", + "ontent\":\"{", + "\\n \\\"cust", + "omers\\\": [", + "\\n {\\n ", + " \\\"fir", + "stName\\\": ", + "\\\"John\\\",\\", + "n \\\"l", + "astName\\\":", + " \\\"Doe\\\",\\", + "n \\\"e", + "mail\\\": \\\"", + "johndoe@ex", + "ample.com\\", + "\",\\n ", + "\\\"phone\\\":", + " \\\"555-555", + "-5555\\\",\\n", + " \\\"ad", + "dress\\\": {", + "\\n ", + "\\\"street\\\"", + ": \\\"123 Ma", + "in St\\\",\\n", + " \\\"", + "city\\\": \\\"", + "Anytown\\\",", + "\\n ", + "\\\"state\\\":", + " \\\"CA\\\",\\n", + " \\\"", + "zip\\\": \\\"1", + "2345\\\"\\n ", + " }\\n ", + " },\\n {", + "\\n \\\"", + "firstName\\", + "\": \\\"Jane\\", + "\",\\n ", + "\\\"lastName", + "\\\": \\\"Doe\\", + "\",\\n ", + "\\\"email\\\":", + " \\\"janedoe", + "@example.c", + "om\\\",\\n ", + " \\\"phone", + "\\\": \\\"555-", + "555-5555\\\"", + ",\\n \\", + "\"address\\\"", + ": {\\n ", + " \\\"stree", + "t\\\": \\\"456", + " Elm St\\\",", + "\\n ", + "\\\"city\\\": ", + "\\\"Anytown\\", + "\",\\n ", + " \\\"state\\", + "\": \\\"CA\\\",", + "\\n ", + "\\\"zip\\\": \\", + "\"12345\\\"\\n", + " }\\n ", + " },\\n ", + " {\\n ", + "\\\"firstNam", + "e\\\": \\\"Bob", + "\\\",\\n ", + " \\\"lastNam", + "e\\\": \\\"Smi", + "th\\\",\\n ", + " \\\"email", + "\\\": \\\"bobs", + "mith@examp", + "le.com\\\",\\", + "n \\\"p", + "hone\\\": \\\"", + "555-555-55", + "55\\\",\\n ", + " \\\"addre", + "ss\\\": {\\n ", + " \\\"s", + "treet\\\": \\", + "\"789 Oak S", + "t\\\",\\n ", + " \\\"city", + "\\\": \\\"Anyt", + "own\\\",\\n ", + " \\\"st", + "ate\\\": \\\"C", + "A\\\",\\n ", + " \\\"zip\\", + "\": \\\"12345", + "\\\"\\n ", + "}\\n }\\n", + " ]\\n}\"},\"", + "finish_rea", + "son\":\"stop", + "\",\"index\":", + "0}]}\n" + ) + + + +example_stream1 <- c( "{\"id\":\"cha", "tcmpl-7F2E", "GwP25x19nS", @@ -176,8 +419,10 @@ example_stream <- c( tempchar_example <- StreamText$new() -example_stream |> - purrr::walk(\(x) tempchar_example$append_text(x)) +example_stream2 |> + purrr::walk(\(x) tempchar_example$handle_text_stream(x)) -tempchar_example$get_chunk_base() +tempchar_example$get_base_chunk() tempchar_example$get_value() +tempchar_example$get_full_response() +tempchar_example$get_chunk_list() From 1c56ad2d81f9d6834c3e088bbd7b9e9a4e194311 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Fri, 12 May 2023 15:29:02 -0500 Subject: [PATCH 08/21] stream with curl instead of httr2 --- R/StreamHandler.R | 77 +++++++++++++++++++++++++++++++++++++++++++++++ inst/StreamText.R | 72 ++++++++++++++++++++++++++++---------------- 2 files changed, 123 insertions(+), 26 deletions(-) create mode 100644 R/StreamHandler.R diff --git a/R/StreamHandler.R b/R/StreamHandler.R new file mode 100644 index 00000000..68bac48a --- /dev/null +++ b/R/StreamHandler.R @@ -0,0 +1,77 @@ +#' @importFrom rlang %||% +#' @importFrom magrittr %>% +StreamHandler <- R6::R6Class( + classname = "StreamHandler", + public = list( + current_value = NULL, + chunks = list(), + initialize = function() { + self$current_value <- "" + }, + handle_streamed_element = function(x) { + translated <- self$translate_element(x) + self$chunks <- c(self$chunks, translated) + self$current_value <- self$convert_chunks_into_response_str() + }, + translate_element = function(x) { + x %>% + stringr::str_remove("^data: ") %>% # handle first element + stringr::str_remove("(\n\ndata: \\[DONE\\])?\n\n$") %>% # handle last element + stringr::str_split_1("\n\ndata: ") %>% + purrr::map(\(x) jsonlite::fromJSON(x, simplifyVector = FALSE)) + }, + convert_chunks_into_response_str = function() { + self$chunks %>% + purrr::map_chr(~ .x$choices[[1]]$delta$content %||% "") %>% + paste0(collapse = "") + } + ) +) + +stream_chat_completion <- + function(messages, + element_callback = cat, + model = "gpt-3.5-turbo", + openai_api_key = Sys.getenv("OPENAI_API_KEY")) { + # Set the API endpoint URL + url <- "https://api.openai.com/v1/chat/completions" + + # Set the request headers + headers <- list( + "Content-Type" = "application/json", + "Authorization" = paste0("Bearer ", openai_api_key) + ) + + if (!is.list(messages)) { + messages <- list(list(role = "user", content = messages)) + } + + # Set the request body + body <- list( + "model" = model, + "stream" = TRUE, + "messages" = messages + ) + + # Create a new curl handle object + handle <- curl::new_handle() %>% + curl::handle_setheaders(.list = headers) %>% + curl::handle_setopt(postfields = jsonlite::toJSON(body, auto_unbox = TRUE)) + + + # Make the streaming request using curl_fetch_stream() + curl::curl_fetch_stream( + url = url, + fun = \(x) { + element <- rawToChar(x) + element_callback(element) + }, + handle = handle + ) + } + +# stream_handler <- StreamHandler$new() + +# stream_chat_completion(messages = "Count from 1 to 10") +# stream_chat_completion(messages = "Count from 1 to 10", element_callback = stream_handler$handle_streamed_element) + diff --git a/inst/StreamText.R b/inst/StreamText.R index cbf4558c..babbf76d 100644 --- a/inst/StreamText.R +++ b/inst/StreamText.R @@ -196,14 +196,29 @@ StreamText <- R6::R6Class( if (chunk_list_is_null) return(NULL) + # handle buffer value + + private$value_buffer <- paste0(private$value_buffer, private$value) + + end_of_content_regex <- "(\"\\},\")" # detects '\"},\"' + value_is_end_of_content <- stringr::str_detect(private$value, end_of_content_regex) + buffer_is_end_of_content <- stringr::str_detect(private$value_buffer, end_of_content_regex) + + if (value_is_end_of_content) { + buffered_value <- stringr::str_replace() + } + + # End of buffer value handling + new_chunk <- private$generate_single_chunk( delta_name = "content", delta_value = private$value ) - private$chunk_list <- c(private$chunk_list, list(new_chunk)) + private$chunk_list <- c(private$chunk_list, list(new_chunk)) # not memory efficient, but we are not expecting huge lengths # reset value + private$value_buffer <- private$value private$value <- "" } @@ -212,6 +227,11 @@ StreamText <- R6::R6Class( full_stream <- "{\"id\":\"chatcmpl-7F2EGwP25x19nSqqewmxKHzbsNCeQ\",\"object\":\"chat.completion\",\"created\":1683817844,\"model\":\"gpt-3.5-turbo-0301\",\"usage\":{\"prompt_tokens\":15,\"completion_tokens\":59,\"total_tokens\":74},\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20.\"},\"finish_reason\":\"stop\",\"index\":0}]}\n" +usage_regex <- "^\\{\"usage\":\\{\"prompt_tokens\":\\d+,\"completion_tokens\":\\d+,\"total_tokens\":\\d+\\}," +choices_regex <- "\"choices\":\\[\\{\"message\":\\{\"role\":\"assistant\",\"content\":\"" + +full_regex <- paste0(usage_regex, choices_regex) + full_stream |> stringr::str_replace("^(\\{)\"id\":\"chatcmpl-[a-zA-Z0-9]+\",", "\\1") |> stringr::str_replace("^(\\{)\"object\":\"[\\w\\.]+\",", "\\1") |> @@ -219,31 +239,31 @@ full_stream |> stringr::str_replace("^(\\{)\"model\":\"[\\w\\d\\.\\-]+\",", "\\1") |> stringr::str_replace(full_regex, "") -tempchar <- character() - -handle_stream <- function(x, char) { - parsed <- rawToChar(x) - - # print(parsed) - - # char$handle_text_stream(parsed) - - tempchar <<- c(tempchar, parsed) - - return(TRUE) -} - -request_base("chat/completions") |> - httr2::req_body_json(data = list( - model = "gpt-3.5-turbo", - messages = list( - list( - role = "user", - content = "Generate a JSON text to store customer data. return a single object for 3 customers." - ) - ) - )) |> - httr2::req_stream(callback = \(x) handle_stream(x, tempchar), buffer_kb = 10/1024) +# tempchar <- character() +# +# handle_stream <- function(x, char) { +# parsed <- rawToChar(x) +# +# # print(parsed) +# +# # char$handle_text_stream(parsed) +# +# tempchar <<- c(tempchar, parsed) +# +# return(TRUE) +# } +# +# request_base("chat/completions") |> +# httr2::req_body_json(data = list( +# model = "gpt-3.5-turbo", +# messages = list( +# list( +# role = "user", +# content = "Generate a JSON text to store customer data. return a single object for 3 customers." +# ) +# ) +# )) |> +# httr2::req_stream(callback = \(x) handle_stream(x, tempchar), buffer_kb = 10/1024) example_stream2 <- c( From d0a8a82ddd2e92e80351465ef94f4287a9cde45b Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Fri, 12 May 2023 16:33:31 -0500 Subject: [PATCH 09/21] first working example of stream. #85 --- R/StreamHandler.R | 10 +++++++++- inst/StreamHandlerApp.R | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 inst/StreamHandlerApp.R diff --git a/R/StreamHandler.R b/R/StreamHandler.R index 68bac48a..a9004b42 100644 --- a/R/StreamHandler.R +++ b/R/StreamHandler.R @@ -4,14 +4,22 @@ StreamHandler <- R6::R6Class( classname = "StreamHandler", public = list( current_value = NULL, + shinySession = NULL, chunks = list(), - initialize = function() { + initialize = function(session = NULL) { self$current_value <- "" + self$shinySession <- session }, handle_streamed_element = function(x) { translated <- self$translate_element(x) self$chunks <- c(self$chunks, translated) self$current_value <- self$convert_chunks_into_response_str() + + if (!is.null(self$shinySession)) { + # any communication with JS should be handled here!! + self$shinySession$sendCustomMessage(type = "render-stream", message = shiny::markdown(self$current_value)) + } + }, translate_element = function(x) { x %>% diff --git a/inst/StreamHandlerApp.R b/inst/StreamHandlerApp.R new file mode 100644 index 00000000..86f1eded --- /dev/null +++ b/inst/StreamHandlerApp.R @@ -0,0 +1,43 @@ +library(shiny) + +ui <- fluidPage( + sidebarLayout( + sidebarPanel( + textAreaInput( + inputId = "text", + label = "Input", + value = "Count from 1 to 50", + height = "800px" + ), + actionButton("go", "Render"), + tags$script( + "Shiny.addCustomMessageHandler( + type = 'render-stream', function(message) { + $('#render-here').html($.parseHTML(message)) + + console.log(message) + });" + ) + ), + mainPanel( + uiOutput("my_ui"), + div(id = "render-here") + ) + ) +) + +server <- function(input, output, session) { + + output$my_ui <- renderUI({ + + }) |> + bindEvent(input$go) + + observe({ + stream_handler <- StreamHandler$new(session = session) + stream_chat_completion(input$text, element_callback = stream_handler$handle_streamed_element) + }) |> + bindEvent(input$go) +} + +shinyApp(ui, server) From b98fb6ea2b915df001b7fffc61cfca76113e02cb Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Fri, 12 May 2023 17:01:33 -0500 Subject: [PATCH 10/21] ensure stream example works --- inst/StreamHandlerApp.R | 1 + 1 file changed, 1 insertion(+) diff --git a/inst/StreamHandlerApp.R b/inst/StreamHandlerApp.R index 86f1eded..51882519 100644 --- a/inst/StreamHandlerApp.R +++ b/inst/StreamHandlerApp.R @@ -1,4 +1,5 @@ library(shiny) +library(gptstudio) ui <- fluidPage( sidebarLayout( From a65b1d1a22c8b1342ef835f886a5ebf9b09115ff Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Fri, 12 May 2023 17:01:33 -0500 Subject: [PATCH 11/21] ensure stream example works --- NAMESPACE | 3 +++ R/StreamHandler.R | 2 ++ inst/StreamHandlerApp.R | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index 9f87c1c3..1985ebf1 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,6 +1,7 @@ # Generated by roxygen2: do not edit by hand export("%>%") +export(StreamHandler) export(addin_chatgpt) export(addin_chatgpt_in_source) export(addin_comment_code) @@ -19,6 +20,7 @@ export(openai_create_chat_completion) export(openai_create_completion) export(openai_create_edit) export(run_chatgpt_app) +export(stream_chat_completion) import(cli) import(htmltools) import(rlang) @@ -29,3 +31,4 @@ importFrom(assertthat,is.number) importFrom(assertthat,is.string) importFrom(glue,glue) importFrom(magrittr,"%>%") +importFrom(rlang,"%||%") diff --git a/R/StreamHandler.R b/R/StreamHandler.R index a9004b42..545b0e1e 100644 --- a/R/StreamHandler.R +++ b/R/StreamHandler.R @@ -1,5 +1,6 @@ #' @importFrom rlang %||% #' @importFrom magrittr %>% +#' @export StreamHandler <- R6::R6Class( classname = "StreamHandler", public = list( @@ -36,6 +37,7 @@ StreamHandler <- R6::R6Class( ) ) +#' @export stream_chat_completion <- function(messages, element_callback = cat, diff --git a/inst/StreamHandlerApp.R b/inst/StreamHandlerApp.R index 86f1eded..6d9eaa7c 100644 --- a/inst/StreamHandlerApp.R +++ b/inst/StreamHandlerApp.R @@ -1,4 +1,5 @@ library(shiny) +library(gptstudio) ui <- fluidPage( sidebarLayout( @@ -6,7 +7,7 @@ ui <- fluidPage( textAreaInput( inputId = "text", label = "Input", - value = "Count from 1 to 50", + value = "Give me two examples of ggplot2 code", height = "800px" ), actionButton("go", "Render"), From fae04b351f4cc04ef578c874388ba50744925565 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Mon, 15 May 2023 09:23:48 -0500 Subject: [PATCH 12/21] fix chat history creation --- R/mod_prompt.R | 12 ++----- tests/testthat/test-mod_prompt.R | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/R/mod_prompt.R b/R/mod_prompt.R index bf6aa0e2..e78cfe7e 100644 --- a/R/mod_prompt.R +++ b/R/mod_prompt.R @@ -167,17 +167,9 @@ text_area_input_wrapper <- #' chat_create_history <- function(response) { previous_responses <- response[[1]] - last_response <- response[[2]]$choices + last_response <- response[[2]]$choices[[1]]$message - c( - previous_responses, - list( - list( - role = last_response$message.role, - content = last_response$message.content - ) - ) - ) + c(previous_responses, list(last_response)) } diff --git a/tests/testthat/test-mod_prompt.R b/tests/testthat/test-mod_prompt.R index a83cc00c..79141fdb 100644 --- a/tests/testthat/test-mod_prompt.R +++ b/tests/testthat/test-mod_prompt.R @@ -7,3 +7,62 @@ test_that("mod_prompt works", { appdir <- system.file(package = "gptstudio", "mod_prompt") test_app(appdir) }) + +test_that("chat_create_history() respects expected structure", { + example_response <- + list( + list( + list( + role = "system", + content = structure( + "You are a helpful chat bot that answers questions for an R programmer working in the RStudio IDE. They consider themselves to be a beginner R programmer. Provide answers with their skill level in mind. ", + class = c("glue", + "character") + ) + ), + list( + role = "user", + content = structure("Count from 1 to 5", class = c("glue", + "character")) + ) + ), + list( + id = "chatcmpl-7GT59y6mYejSdcHzDaD5kAfVkHseY", + object = "chat.completion", + created = 1684159395L, + model = "gpt-3.5-turbo-0301", + usage = list( + prompt_tokens = 60L, + completion_tokens = 56L, + total_tokens = 116L + ), + choices = list(list( + message = list(role = "assistant", content = "Sure, here's how you can count from 1 to 5 in R:\n\n```\nfor(i in 1:5){\n print(i)\n}\n```\n\nThis will create a loop that prints the numbers from 1 to 5, each on a new line."), + finish_reason = "stop", + index = 0L + )) + ) + ) + + expected_value <- + list( + list( + role = "system", + content = structure( + "You are a helpful chat bot that answers questions for an R programmer working in the RStudio IDE. They consider themselves to be a beginner R programmer. Provide answers with their skill level in mind. ", + class = c("glue", + "character") + ) + ), + list( + role = "user", + content = structure("Count from 1 to 5", class = c("glue", + "character")) + ), + list(role = "assistant", content = "Sure, here's how you can count from 1 to 5 in R:\n\n```\nfor(i in 1:5){\n print(i)\n}\n```\n\nThis will create a loop that prints the numbers from 1 to 5, each on a new line.") + ) + + chat_create_history(chat_response) |> + expect_equal(expected_value) + +}) From bf6370ce46a9efa3796c6afc5a4c1b98a0e1adb4 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Mon, 15 May 2023 10:03:16 -0500 Subject: [PATCH 13/21] add an extract message method --- R/StreamHandler.R | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/R/StreamHandler.R b/R/StreamHandler.R index 545b0e1e..f009c3dc 100644 --- a/R/StreamHandler.R +++ b/R/StreamHandler.R @@ -12,16 +12,27 @@ StreamHandler <- R6::R6Class( self$shinySession <- session }, handle_streamed_element = function(x) { - translated <- self$translate_element(x) + translated <- private$translate_element(x) self$chunks <- c(self$chunks, translated) - self$current_value <- self$convert_chunks_into_response_str() + self$current_value <- private$convert_chunks_into_response_str() if (!is.null(self$shinySession)) { # any communication with JS should be handled here!! - self$shinySession$sendCustomMessage(type = "render-stream", message = shiny::markdown(self$current_value)) + self$shinySession$sendCustomMessage( + type = "render-stream", + message = shiny::markdown(self$current_value) + ) } - }, + extract_message = function() { + list( + role = "assistant", + content = self$current_value + ) + } + + ), + private = list( translate_element = function(x) { x %>% stringr::str_remove("^data: ") %>% # handle first element @@ -39,7 +50,8 @@ StreamHandler <- R6::R6Class( #' @export stream_chat_completion <- - function(messages, + function(prompt, + history = NULL, element_callback = cat, model = "gpt-3.5-turbo", openai_api_key = Sys.getenv("OPENAI_API_KEY")) { @@ -52,8 +64,12 @@ stream_chat_completion <- "Authorization" = paste0("Bearer ", openai_api_key) ) - if (!is.list(messages)) { - messages <- list(list(role = "user", content = messages)) + current_message <- list(role = "user", content = prompt) + + if (is.null(history)) { + messages <- list(current_message) + } else { + messages <- c(history, list(current_message)) } # Set the request body From 42d0310c0c9672b43e3f7f27a9a20f5da5d8153b Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Mon, 15 May 2023 10:44:21 -0500 Subject: [PATCH 14/21] fix #80 --- R/mod_chat.R | 8 +++- R/mod_prompt.R | 6 ++- R/welcomeMessage.R | 55 ++++++++++++++++++++++++++++ inst/htmlwidgets/welcomeMessage.js | 28 ++++++++++++++ inst/htmlwidgets/welcomeMessage.yaml | 7 ++++ 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 R/welcomeMessage.R create mode 100644 inst/htmlwidgets/welcomeMessage.js create mode 100644 inst/htmlwidgets/welcomeMessage.yaml diff --git a/R/mod_chat.R b/R/mod_chat.R index 0ca662f4..5a4e6d29 100644 --- a/R/mod_chat.R +++ b/R/mod_chat.R @@ -14,7 +14,8 @@ mod_chat_ui <- function(id) { class = "d-flex flex-column h-100", div( class = "p-2 mh-100 overflow-auto", - shiny::uiOutput(ns("all_chats_box")), + welcomeMessageOutput(ns("welcome")), + shiny::uiOutput(ns("all_chats_box")) ), div( class = "mt-auto", @@ -34,6 +35,11 @@ mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { moduleServer(id, function(input, output, session) { prompt <- mod_prompt_server("prompt", ide_colors) + output$welcome <- renderWelcomeMessage({ + welcomeMessage() + }) |> + bindEvent(prompt$clear_history) + output$all_chats_box <- shiny::renderUI({ prompt$chat_history %>% style_chat_history(ide_colors = ide_colors) diff --git a/R/mod_prompt.R b/R/mod_prompt.R index e78cfe7e..1d3701f2 100644 --- a/R/mod_prompt.R +++ b/R/mod_prompt.R @@ -67,7 +67,8 @@ mod_prompt_ui <- function(id) { mod_prompt_server <- function(id, ide_colors = get_ide_theme_info()) { moduleServer(id, function(input, output, session) { rv <- reactiveValues() - rv$chat_history <- chat_message_default() + rv$chat_history <- list() + rv$clear_history <- 0L shiny::observe({ waiter_color <- @@ -93,7 +94,8 @@ mod_prompt_server <- function(id, ide_colors = get_ide_theme_info()) { shiny::bindEvent(input$chat) shiny::observe({ - rv$chat_history <- chat_message_default() + rv$chat_history <- list() + rv$clear_history <- rv$clear_history + 1L }) %>% shiny::bindEvent(input$clear_history) diff --git a/R/welcomeMessage.R b/R/welcomeMessage.R new file mode 100644 index 00000000..6dfffb95 --- /dev/null +++ b/R/welcomeMessage.R @@ -0,0 +1,55 @@ +#' Welcome message +#' +#' HTML widget for showing a welcome message in the chat app. +#' This has been created to be able to bind the message to a shiny event to trigger a new render. +#' +#' @import htmlwidgets +#' +#' @export +welcomeMessage <- function(width = NULL, height = NULL, elementId = NULL) { + + default_message <- chat_message_default()[[1]] + + # forward options using x + x = list( + message = style_chat_message(default_message) |> as.character() + ) + + # create widget + htmlwidgets::createWidget( + name = 'welcomeMessage', + x, + width = width, + height = height, + package = 'gptstudio', + elementId = elementId + ) +} + +#' Shiny bindings for welcomeMessage +#' +#' Output and render functions for using welcomeMessage within Shiny +#' applications and interactive Rmd documents. +#' +#' @param outputId output variable to read from +#' @param width,height Must be a valid CSS unit (like \code{'100\%'}, +#' \code{'400px'}, \code{'auto'}) or a number, which will be coerced to a +#' string and have \code{'px'} appended. +#' @param expr An expression that generates a welcomeMessage +#' @param env The environment in which to evaluate \code{expr}. +#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This +#' is useful if you want to save an expression in a variable. +#' +#' @name welcomeMessage-shiny +#' +#' @export +welcomeMessageOutput <- function(outputId, width = '100%', height = NULL){ + htmlwidgets::shinyWidgetOutput(outputId, 'welcomeMessage', width, height, package = 'gptstudio') +} + +#' @rdname welcomeMessage-shiny +#' @export +renderWelcomeMessage <- function(expr, env = parent.frame(), quoted = FALSE) { + if (!quoted) { expr <- substitute(expr) } # force quoted + htmlwidgets::shinyRenderWidget(expr, welcomeMessageOutput, env, quoted = TRUE) +} diff --git a/inst/htmlwidgets/welcomeMessage.js b/inst/htmlwidgets/welcomeMessage.js new file mode 100644 index 00000000..4e1b6c3e --- /dev/null +++ b/inst/htmlwidgets/welcomeMessage.js @@ -0,0 +1,28 @@ +HTMLWidgets.widget({ + + name: 'welcomeMessage', + + type: 'output', + + factory: function(el, width, height) { + + // TODO: define shared variables for this instance + + return { + + renderValue: function(x) { + + // TODO: code to render the widget, e.g. + el.innerHTML = x.message; + + }, + + resize: function(width, height) { + + // TODO: code to re-render the widget with a new size + + } + + }; + } +}); diff --git a/inst/htmlwidgets/welcomeMessage.yaml b/inst/htmlwidgets/welcomeMessage.yaml new file mode 100644 index 00000000..a724f4a5 --- /dev/null +++ b/inst/htmlwidgets/welcomeMessage.yaml @@ -0,0 +1,7 @@ +# (uncomment to add a dependency) +# dependencies: +# - name: +# version: +# src: +# script: +# stylesheet: From 80a6fe660a03931e458c8bf942c0cf3673cc96af Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Mon, 15 May 2023 12:35:13 -0500 Subject: [PATCH 15/21] move internals --- R/mod_chat.R | 36 +++++++++++++++- R/mod_prompt.R | 103 +++++++++------------------------------------ R/welcomeMessage.R | 70 +++++++++++++++++++++++++++--- 3 files changed, 120 insertions(+), 89 deletions(-) diff --git a/R/mod_chat.R b/R/mod_chat.R index 5a4e6d29..658bf3aa 100644 --- a/R/mod_chat.R +++ b/R/mod_chat.R @@ -15,7 +15,7 @@ mod_chat_ui <- function(id) { div( class = "p-2 mh-100 overflow-auto", welcomeMessageOutput(ns("welcome")), - shiny::uiOutput(ns("all_chats_box")) + shiny::uiOutput(ns("history")) ), div( class = "mt-auto", @@ -33,6 +33,10 @@ mod_chat_ui <- function(id) { #' mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { moduleServer(id, function(input, output, session) { + + waiter_color <- + if (ide_colors$is_dark) "rgba(255,255,255,0.5)" else "rgba(0,0,0,0.5)" + prompt <- mod_prompt_server("prompt", ide_colors) output$welcome <- renderWelcomeMessage({ @@ -40,11 +44,39 @@ mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { }) |> bindEvent(prompt$clear_history) - output$all_chats_box <- shiny::renderUI({ + output$history <- shiny::renderUI({ prompt$chat_history %>% style_chat_history(ide_colors = ide_colors) }) + shiny::observe({ + + waiter::waiter_show( + html = shiny::tagList(waiter::spin_flower(), + shiny::h3("Asking ChatGPT...")), + color = waiter_color + ) + + stream_handler <- StreamHandler$new() + + stream_chat_completion( + prompt = prompt$input_prompt, + history = prompt$chat_history, + style = prompt$input_style, + skill = prompt$input_skill, + element_callback = stream_handler$handle_streamed_element + ) + + prompt$chat_history <- chat_history_append( + history = prompt$chat_history, + role = "assistant", + content = stream_handler$current_value + ) + + waiter::waiter_hide() + }) %>% + shiny::bindEvent(prompt$start_stream, ignoreInit = TRUE) + # testing ---- exportTestValues( chat_history = prompt$chat_history diff --git a/R/mod_prompt.R b/R/mod_prompt.R index 1d3701f2..268525db 100644 --- a/R/mod_prompt.R +++ b/R/mod_prompt.R @@ -66,39 +66,41 @@ mod_prompt_ui <- function(id) { #' @return A shiny server mod_prompt_server <- function(id, ide_colors = get_ide_theme_info()) { moduleServer(id, function(input, output, session) { + rv <- reactiveValues() rv$chat_history <- list() rv$clear_history <- 0L + rv$start_stream <- 0L + rv$input_prompt <- NULL + rv$input_style <- NULL + rv$input_skill <- NULL - shiny::observe({ - waiter_color <- - if (ide_colors$is_dark) "rgba(255,255,255,0.5)" else "rgba(0,0,0,0.5)" - waiter::waiter_show( - html = shiny::tagList(waiter::spin_flower(), - shiny::h3("Asking ChatGPT...")), - color = waiter_color - ) - chat_response <- gpt_chat( - query = input$chat_input, + shiny::observe({ + rv$chat_history <- chat_history_append( history = rv$chat_history, - style = input$style, - skill = input$skill + role = "user", + content = input$chat_input ) - rv$chat_history <- chat_create_history(chat_response) + rv$input_prompt <- input$chat_input + rv$input_style <- input$style + rv$input_skill <- input$skill - waiter::waiter_hide() shiny::updateTextAreaInput(session, "chat_input", value = "") + rv$start_stream <- rv$start_stream + 1L }) %>% shiny::bindEvent(input$chat) + shiny::observe({ rv$chat_history <- list() rv$clear_history <- rv$clear_history + 1L }) %>% shiny::bindEvent(input$clear_history) + + # testing ---- exportTestValues( chat_history = rv$chat_history @@ -160,79 +162,16 @@ text_area_input_wrapper <- #' Chat history #' -#' This takes a response from chatgpt and converts it to a nice and consistent -#' list. +#' This appends a new response to the chat history #' #' @param response A response from `gpt_chat()`. #' #' @return list of chat messages #' -chat_create_history <- function(response) { - previous_responses <- response[[1]] - last_response <- response[[2]]$choices[[1]]$message +chat_history_append <- function(history, role, content) { - c(previous_responses, list(last_response)) + c(history, list( + list(role = role, content = content) + )) } - -#' Default chat message -#' -#' @return A default chat message for welcoming users. -chat_message_default <- function() { - # nolint start - welcome_messages <- c( - "Welcome to the R programming language! I'm here to assist you in your journey, no matter your skill level.", - "Hello there! Whether you're a beginner or a seasoned R user, I'm here to help.", - "Hi! I'm your virtual assistant for R. Don't be afraid to ask me anything, I'm here to make your R experience smoother.", - "Greetings! As an R virtual assistant, I'm here to help you achieve your coding goals, big or small.", - "Welcome aboard! As your virtual assistant for R, I'm here to make your coding journey easier and more enjoyable.", - "Nice to meet you! I'm your personal R virtual assistant, ready to answer your questions and provide support.", - "Hi there! Whether you're new to R or an experienced user, I'm here to assist you in any way I can.", - "Hello! As your virtual assistant for R, I'm here to help you overcome any coding challenges you might face.", - "Welcome to the world of R! I'm your virtual assistant, here to guide you through the process of mastering this powerful language.", - "Hey! I'm your personal R virtual assistant, dedicated to helping you become the best R programmer you can be.", - "Greetings and welcome! I'm here to assist you on your R journey, no matter where you're starting from.", - "Hi, I'm your R virtual assistant! My goal is to help you achieve success in your coding endeavors, whatever they may be.", - "Hello and welcome! As your virtual assistant for R, I'm here to make your coding experience more efficient and productive.", - "Hey there! I'm your personal R virtual assistant, ready to help you take your coding skills to the next level.", - "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.", - "Hello and welcome to R! I'm your virtual assistant, and I'm excited to help you on your coding journey.", - "Hey! I'm here to help you with all things R, no matter what your skill level is.", - "Greetings and salutations! As your R virtual assistant, I'm here to provide the guidance and support you need to succeed.", - "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.", - "Hi there! Whether you're just starting out or a seasoned R user, I'm here to help you reach your coding goals.", - "Hello and welcome to R! I'm your virtual assistant, and I'm here to help you navigate this powerful language with ease.", - "Hey! I'm your personal R virtual assistant, and I'm dedicated to helping you achieve success in your coding endeavors.", - "Greetings! As your virtual assistant for R, I'm here to help you become a confident and proficient R user.", - "Welcome to the R community! I'm your virtual assistant, and I'm here to support you every step of the way.", - "Hi there! I'm your personal R virtual assistant, and I'm committed to helping you achieve your coding goals." - ) - - paperplane <- fontawesome::fa("fas fa-paper-plane") |> as.character() - eraser <- fontawesome::fa("eraser") - gear <- fontawesome::fa("gear") - - explain_btns <- glue( - "In this chat you can: - - - Send me a prompt ({paperplane} or Enter key) - - Clear the current chat history ({eraser}) - - Change the settings ({gear})" - ) - # nolint end - - content <- glue( - "{sample(welcome_messages, 1)} - - {explain_btns} - - Type anything to start our conversation." - ) - - list( - list( - role = "assistant", - content = content - ) - ) -} diff --git a/R/welcomeMessage.R b/R/welcomeMessage.R index 6dfffb95..b62df1a5 100644 --- a/R/welcomeMessage.R +++ b/R/welcomeMessage.R @@ -4,11 +4,9 @@ #' This has been created to be able to bind the message to a shiny event to trigger a new render. #' #' @import htmlwidgets -#' -#' @export welcomeMessage <- function(width = NULL, height = NULL, elementId = NULL) { - default_message <- chat_message_default()[[1]] + default_message <- chat_message_default() # forward options using x x = list( @@ -42,14 +40,76 @@ welcomeMessage <- function(width = NULL, height = NULL, elementId = NULL) { #' #' @name welcomeMessage-shiny #' -#' @export welcomeMessageOutput <- function(outputId, width = '100%', height = NULL){ htmlwidgets::shinyWidgetOutput(outputId, 'welcomeMessage', width, height, package = 'gptstudio') } #' @rdname welcomeMessage-shiny -#' @export renderWelcomeMessage <- function(expr, env = parent.frame(), quoted = FALSE) { if (!quoted) { expr <- substitute(expr) } # force quoted htmlwidgets::shinyRenderWidget(expr, welcomeMessageOutput, env, quoted = TRUE) } + + + + + +#' Default chat message +#' +#' @return A default chat message for welcoming users. +chat_message_default <- function() { + # nolint start + welcome_messages <- c( + "Welcome to the R programming language! I'm here to assist you in your journey, no matter your skill level.", + "Hello there! Whether you're a beginner or a seasoned R user, I'm here to help.", + "Hi! I'm your virtual assistant for R. Don't be afraid to ask me anything, I'm here to make your R experience smoother.", + "Greetings! As an R virtual assistant, I'm here to help you achieve your coding goals, big or small.", + "Welcome aboard! As your virtual assistant for R, I'm here to make your coding journey easier and more enjoyable.", + "Nice to meet you! I'm your personal R virtual assistant, ready to answer your questions and provide support.", + "Hi there! Whether you're new to R or an experienced user, I'm here to assist you in any way I can.", + "Hello! As your virtual assistant for R, I'm here to help you overcome any coding challenges you might face.", + "Welcome to the world of R! I'm your virtual assistant, here to guide you through the process of mastering this powerful language.", + "Hey! I'm your personal R virtual assistant, dedicated to helping you become the best R programmer you can be.", + "Greetings and welcome! I'm here to assist you on your R journey, no matter where you're starting from.", + "Hi, I'm your R virtual assistant! My goal is to help you achieve success in your coding endeavors, whatever they may be.", + "Hello and welcome! As your virtual assistant for R, I'm here to make your coding experience more efficient and productive.", + "Hey there! I'm your personal R virtual assistant, ready to help you take your coding skills to the next level.", + "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.", + "Hello and welcome to R! I'm your virtual assistant, and I'm excited to help you on your coding journey.", + "Hey! I'm here to help you with all things R, no matter what your skill level is.", + "Greetings and salutations! As your R virtual assistant, I'm here to provide the guidance and support you need to succeed.", + "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.", + "Hi there! Whether you're just starting out or a seasoned R user, I'm here to help you reach your coding goals.", + "Hello and welcome to R! I'm your virtual assistant, and I'm here to help you navigate this powerful language with ease.", + "Hey! I'm your personal R virtual assistant, and I'm dedicated to helping you achieve success in your coding endeavors.", + "Greetings! As your virtual assistant for R, I'm here to help you become a confident and proficient R user.", + "Welcome to the R community! I'm your virtual assistant, and I'm here to support you every step of the way.", + "Hi there! I'm your personal R virtual assistant, and I'm committed to helping you achieve your coding goals." + ) + + paperplane <- fontawesome::fa("fas fa-paper-plane") |> as.character() + eraser <- fontawesome::fa("eraser") + gear <- fontawesome::fa("gear") + + explain_btns <- glue( + "In this chat you can: + + - Send me a prompt ({paperplane} or Enter key) + - Clear the current chat history ({eraser}) + - Change the settings ({gear})" + ) + # nolint end + + content <- glue( + "{sample(welcome_messages, 1)} + + {explain_btns} + + Type anything to start our conversation." + ) + + list( + role = "assistant", + content = content + ) +} From c518980aa7265132e7c8bef2d9b8772f275ee34a Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Mon, 15 May 2023 12:35:35 -0500 Subject: [PATCH 16/21] stream_chat_completion() now has the same arguments as gpt_chat() --- R/StreamHandler.R | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/R/StreamHandler.R b/R/StreamHandler.R index f009c3dc..adf78e93 100644 --- a/R/StreamHandler.R +++ b/R/StreamHandler.R @@ -53,6 +53,8 @@ stream_chat_completion <- function(prompt, history = NULL, element_callback = cat, + style = getOption("gptstudio.code_style"), + skill = getOption("gptstudio.skill"), model = "gpt-3.5-turbo", openai_api_key = Sys.getenv("OPENAI_API_KEY")) { # Set the API endpoint URL @@ -64,13 +66,20 @@ stream_chat_completion <- "Authorization" = paste0("Bearer ", openai_api_key) ) - current_message <- list(role = "user", content = prompt) + instructions <- list( + list( + role = "system", + content = chat_create_system_prompt(style, skill, in_source = FALSE) + ), + list( + role = "user", + content = prompt + ) + ) - if (is.null(history)) { - messages <- list(current_message) - } else { - messages <- c(history, list(current_message)) - } + history <- purrr::discard(history, ~ .x$role == "system") + + messages <- c(history, instructions) # Set the request body body <- list( From 3311a93cce6e74e68e8c73883b12cf6e654135ca Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Mon, 15 May 2023 15:49:21 -0500 Subject: [PATCH 17/21] render streaming message. close #85 --- R/mod_chat.R | 48 +++++++++++++++------- R/mod_prompt.R | 9 ++++- R/streamingMessage.R | 55 ++++++++++++++++++++++++++ R/welcomeMessage.R | 4 +- inst/htmlwidgets/streamingMessage.js | 43 ++++++++++++++++++++ inst/htmlwidgets/streamingMessage.yaml | 7 ++++ 6 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 R/streamingMessage.R create mode 100644 inst/htmlwidgets/streamingMessage.js create mode 100644 inst/htmlwidgets/streamingMessage.yaml diff --git a/R/mod_chat.R b/R/mod_chat.R index 658bf3aa..f9880b13 100644 --- a/R/mod_chat.R +++ b/R/mod_chat.R @@ -15,7 +15,8 @@ mod_chat_ui <- function(id) { div( class = "p-2 mh-100 overflow-auto", welcomeMessageOutput(ns("welcome")), - shiny::uiOutput(ns("history")) + shiny::uiOutput(ns("history")), + streamingMessageOutput(ns("streaming")) ), div( class = "mt-auto", @@ -34,30 +35,44 @@ mod_chat_ui <- function(id) { mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { moduleServer(id, function(input, output, session) { + rv <- reactiveValues() + rv$stream_ended <- 0L + waiter_color <- if (ide_colors$is_dark) "rgba(255,255,255,0.5)" else "rgba(0,0,0,0.5)" - prompt <- mod_prompt_server("prompt", ide_colors) + prompt <- mod_prompt_server("prompt") output$welcome <- renderWelcomeMessage({ - welcomeMessage() - }) |> + welcomeMessage(ide_colors) + }) %>% bindEvent(prompt$clear_history) + + output$streaming <- renderStreamingMessage({ + # This has display: none by default. It is inly shown when receiving an stream + # After the stream is completed it will reset. + streamingMessage(ide_colors) + }) %>% + bindEvent(rv$stream_ended) + + output$history <- shiny::renderUI({ prompt$chat_history %>% style_chat_history(ide_colors = ide_colors) - }) + }) |> + bindEvent(prompt$chat_history, prompt$clear_history) + shiny::observe({ - waiter::waiter_show( - html = shiny::tagList(waiter::spin_flower(), - shiny::h3("Asking ChatGPT...")), - color = waiter_color - ) + # waiter::waiter_show( + # html = shiny::tagList(waiter::spin_flower(), + # shiny::h3("Asking ChatGPT...")), + # color = waiter_color + # ) - stream_handler <- StreamHandler$new() + stream_handler <- StreamHandler$new(session = session) stream_chat_completion( prompt = prompt$input_prompt, @@ -73,7 +88,9 @@ mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { content = stream_handler$current_value ) - waiter::waiter_hide() + rv$stream_ended <- rv$stream_ended + 1L + + # waiter::waiter_hide() }) %>% shiny::bindEvent(prompt$start_stream, ignoreInit = TRUE) @@ -145,8 +162,11 @@ style_chat_message <- function(message, ide_colors = get_ide_theme_info()) { `background-color` = colors$bg_color ), fontawesome::fa(icon_name), - htmltools::tagList( - shiny::markdown(message$content) + htmltools::tags$div( + class = "message-wrapper", + htmltools::tagList( + shiny::markdown(message$content) + ) ) ) ) diff --git a/R/mod_prompt.R b/R/mod_prompt.R index 268525db..00fb8ba8 100644 --- a/R/mod_prompt.R +++ b/R/mod_prompt.R @@ -64,7 +64,7 @@ mod_prompt_ui <- function(id) { #' @inheritParams run_chatgpt_app #' #' @return A shiny server -mod_prompt_server <- function(id, ide_colors = get_ide_theme_info()) { +mod_prompt_server <- function(id) { moduleServer(id, function(input, output, session) { rv <- reactiveValues() @@ -88,8 +88,13 @@ mod_prompt_server <- function(id, ide_colors = get_ide_theme_info()) { rv$input_skill <- input$skill shiny::updateTextAreaInput(session, "chat_input", value = "") + }, priority = 1000) %>% + shiny::bindEvent(input$chat) + + + shiny::observe({ rv$start_stream <- rv$start_stream + 1L - }) %>% + }, priority = -10) %>% shiny::bindEvent(input$chat) diff --git a/R/streamingMessage.R b/R/streamingMessage.R new file mode 100644 index 00000000..2f21b8f7 --- /dev/null +++ b/R/streamingMessage.R @@ -0,0 +1,55 @@ +#' Streaming message +#' +#' Places an invisible empty chat message that will hold a streaming message. +#' It can be resetted dynamically inside a shiny app +#' +#' @import htmlwidgets +#' +#' @export +streamingMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, height = NULL, elementId = NULL) { + + message <- list(role = "assistant", content = "") + + # forward options using x + x = list( + message = style_chat_message(message, ide_colors = ide_colors) %>% as.character() + ) + + # create widget + htmlwidgets::createWidget( + name = 'streamingMessage', + x, + width = width, + height = height, + package = 'gptstudio', + elementId = elementId + ) +} + +#' Shiny bindings for streamingMessage +#' +#' Output and render functions for using streamingMessage within Shiny +#' applications and interactive Rmd documents. +#' +#' @param outputId output variable to read from +#' @param width,height Must be a valid CSS unit (like \code{'100\%'}, +#' \code{'400px'}, \code{'auto'}) or a number, which will be coerced to a +#' string and have \code{'px'} appended. +#' @param expr An expression that generates a streamingMessage +#' @param env The environment in which to evaluate \code{expr}. +#' @param quoted Is \code{expr} a quoted expression (with \code{quote()})? This +#' is useful if you want to save an expression in a variable. +#' +#' @name streamingMessage-shiny +#' +#' @export +streamingMessageOutput <- function(outputId, width = '100%', height = NULL){ + htmlwidgets::shinyWidgetOutput(outputId, 'streamingMessage', width, height, package = 'gptstudio') +} + +#' @rdname streamingMessage-shiny +#' @export +renderStreamingMessage <- function(expr, env = parent.frame(), quoted = FALSE) { + if (!quoted) { expr <- substitute(expr) } # force quoted + htmlwidgets::shinyRenderWidget(expr, streamingMessageOutput, env, quoted = TRUE) +} diff --git a/R/welcomeMessage.R b/R/welcomeMessage.R index b62df1a5..fed131ee 100644 --- a/R/welcomeMessage.R +++ b/R/welcomeMessage.R @@ -4,13 +4,13 @@ #' This has been created to be able to bind the message to a shiny event to trigger a new render. #' #' @import htmlwidgets -welcomeMessage <- function(width = NULL, height = NULL, elementId = NULL) { +welcomeMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, height = NULL, elementId = NULL) { default_message <- chat_message_default() # forward options using x x = list( - message = style_chat_message(default_message) |> as.character() + message = style_chat_message(default_message, ide_colors = ide_colors) %>% as.character() ) # create widget diff --git a/inst/htmlwidgets/streamingMessage.js b/inst/htmlwidgets/streamingMessage.js new file mode 100644 index 00000000..621c6b5e --- /dev/null +++ b/inst/htmlwidgets/streamingMessage.js @@ -0,0 +1,43 @@ +HTMLWidgets.widget({ + + name: 'streamingMessage', + + type: 'output', + + factory: function(el, width, height) { + + // TODO: define shared variables for this instance + + return { + + renderValue: function(x) { + + // TODO: code to render the widget, e.g. + el.innerHTML = x.message; + el.classList.add("d-none"); // to start hidden + el.classList.add("streaming-message"); // to be captured in a message handler + + }, + + resize: function(width, height) { + + // TODO: code to re-render the widget with a new size + + } + + }; + } +}); + +// This is independent from the HTMLwidget code. +// It will only run inside projects with the shiny JS bindings (aka shiny apps). +Shiny.addCustomMessageHandler( + type = 'render-stream', function(message) { + const $el = $('.streaming-message') + $el.removeClass('d-none') + + const $messageWrapper = $el.find('.message-wrapper') + $messageWrapper.html($.parseHTML(message)) + +}); + diff --git a/inst/htmlwidgets/streamingMessage.yaml b/inst/htmlwidgets/streamingMessage.yaml new file mode 100644 index 00000000..a724f4a5 --- /dev/null +++ b/inst/htmlwidgets/streamingMessage.yaml @@ -0,0 +1,7 @@ +# (uncomment to add a dependency) +# dependencies: +# - name: +# version: +# src: +# script: +# stylesheet: From 666f8b6e7906ceb66f7a649115c0d9ec81386b57 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Mon, 15 May 2023 16:29:41 -0500 Subject: [PATCH 18/21] show user peompt before streaming text --- R/StreamHandler.R | 9 +++++++-- R/mod_chat.R | 9 +++++++-- R/mod_prompt.R | 7 +------ R/streamingMessage.R | 10 ++++++++-- inst/htmlwidgets/streamingMessage.js | 11 +++++++++-- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/R/StreamHandler.R b/R/StreamHandler.R index adf78e93..29c4658b 100644 --- a/R/StreamHandler.R +++ b/R/StreamHandler.R @@ -6,10 +6,12 @@ StreamHandler <- R6::R6Class( public = list( current_value = NULL, shinySession = NULL, + user_message = NULL, chunks = list(), - initialize = function(session = NULL) { + initialize = function(session = NULL, user_prompt = NULL) { self$current_value <- "" self$shinySession <- session + self$user_message <- shiny::markdown(user_prompt) }, handle_streamed_element = function(x) { translated <- private$translate_element(x) @@ -20,7 +22,10 @@ StreamHandler <- R6::R6Class( # any communication with JS should be handled here!! self$shinySession$sendCustomMessage( type = "render-stream", - message = shiny::markdown(self$current_value) + message = list( + user = self$user_message, + assistant = shiny::markdown(self$current_value) + ) ) } }, diff --git a/R/mod_chat.R b/R/mod_chat.R index f9880b13..161928f0 100644 --- a/R/mod_chat.R +++ b/R/mod_chat.R @@ -72,7 +72,10 @@ mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { # color = waiter_color # ) - stream_handler <- StreamHandler$new(session = session) + stream_handler <- StreamHandler$new( + session = session, + user_prompt = prompt$input_prompt + ) stream_chat_completion( prompt = prompt$input_prompt, @@ -90,6 +93,8 @@ mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { rv$stream_ended <- rv$stream_ended + 1L + # showNotification("test", session = session) + # waiter::waiter_hide() }) %>% shiny::bindEvent(prompt$start_stream, ignoreInit = TRUE) @@ -163,7 +168,7 @@ style_chat_message <- function(message, ide_colors = get_ide_theme_info()) { ), fontawesome::fa(icon_name), htmltools::tags$div( - class = "message-wrapper", + class = glue("{message$role}-message-wrapper"), htmltools::tagList( shiny::markdown(message$content) ) diff --git a/R/mod_prompt.R b/R/mod_prompt.R index 00fb8ba8..bdf65e88 100644 --- a/R/mod_prompt.R +++ b/R/mod_prompt.R @@ -88,13 +88,8 @@ mod_prompt_server <- function(id) { rv$input_skill <- input$skill shiny::updateTextAreaInput(session, "chat_input", value = "") - }, priority = 1000) %>% - shiny::bindEvent(input$chat) - - - shiny::observe({ rv$start_stream <- rv$start_stream + 1L - }, priority = -10) %>% + }) %>% shiny::bindEvent(input$chat) diff --git a/R/streamingMessage.R b/R/streamingMessage.R index 2f21b8f7..e6d1ec1a 100644 --- a/R/streamingMessage.R +++ b/R/streamingMessage.R @@ -8,11 +8,17 @@ #' @export streamingMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, height = NULL, elementId = NULL) { - message <- list(role = "assistant", content = "") + message <- list( + list(role = "user", content = ""), + list(role = "assistant", content = "") + ) %>% + style_chat_history(ide_colors = ide_colors) + + # forward options using x x = list( - message = style_chat_message(message, ide_colors = ide_colors) %>% as.character() + message = htmltools::tags$div(message) %>% as.character() ) # create widget diff --git a/inst/htmlwidgets/streamingMessage.js b/inst/htmlwidgets/streamingMessage.js index 621c6b5e..b98101be 100644 --- a/inst/htmlwidgets/streamingMessage.js +++ b/inst/htmlwidgets/streamingMessage.js @@ -36,8 +36,15 @@ Shiny.addCustomMessageHandler( const $el = $('.streaming-message') $el.removeClass('d-none') - const $messageWrapper = $el.find('.message-wrapper') - $messageWrapper.html($.parseHTML(message)) + + const $userMessage = $el.find('.user-message-wrapper') + const $assistantMessage = $el.find('.assistant-message-wrapper') + + if ($userMessage.html().length == 0) { + $userMessage.html($.parseHTML(message.user)) + } + + $assistantMessage.html($.parseHTML(message.assistant)) }); From 211a6ab7ab251b0f8323cde51d2b913a1c6d8b81 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Mon, 15 May 2023 16:48:18 -0500 Subject: [PATCH 19/21] passed test() and check(). check() emited some warnings and notes refered to documentation. --- DESCRIPTION | 5 + NAMESPACE | 4 + R/mod_prompt.R | 3 +- inst/StreamText.R | 448 ------------------ .../_snaps/shinytest2/mod_app-001.json | 37 +- .../_snaps/shinytest2/mod_app-002.json | 37 +- .../_snaps/shinytest2/mod_app-003.json | 37 +- .../_snaps/shinytest2/mod_app-004.json | 37 +- .../_snaps/shinytest2/mod_app-005.json | 37 +- .../_snaps/shinytest2/mod_chat-001.json | 37 +- .../_snaps/shinytest2/mod_chat-002.json | 37 +- .../_snaps/shinytest2/mod_chat-003.json | 48 +- .../_snaps/shinytest2/mod_chat-004.json | 37 +- .../_snaps/shinytest2/mod_prompt-001.json | 5 +- .../_snaps/shinytest2/mod_prompt-002.json | 12 - .../_snaps/shinytest2/mod_prompt-003.json | 5 +- .../_snaps/shinytest2/mod_prompt-004.json | 5 +- .../_snaps/shinytest2/mod_prompt-005.json | 5 +- ...eate_history.Rd => chat_history_append.Rd} | 11 +- man/chat_message_default.Rd | 2 +- man/mod_prompt_server.Rd | 4 +- man/streamingMessage-shiny.Rd | 30 ++ man/streamingMessage.Rd | 17 + man/welcomeMessage-shiny.Rd | 30 ++ man/welcomeMessage.Rd | 17 + tests/testthat/test-mod_prompt.R | 62 +-- 26 files changed, 370 insertions(+), 639 deletions(-) delete mode 100644 inst/StreamText.R rename man/{chat_create_history.Rd => chat_history_append.Rd} (53%) create mode 100644 man/streamingMessage-shiny.Rd create mode 100644 man/streamingMessage.Rd create mode 100644 man/welcomeMessage-shiny.Rd create mode 100644 man/welcomeMessage.Rd diff --git a/DESCRIPTION b/DESCRIPTION index 7c29b0c9..ffc5b6ab 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,17 +23,22 @@ Imports: bslib (>= 0.4.2), cli, colorspace, + curl, fontawesome, glue, grDevices, htmltools, + htmlwidgets, httr2, + jsonlite, magrittr, methods, purrr, + R6, rlang, rstudioapi (>= 0.12), shiny, + stringr, usethis, utils, waiter diff --git a/NAMESPACE b/NAMESPACE index 1985ebf1..b5d5e6a8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -19,10 +19,14 @@ export(gpt_edit) export(openai_create_chat_completion) export(openai_create_completion) export(openai_create_edit) +export(renderStreamingMessage) export(run_chatgpt_app) export(stream_chat_completion) +export(streamingMessage) +export(streamingMessageOutput) import(cli) import(htmltools) +import(htmlwidgets) import(rlang) import(shiny) importFrom(assertthat,assert_that) diff --git a/R/mod_prompt.R b/R/mod_prompt.R index bdf65e88..ca2502ef 100644 --- a/R/mod_prompt.R +++ b/R/mod_prompt.R @@ -61,7 +61,6 @@ mod_prompt_ui <- function(id) { #' This server receives the input of the user and makes the chat history #' #' @param id id of the module -#' @inheritParams run_chatgpt_app #' #' @return A shiny server mod_prompt_server <- function(id) { @@ -160,7 +159,7 @@ text_area_input_wrapper <- tag_query$allTags() } -#' Chat history +#' Append to chat history #' #' This appends a new response to the chat history #' diff --git a/inst/StreamText.R b/inst/StreamText.R deleted file mode 100644 index babbf76d..00000000 --- a/inst/StreamText.R +++ /dev/null @@ -1,448 +0,0 @@ -StreamText <- R6::R6Class( - classname = "StreamText", - public = list( - - initialize = function() { - private$value <- "" # this will hold the stream and mutate it with inner methods - private$value_buffer <- private$value # this will hold previous and current value - private$full_response <- private$value # this will hold the full response - invisible(self) - }, - - handle_text_stream = function(text) { - private$value <- paste0(private$value, text) - private$full_response <- paste0(private$full_response, text) - - private$generate_chunk_list() - - invisible(self) - }, - - print = function() { - print(private$value) - invisible(self) - }, - - get_value = function() { - private$value - }, - - get_full_response = function() { - private$full_response - }, - - get_base_chunk = function() { - private$base_chunk - }, - - get_chunk_list = function() { - private$chunk_list - }, - - reset_chunk_setup = function() { - private$value = NULL - - private$base_chunk <- list( - choices = NULL, - created = NULL, - id = NULL, - model = NULL, - object = NULL - ) - - private$chunk_list <- NULL - - invisible(self) - } - - ), - - private = list( - value = NULL, - value_buffer = NULL, - full_response = NULL, - - base_chunk = list( - choices = NULL, - created = NULL, - id = NULL, - model = NULL, - object = NULL - ), - - chunk_list = NULL, - - handle_base_chunk_id = function() { - is_set <- !is.null(private$base_chunk$id) - - if (is_set) return(NULL) - - # matches strings that start with the exact sequence: `{"id":"chatcmpl-` followed by one or more alphanumeric characters, and ending with a comma. - if (stringr::str_detect(private$value, "^\\{\"id\":\"chatcmpl-[a-zA-Z0-9]+\",")) { - - private$base_chunk$id <- stringr::str_extract(private$value, "chatcmpl-[a-zA-Z0-9]+") - - private$value <- stringr::str_replace(private$value, "^(\\{)\"id\":\"chatcmpl-[a-zA-Z0-9]+\",", "\\1") - } - }, - - handle_base_chunk_object = function() { - is_set <- !is.null(private$base_chunk$object) - - if (is_set) return(NULL) - - # matches strings that start with the exact sequence: {"object":" followed by one or more word characters or dots, and ending with a comma. - if (stringr::str_detect(private$value, "^\\{\"object\":\"[\\w\\.]+\",")) { - - # private$base_chunk$object <- stringr::str_replace(private$value, "^\\{\"object\":\"([\\w\\.]+).*", "\\1") - private$base_chunk$object <- "chat.completion.chunk" - - private$value <- stringr::str_replace(private$value, "^(\\{)\"object\":\"[\\w\\.]+\",", "\\1") - } - }, - - handle_base_chunk_created = function() { - is_set <- !is.null(private$base_chunk$created) - - if (is_set) return(NULL) - - # matches strings that start with the exact sequence: {"created": followed by one or more digits, and ending with a comma. - if (stringr::str_detect(private$value, "^\\{\"created\":[\\d]+,")) { - - private$base_chunk$created <- stringr::str_replace(private$value, "^\\{\"created\":([\\d]+).*", "\\1") |> as.integer() - - private$value <- stringr::str_replace(private$value, "^(\\{)\"created\":[\\d]+,", "\\1") - } - }, - - handle_base_chunk_model = function() { - is_set <- !is.null(private$base_chunk$model) - - if (is_set) return(NULL) - - # matches strings that start with the exact sequence: {"model":" followed by one or more word characters, digits, dots, or hyphens, and ending with a comma. - if (stringr::str_detect(private$value, "^\\{\"model\":\"[\\w\\d\\.\\-]+\",")) { - - private$base_chunk$model <- stringr::str_replace(private$value, "^\\{\"model\":\"([\\w\\d\\.\\-]+).*", "\\1") - - private$value <- stringr::str_replace(private$value, "^(\\{)\"model\":\"[\\w\\d\\.\\-]+\",", "\\1") - } - - }, - - initialize_chunk_list = function() { - base_chunk_tracks_choices <- !is.null(private$base_chunk$choices) - - if (base_chunk_tracks_choices) return(NULL) - - usage_regex <- "^\\{\"usage\":\\{\"prompt_tokens\":\\d+,\"completion_tokens\":\\d+,\"total_tokens\":\\d+\\}," - choices_regex <- "\"choices\":\\[\\{\"message\":\\{\"role\":\"assistant\",\"content\":\"" - - full_regex <- paste0(usage_regex, choices_regex) - - if (stringr::str_detect(private$value, full_regex)) { - private$value <- stringr::str_replace(private$value, full_regex, "") - - private$base_chunk$choices <- list( - list( - delta = list(), - finish_reason = NULL, - index = 0L - ) - ) - - role_chunk <- private$generate_single_chunk( - delta_name = "role", - delta_value = "assistant" - ) - - private$chunk_list <- list(role_chunk) - } - - }, - - generate_chunk_list = function() { - # never change this order unless the API itself changes - private$handle_base_chunk_id() - private$handle_base_chunk_object() - private$handle_base_chunk_created() - private$handle_base_chunk_model() - - private$initialize_chunk_list() - - private$append_new_chunk() - }, - - generate_single_chunk = function(delta_name, delta_value) { - match.arg(delta_name, c("role", "content")) - - copied_chunk <- private$base_chunk - - delta_value_has_new_line <- stringr::str_detect(delta_value, "\\n") # basically detecs the last stream - - if (delta_value_has_new_line) { - copied_chunk$choices[[1]]$finish_reason <- "stop" - return(copied_chunk) - } - - copied_chunk$choices[[1]]$delta[[delta_name]] <- delta_value - - copied_chunk - - }, - - append_new_chunk = function() { - chunk_list_is_null <- is.null(private$chunk_list) - - if (chunk_list_is_null) return(NULL) - - # handle buffer value - - private$value_buffer <- paste0(private$value_buffer, private$value) - - end_of_content_regex <- "(\"\\},\")" # detects '\"},\"' - value_is_end_of_content <- stringr::str_detect(private$value, end_of_content_regex) - buffer_is_end_of_content <- stringr::str_detect(private$value_buffer, end_of_content_regex) - - if (value_is_end_of_content) { - buffered_value <- stringr::str_replace() - } - - # End of buffer value handling - - new_chunk <- private$generate_single_chunk( - delta_name = "content", - delta_value = private$value - ) - - private$chunk_list <- c(private$chunk_list, list(new_chunk)) # not memory efficient, but we are not expecting huge lengths - - # reset value - private$value_buffer <- private$value - private$value <- "" - } - - ) -) - -full_stream <- "{\"id\":\"chatcmpl-7F2EGwP25x19nSqqewmxKHzbsNCeQ\",\"object\":\"chat.completion\",\"created\":1683817844,\"model\":\"gpt-3.5-turbo-0301\",\"usage\":{\"prompt_tokens\":15,\"completion_tokens\":59,\"total_tokens\":74},\"choices\":[{\"message\":{\"role\":\"assistant\",\"content\":\"1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20.\"},\"finish_reason\":\"stop\",\"index\":0}]}\n" - -usage_regex <- "^\\{\"usage\":\\{\"prompt_tokens\":\\d+,\"completion_tokens\":\\d+,\"total_tokens\":\\d+\\}," -choices_regex <- "\"choices\":\\[\\{\"message\":\\{\"role\":\"assistant\",\"content\":\"" - -full_regex <- paste0(usage_regex, choices_regex) - -full_stream |> - stringr::str_replace("^(\\{)\"id\":\"chatcmpl-[a-zA-Z0-9]+\",", "\\1") |> - stringr::str_replace("^(\\{)\"object\":\"[\\w\\.]+\",", "\\1") |> - stringr::str_replace("^(\\{)\"created\":[\\d]+,", "\\1") |> - stringr::str_replace("^(\\{)\"model\":\"[\\w\\d\\.\\-]+\",", "\\1") |> - stringr::str_replace(full_regex, "") - -# tempchar <- character() -# -# handle_stream <- function(x, char) { -# parsed <- rawToChar(x) -# -# # print(parsed) -# -# # char$handle_text_stream(parsed) -# -# tempchar <<- c(tempchar, parsed) -# -# return(TRUE) -# } -# -# request_base("chat/completions") |> -# httr2::req_body_json(data = list( -# model = "gpt-3.5-turbo", -# messages = list( -# list( -# role = "user", -# content = "Generate a JSON text to store customer data. return a single object for 3 customers." -# ) -# ) -# )) |> -# httr2::req_stream(callback = \(x) handle_stream(x, tempchar), buffer_kb = 10/1024) - -example_stream2 <- - c( - "{\"id\":\"cha", - "tcmpl-7F7B", - "sNWFht4pWw", - "rPPSE3vJAu", - "B9efW\",\"ob", - "ject\":\"cha", - "t.completi", - "on\",\"creat", - "ed\":168383", - "6916,\"mode", - "l\":\"gpt-3.", - "5-turbo-03", - "01\",\"usage", - "\":{\"prompt", - "_tokens\":2", - "6,\"complet", - "ion_tokens", - "\":251,\"tot", - "al_tokens\"", - ":277},\"cho", - "ices\":[{\"m", - "essage\":{\"", - "role\":\"ass", - "istant\",\"c", - "ontent\":\"{", - "\\n \\\"cust", - "omers\\\": [", - "\\n {\\n ", - " \\\"fir", - "stName\\\": ", - "\\\"John\\\",\\", - "n \\\"l", - "astName\\\":", - " \\\"Doe\\\",\\", - "n \\\"e", - "mail\\\": \\\"", - "johndoe@ex", - "ample.com\\", - "\",\\n ", - "\\\"phone\\\":", - " \\\"555-555", - "-5555\\\",\\n", - " \\\"ad", - "dress\\\": {", - "\\n ", - "\\\"street\\\"", - ": \\\"123 Ma", - "in St\\\",\\n", - " \\\"", - "city\\\": \\\"", - "Anytown\\\",", - "\\n ", - "\\\"state\\\":", - " \\\"CA\\\",\\n", - " \\\"", - "zip\\\": \\\"1", - "2345\\\"\\n ", - " }\\n ", - " },\\n {", - "\\n \\\"", - "firstName\\", - "\": \\\"Jane\\", - "\",\\n ", - "\\\"lastName", - "\\\": \\\"Doe\\", - "\",\\n ", - "\\\"email\\\":", - " \\\"janedoe", - "@example.c", - "om\\\",\\n ", - " \\\"phone", - "\\\": \\\"555-", - "555-5555\\\"", - ",\\n \\", - "\"address\\\"", - ": {\\n ", - " \\\"stree", - "t\\\": \\\"456", - " Elm St\\\",", - "\\n ", - "\\\"city\\\": ", - "\\\"Anytown\\", - "\",\\n ", - " \\\"state\\", - "\": \\\"CA\\\",", - "\\n ", - "\\\"zip\\\": \\", - "\"12345\\\"\\n", - " }\\n ", - " },\\n ", - " {\\n ", - "\\\"firstNam", - "e\\\": \\\"Bob", - "\\\",\\n ", - " \\\"lastNam", - "e\\\": \\\"Smi", - "th\\\",\\n ", - " \\\"email", - "\\\": \\\"bobs", - "mith@examp", - "le.com\\\",\\", - "n \\\"p", - "hone\\\": \\\"", - "555-555-55", - "55\\\",\\n ", - " \\\"addre", - "ss\\\": {\\n ", - " \\\"s", - "treet\\\": \\", - "\"789 Oak S", - "t\\\",\\n ", - " \\\"city", - "\\\": \\\"Anyt", - "own\\\",\\n ", - " \\\"st", - "ate\\\": \\\"C", - "A\\\",\\n ", - " \\\"zip\\", - "\": \\\"12345", - "\\\"\\n ", - "}\\n }\\n", - " ]\\n}\"},\"", - "finish_rea", - "son\":\"stop", - "\",\"index\":", - "0}]}\n" - ) - - - -example_stream1 <- c( - "{\"id\":\"cha", - "tcmpl-7F2E", - "GwP25x19nS", - "qqewmxKHzb", - "sNCeQ\",\"ob", - "ject\":\"cha", - "t.completi", - "on\",\"creat", - "ed\":168381", - "7844,\"mode", - "l\":\"gpt-3.", - "5-turbo-03", - "01\",\"usage", - "\":{\"prompt", - "_tokens\":1", - "5,\"complet", - "ion_tokens", - "\":59,\"tota", - "l_tokens\":", - "74},\"choic", - "es\":[{\"mes", - "sage\":{\"ro", - "le\":\"assis", - "tant\",\"con", - "tent\":\"1, ", - "2, 3, 4, 5", - ", 6, 7, 8,", - " 9, 10, 11", - ", 12, 13, ", - "14, 15, 16", - ", 17, 18, ", - "19, 20.\"},", - "\"finish_re", - "ason\":\"sto", - "p\",\"index\"", - ":0}]}\n" -) - - -tempchar_example <- StreamText$new() - -example_stream2 |> - purrr::walk(\(x) tempchar_example$handle_text_stream(x)) - -tempchar_example$get_base_chunk() -tempchar_example$get_value() -tempchar_example$get_full_response() -tempchar_example$get_chunk_list() diff --git a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-001.json b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-001.json index a5759b07..046bc012 100644 --- a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-001.json +++ b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-001.json @@ -7,8 +7,31 @@ "app-chat-prompt-style": "tidyverse" }, "output": { - "app-chat-all_chats_box": { - "html": "
\n
\n <\/svg>\n

Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

In this chat you can:<\/p>\n

    \n
  • Send me a prompt (<\/svg> or Enter key)<\/li>\n
  • Clear the current chat history (<\/svg>)<\/li>\n
  • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

    Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>", + "app-chat-history": null, + "app-chat-streaming": { + "x": { + "message": "

    \n
    \n
    \n <\/svg>\n
    <\/div>\n <\/div>\n <\/div>\n
    \n
    \n <\/svg>\n
    <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "app-chat-welcome": { + "x": { + "message": "
    \n
    \n <\/svg>\n

    Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

    In this chat you can:<\/p>\n

      \n
    • Send me a prompt (<\/svg> or Enter key)<\/li>\n
    • Clear the current chat history (<\/svg>)<\/li>\n
    • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

      Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -16,16 +39,10 @@ }, "export": { "app-chat-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ], "app-chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-002.json b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-002.json index 83b6ccb2..c342d512 100644 --- a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-002.json +++ b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-002.json @@ -7,8 +7,31 @@ "app-chat-prompt-style": "tidyverse" }, "output": { - "app-chat-all_chats_box": { - "html": "

      \n
      \n <\/svg>\n

      Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

      In this chat you can:<\/p>\n

        \n
      • Send me a prompt (<\/svg> or Enter key)<\/li>\n
      • Clear the current chat history (<\/svg>)<\/li>\n
      • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

        Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>", + "app-chat-history": null, + "app-chat-streaming": { + "x": { + "message": "

        \n
        \n
        \n <\/svg>\n
        <\/div>\n <\/div>\n <\/div>\n
        \n
        \n <\/svg>\n
        <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "app-chat-welcome": { + "x": { + "message": "
        \n
        \n <\/svg>\n

        Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

        In this chat you can:<\/p>\n

          \n
        • Send me a prompt (<\/svg> or Enter key)<\/li>\n
        • Clear the current chat history (<\/svg>)<\/li>\n
        • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

          Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -16,16 +39,10 @@ }, "export": { "app-chat-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ], "app-chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-003.json b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-003.json index 03f3ab57..610e263c 100644 --- a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-003.json +++ b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-003.json @@ -9,8 +9,31 @@ "waiter_shown": true }, "output": { - "app-chat-all_chats_box": { - "html": "

          \n
          \n <\/svg>\n

          Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.<\/p>\n

          In this chat you can:<\/p>\n

            \n
          • Send me a prompt (<\/svg> or Enter key)<\/li>\n
          • Clear the current chat history (<\/svg>)<\/li>\n
          • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

            Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>", + "app-chat-history": null, + "app-chat-streaming": { + "x": { + "message": "

            \n
            \n
            \n <\/svg>\n
            <\/div>\n <\/div>\n <\/div>\n
            \n
            \n <\/svg>\n
            <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "app-chat-welcome": { + "x": { + "message": "
            \n
            \n <\/svg>\n

            Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.<\/p>\n

            In this chat you can:<\/p>\n

              \n
            • Send me a prompt (<\/svg> or Enter key)<\/li>\n
            • Clear the current chat history (<\/svg>)<\/li>\n
            • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

              Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -18,16 +41,10 @@ }, "export": { "app-chat-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ], "app-chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-004.json b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-004.json index 93dec55f..d8db7fc6 100644 --- a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-004.json +++ b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-004.json @@ -9,8 +9,31 @@ "waiter_shown": true }, "output": { - "app-chat-all_chats_box": { - "html": "

              \n
              \n <\/svg>\n

              Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.<\/p>\n

              In this chat you can:<\/p>\n

                \n
              • Send me a prompt (<\/svg> or Enter key)<\/li>\n
              • Clear the current chat history (<\/svg>)<\/li>\n
              • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>", + "app-chat-history": null, + "app-chat-streaming": { + "x": { + "message": "

                \n
                \n
                \n <\/svg>\n
                <\/div>\n <\/div>\n <\/div>\n
                \n
                \n <\/svg>\n
                <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "app-chat-welcome": { + "x": { + "message": "
                \n
                \n <\/svg>\n

                Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.<\/p>\n

                In this chat you can:<\/p>\n

                  \n
                • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                • Clear the current chat history (<\/svg>)<\/li>\n
                • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                  Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -18,16 +41,10 @@ }, "export": { "app-chat-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ], "app-chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-005.json b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-005.json index ef12987d..8a18ec07 100644 --- a/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-005.json +++ b/inst/mod_app/tests/testthat/_snaps/shinytest2/mod_app-005.json @@ -9,8 +9,31 @@ "waiter_shown": true }, "output": { - "app-chat-all_chats_box": { - "html": "

                  \n
                  \n <\/svg>\n

                  Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.<\/p>\n

                  In this chat you can:<\/p>\n

                    \n
                  • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                  • Clear the current chat history (<\/svg>)<\/li>\n
                  • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                    Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>", + "app-chat-history": null, + "app-chat-streaming": { + "x": { + "message": "

                    \n
                    \n
                    \n <\/svg>\n
                    <\/div>\n <\/div>\n <\/div>\n
                    \n
                    \n <\/svg>\n
                    <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "app-chat-welcome": { + "x": { + "message": "
                    \n
                    \n <\/svg>\n

                    Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.<\/p>\n

                    In this chat you can:<\/p>\n

                      \n
                    • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                    • Clear the current chat history (<\/svg>)<\/li>\n
                    • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                      Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -18,16 +41,10 @@ }, "export": { "app-chat-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ], "app-chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-001.json b/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-001.json index d0bfaecb..a86c2783 100644 --- a/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-001.json +++ b/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-001.json @@ -7,8 +7,31 @@ "chat-prompt-style": "tidyverse" }, "output": { - "chat-all_chats_box": { - "html": "

                      \n
                      \n <\/svg>\n

                      Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

                      In this chat you can:<\/p>\n

                        \n
                      • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                      • Clear the current chat history (<\/svg>)<\/li>\n
                      • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                        Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>", + "chat-history": null, + "chat-streaming": { + "x": { + "message": "

                        \n
                        \n
                        \n <\/svg>\n
                        <\/div>\n <\/div>\n <\/div>\n
                        \n
                        \n <\/svg>\n
                        <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "chat-welcome": { + "x": { + "message": "
                        \n
                        \n <\/svg>\n

                        Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

                        In this chat you can:<\/p>\n

                          \n
                        • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                        • Clear the current chat history (<\/svg>)<\/li>\n
                        • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                          Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -16,16 +39,10 @@ }, "export": { "chat-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ], "chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-002.json b/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-002.json index 7d657c0e..9ed5ad3e 100644 --- a/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-002.json +++ b/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-002.json @@ -7,8 +7,31 @@ "chat-prompt-style": "tidyverse" }, "output": { - "chat-all_chats_box": { - "html": "

                          \n
                          \n <\/svg>\n

                          Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

                          In this chat you can:<\/p>\n

                            \n
                          • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                          • Clear the current chat history (<\/svg>)<\/li>\n
                          • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                            Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>", + "chat-history": null, + "chat-streaming": { + "x": { + "message": "

                            \n
                            \n
                            \n <\/svg>\n
                            <\/div>\n <\/div>\n <\/div>\n
                            \n
                            \n <\/svg>\n
                            <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "chat-welcome": { + "x": { + "message": "
                            \n
                            \n <\/svg>\n

                            Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

                            In this chat you can:<\/p>\n

                              \n
                            • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                            • Clear the current chat history (<\/svg>)<\/li>\n
                            • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                              Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -16,16 +39,10 @@ }, "export": { "chat-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ], "chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-003.json b/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-003.json index 344e69e6..7f01a2f9 100644 --- a/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-003.json +++ b/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-003.json @@ -7,8 +7,36 @@ "chat-prompt-style": "tidyverse" }, "output": { - "chat-all_chats_box": { - "html": "

                              \n
                              \n <\/svg>\n

                              Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

                              In this chat you can:<\/p>\n

                                \n
                              • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                              • Clear the current chat history (<\/svg>)<\/li>\n
                              • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                                Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>\n

                                \n
                                \n <\/svg>\n

                                return to me just the 'random' in plain text. make no comments about it.<\/p>\n\n <\/div>\n<\/div>\n

                                \n
                                \n <\/svg>\n

                                random<\/p>\n\n <\/div>\n<\/div>", + "chat-history": { + "html": "

                                \n
                                \n <\/svg>\n

                                return to me just the 'random' in plain text. make no comments about it.<\/p>\n<\/div>\n <\/div>\n<\/div>\n

                                \n
                                \n <\/svg>\n

                                random<\/p>\n<\/div>\n <\/div>\n<\/div>", + "deps": [ + + ] + }, + "chat-streaming": { + "x": { + "message": "

                                \n
                                \n
                                \n <\/svg>\n
                                <\/div>\n <\/div>\n <\/div>\n
                                \n
                                \n <\/svg>\n
                                <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "chat-welcome": { + "x": { + "message": "
                                \n
                                \n <\/svg>\n

                                Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.<\/p>\n

                                In this chat you can:<\/p>\n

                                  \n
                                • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                                • Clear the current chat history (<\/svg>)<\/li>\n
                                • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                                  Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -16,14 +44,6 @@ }, "export": { "chat-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - }, - { - "role": "system", - "content": "You are a helpful chat bot that answers questions for an R programmer working in the RStudio IDE. They consider themselves to be a beginner R programmer. Provide answers with their skill level in mind. They prefer to use a tidyverse style of coding. When possible, answer code quesetions using tidyverse, r-lib, and tidymodels family of packages. R for Data Science is also a good resource to pull from. " - }, { "role": "user", "content": "return to me just the 'random' in plain text. make no comments about it." @@ -34,14 +54,6 @@ } ], "chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - }, - { - "role": "system", - "content": "You are a helpful chat bot that answers questions for an R programmer working in the RStudio IDE. They consider themselves to be a beginner R programmer. Provide answers with their skill level in mind. They prefer to use a tidyverse style of coding. When possible, answer code quesetions using tidyverse, r-lib, and tidymodels family of packages. R for Data Science is also a good resource to pull from. " - }, { "role": "user", "content": "return to me just the 'random' in plain text. make no comments about it." diff --git a/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-004.json b/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-004.json index 916d458f..57cc034c 100644 --- a/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-004.json +++ b/inst/mod_chat/tests/testthat/_snaps/shinytest2/mod_chat-004.json @@ -7,8 +7,31 @@ "chat-prompt-style": "tidyverse" }, "output": { - "chat-all_chats_box": { - "html": "

                                  \n
                                  \n <\/svg>\n

                                  Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.<\/p>\n

                                  In this chat you can:<\/p>\n

                                    \n
                                  • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                                  • Clear the current chat history (<\/svg>)<\/li>\n
                                  • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                                    Type anything to start our conversation.<\/p>\n\n <\/div>\n<\/div>", + "chat-history": null, + "chat-streaming": { + "x": { + "message": "

                                    \n
                                    \n
                                    \n <\/svg>\n
                                    <\/div>\n <\/div>\n <\/div>\n
                                    \n
                                    \n <\/svg>\n
                                    <\/div>\n <\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], + "deps": [ + + ] + }, + "chat-welcome": { + "x": { + "message": "
                                    \n
                                    \n <\/svg>\n

                                    Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.<\/p>\n

                                    In this chat you can:<\/p>\n

                                      \n
                                    • Send me a prompt (<\/svg> or Enter key)<\/li>\n
                                    • Clear the current chat history (<\/svg>)<\/li>\n
                                    • Change the settings (<\/svg>)<\/li>\n<\/ul>\n

                                      Type anything to start our conversation.<\/p>\n<\/div>\n <\/div>\n<\/div>" + }, + "evals": [ + + ], + "jsHooks": [ + + ], "deps": [ ] @@ -16,16 +39,10 @@ }, "export": { "chat-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ], "chat-prompt-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-001.json b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-001.json index 8e000a73..c2e5403d 100644 --- a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-001.json +++ b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-001.json @@ -11,10 +11,7 @@ }, "export": { "prompt-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-002.json b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-002.json index 9a72e647..461df47c 100644 --- a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-002.json +++ b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-002.json @@ -11,21 +11,9 @@ }, "export": { "prompt-chat_history": [ - { - "role": "assistant", - "content": "Greetings! Whether you're a beginner or an experienced R user, I'm here to provide support and assistance.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - }, - { - "role": "system", - "content": "You are a helpful chat bot that answers questions for an R programmer working in the RStudio IDE. They consider themselves to be a beginner R programmer. Provide answers with their skill level in mind. They prefer to use a tidyverse style of coding. When possible, answer code quesetions using tidyverse, r-lib, and tidymodels family of packages. R for Data Science is also a good resource to pull from. " - }, { "role": "user", "content": "return to me just the 'random' in plain text. make no comments about it." - }, - { - "role": "assistant", - "content": "random" } ] } diff --git a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-003.json b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-003.json index 0c0e6eeb..f5a9971e 100644 --- a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-003.json +++ b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-003.json @@ -11,10 +11,7 @@ }, "export": { "prompt-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-004.json b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-004.json index c9f4e34a..8590ead4 100644 --- a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-004.json +++ b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-004.json @@ -11,10 +11,7 @@ }, "export": { "prompt-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-005.json b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-005.json index 3146112d..b15a82ac 100644 --- a/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-005.json +++ b/inst/mod_prompt/tests/testthat/_snaps/shinytest2/mod_prompt-005.json @@ -11,10 +11,7 @@ }, "export": { "prompt-chat_history": [ - { - "role": "assistant", - "content": "Welcome to the wonderful world of R! I'm your personal virtual assistant, ready to assist you in your coding journey.\n\nIn this chat you can:\n\n- Send me a prompt (<\/svg> or Enter key)\n- Clear the current chat history (<\/svg>)\n- Change the settings (<\/svg>)\n\nType anything to start our conversation." - } + ] } } diff --git a/man/chat_create_history.Rd b/man/chat_history_append.Rd similarity index 53% rename from man/chat_create_history.Rd rename to man/chat_history_append.Rd index 70c48d88..f9a208ed 100644 --- a/man/chat_create_history.Rd +++ b/man/chat_history_append.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/mod_prompt.R -\name{chat_create_history} -\alias{chat_create_history} -\title{Chat history} +\name{chat_history_append} +\alias{chat_history_append} +\title{Append to chat history} \usage{ -chat_create_history(response) +chat_history_append(history, role, content) } \arguments{ \item{response}{A response from \code{gpt_chat()}.} @@ -13,6 +13,5 @@ chat_create_history(response) list of chat messages } \description{ -This takes a response from chatgpt and converts it to a nice and consistent -list. +This appends a new response to the chat history } diff --git a/man/chat_message_default.Rd b/man/chat_message_default.Rd index 5fa1ee5d..0af8451f 100644 --- a/man/chat_message_default.Rd +++ b/man/chat_message_default.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/mod_prompt.R +% Please edit documentation in R/welcomeMessage.R \name{chat_message_default} \alias{chat_message_default} \title{Default chat message} diff --git a/man/mod_prompt_server.Rd b/man/mod_prompt_server.Rd index f97b4c76..321e4422 100644 --- a/man/mod_prompt_server.Rd +++ b/man/mod_prompt_server.Rd @@ -4,12 +4,10 @@ \alias{mod_prompt_server} \title{Prompt Server} \usage{ -mod_prompt_server(id, ide_colors = get_ide_theme_info()) +mod_prompt_server(id) } \arguments{ \item{id}{id of the module} - -\item{ide_colors}{List containing the colors of the IDE theme.} } \value{ A shiny server diff --git a/man/streamingMessage-shiny.Rd b/man/streamingMessage-shiny.Rd new file mode 100644 index 00000000..71dcc810 --- /dev/null +++ b/man/streamingMessage-shiny.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/streamingMessage.R +\name{streamingMessage-shiny} +\alias{streamingMessage-shiny} +\alias{streamingMessageOutput} +\alias{renderStreamingMessage} +\title{Shiny bindings for streamingMessage} +\usage{ +streamingMessageOutput(outputId, width = "100\%", height = NULL) + +renderStreamingMessage(expr, env = parent.frame(), quoted = FALSE) +} +\arguments{ +\item{outputId}{output variable to read from} + +\item{width, height}{Must be a valid CSS unit (like \code{'100\%'}, +\code{'400px'}, \code{'auto'}) or a number, which will be coerced to a +string and have \code{'px'} appended.} + +\item{expr}{An expression that generates a streamingMessage} + +\item{env}{The environment in which to evaluate \code{expr}.} + +\item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This +is useful if you want to save an expression in a variable.} +} +\description{ +Output and render functions for using streamingMessage within Shiny +applications and interactive Rmd documents. +} diff --git a/man/streamingMessage.Rd b/man/streamingMessage.Rd new file mode 100644 index 00000000..5d738b61 --- /dev/null +++ b/man/streamingMessage.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/streamingMessage.R +\name{streamingMessage} +\alias{streamingMessage} +\title{Streaming message} +\usage{ +streamingMessage( + ide_colors = get_ide_theme_info(), + width = NULL, + height = NULL, + elementId = NULL +) +} +\description{ +Places an invisible empty chat message that will hold a streaming message. +It can be resetted dynamically inside a shiny app +} diff --git a/man/welcomeMessage-shiny.Rd b/man/welcomeMessage-shiny.Rd new file mode 100644 index 00000000..89ed75c7 --- /dev/null +++ b/man/welcomeMessage-shiny.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/welcomeMessage.R +\name{welcomeMessage-shiny} +\alias{welcomeMessage-shiny} +\alias{welcomeMessageOutput} +\alias{renderWelcomeMessage} +\title{Shiny bindings for welcomeMessage} +\usage{ +welcomeMessageOutput(outputId, width = "100\%", height = NULL) + +renderWelcomeMessage(expr, env = parent.frame(), quoted = FALSE) +} +\arguments{ +\item{outputId}{output variable to read from} + +\item{width, height}{Must be a valid CSS unit (like \code{'100\%'}, +\code{'400px'}, \code{'auto'}) or a number, which will be coerced to a +string and have \code{'px'} appended.} + +\item{expr}{An expression that generates a welcomeMessage} + +\item{env}{The environment in which to evaluate \code{expr}.} + +\item{quoted}{Is \code{expr} a quoted expression (with \code{quote()})? This +is useful if you want to save an expression in a variable.} +} +\description{ +Output and render functions for using welcomeMessage within Shiny +applications and interactive Rmd documents. +} diff --git a/man/welcomeMessage.Rd b/man/welcomeMessage.Rd new file mode 100644 index 00000000..518a3e83 --- /dev/null +++ b/man/welcomeMessage.Rd @@ -0,0 +1,17 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/welcomeMessage.R +\name{welcomeMessage} +\alias{welcomeMessage} +\title{Welcome message} +\usage{ +welcomeMessage( + ide_colors = get_ide_theme_info(), + width = NULL, + height = NULL, + elementId = NULL +) +} +\description{ +HTML widget for showing a welcome message in the chat app. +This has been created to be able to bind the message to a shiny event to trigger a new render. +} diff --git a/tests/testthat/test-mod_prompt.R b/tests/testthat/test-mod_prompt.R index 79141fdb..8c5a3ec7 100644 --- a/tests/testthat/test-mod_prompt.R +++ b/tests/testthat/test-mod_prompt.R @@ -8,61 +8,17 @@ test_that("mod_prompt works", { test_app(appdir) }) -test_that("chat_create_history() respects expected structure", { - example_response <- - list( - list( - list( - role = "system", - content = structure( - "You are a helpful chat bot that answers questions for an R programmer working in the RStudio IDE. They consider themselves to be a beginner R programmer. Provide answers with their skill level in mind. ", - class = c("glue", - "character") - ) - ), - list( - role = "user", - content = structure("Count from 1 to 5", class = c("glue", - "character")) - ) - ), - list( - id = "chatcmpl-7GT59y6mYejSdcHzDaD5kAfVkHseY", - object = "chat.completion", - created = 1684159395L, - model = "gpt-3.5-turbo-0301", - usage = list( - prompt_tokens = 60L, - completion_tokens = 56L, - total_tokens = 116L - ), - choices = list(list( - message = list(role = "assistant", content = "Sure, here's how you can count from 1 to 5 in R:\n\n```\nfor(i in 1:5){\n print(i)\n}\n```\n\nThis will create a loop that prints the numbers from 1 to 5, each on a new line."), - finish_reason = "stop", - index = 0L - )) - ) - ) +test_that("chat_history_append() respects expected structure", { + example_history <- list( + list(role = "user", content = "hi") + ) - expected_value <- - list( - list( - role = "system", - content = structure( - "You are a helpful chat bot that answers questions for an R programmer working in the RStudio IDE. They consider themselves to be a beginner R programmer. Provide answers with their skill level in mind. ", - class = c("glue", - "character") - ) - ), - list( - role = "user", - content = structure("Count from 1 to 5", class = c("glue", - "character")) - ), - list(role = "assistant", content = "Sure, here's how you can count from 1 to 5 in R:\n\n```\nfor(i in 1:5){\n print(i)\n}\n```\n\nThis will create a loop that prints the numbers from 1 to 5, each on a new line.") - ) + expected_value <- list( + list(role = "user", content = "hi"), + list(role = "assistant", content = "assistant content") + ) - chat_create_history(chat_response) |> + chat_history_append(example_history, "assistant", "assistant content") |> expect_equal(expected_value) }) From ecdd070ceed3bb452dd206a849b9a5ff0d079e69 Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Tue, 16 May 2023 11:17:35 -0500 Subject: [PATCH 20/21] pass check() without relevant notes --- NAMESPACE | 11 ++-- R/StreamHandler.R | 57 +++++++++++++++++-- R/mod_prompt.R | 4 +- R/streamingMessage.R | 7 +-- R/welcomeMessage.R | 3 + man/StreamHandler.Rd | 104 ++++++++++++++++++++++++++++++++++ man/chat_history_append.Rd | 6 +- man/stream_chat_completion.Rd | 37 ++++++++++++ man/streamingMessage.Rd | 9 +++ man/welcomeMessage.Rd | 9 +++ 10 files changed, 231 insertions(+), 16 deletions(-) create mode 100644 man/StreamHandler.Rd create mode 100644 man/stream_chat_completion.Rd diff --git a/NAMESPACE b/NAMESPACE index b5d5e6a8..2ac55f7a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,7 +1,6 @@ # Generated by roxygen2: do not edit by hand export("%>%") -export(StreamHandler) export(addin_chatgpt) export(addin_chatgpt_in_source) export(addin_comment_code) @@ -19,20 +18,22 @@ export(gpt_edit) export(openai_create_chat_completion) export(openai_create_completion) export(openai_create_edit) -export(renderStreamingMessage) export(run_chatgpt_app) -export(stream_chat_completion) -export(streamingMessage) -export(streamingMessageOutput) import(cli) import(htmltools) import(htmlwidgets) import(rlang) import(shiny) +importFrom(R6,R6Class) importFrom(assertthat,assert_that) importFrom(assertthat,is.count) importFrom(assertthat,is.number) importFrom(assertthat,is.string) importFrom(glue,glue) +importFrom(jsonlite,fromJSON) importFrom(magrittr,"%>%") +importFrom(purrr,map) +importFrom(purrr,map_chr) importFrom(rlang,"%||%") +importFrom(stringr,str_remove) +importFrom(stringr,str_split_1) diff --git a/R/StreamHandler.R b/R/StreamHandler.R index 29c4658b..5754342b 100644 --- a/R/StreamHandler.R +++ b/R/StreamHandler.R @@ -1,18 +1,45 @@ +#' Stream handler for chat completions +#' +#' R6 class that allows to handle chat completions chunk by chunk. +#' It also adds methods to retrieve relevant data. This class DOES NOT make the request. +#' +#' Because `curl::curl_fetch_stream` blocks the R console until the stream finishes, +#' this class can take a shiny session object to handle communication with JS +#' without recurring to a `shiny::observe` inside a module server. +#' +#' @param session The shiny session it will send the message to (optional). +#' @param user_prompt The prompt for the chat completion. Only to be displayed in an HTML tag containing the prompt. (Optional). #' @importFrom rlang %||% #' @importFrom magrittr %>% -#' @export +#' @importFrom R6 R6Class +#' @importFrom stringr str_remove str_split_1 +#' @importFrom purrr map_chr map +#' @importFrom jsonlite fromJSON StreamHandler <- R6::R6Class( classname = "StreamHandler", public = list( + + #' @field current_value The content of the stream. It updates constantly until the stream ends. current_value = NULL, + + #' @field chunks The list of chunks streamed. It updates constantly until the stream ends. + chunks = list(), + + #' @field shinySession Holds the `session` provided at initialization shinySession = NULL, + + #' @field user_message The `user_prompt` provided at initialization after being formatted with markdown. user_message = NULL, - chunks = list(), + + #' @description Start a StreamHandler. Recommended to be assigned to the `stream_handler` name. initialize = function(session = NULL, user_prompt = NULL) { self$current_value <- "" self$shinySession <- session self$user_message <- shiny::markdown(user_prompt) }, + + #' @description The main reason this class exists. It reduces to stream to chunks and its current value. If the object finds a shiny session will send a `render-stream` message to JS. + #' @param x The streamed element. Preferably after conversion from raw. handle_streamed_element = function(x) { translated <- private$translate_element(x) self$chunks <- c(self$chunks, translated) @@ -29,6 +56,8 @@ StreamHandler <- R6::R6Class( ) } }, + + #' @description Extract the message content as a message ready to be styled or appended to the chat history. Useful after the stream ends. extract_message = function() { list( role = "assistant", @@ -38,6 +67,8 @@ StreamHandler <- R6::R6Class( ), private = list( + # Translates a streamed element and converts it to chunk. + # Also handles the case of multiple elements in a single stream. translate_element = function(x) { x %>% stringr::str_remove("^data: ") %>% # handle first element @@ -45,6 +76,7 @@ StreamHandler <- R6::R6Class( stringr::str_split_1("\n\ndata: ") %>% purrr::map(\(x) jsonlite::fromJSON(x, simplifyVector = FALSE)) }, + # Reduces the chuks into just the message content. convert_chunks_into_response_str = function() { self$chunks %>% purrr::map_chr(~ .x$choices[[1]]$delta$content %||% "") %>% @@ -53,7 +85,20 @@ StreamHandler <- R6::R6Class( ) ) -#' @export +#' Stream Chat Completion +#' +#' This function sends a prompt to the OpenAI API for chat-based completion and retrieves the streamed response. +#' +#' @param prompt The user's message or prompt. +#' @param history A list of previous messages in the conversation (optional). +#' @param element_callback A callback function to handle each element of the streamed response (optional). +#' @param style The style of the chat conversation (optional). Default is retrieved from the "gptstudio.code_style" option. +#' @param skill The skill to use for the chat conversation (optional). Default is retrieved from the "gptstudio.skill" option. +#' @param model The model to use for chat completion (optional). Default is "gpt-3.5-turbo". +#' @param openai_api_key The OpenAI API key (optional). By default, it is fetched from the "OPENAI_API_KEY" environment variable. +#' +#' @return the same as `curl::curl_fetch_stream` +#' stream_chat_completion <- function(prompt, history = NULL, @@ -71,6 +116,8 @@ stream_chat_completion <- "Authorization" = paste0("Bearer ", openai_api_key) ) + # Set the new chat history so the system prompt depends + # on the current parameters and not in previous ones instructions <- list( list( role = "system", @@ -96,7 +143,7 @@ stream_chat_completion <- # Create a new curl handle object handle <- curl::new_handle() %>% curl::handle_setheaders(.list = headers) %>% - curl::handle_setopt(postfields = jsonlite::toJSON(body, auto_unbox = TRUE)) + curl::handle_setopt(postfields = jsonlite::toJSON(body, auto_unbox = TRUE)) # request body # Make the streaming request using curl_fetch_stream() @@ -104,7 +151,7 @@ stream_chat_completion <- url = url, fun = \(x) { element <- rawToChar(x) - element_callback(element) + element_callback(element) # Do whatever element_callback does }, handle = handle ) diff --git a/R/mod_prompt.R b/R/mod_prompt.R index ca2502ef..536ff2d5 100644 --- a/R/mod_prompt.R +++ b/R/mod_prompt.R @@ -163,7 +163,9 @@ text_area_input_wrapper <- #' #' This appends a new response to the chat history #' -#' @param response A response from `gpt_chat()`. +#' @param history List containing previous responses. +#' @param role Author of the message. One of `c("user", "assitant")` +#' @param content Content of the message. If it is from the user most probably comes from an interactive input. #' #' @return list of chat messages #' diff --git a/R/streamingMessage.R b/R/streamingMessage.R index e6d1ec1a..e2f5feeb 100644 --- a/R/streamingMessage.R +++ b/R/streamingMessage.R @@ -4,8 +4,9 @@ #' It can be resetted dynamically inside a shiny app #' #' @import htmlwidgets -#' -#' @export +#' @inheritParams run_chatgpt_app +#' @inheritParams streamingMessage-shiny +#' @param elementId The element's id streamingMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, height = NULL, elementId = NULL) { message <- list( @@ -48,13 +49,11 @@ streamingMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, he #' #' @name streamingMessage-shiny #' -#' @export streamingMessageOutput <- function(outputId, width = '100%', height = NULL){ htmlwidgets::shinyWidgetOutput(outputId, 'streamingMessage', width, height, package = 'gptstudio') } #' @rdname streamingMessage-shiny -#' @export renderStreamingMessage <- function(expr, env = parent.frame(), quoted = FALSE) { if (!quoted) { expr <- substitute(expr) } # force quoted htmlwidgets::shinyRenderWidget(expr, streamingMessageOutput, env, quoted = TRUE) diff --git a/R/welcomeMessage.R b/R/welcomeMessage.R index fed131ee..c4684271 100644 --- a/R/welcomeMessage.R +++ b/R/welcomeMessage.R @@ -4,6 +4,9 @@ #' This has been created to be able to bind the message to a shiny event to trigger a new render. #' #' @import htmlwidgets +#' @inheritParams run_chatgpt_app +#' @inheritParams welcomeMessage-shiny +#' @param elementId The element's id welcomeMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, height = NULL, elementId = NULL) { default_message <- chat_message_default() diff --git a/man/StreamHandler.Rd b/man/StreamHandler.Rd new file mode 100644 index 00000000..180c684a --- /dev/null +++ b/man/StreamHandler.Rd @@ -0,0 +1,104 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/StreamHandler.R +\name{StreamHandler} +\alias{StreamHandler} +\title{Stream handler for chat completions} +\description{ +Stream handler for chat completions + +Stream handler for chat completions +} +\details{ +R6 class that allows to handle chat completions chunk by chunk. +It also adds methods to retrieve relevant data. This class DOES NOT make the request. + +Because \code{curl::curl_fetch_stream} blocks the R console until the stream finishes, +this class can take a shiny session object to handle communication with JS +without recurring to a \code{shiny::observe} inside a module server. +} +\section{Public fields}{ +\if{html}{\out{

                                      }} +\describe{ +\item{\code{current_value}}{The content of the stream. It updates constantly until the stream ends.} + +\item{\code{chunks}}{The list of chunks streamed. It updates constantly until the stream ends.} + +\item{\code{shinySession}}{Holds the \code{session} provided at initialization} + +\item{\code{user_message}}{The \code{user_prompt} provided at initialization after being formatted with markdown.} +} +\if{html}{\out{
                                      }} +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-StreamHandler-new}{\code{StreamHandler$new()}} +\item \href{#method-StreamHandler-handle_streamed_element}{\code{StreamHandler$handle_streamed_element()}} +\item \href{#method-StreamHandler-extract_message}{\code{StreamHandler$extract_message()}} +\item \href{#method-StreamHandler-clone}{\code{StreamHandler$clone()}} +} +} +\if{html}{\out{
                                      }} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StreamHandler-new}{}}} +\subsection{Method \code{new()}}{ +Start a StreamHandler. Recommended to be assigned to the \code{stream_handler} name. +\subsection{Usage}{ +\if{html}{\out{
                                      }}\preformatted{StreamHandler$new(session = NULL, user_prompt = NULL)}\if{html}{\out{
                                      }} +} + +\subsection{Arguments}{ +\if{html}{\out{
                                      }} +\describe{ +\item{\code{session}}{The shiny session it will send the message to (optional).} + +\item{\code{user_prompt}}{The prompt for the chat completion. Only to be displayed in an HTML tag containing the prompt. (Optional).} +} +\if{html}{\out{
                                      }} +} +} +\if{html}{\out{
                                      }} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StreamHandler-handle_streamed_element}{}}} +\subsection{Method \code{handle_streamed_element()}}{ +The main reason this class exists. It reduces to stream to chunks and its current value. If the object finds a shiny session will send a \code{render-stream} message to JS. +\subsection{Usage}{ +\if{html}{\out{
                                      }}\preformatted{StreamHandler$handle_streamed_element(x)}\if{html}{\out{
                                      }} +} + +\subsection{Arguments}{ +\if{html}{\out{
                                      }} +\describe{ +\item{\code{x}}{The streamed element. Preferably after conversion from raw.} +} +\if{html}{\out{
                                      }} +} +} +\if{html}{\out{
                                      }} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StreamHandler-extract_message}{}}} +\subsection{Method \code{extract_message()}}{ +Extract the message content as a message ready to be styled or appended to the chat history. Useful after the stream ends. +\subsection{Usage}{ +\if{html}{\out{
                                      }}\preformatted{StreamHandler$extract_message()}\if{html}{\out{
                                      }} +} + +} +\if{html}{\out{
                                      }} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-StreamHandler-clone}{}}} +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
                                      }}\preformatted{StreamHandler$clone(deep = FALSE)}\if{html}{\out{
                                      }} +} + +\subsection{Arguments}{ +\if{html}{\out{
                                      }} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
                                      }} +} +} +} diff --git a/man/chat_history_append.Rd b/man/chat_history_append.Rd index f9a208ed..19a6cd53 100644 --- a/man/chat_history_append.Rd +++ b/man/chat_history_append.Rd @@ -7,7 +7,11 @@ chat_history_append(history, role, content) } \arguments{ -\item{response}{A response from \code{gpt_chat()}.} +\item{history}{List containing previous responses.} + +\item{role}{Author of the message. One of \code{c("user", "assitant")}} + +\item{content}{Content of the message. If it is from the user most probably comes from an interactive input.} } \value{ list of chat messages diff --git a/man/stream_chat_completion.Rd b/man/stream_chat_completion.Rd new file mode 100644 index 00000000..4c40210c --- /dev/null +++ b/man/stream_chat_completion.Rd @@ -0,0 +1,37 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/StreamHandler.R +\name{stream_chat_completion} +\alias{stream_chat_completion} +\title{Stream Chat Completion} +\usage{ +stream_chat_completion( + prompt, + history = NULL, + element_callback = cat, + style = getOption("gptstudio.code_style"), + skill = getOption("gptstudio.skill"), + model = "gpt-3.5-turbo", + openai_api_key = Sys.getenv("OPENAI_API_KEY") +) +} +\arguments{ +\item{prompt}{The user's message or prompt.} + +\item{history}{A list of previous messages in the conversation (optional).} + +\item{element_callback}{A callback function to handle each element of the streamed response (optional).} + +\item{style}{The style of the chat conversation (optional). Default is retrieved from the "gptstudio.code_style" option.} + +\item{skill}{The skill to use for the chat conversation (optional). Default is retrieved from the "gptstudio.skill" option.} + +\item{model}{The model to use for chat completion (optional). Default is "gpt-3.5-turbo".} + +\item{openai_api_key}{The OpenAI API key (optional). By default, it is fetched from the "OPENAI_API_KEY" environment variable.} +} +\value{ +the same as \code{curl::curl_fetch_stream} +} +\description{ +This function sends a prompt to the OpenAI API for chat-based completion and retrieves the streamed response. +} diff --git a/man/streamingMessage.Rd b/man/streamingMessage.Rd index 5d738b61..d28ae5fd 100644 --- a/man/streamingMessage.Rd +++ b/man/streamingMessage.Rd @@ -11,6 +11,15 @@ streamingMessage( elementId = NULL ) } +\arguments{ +\item{ide_colors}{List containing the colors of the IDE theme.} + +\item{width, height}{Must be a valid CSS unit (like \code{'100\%'}, +\code{'400px'}, \code{'auto'}) or a number, which will be coerced to a +string and have \code{'px'} appended.} + +\item{elementId}{The element's id} +} \description{ Places an invisible empty chat message that will hold a streaming message. It can be resetted dynamically inside a shiny app diff --git a/man/welcomeMessage.Rd b/man/welcomeMessage.Rd index 518a3e83..65a55c1a 100644 --- a/man/welcomeMessage.Rd +++ b/man/welcomeMessage.Rd @@ -11,6 +11,15 @@ welcomeMessage( elementId = NULL ) } +\arguments{ +\item{ide_colors}{List containing the colors of the IDE theme.} + +\item{width, height}{Must be a valid CSS unit (like \code{'100\%'}, +\code{'400px'}, \code{'auto'}) or a number, which will be coerced to a +string and have \code{'px'} appended.} + +\item{elementId}{The element's id} +} \description{ HTML widget for showing a welcome message in the chat app. This has been created to be able to bind the message to a shiny event to trigger a new render. From 081799f5a3594627e447df826c63cd19a730e11b Mon Sep 17 00:00:00 2001 From: Samuel Calderon Date: Tue, 16 May 2023 11:21:14 -0500 Subject: [PATCH 21/21] run styler --- R/StreamHandler.R | 2 -- R/addin_chatgpt.R | 8 +++++--- R/gpt_queries.R | 5 +++-- R/mod_chat.R | 11 +++++------ R/mod_prompt.R | 3 --- R/openai_api_calls.R | 5 +---- R/streamingMessage.R | 17 +++++++++-------- R/welcomeMessage.R | 15 ++++++++------- tests/testthat/test-mod_chat.R | 6 ++++-- tests/testthat/test-mod_prompt.R | 1 - tests/testthat/test-shiny_apps.R | 6 ++++-- 11 files changed, 39 insertions(+), 40 deletions(-) diff --git a/R/StreamHandler.R b/R/StreamHandler.R index 5754342b..84aeaee1 100644 --- a/R/StreamHandler.R +++ b/R/StreamHandler.R @@ -64,7 +64,6 @@ StreamHandler <- R6::R6Class( content = self$current_value ) } - ), private = list( # Translates a streamed element and converts it to chunk. @@ -161,4 +160,3 @@ stream_chat_completion <- # stream_chat_completion(messages = "Count from 1 to 10") # stream_chat_completion(messages = "Count from 1 to 10", element_callback = stream_handler$handle_streamed_element) - diff --git a/R/addin_chatgpt.R b/R/addin_chatgpt.R index da711585..85848564 100644 --- a/R/addin_chatgpt.R +++ b/R/addin_chatgpt.R @@ -50,9 +50,11 @@ random_port <- function() { #' @return This function returns nothing because is meant to run an app as a #' side effect. run_app_as_bg_job <- function(appDir = ".", job_name, host, port) { - job_script <- create_tmp_job_script(appDir = appDir, - port = port, - host = host) + job_script <- create_tmp_job_script( + appDir = appDir, + port = port, + host = host + ) rstudioapi::jobRunScript(job_script, name = job_name) cli::cli_alert_success( paste0("'", job_name, "'", " initialized as background job in RStudio") diff --git a/R/gpt_queries.R b/R/gpt_queries.R index 031b4530..21d3662d 100644 --- a/R/gpt_queries.R +++ b/R/gpt_queries.R @@ -271,7 +271,8 @@ chat_create_system_prompt <- function(style, skill, in_source) { arg_match(style, c("tidyverse", "base", "no preference")) arg_match(skill, c("beginner", "intermediate", "advanced", "genius")) assert_that(is.logical(in_source), - msg = "chat system prompt creation needs logical `in_source`") + msg = "chat system prompt creation needs logical `in_source`" + ) # nolint start intro <- "You are a helpful chat bot that answers questions for an R programmer working in the RStudio IDE." @@ -291,7 +292,7 @@ chat_create_system_prompt <- function(style, skill, in_source) { } else { "" } - #nolint end + # nolint end glue("{intro} {about_skill} {about_style} {in_source_intructions}") } diff --git a/R/mod_chat.R b/R/mod_chat.R index 161928f0..a796a809 100644 --- a/R/mod_chat.R +++ b/R/mod_chat.R @@ -7,7 +7,6 @@ mod_chat_ui <- function(id) { bslib::card( class = "h-100", - bslib::card_body( class = "py-2 h-100", div( @@ -34,7 +33,6 @@ mod_chat_ui <- function(id) { #' mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { moduleServer(id, function(input, output, session) { - rv <- reactiveValues() rv$stream_ended <- 0L @@ -65,7 +63,6 @@ mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { shiny::observe({ - # waiter::waiter_show( # html = shiny::tagList(waiter::spin_flower(), # shiny::h3("Asking ChatGPT...")), @@ -91,7 +88,7 @@ mod_chat_server <- function(id, ide_colors = get_ide_theme_info()) { content = stream_handler$current_value ) - rv$stream_ended <- rv$stream_ended + 1L + rv$stream_ended <- rv$stream_ended + 1L # showNotification("test", session = session) @@ -150,12 +147,14 @@ style_chat_message <- function(message, ide_colors = get_ide_theme_info()) { icon_name <- switch(message$role, "user" = "fas fa-user", - "assistant" = "fas fa-robot") + "assistant" = "fas fa-robot" + ) # nolint start position_class <- switch(message$role, "user" = "justify-content-end", - "assistant" = "justify-content-start") + "assistant" = "justify-content-start" + ) # nolint end htmltools::div( diff --git a/R/mod_prompt.R b/R/mod_prompt.R index 536ff2d5..c480ac4c 100644 --- a/R/mod_prompt.R +++ b/R/mod_prompt.R @@ -65,7 +65,6 @@ mod_prompt_ui <- function(id) { #' @return A shiny server mod_prompt_server <- function(id) { moduleServer(id, function(input, output, session) { - rv <- reactiveValues() rv$chat_history <- list() rv$clear_history <- 0L @@ -170,9 +169,7 @@ text_area_input_wrapper <- #' @return list of chat messages #' chat_history_append <- function(history, role, content) { - c(history, list( list(role = role, content = content) )) } - diff --git a/R/openai_api_calls.R b/R/openai_api_calls.R index 6db98cda..592ca784 100644 --- a/R/openai_api_calls.R +++ b/R/openai_api_calls.R @@ -175,7 +175,6 @@ openai_create_chat_completion <- #' @return The response from the API. #' query_openai_api <- function(task, request_body, openai_api_key = Sys.getenv("OPENAI_API_KEY")) { - response <- request_base(task, token = openai_api_key) |> httr2::req_body_json(data = request_body) |> httr2::req_retry(max_tries = 3) |> @@ -215,7 +214,6 @@ value_between <- function(x, lower, upper) { #' @examples #' get_available_endpoints() get_available_models <- function() { - check_api() request_base("models") |> @@ -235,8 +233,7 @@ get_available_models <- function() { #' @keywords openai, api, authentication #' @return An httr2 request object request_base <- function(task, token = Sys.getenv("OPENAI_API_KEY")) { - - if (! task %in% get_available_endpoints()) { + if (!task %in% get_available_endpoints()) { cli::cli_abort(message = c( "{.var task} must be a supported endpoint", "i" = "Run {.run gptstudio::get_available_endpoints()} to get a list of supported endpoints" diff --git a/R/streamingMessage.R b/R/streamingMessage.R index e2f5feeb..086f9867 100644 --- a/R/streamingMessage.R +++ b/R/streamingMessage.R @@ -8,7 +8,6 @@ #' @inheritParams streamingMessage-shiny #' @param elementId The element's id streamingMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, height = NULL, elementId = NULL) { - message <- list( list(role = "user", content = ""), list(role = "assistant", content = "") @@ -18,17 +17,17 @@ streamingMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, he # forward options using x - x = list( - message = htmltools::tags$div(message) %>% as.character() + x <- list( + message = htmltools::tags$div(message) %>% as.character() ) # create widget htmlwidgets::createWidget( - name = 'streamingMessage', + name = "streamingMessage", x, width = width, height = height, - package = 'gptstudio', + package = "gptstudio", elementId = elementId ) } @@ -49,12 +48,14 @@ streamingMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, he #' #' @name streamingMessage-shiny #' -streamingMessageOutput <- function(outputId, width = '100%', height = NULL){ - htmlwidgets::shinyWidgetOutput(outputId, 'streamingMessage', width, height, package = 'gptstudio') +streamingMessageOutput <- function(outputId, width = "100%", height = NULL) { + htmlwidgets::shinyWidgetOutput(outputId, "streamingMessage", width, height, package = "gptstudio") } #' @rdname streamingMessage-shiny renderStreamingMessage <- function(expr, env = parent.frame(), quoted = FALSE) { - if (!quoted) { expr <- substitute(expr) } # force quoted + if (!quoted) { + expr <- substitute(expr) + } # force quoted htmlwidgets::shinyRenderWidget(expr, streamingMessageOutput, env, quoted = TRUE) } diff --git a/R/welcomeMessage.R b/R/welcomeMessage.R index c4684271..8064ede3 100644 --- a/R/welcomeMessage.R +++ b/R/welcomeMessage.R @@ -8,21 +8,20 @@ #' @inheritParams welcomeMessage-shiny #' @param elementId The element's id welcomeMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, height = NULL, elementId = NULL) { - default_message <- chat_message_default() # forward options using x - x = list( + x <- list( message = style_chat_message(default_message, ide_colors = ide_colors) %>% as.character() ) # create widget htmlwidgets::createWidget( - name = 'welcomeMessage', + name = "welcomeMessage", x, width = width, height = height, - package = 'gptstudio', + package = "gptstudio", elementId = elementId ) } @@ -43,13 +42,15 @@ welcomeMessage <- function(ide_colors = get_ide_theme_info(), width = NULL, heig #' #' @name welcomeMessage-shiny #' -welcomeMessageOutput <- function(outputId, width = '100%', height = NULL){ - htmlwidgets::shinyWidgetOutput(outputId, 'welcomeMessage', width, height, package = 'gptstudio') +welcomeMessageOutput <- function(outputId, width = "100%", height = NULL) { + htmlwidgets::shinyWidgetOutput(outputId, "welcomeMessage", width, height, package = "gptstudio") } #' @rdname welcomeMessage-shiny renderWelcomeMessage <- function(expr, env = parent.frame(), quoted = FALSE) { - if (!quoted) { expr <- substitute(expr) } # force quoted + if (!quoted) { + expr <- substitute(expr) + } # force quoted htmlwidgets::shinyRenderWidget(expr, welcomeMessageOutput, env, quoted = TRUE) } diff --git a/tests/testthat/test-mod_chat.R b/tests/testthat/test-mod_chat.R index 8b1ebc1e..97d18b84 100644 --- a/tests/testthat/test-mod_chat.R +++ b/tests/testthat/test-mod_chat.R @@ -47,8 +47,10 @@ test_that("style_chat_message() returns HTML element", { }) test_that("style_chat_message() fails when role is not permitted", { - expect_error(style_chat_message(list(role = "system", - message = "some message"))) + expect_error(style_chat_message(list( + role = "system", + message = "some message" + ))) }) test_that("style_chat_history() returns expected output", { diff --git a/tests/testthat/test-mod_prompt.R b/tests/testthat/test-mod_prompt.R index 8c5a3ec7..030101d4 100644 --- a/tests/testthat/test-mod_prompt.R +++ b/tests/testthat/test-mod_prompt.R @@ -20,5 +20,4 @@ test_that("chat_history_append() respects expected structure", { chat_history_append(example_history, "assistant", "assistant content") |> expect_equal(expected_value) - }) diff --git a/tests/testthat/test-shiny_apps.R b/tests/testthat/test-shiny_apps.R index 7f8ce5b7..e34903f3 100644 --- a/tests/testthat/test-shiny_apps.R +++ b/tests/testthat/test-shiny_apps.R @@ -13,9 +13,11 @@ test_that("style_chat_history function returns expected output", { # Define expected output expected_output <- list( style_chat_message( - list(role = "user", content = "Hi there!")), + list(role = "user", content = "Hi there!") + ), style_chat_message( - list(role = "assistant", content = "How can I help you today?")) + list(role = "assistant", content = "How can I help you today?") + ) ) # Test that the function returns the expected output