diff --git a/.Rbuildignore b/.Rbuildignore index 26544728..dc20627b 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -17,3 +17,4 @@ dev/ ^vignette/chat\.Rmd$ ^vignette/ollama\.Rmd$ ^vignette/chat-in-source\.Rmd$ +.lintr diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..c3a09d8d --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,34 @@ +# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples +# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +name: lint + +permissions: read-all + +jobs: + lint: + runs-on: ubuntu-latest + env: + GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + + - uses: r-lib/actions/setup-r@v2 + with: + use-public-rspm: true + + - uses: r-lib/actions/setup-r-dependencies@v2 + with: + extra-packages: any::lintr, local::. + needs: lint + + - name: Lint + run: lintr::lint_package() + shell: Rscript {0} + env: + LINTR_ERROR_ON_LINT: true diff --git a/.github/workflows/lintr.yml b/.github/workflows/lintr.yml deleted file mode 100644 index 421cbb34..00000000 --- a/.github/workflows/lintr.yml +++ /dev/null @@ -1,55 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. -# lintr provides static code analysis for R. -# It checks for adherence to a given style, -# identifying syntax errors and possible semantic issues, -# then reports them to you so you can take action. -# More details at https://lintr.r-lib.org/ - -name: lintr - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '20 10 * * 6' - -permissions: - contents: read - -jobs: - lintr: - name: Run lintr scanning - runs-on: ubuntu-latest - permissions: - contents: read # for checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Setup R - uses: r-lib/actions/setup-r@4e1feaf90520ec1215d1882fdddfe3411c08e492 - - - name: Setup lintr - uses: r-lib/actions/setup-r-dependencies@4e1feaf90520ec1215d1882fdddfe3411c08e492 - with: - extra-packages: lintr - - - name: Run lintr - run: lintr::sarif_output(lintr::lint_dir("."), "lintr-results.sarif") - shell: Rscript {0} - continue-on-error: true - - - name: Upload analysis results to GitHub - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: lintr-results.sarif - wait-for-processing: true diff --git a/.lintr b/.lintr new file mode 100644 index 00000000..2b6eb56e --- /dev/null +++ b/.lintr @@ -0,0 +1,5 @@ +linters: + linters_with_defaults( + line_length_linter = line_length_linter(100), + object_length_linter = NULL + ) diff --git a/NEWS.md b/NEWS.md index f8358b39..66b68fed 100644 --- a/NEWS.md +++ b/NEWS.md @@ -23,6 +23,7 @@ Cohere is now available as another service. The current version includes the fol ### Internal - Reverted back to use an R6 class for OpenAI streaming (which now inherits from `SSEparser::SSEparser`). This doesn't affect how the users interact with the addins, but avoids a wider range of server errors. +- We now make heavy use of `{lintr}` for keeping code consistency. - Fixed a bug in retrieval of OpenAI models - Fixed a bug in Azure OpenAI request formation. - Fixed a bug in "in source" calls for addins. diff --git a/R/addin_chatgpt.R b/R/addin_chatgpt.R index 20c5fcfc..f98bdfd6 100644 --- a/R/addin_chatgpt.R +++ b/R/addin_chatgpt.R @@ -47,7 +47,7 @@ random_port <- function() { #' @inheritParams shiny::runApp #' @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) { +run_app_as_bg_job <- function(appDir = ".", job_name, host, port) { # nolint job_script <- create_tmp_job_script( appDir = appDir, port = port, @@ -66,7 +66,7 @@ run_app_as_bg_job <- function(appDir = ".", job_name, host, port) { #' application from the specified directory with the specified port and host. #' @inheritParams shiny::runApp #' @return A string containing the path of a temporary job script -create_tmp_job_script <- function(appDir, port, host) { +create_tmp_job_script <- function(appDir, port, host) { # nolint script_file <- tempfile(fileext = ".R") line <- diff --git a/R/api_perform_request.R b/R/api_perform_request.R index 5f2e7db3..af0204f4 100644 --- a/R/api_perform_request.R +++ b/R/api_perform_request.R @@ -22,7 +22,8 @@ gptstudio_request_perform <- function(skeleton, ...) { } #' @export -gptstudio_request_perform.gptstudio_request_openai <- function(skeleton, shinySession = NULL, ...) { +gptstudio_request_perform.gptstudio_request_openai <- function(skeleton, ..., + shiny_session = NULL) { # Translate request skeleton$history <- chat_history_append( @@ -53,26 +54,13 @@ gptstudio_request_perform.gptstudio_request_openai <- function(skeleton, shinySe response <- NULL if (isTRUE(skeleton$stream)) { - if (is.null(shinySession)) stop("Stream requires a shiny session object") + if (is.null(shiny_session)) stop("Stream requires a shiny session object") stream_handler <- OpenaiStreamParser$new( - session = shinySession, + session = shiny_session, user_prompt = skeleton$prompt ) - # This should work exactly the same as stream_chat_completion - # but it uses curl::curl_connection(partial=FALSE), which makes it - # somehow different. `partial` has no documentation and can't be be changed - - # request %>% - # req_perform_stream( - # buffer_kb = 32, - # callback = function(x) { - # rawToChar(x) %>% stream_handler$handle_streamed_element() - # TRUE - # } - # ) - stream_chat_completion( messages = skeleton$history, element_callback = stream_handler$parse_sse, @@ -185,7 +173,8 @@ gptstudio_request_perform.gptstudio_request_azure_openai <- function(skeleton, . } #' @export -gptstudio_request_perform.gptstudio_request_ollama <- function(skeleton, shinySession = NULL, ...) { +gptstudio_request_perform.gptstudio_request_ollama <- function(skeleton, ..., + shiny_session = NULL) { # Translate request skeleton$history <- chat_history_append( @@ -203,7 +192,7 @@ gptstudio_request_perform.gptstudio_request_ollama <- function(skeleton, shinySe model = skeleton$model, messages = skeleton$history, stream = skeleton$stream, - shinySession = shinySession, + shiny_session = shiny_session, user_prompt = skeleton$prompt ) diff --git a/R/api_skeletons.R b/R/api_skeletons.R index ce4945a2..25249f5a 100644 --- a/R/api_skeletons.R +++ b/R/api_skeletons.R @@ -192,12 +192,8 @@ new_gptstudio_request_skeleton_perplexity <- function( } # Cohere Skeleton Creation Function -new_gptstudio_request_skeleton_cohere <- function( - model = "command", - prompt = "What is R?", - history = NULL, - stream = FALSE # forcing false until streaming implemented for cohere - ) { +new_gptstudio_request_skeleton_cohere <- function(model = "command", prompt = "What is R?", + history = NULL, stream = FALSE) { new_gpstudio_request_skeleton( url = "https://api.cohere.ai/v1/chat", api_key = Sys.getenv("COHERE_API_KEY"), diff --git a/R/app_chat_style.R b/R/app_chat_style.R index 3c467980..c868645d 100644 --- a/R/app_chat_style.R +++ b/R/app_chat_style.R @@ -132,7 +132,7 @@ render_docs_message_content <- function(x) { #' #' @return A modified textAreaInput text_area_input_wrapper <- - function(inputId, + function(inputId, # nolint label, value = "", width = NULL, diff --git a/R/app_config.R b/R/app_config.R index cf7110cc..48d3d992 100644 --- a/R/app_config.R +++ b/R/app_config.R @@ -30,8 +30,6 @@ save_user_config <- function(code_style, } set_user_options <- function(config) { - op <- options() - op_gptstudio <- list( gptstudio.code_style = config$code_style, gptstudio.skill = config$skill, diff --git a/R/create_prompt.R b/R/create_prompt.R index ab7a4ae8..ead8b9ff 100644 --- a/R/create_prompt.R +++ b/R/create_prompt.R @@ -87,7 +87,7 @@ chat_create_system_prompt <- } else { "" } - + # nolint start about_style <- if (!is.null(style)) { switch(style, "no preference" = "", @@ -97,6 +97,7 @@ chat_create_system_prompt <- } else { "" } + # nolint end in_source_instructions <- if (in_source) { diff --git a/R/gptstudio-package.R b/R/gptstudio-package.R index 87a8ee9b..08dfeaed 100644 --- a/R/gptstudio-package.R +++ b/R/gptstudio-package.R @@ -9,3 +9,7 @@ #' @importFrom glue glue ## gptstudio namespace: end NULL + +dummy <- function() { + SSEparser::SSEparser +} diff --git a/R/gptstudio-sitrep.R b/R/gptstudio-sitrep.R index f7a2f425..d2d40cb4 100644 --- a/R/gptstudio-sitrep.R +++ b/R/gptstudio-sitrep.R @@ -1,6 +1,7 @@ #' Check API Connection #' -#' This generic function checks the API connection for a specified service by dispatching to related methods. +#' This generic function checks the API connection for a specified service +#' by dispatching to related methods. #' #' @param service The name of the API service for which the connection is being checked. #' @param api_key The API key used for authentication. @@ -148,10 +149,12 @@ gptstudio_sitrep <- function(verbose = TRUE) { if (file.exists(user_config)) { cli::cli_inform("Using user configuration file at {.file {user_config}}") } else { - cli::cli_text("No user configuration file found at {.file {user_config}}. - Using default configuration. - Change configuration settings in the chat app. - Lauch the chat app with addins or {.run [gptstudio_chat()](gptstudio::gptstudio_chat())}.") + cli::cli_text( + "No user configuration file found at {.file {user_config}}. + Using default configuration. + Change configuration settings in the chat app. + Lauch the chat app with addins or {.run [gptstudio_chat()](gptstudio::gptstudio_chat())}." + ) } cli::cli_h2("Current Settings") cli::cli_bullets(c( @@ -204,9 +207,9 @@ gptstudio_sitrep <- function(verbose = TRUE) { cli::cli_h3("Check Ollama for Local API connection") ollama_is_available(verbose = TRUE) cli::cli_h2("Getting help") - cli::cli_inform("See the {.href [gptstudio homepage](https://michelnivard.github.io/gptstudio/)} for getting started guides and package documentation. File an issue or contribute to the package at the {.href [GitHub repo](https://github.com/MichelNivard/gptstudio)}.") + cli::cli_inform("See the {.href [gptstudio homepage](https://michelnivard.github.io/gptstudio/)} for getting started guides and package documentation. File an issue or contribute to the package at the {.href [GitHub repo](https://github.com/MichelNivard/gptstudio)}.") # nolint } else { - cli::cli_text("Run {.run [gptstudio_sitrep(verbose = TRUE)](gptstudio::gptstudio_sitrep(verbose = TRUE))} to check API connections.") + cli::cli_text("Run {.run [gptstudio_sitrep(verbose = TRUE)](gptstudio::gptstudio_sitrep(verbose = TRUE))} to check API connections.") # nolint } cli::cli_rule(left = "End of gptstudio configuration") } diff --git a/R/mod_app.R b/R/mod_app.R index 5535ea78..e6344b2e 100644 --- a/R/mod_app.R +++ b/R/mod_app.R @@ -145,11 +145,13 @@ html_dependencies <- function() { #' #' @return A Translator from `shiny.i18n::Translator` create_translator <- function(language = getOption("gptstudio.language")) { - translator <- shiny.i18n::Translator$new(translation_json_path = system.file("translations/translation.json", package = "gptstudio")) + translator <- shiny.i18n::Translator$new( + translation_json_path = system.file("translations/translation.json", package = "gptstudio") + ) supported_languages <- translator$get_languages() if (!language %in% supported_languages) { - cli::cli_abort("Language {.val {language}} is not supported. Must be one of {.val {supported_languages}}") + cli::cli_abort("Language {.val {language}} is not supported. Must be one of {.val {supported_languages}}") # nolint } translator$set_translation_language(language) diff --git a/R/mod_chat.R b/R/mod_chat.R index 2572bfff..19f8c22b 100644 --- a/R/mod_chat.R +++ b/R/mod_chat.R @@ -17,7 +17,6 @@ mod_chat_ui <- function(id, translator = create_translator()) { welcomeMessageOutput(ns("welcome")), uiOutput(ns("history")), streamingMessageOutput(ns("streaming")), - # uiOutput(ns("streaming")) ), div( class = "mt-auto", @@ -73,8 +72,6 @@ mod_chat_server <- function(id, moduleServer(id, function(input, output, session) { # Session data ---- - ns <- session$ns - rv <- reactiveValues() rv$reset_welcome_message <- 0L rv$reset_streaming_message <- 0L @@ -127,7 +124,7 @@ mod_chat_server <- function(id, response <- gptstudio_request_perform( skeleton = skeleton, - shinySession = session + shiny_session = session ) %>% gptstudio_response_process() diff --git a/R/mod_history.R b/R/mod_history.R index ee08ce41..ffd33c7d 100644 --- a/R/mod_history.R +++ b/R/mod_history.R @@ -1,6 +1,5 @@ mod_history_ui <- function(id) { ns <- NS(id) - conversation_history <- read_conversation_history() btn_new_chat <- actionButton( inputId = ns("new_chat"), @@ -96,7 +95,12 @@ mod_history_server <- function(id, settings) { file.remove(conversation_history_file) removeModal(session) - showNotification("Deleted all conversations", type = "warning", duration = 3, session = session) + showNotification( + ui = "Deleted all conversations", + type = "warning", + duration = 3, + session = session + ) rv$reload_conversation_history <- rv$reload_conversation_history + 1L }) %>% bindEvent(input$confirm_delete_all) diff --git a/R/mod_settings.R b/R/mod_settings.R index 121b4bc3..8d27b276 100644 --- a/R/mod_settings.R +++ b/R/mod_settings.R @@ -38,7 +38,6 @@ mod_settings_ui <- function(id, translator = create_translator()) { selectInput( inputId = ns("skill"), label = "Programming Skill", # TODO: update translator - # label = translator$t("Programming Skill"), choices = c("beginner", "intermediate", "advanced", "genius"), selected = getOption("gptstudio.skill"), width = "100%" @@ -132,10 +131,6 @@ mod_settings_server <- function(id) { rv$modify_session_settings <- 0L rv$create_new_chat <- 0L - api_services <- utils::methods("gptstudio_request_perform") %>% - stringr::str_remove(pattern = "gptstudio_request_perform.gptstudio_request_") %>% - purrr::discard(~ .x == "gptstudio_request_perform.default") - observe({ msg <- glue::glue("Fetching models for {input$service} service...") showNotification(ui = msg, type = "message", duration = 3, session = session) @@ -170,7 +165,12 @@ mod_settings_server <- function(id) { selected = if (default_model %in% models) default_model else models[1] ) } else { - showNotification(ui = "No models available", duration = 3, type = "error", session = session) + showNotification( + ui = "No models available", + duration = 3, + type = "error", + session = session + ) cli::cli_alert_danger("No models available") updateSelectInput( diff --git a/R/read_docs.R b/R/read_docs.R index 48e797fe..ee0bfb6c 100644 --- a/R/read_docs.R +++ b/R/read_docs.R @@ -30,15 +30,6 @@ read_html_docs <- function(pkg_ref, topic_name) { get_help_file_path() %>% lazyLoad(envir = env) - ################# - # This is an alternative way to read the help but - # requires writing to disk first - - # tmp <- tempfile(fileext = ".html") - # tools::Rd2HTML(Rd = env[[topic_name]], out = tmp) - # rvest::read_html(tmp) - ################## - env[[topic_name]] %>% tools::Rd2HTML() %>% utils::capture.output() %>% @@ -111,7 +102,7 @@ docs_get_sections <- function(children) { }) section_ranges %>% - purrr::map(~ inner_texts[.x$begin:.x$end] %>% paste0(collapse = "\n\n")) %>% + purrr::map(~ inner_texts[.x$begin:.x$end] %>% paste0(collapse = "\n\n")) %>% # nolint purrr::set_names(inner_texts[h3_locations]) } @@ -139,7 +130,7 @@ docs_to_message <- function(x) { }) %>% paste0(collapse = "\n\n") - glue::glue("gptstudio-metadata-docs-start-{x$pkg_ref}-{x$topic}-gptstudio-metadata-docs-end{inner_content}") + glue::glue("gptstudio-metadata-docs-start-{x$pkg_ref}-{x$topic}-gptstudio-metadata-docs-end{inner_content}") # nolint } add_docs_messages_to_history <- function(skeleton_history) { diff --git a/R/service-anthropic.R b/R/service-anthropic.R index 3d62ab0e..979b3c81 100644 --- a/R/service-anthropic.R +++ b/R/service-anthropic.R @@ -34,8 +34,8 @@ query_api_anthropic <- function(request_body, # error handling if (resp_is_error(response)) { - status <- resp_status(response) - description <- resp_status_desc(response) + status <- resp_status(response) # nolint + description <- resp_status_desc(response) # nolint cli::cli_abort(message = c( "x" = "Anthropic API request failed. Error {status} - {description}", diff --git a/R/service-cohere.R b/R/service-cohere.R index 3afa12a3..e84eec39 100644 --- a/R/service-cohere.R +++ b/R/service-cohere.R @@ -21,8 +21,9 @@ request_base_cohere <- function(api_key = Sys.getenv("COHERE_API_KEY")) { #' Send a request to the Cohere Chat API and return the response #' -#' This function sends a JSON post request to the Cohere Chat API, retries on failure up to three times, and -#' returns the response. The function handles errors by providing a descriptive message and failing gracefully. +#' This function sends a JSON post request to the Cohere Chat API, +#' retries on failure up to three times, and returns the response. +#' The function handles errors by providing a descriptive message and failing gracefully. #' #' @param request_body A list containing the body of the POST request. #' @param api_key String containing a Cohere API key. Defaults to the @@ -54,13 +55,15 @@ query_api_cohere <- function(request_body, api_key = Sys.getenv("COHERE_API_KEY" #' Create a chat with the Cohere Chat API #' -#' This function submits a user message to the Cohere Chat API, potentially along with other parameters such as -#' chat history or connectors, and returns the API's response. +#' This function submits a user message to the Cohere Chat API, +#' potentially along with other parameters such as chat history or connectors, +#' and returns the API's response. #' #' @param prompt A string containing the user message. #' @param chat_history A list of previous messages for context, if any. #' @param connectors A list of connector objects, if any. -#' @param model A string representing the Cohere model to be used, defaulting to "command". Other options include "command-light", "command-nightly", and "command-light-nightly". +#' @param model A string representing the Cohere model to be used, defaulting to "command". +#' Other options include "command-light", "command-nightly", and "command-light-nightly". #' @param api_key The API key for accessing the Cohere API, defaults to the #' COHERE_API_KEY environment variable. #' diff --git a/R/service-google.R b/R/service-google.R index 0a479ddd..50f100e7 100644 --- a/R/service-google.R +++ b/R/service-google.R @@ -8,7 +8,11 @@ #' GOOGLE_API_KEY environmental variable if not specified. #' @return An httr2 request object request_base_google <- function(model, key = Sys.getenv("GOOGLE_API_KEY")) { - request(glue::glue("https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent")) %>% + url <- glue::glue( + "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent" + ) + + request(url) %>% req_url_query(key = key) } @@ -34,8 +38,8 @@ query_api_google <- function(model, # error handling if (resp_is_error(response)) { - status <- resp_status(response) - description <- resp_status_desc(response) + status <- resp_status(response) # nolint + description <- resp_status_desc(response) # nolint cli::cli_abort(message = c( "x" = "Google AI Studio API request failed. Error {status} - {description}", @@ -84,7 +88,7 @@ create_completion_google <- function(prompt, response <- query_api_google(model = model, request_body = request_body, key = key) - # Assuming the response structure aligns with the API documentation example, parsing it accordingly. + # Assuming the response structure follows the API documentation example, parsing it accordingly. # Please adjust if the actual API response has a different structure. purrr::map_chr(response$candidates, ~ .x$content$parts[[1]]$text) } @@ -98,8 +102,8 @@ get_available_models_google <- function(key = Sys.getenv("GOOGLE_API_KEY")) { # error handling if (resp_is_error(response)) { - status <- resp_status(response) - description <- resp_status_desc(response) + status <- resp_status(response) # nolint + description <- resp_status_desc(response) # nolint cli::cli_abort(message = c( "x" = "Google AI Studio API request failed. Error {status} - {description}", diff --git a/R/service-huggingface.R b/R/service-huggingface.R index 615b36ee..27e0d319 100644 --- a/R/service-huggingface.R +++ b/R/service-huggingface.R @@ -35,8 +35,8 @@ query_api_huggingface <- function(task, # error handling if (resp_is_error(response)) { - status <- resp_status(response) - description <- resp_status_desc(response) + status <- resp_status(response) # nolint + description <- resp_status_desc(response) # nolint cli::cli_abort(message = c( "x" = "HuggingFace API request failed. Error {status} - {description}", diff --git a/R/service-ollama.R b/R/service-ollama.R index 09e6213c..c4e3f885 100644 --- a/R/service-ollama.R +++ b/R/service-ollama.R @@ -31,11 +31,11 @@ ollama_is_available <- function(verbose = FALSE) { }, error = function(cnd) { if (inherits(cnd, "httr2_failure")) { - if (verbose) cli::cli_alert_danger("Couldn't connect to Ollama in {.url {ollama_api_url()}}. Is it running there?") + if (verbose) cli::cli_alert_danger("Couldn't connect to Ollama in {.url {ollama_api_url()}}. Is it running there?") # nolint } else { if (verbose) cli::cli_alert_danger(cnd) } - check_value <- FALSE + check_value <- FALSE # nolint } ) @@ -43,8 +43,8 @@ ollama_is_available <- function(verbose = FALSE) { } body_to_json_str <- function(x) { - toJSON_params <- rlang::list2(x = x$data, !!!x$params) - do.call(jsonlite::toJSON, toJSON_params) + to_json_params <- rlang::list2(x = x$data, !!!x$params) + do.call(jsonlite::toJSON, to_json_params) } @@ -71,7 +71,7 @@ ollama_perform_stream <- function(request, parser) { ) } -ollama_chat <- function(model, messages, stream = TRUE, shinySession = NULL, user_prompt = NULL) { +ollama_chat <- function(model, messages, stream = TRUE, shiny_session = NULL, user_prompt = NULL) { body <- list( model = model, messages = messages, @@ -84,7 +84,7 @@ ollama_chat <- function(model, messages, stream = TRUE, shinySession = NULL, use if (stream) { parser <- OllamaStreamParser$new( - session = shinySession, + session = shiny_session, user_prompt = user_prompt ) @@ -100,12 +100,6 @@ ollama_chat <- function(model, messages, stream = TRUE, shinySession = NULL, use content = parser$value ) - # response_json( - # url = request$url, - # method = "POST", - # body = last_line - # ) - last_line } else { request %>% @@ -114,7 +108,7 @@ ollama_chat <- function(model, messages, stream = TRUE, shinySession = NULL, use } } -OllamaStreamParser <- R6::R6Class( +OllamaStreamParser <- R6::R6Class( # nolint classname = "OllamaStreamParser", portable = TRUE, public = list( @@ -139,7 +133,7 @@ OllamaStreamParser <- R6::R6Class( invisible(self) }, - parse_ndjson = function(ndjson, pagesize = 500, verbose = FALSE, simplifyDataFrame = FALSE) { + parse_ndjson = function(ndjson, pagesize = 500, verbose = FALSE, simplifyDataFrame = FALSE) { # nolint jsonlite::stream_in( con = textConnection(ndjson), pagesize = pagesize, diff --git a/R/service-openai_api_calls.R b/R/service-openai_api_calls.R index a7054ebd..4ecea827 100644 --- a/R/service-openai_api_calls.R +++ b/R/service-openai_api_calls.R @@ -1,9 +1,12 @@ #' 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. +#' 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 String containing an OpenAI API key. 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. #' @return An httr2 request object request_base <- function(task, token = Sys.getenv("OPENAI_API_KEY")) { if (!task %in% get_available_endpoints()) { @@ -68,7 +71,8 @@ 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. +#' @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. #' @@ -81,14 +85,16 @@ query_openai_api <- function(task, request_body, openai_api_key = Sys.getenv("OP # error handling if (resp_is_error(response)) { - status <- resp_status(response) - description <- resp_status_desc(response) + status <- resp_status(response) # nolint + description <- resp_status_desc(response) # nolint + # nolint start 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)}" )) + # nolint end } response %>% diff --git a/R/service-openai_gpt_queries.R b/R/service-openai_gpt_queries.R index 2c9aeed5..8772ad53 100644 --- a/R/service-openai_gpt_queries.R +++ b/R/service-openai_gpt_queries.R @@ -16,7 +16,7 @@ gptstudio_chat_in_source <- function(task = NULL) { task <- glue::glue( "You are an expert on following instructions without making conversation.", "Do the task specified after the colon,", - "formatting your response to go directly into a .{gptstudio_chat_in_source_file_ext} file without any post processing", + "formatting your response to go directly into a .{gptstudio_chat_in_source_file_ext} file without any post processing", # nolint .sep = " " ) } diff --git a/R/service-openai_streaming.R b/R/service-openai_streaming.R index 756ba05f..278c0b2f 100644 --- a/R/service-openai_streaming.R +++ b/R/service-openai_streaming.R @@ -3,8 +3,10 @@ #' `stream_chat_completion` sends the prepared chat completion request to the #' OpenAI API and retrieves the streamed response. #' -#' @param messages A list of messages in the conversation, including the current user prompt (optional). -#' @param element_callback A callback function to handle each element of the streamed response (optional). +#' @param messages A list of messages in the conversation, +#' including the current user prompt (optional). +#' @param element_callback A callback function to handle each element +#' of the streamed response (optional). #' @param model A character string specifying the model to use for chat completion. #' The default model is "gpt-3.5-turbo". #' @param openai_api_key A character string of the OpenAI API key. @@ -61,17 +63,19 @@ stream_chat_completion <- #' 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). +#' @param user_prompt The prompt for the chat completion. +#' Only to be displayed in an HTML tag containing the prompt. (Optional). #' @param parsed_event An already parsed server-sent event to append to the events field. #' @importFrom R6 R6Class #' @importFrom jsonlite fromJSON -OpenaiStreamParser <- R6::R6Class( +OpenaiStreamParser <- R6::R6Class( # nolint classname = "OpenaiStreamParser", inherit = SSEparser::SSEparser, public = list( #' @field shinySession Holds the `session` provided at initialization shinySession = NULL, - #' @field user_prompt The `user_prompt` provided at initialization after being formatted with markdown. + #' @field user_prompt The `user_prompt` provided at initialization, + #' after being formatted with markdown. user_prompt = NULL, #' @field value The content of the stream. It updates constantly until the stream ends. value = NULL, # this will be our buffer @@ -83,7 +87,8 @@ OpenaiStreamParser <- R6::R6Class( super$initialize() }, - #' @description Overwrites `SSEparser$append_parsed_sse()` to be able to send a custom message to a shiny session, escaping shiny's reactivity. + #' @description Overwrites `SSEparser$append_parsed_sse()` to be able to send a custom message + #' to a shiny session, escaping shiny's reactivity. append_parsed_sse = function(parsed_event) { # ----- here you can do whatever you want with the event data ----- if (is.null(parsed_event$data) || parsed_event$data == "[DONE]") { diff --git a/R/service-perplexity.R b/R/service-perplexity.R index ea90c552..40602abb 100644 --- a/R/service-perplexity.R +++ b/R/service-perplexity.R @@ -21,8 +21,9 @@ request_base_perplexity <- function(api_key = Sys.getenv("PERPLEXITY_API_KEY")) #' Send a request to the Perplexity API and return the response #' -#' This function sends a JSON post request to the Perplexity API, retries on failure up to three times, and -#' returns the response. The function handles errors by providing a descriptive message and failing gracefully. +#' This function sends a JSON post request to the Perplexity API, +#' retries on failure up to three times, and returns the response. +#' The function handles errors by providing a descriptive message and failing gracefully. #' #' @param request_body A list containing the body of the POST request. #' @param api_key String containing a Perplexity API key. Defaults to the @@ -38,8 +39,8 @@ query_api_perplexity <- function(request_body, api_key = Sys.getenv("PERPLEXITY_ # Error handling if (resp_is_error(response)) { - status <- resp_status(response) - description <- resp_status_desc(response) + status <- resp_status(response) # nolint + description <- resp_status_desc(response) # nolint cli::cli_abort(message = c( "x" = "Perplexity API request failed. Error {status} - {description}", @@ -53,8 +54,8 @@ query_api_perplexity <- function(request_body, api_key = Sys.getenv("PERPLEXITY_ #' Create a chat completion request to the Perplexity API #' -#' This function sends a series of messages alongside a chosen model to the Perplexity API to generate a chat -#' completion. It returns the API's generated responses. +#' This function sends a series of messages alongside a chosen model to the Perplexity API +#' to generate a chat completion. It returns the API's generated responses. #' #' @param prompt A list containing prompts to be sent in the chat. #' @param model A character string representing the Perplexity model to be used. @@ -63,7 +64,9 @@ query_api_perplexity <- function(request_body, api_key = Sys.getenv("PERPLEXITY_ #' PERPLEXITY_API_KEY environment variable. #' #' @return The response from the Perplexity API containing the completion for the chat. -create_completion_perplexity <- function(prompt, model = "mistral-7b-instruct", api_key = Sys.getenv("PERPLEXITY_API_KEY")) { +create_completion_perplexity <- function(prompt, + model = "mistral-7b-instruct", + api_key = Sys.getenv("PERPLEXITY_API_KEY")) { request_body <- list( model = model, messages = list(list(role = "user", content = prompt)) diff --git a/R/streamingMessage.R b/R/streamingMessage.R index 847884c7..b0c5724e 100644 --- a/R/streamingMessage.R +++ b/R/streamingMessage.R @@ -6,11 +6,11 @@ #' @import htmlwidgets #' @inheritParams run_chatgpt_app #' @inheritParams streamingMessage-shiny -#' @param elementId The element's id -streamingMessage <- function(ide_colors = get_ide_theme_info(), +#' @param element_id The element's id +streamingMessage <- function(ide_colors = get_ide_theme_info(), # nolint width = NULL, height = NULL, - elementId = NULL) { + element_id = NULL) { message <- list( list(role = "user", content = ""), list(role = "assistant", content = "") @@ -29,7 +29,7 @@ streamingMessage <- function(ide_colors = get_ide_theme_info(), width = width, height = height, package = "gptstudio", - elementId = elementId + elementId = element_id ) } @@ -49,12 +49,12 @@ streamingMessage <- function(ide_colors = get_ide_theme_info(), #' #' @name streamingMessage-shiny #' -streamingMessageOutput <- function(outputId, width = "100%", height = NULL) { +streamingMessageOutput <- function(outputId, width = "100%", height = NULL) { # nolint htmlwidgets::shinyWidgetOutput(outputId, "streamingMessage", width, height, package = "gptstudio") } #' @rdname streamingMessage-shiny -renderStreamingMessage <- function(expr, env = parent.frame(), quoted = FALSE) { +renderStreamingMessage <- function(expr, env = parent.frame(), quoted = FALSE) { # nolint if (!quoted) { expr <- substitute(expr) } # force quoted diff --git a/R/welcomeMessage.R b/R/welcomeMessage.R index 96ae34fe..a9f72b04 100644 --- a/R/welcomeMessage.R +++ b/R/welcomeMessage.R @@ -7,12 +7,12 @@ #' @inheritParams run_chatgpt_app #' @inheritParams welcomeMessage-shiny #' @inheritParams chat_message_default -#' @param elementId The element's id -welcomeMessage <- function(ide_colors = get_ide_theme_info(), +#' @param element_id The element's id +welcomeMessage <- function(ide_colors = get_ide_theme_info(), # nolint translator = create_translator(), width = NULL, height = NULL, - elementId = NULL) { + element_id = NULL) { default_message <- chat_message_default(translator = translator) # forward options using x @@ -27,7 +27,7 @@ welcomeMessage <- function(ide_colors = get_ide_theme_info(), width = width, height = height, package = "gptstudio", - elementId = elementId + elementId = element_id ) } @@ -47,12 +47,12 @@ welcomeMessage <- function(ide_colors = get_ide_theme_info(), #' #' @name welcomeMessage-shiny #' -welcomeMessageOutput <- function(outputId, width = "100%", height = NULL) { +welcomeMessageOutput <- function(outputId, width = "100%", height = NULL) { # nolint htmlwidgets::shinyWidgetOutput(outputId, "welcomeMessage", width, height, package = "gptstudio") } #' @rdname welcomeMessage-shiny -renderWelcomeMessage <- function(expr, env = parent.frame(), quoted = FALSE) { +renderWelcomeMessage <- function(expr, env = parent.frame(), quoted = FALSE) { # nolint if (!quoted) { expr <- substitute(expr) } # force quoted diff --git a/inst/scratchpad/api_helpers.R b/inst/scratchpad/api_helpers.R deleted file mode 100644 index 29ae8ea4..00000000 --- a/inst/scratchpad/api_helpers.R +++ /dev/null @@ -1,65 +0,0 @@ -#' Base for a request to an API service -#' -#' This function sends a request to a specific API \code{task} endpoint at the base URL and authenticates with an API key using a Bearer token. The default is OpenAI's API using a base URL of \code{https://api.openai.com/v1}. -#' -#' @param task character string specifying an OpenAI API endpoint task -#' @param token String containing an OpenAI API key. Defaults to the OPENAI_API_KEY environmental variable if not specified. -#' @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" - )) - } - request(getOption("gptstudio.openai_url")) %>% - req_url_path_append(task) %>% - req_auth_bearer_token(token = token) -} - -#' A function that sends an API request 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. -#' @param token String containing an API key. Defaults to the OPENAI_API_KEY environmental variable if not specified. -#' -#' @return The response from the API. -#' -query_api <- function(task, request_body, token = Sys.getenv("OPENAI_API_KEY")) { - response <- request_base(task, token = token) %>% - req_body_json(data = request_body) %>% - req_retry(max_tries = 3) %>% - req_error(is_error = \(resp) FALSE) - - response %>% req_dry_run() - - response <- req_perform(response) - - # error handling - if (resp_is_error(response)) { - status <- resp_status(response) - description <- resp_status_desc(response) - send_abort_message(service, status, description) - } - - response %>% - resp_body_json() -} - -send_abort_message <- function(service = "openai", - status = NULL, - description = NULL) { - switch(service, - "openai" = 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)}" - )), - "huggingface" = cli::cli_abort( - message = c( - "x" = "HuggingFace API request failed. Error {status} - {description}", - "i" = "Visit the {.href [HuggingFace Inference API documentation](https://huggingface.co/inference-api)} for more details." - )) - ) -} diff --git a/inst/shiny-recorder/app.R b/inst/shiny-recorder/app.R index 53a3c70d..66a8bca7 100644 --- a/inst/shiny-recorder/app.R +++ b/inst/shiny-recorder/app.R @@ -1,3 +1,4 @@ +# nolint start library(shiny) library(shinyjs) library(httr) @@ -163,3 +164,4 @@ server <- function(input, output, session) { } shinyApp(ui, server) +# nolint end diff --git a/man/OpenaiStreamParser.Rd b/man/OpenaiStreamParser.Rd index 905ed478..0965ee84 100644 --- a/man/OpenaiStreamParser.Rd +++ b/man/OpenaiStreamParser.Rd @@ -24,7 +24,8 @@ without recurring to a \code{shiny::observe} inside a module server. \describe{ \item{\code{shinySession}}{Holds the \code{session} provided at initialization} -\item{\code{user_prompt}}{The \code{user_prompt} provided at initialization after being formatted with markdown.} +\item{\code{user_prompt}}{The \code{user_prompt} provided at initialization, +after being formatted with markdown.} \item{\code{value}}{The content of the stream. It updates constantly until the stream ends.} } @@ -59,7 +60,8 @@ Start a StreamHandler. Recommended to be assigned to the \code{stream_handler} n \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).} +\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{}} } @@ -68,7 +70,8 @@ Start a StreamHandler. Recommended to be assigned to the \code{stream_handler} n \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-OpenaiStreamParser-append_parsed_sse}{}}} \subsection{Method \code{append_parsed_sse()}}{ -Overwrites \code{SSEparser$append_parsed_sse()} to be able to send a custom message to a shiny session, escaping shiny's reactivity. +Overwrites \code{SSEparser$append_parsed_sse()} to be able to send a custom message +to a shiny session, escaping shiny's reactivity. \subsection{Usage}{ \if{html}{\out{
}}\preformatted{OpenaiStreamParser$append_parsed_sse(parsed_event)}\if{html}{\out{
}} } diff --git a/man/check_api_connection_openai.Rd b/man/check_api_connection_openai.Rd index 8026c21c..7ca264fd 100644 --- a/man/check_api_connection_openai.Rd +++ b/man/check_api_connection_openai.Rd @@ -15,5 +15,6 @@ check_api_connection_openai(service, api_key) A logical value indicating whether the connection was successful. } \description{ -This generic function checks the API connection for a specified service by dispatching to related methods. +This generic function checks the API connection for a specified service +by dispatching to related methods. } diff --git a/man/create_chat_cohere.Rd b/man/create_chat_cohere.Rd index b460e0b3..34b3b566 100644 --- a/man/create_chat_cohere.Rd +++ b/man/create_chat_cohere.Rd @@ -19,7 +19,8 @@ create_chat_cohere( \item{connectors}{A list of connector objects, if any.} -\item{model}{A string representing the Cohere model to be used, defaulting to "command". Other options include "command-light", "command-nightly", and "command-light-nightly".} +\item{model}{A string representing the Cohere model to be used, defaulting to "command". +Other options include "command-light", "command-nightly", and "command-light-nightly".} \item{api_key}{The API key for accessing the Cohere API, defaults to the COHERE_API_KEY environment variable.} @@ -28,6 +29,7 @@ COHERE_API_KEY environment variable.} The response from the Cohere Chat API containing the model's reply. } \description{ -This function submits a user message to the Cohere Chat API, potentially along with other parameters such as -chat history or connectors, and returns the API's response. +This function submits a user message to the Cohere Chat API, +potentially along with other parameters such as chat history or connectors, +and returns the API's response. } diff --git a/man/create_completion_perplexity.Rd b/man/create_completion_perplexity.Rd index b3d5e769..f4159d41 100644 --- a/man/create_completion_perplexity.Rd +++ b/man/create_completion_perplexity.Rd @@ -23,6 +23,6 @@ PERPLEXITY_API_KEY environment variable.} The response from the Perplexity API containing the completion for the chat. } \description{ -This function sends a series of messages alongside a chosen model to the Perplexity API to generate a chat -completion. It returns the API's generated responses. +This function sends a series of messages alongside a chosen model to the Perplexity API +to generate a chat completion. It returns the API's generated responses. } diff --git a/man/query_api_cohere.Rd b/man/query_api_cohere.Rd index 7419ea7c..76ae4d25 100644 --- a/man/query_api_cohere.Rd +++ b/man/query_api_cohere.Rd @@ -16,6 +16,7 @@ COHERE_API_KEY environmental variable if not specified.} A parsed JSON object as the API response. } \description{ -This function sends a JSON post request to the Cohere Chat API, retries on failure up to three times, and -returns the response. The function handles errors by providing a descriptive message and failing gracefully. +This function sends a JSON post request to the Cohere Chat API, +retries on failure up to three times, and returns the response. +The function handles errors by providing a descriptive message and failing gracefully. } diff --git a/man/query_api_perplexity.Rd b/man/query_api_perplexity.Rd index bea74be9..6dd957c9 100644 --- a/man/query_api_perplexity.Rd +++ b/man/query_api_perplexity.Rd @@ -16,6 +16,7 @@ PERPLEXITY_API_KEY environmental variable if not specified.} A parsed JSON object as the API response. } \description{ -This function sends a JSON post request to the Perplexity API, retries on failure up to three times, and -returns the response. The function handles errors by providing a descriptive message and failing gracefully. +This function sends a JSON post request to the Perplexity API, +retries on failure up to three times, and returns the response. +The function handles errors by providing a descriptive message and failing gracefully. } diff --git a/man/query_openai_api.Rd b/man/query_openai_api.Rd index 9dbd1557..d1e33175 100644 --- a/man/query_openai_api.Rd +++ b/man/query_openai_api.Rd @@ -15,7 +15,8 @@ query_openai_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.} +\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 cb890bdf..739dac25 100644 --- a/man/request_base.Rd +++ b/man/request_base.Rd @@ -9,11 +9,14 @@ request_base(task, token = Sys.getenv("OPENAI_API_KEY")) \arguments{ \item{task}{character string specifying an OpenAI API endpoint task} -\item{token}{String containing an OpenAI API key. 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 } \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. +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. } diff --git a/man/stream_chat_completion.Rd b/man/stream_chat_completion.Rd index f6b5807a..56ac4d49 100644 --- a/man/stream_chat_completion.Rd +++ b/man/stream_chat_completion.Rd @@ -12,9 +12,11 @@ stream_chat_completion( ) } \arguments{ -\item{messages}{A list of messages in the conversation, including the current user prompt (optional).} +\item{messages}{A list of messages in the conversation, +including the current user prompt (optional).} -\item{element_callback}{A callback function to handle each element of the streamed response (optional).} +\item{element_callback}{A callback function to handle each element +of the streamed response (optional).} \item{model}{A character string specifying the model to use for chat completion. The default model is "gpt-3.5-turbo".} diff --git a/man/streamingMessage.Rd b/man/streamingMessage.Rd index 4cb9cd27..6ef6d5c6 100644 --- a/man/streamingMessage.Rd +++ b/man/streamingMessage.Rd @@ -8,7 +8,7 @@ streamingMessage( ide_colors = get_ide_theme_info(), width = NULL, height = NULL, - elementId = NULL + element_id = NULL ) } \arguments{ @@ -18,7 +18,7 @@ streamingMessage( \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} +\item{element_id}{The element's id} } \description{ Places an invisible empty chat message that will hold a streaming message. diff --git a/man/welcomeMessage.Rd b/man/welcomeMessage.Rd index 31356e53..1990fee9 100644 --- a/man/welcomeMessage.Rd +++ b/man/welcomeMessage.Rd @@ -9,7 +9,7 @@ welcomeMessage( translator = create_translator(), width = NULL, height = NULL, - elementId = NULL + element_id = NULL ) } \arguments{ @@ -21,7 +21,7 @@ welcomeMessage( \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} +\item{element_id}{The element's id} } \description{ HTML widget for showing a welcome message in the chat app. diff --git a/tests/testthat/test-addins.R b/tests/testthat/test-addins.R index 813e2211..4e52cf88 100644 --- a/tests/testthat/test-addins.R +++ b/tests/testthat/test-addins.R @@ -11,8 +11,8 @@ test_that("Spelling and grammer editing works", { test_that("Commenting code works", { mockr::local_mock( - gptstudio_chat_in_source = function(task = "Add comments to explain this code. Your output will go directly into - a source (.R) file. Comment the code line by line") { + gptstudio_chat_in_source = function(task = "Add comments to explain this code. + Your output will go directly into a source (.R) file. Comment the code line by line") { list("text" = "new text") } ) diff --git a/vignettes/no-build/chat.Rmd b/vignettes/no-build/chat.Rmd index d821ab2d..16553f8b 100644 --- a/vignettes/no-build/chat.Rmd +++ b/vignettes/no-build/chat.Rmd @@ -13,7 +13,7 @@ knitr::opts_chunk$set( The "Chat" app in `gptstudio` is an interactive Shiny app that can be launched directly from RStudio. It serves as a powerful tool to converse with various AI models, similar to ChatGPT and GitHub Copilot Chat, but integrated within your R environment. ```{r fig.cap = "Chat App from gptstudio", out.width = "80%", fig.align='center', echo = FALSE} -knitr::include_graphics("https://raw.githubusercontent.com/MichelNivard/gptstudio/main/media/gptstudio-chat-app.png") +knitr::include_graphics("https://raw.githubusercontent.com/MichelNivard/gptstudio/main/media/gptstudio-chat-app.png") # nolint ``` **Usage:**