diff --git a/DESCRIPTION b/DESCRIPTION index 54f5bff..2198a07 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: minutemaker Title: GenAI-based meeting and conferences minutes generator -Version: 0.9.0 +Version: 0.10.0 Authors@R: person("Angelo", "D'Ambrosio", , "a.dambrosioMD@gmail.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-2045-5155")) diff --git a/LICENSE b/LICENSE index 8dafa6e..fcd309b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,2 @@ -MIT License - -Copyright (c) 2023 Angelo D'Ambrosio - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +YEAR: 2023 +COPYRIGHT HOLDER: Angelo D'Ambrosio diff --git a/LICENSE.md b/LICENSE.md index c0de061..68c6260 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # MIT License -Copyright (c) 2023 minutemaker authors +Copyright (c) 2023 Angelo D'Ambrosio Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NAMESPACE b/NAMESPACE index 321db63..440d251 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,10 +10,10 @@ export(generate_recording_details) export(get_prompts) export(import_transcript_from_file) export(infer_agenda_from_transcript) -export(interrogate_llm) export(merge_transcripts) export(parse_transcript_json) export(perform_speech_to_text) +export(prompt_llm) export(run_in_terminal) export(set_prompts) export(speech_to_summary_workflow) diff --git a/NEWS.md b/NEWS.md index ae93c42..841c785 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,22 @@ +# minutemaker 0.10.0 + +#### Enhancements +- **Change interrogate_llm to prompt_llm**: Renamed the `interrogate_llm` function to `prompt_llm` to better reflect its purpose of generating prompts for language models (Commit: [2f9eeddd]). +- **Model Logging for LLM Requests**: Added a `log_request` parameter to `use_openai_llm`, `use_azure_llm`, and `use_custom_llm` functions to log the specific endpoint and model being used (Commit: [7e85f2f]). +- **Handling Long LLM Responses**: Improved the handling of LLM responses that exceed the output token limit. Users are now prompted to decide how to proceed, and incomplete answers are saved to a file for reference (Commit: [18cfada]). +- **Model Parameter for Custom LLM APIs**: Added a `model` parameter to LLM calls to allow specifying a model for custom APIs with multiple models (Commit: [cd4227b]). + +#### Fixes +- **Restore Correct Speaker Extraction for Webex VTT**: Fixed the parsing of Webex VTT files which was broken by the implementation of MS Teams VTT parsing (Commit: [d189980]). +- **Remove Newlines from JSON Output**: Fixed an issue where some custom LLMs produced invalid JSON with newlines, causing errors during parsing (Commit: [e9e578a]). +- **Support JSON Mode for Custom LLMs**: Ensured that most custom LLMs now support JSON mode by keeping the JSON mode option in the call (Commit: [f4df24c]). + +#### Documentation +- **Improve Code Comments and Error Handling**: Enhanced code comments and error handling for better clarity and maintenance (Commit: [4b689ff]). + +#### Summary +This pull request introduces several enhancements to the LLM handling, including logging the model being used, better management of long responses, and support for specifying models in custom APIs. It also includes fixes for speaker extraction in Webex VTT files and handling of JSON outputs from custom LLMs. Additionally, code comments and error handling have been improved for better clarity. + # minutemaker 0.9.0 ### Improve agenda review and add custom LLM support diff --git a/R/LLM_calls.R b/R/LLM_calls.R index 576b8a9..8745236 100644 --- a/R/LLM_calls.R +++ b/R/LLM_calls.R @@ -120,7 +120,7 @@ process_messages <- function(messages) { } -#' Interrogate Language Model +#' Interrogate a Language Model #' #' This function sends requests to a specified language model provider (OpenAI, #' Azure, or a locally running LLM server) and returns the response. It handles @@ -149,12 +149,12 @@ process_messages <- function(messages) { #' #' @examples #' \dontrun{ -#' response <- interrogate_llm( +#' response <- prompt_llm( #' messages = c(user = "Hello there!"), #' provider = "openai") #' } #' -interrogate_llm <- function( +prompt_llm <- function( messages = NULL, provider = getOption("minutemaker_llm_provider"), params = list( @@ -183,7 +183,7 @@ interrogate_llm <- function( body$messages <- messages - # Force the LLM to answer in JSON format (only openai and azure) + # Force the LLM to answer in JSON format (not all models support this) if (force_json) { body$response_format <- list("type" = "json_object") } @@ -192,7 +192,8 @@ interrogate_llm <- function( llm_fun <- paste0("use_", provider, "_llm") if (!exists(llm_fun, mode = "function")) { - stop("Unsupported LLM provider.") + stop("Unsupported LLM provider. + You can set it project-wide using the minutemaker_llm_provider option.") } llm_fun <- get(llm_fun) @@ -252,13 +253,62 @@ interrogate_llm <- function( ) |> message() } - answer <- parsed$choices[[1]] - - if (answer$finish_reason == "length") { - stop("Answer exhausted the context window!") - } - - answer$message$content + # Return the response + purrr::imap_chr(parsed$choices, \(ans, i) { + ans_content <- ans$message$content + + # Manage the case when the answer is cut off due to exceeding the + # output token limit + if (ans$finish_reason == "length") { + i <- if (length(parsed$choices) > 1) paste0(" ", i, " ") else " " + + warning("Answer", i, "exhausted the context window!") + + file_name <- paste0("output_", Sys.time(), ".txt") + + warning( + "Answer", i, "exhausted the context window!\n", + "The answer has been saved to a file: ", file_name + ) + + readr::write_lines(ans_content, file_name) + + choice <- utils::menu( + c( + "Try to complete the answer", + "Keep the incomplete answer", + "Stop the process"), + title = "How do you want to proceed?" + ) + + if (choice == 1) { + # Ask the model to continue the answer + messages_new <- c( + messages, + list(list( + role = "assistant", + content = ans_content + )), + list(list( + role = "user", + content = "continue" + )) + ) + + ans_new <- prompt_llm( + messages_new, provider = provider, params = params, + force_json = force_json, + log_request = log_request, ... + ) + + return(paste0(ans_content, ans_new)) + } else if (choice == 2) { + return(ans_content) + } else { + stop("The process has been stopped.") + } + } else ans_content + }) } #' Use OpenAI Language Model @@ -269,13 +319,16 @@ interrogate_llm <- function( #' @param body The body of the request. #' @param model Model identifier for the OpenAI API. Obtained from R options. #' @param api_key API key for the OpenAI service. Obtained from R options. +#' @param log_request A boolean to log the request time. Can be set up globally +#' using the `minutemaker_log_requests` option, which defaults to TRUE. #' #' @return The function returns the response from the OpenAI API. #' use_openai_llm <- function( body, model = getOption("minutemaker_openai_model_gpt"), - api_key = getOption("minutemaker_openai_api_key") + api_key = getOption("minutemaker_openai_api_key"), + log_request = getOption("minutemaker_log_requests", TRUE) ) { if (is.null(api_key) || is.null(model)) { @@ -285,6 +338,10 @@ use_openai_llm <- function( "minutemaker_open_api_key options.") } + if (log_request) { + message("Interrogating OpenAI: ", model, "...") + } + body$model = model # Prepare the request @@ -315,6 +372,8 @@ use_openai_llm <- function( #' options. #' @param api_version API version for the Azure language model service. Obtained #' from R options. +#' @param log_request A boolean to log the request time. Can be set up globally +#' using the `minutemaker_log_requests` option, which defaults to TRUE. #' #' @return The function returns the response from the Azure API. use_azure_llm <- function( @@ -322,7 +381,8 @@ use_azure_llm <- function( deployment_id = getOption("minutemaker_azure_deployment_gpt"), resource_name = getOption("minutemaker_azure_resource_gpt"), api_key = getOption("minutemaker_azure_api_key_gpt"), - api_version = getOption("minutemaker_azure_api_version") + api_version = getOption("minutemaker_azure_api_version"), + log_request = getOption("minutemaker_log_requests", TRUE) ) { if (is.null(resource_name) || is.null(deployment_id) || @@ -337,6 +397,12 @@ use_azure_llm <- function( ) } + if (log_request) { + message( + "Interrogating Azure OpenAI: ", resource_name, "/", deployment_id, + " (", api_version, ")...") + } + # Prepare the request httr::POST( url = paste0( @@ -361,15 +427,21 @@ use_azure_llm <- function( #' @param body The body of the request. #' @param endpoint The local endpoint for the language model service. Can be #' obtained from R options. +#' @param model Model identifier for the custom API, if needed (some API have +#' one model per endpoint, some multiple ones). Obtained from R options. #' @param api_key Optional API key for the custom language model services that #' require it. Obtained from R options. +#' @param log_request A boolean to log the request time. Can be set up globally +#' using the `minutemaker_log_requests` option, which defaults to TRUE. #' #' @return The function returns the response from the local language model #' endpoint. use_custom_llm <- function( body, endpoint = getOption("minutemaker_custom_endpoint_gpt"), - api_key = getOption("minutemaker_custom_api_key") + model = getOption("minutemaker_custom_model_gpt", NULL), + api_key = getOption("minutemaker_custom_api_key"), + log_request = getOption("minutemaker_log_requests", TRUE) ) { if (is.null(endpoint)) { @@ -379,7 +451,13 @@ use_custom_llm <- function( ) } - body$response_format <- NULL + if (log_request) { + message("Interrogating custom LLM: ", endpoint, "/", model, "...") + } + + if (!is.null(model)) { + body$model = model + } # Prepare the request httr::POST( @@ -393,4 +471,3 @@ use_custom_llm <- function( ) } - diff --git a/R/data_management.R b/R/data_management.R index 335f5c6..a5248fc 100644 --- a/R/data_management.R +++ b/R/data_management.R @@ -557,8 +557,9 @@ format_summary_tree <- function( agenda_element$title <- ifelse(is.null( agenda_element$title), "", agenda_element$title) - output_piece <- stringr::str_glue_data(agenda_element, - "Session: {session}; + output_piece <- stringr::str_glue_data( + agenda_element, + "Session: {session}; Title: {title}; Speakers: {speakers}; Moderators: {moderators}; @@ -597,8 +598,8 @@ format_summary_tree <- function( #' #' @export format_agenda <- function( - agenda, - event_start_time = getOption("minutemaker_event_start_time") + agenda, + event_start_time = getOption("minutemaker_event_start_time") ) { # Import agenda from file @@ -624,8 +625,9 @@ format_agenda <- function( .x$title <- ifelse(is.null(.x$title), "", .x$title) .x$description <- ifelse(is.null(.x$description), "", .x$description) - stringr::str_glue_data(.x, - "Session: {session}; + stringr::str_glue_data( + .x, + "Session: {session}; Title: {title}; Description: {description}; Speakers: {speakers}; @@ -697,13 +699,14 @@ import_transcript_from_file <- function( # Normal VTT style if (stringr::str_detect(cur_speaker, "^\\d+ ")) { # the name is between double quotes - stringr::str_extract_all('(?<=").*(?=")') |> - unlist() + cur_speaker <- stringr::str_extract_all( + cur_speaker, '(?<=").*(?=")') |> + unlist() } else if (stringr::str_detect(lines[.x + 1], "^ cur_speaker <- stringr::str_extract_all( lines[.x + 1], '(?<=)') |> - unlist() + unlist() } else { cur_speaker <- NA } @@ -1097,7 +1100,7 @@ add_chat_transcript <- function( #' @param llm_provider A string indicating the LLM provider to use for the #' summarization. See `summarise_transcript` for more details. #' @param extra_summarise_args Additional arguments passed to the -#' `interrogate_llm` function. See `summarise_transcript` for more details. +#' `prompt_llm` function. See `summarise_transcript` for more details. #' @param summarization_window_size The size of the summarization window in #' minutes if the "rolling" method is used. See `summarise_transcript` for #' more details. @@ -1381,8 +1384,8 @@ speech_to_summary_workflow <- function( expected_agenda = expected_agenda, window_size = agenda_generation_window_size, output_file = if (!purrr::is_empty(agenda) && is.character(agenda)) { - file.path(target_dir, basename(agenda)) - } else file.path(target_dir, "agenda.R"), + file.path(target_dir, basename(agenda)) + } else file.path(target_dir, "agenda.R"), provider = llm_provider ), extra_agenda_generation_args) diff --git a/R/summarization.R b/R/summarization.R index 27bfbf9..cd42a1f 100644 --- a/R/summarization.R +++ b/R/summarization.R @@ -111,7 +111,7 @@ generate_recording_details <- function( #' get_prompts("output_rolling_aggregation") prompts depending on the task. #' @param prompt_only If TRUE, only the prompt is returned, the LLM is not #' interrogated. Default is FALSE. -#' @param ... Additional arguments passed to the `interrogate_llm` function, +#' @param ... Additional arguments passed to the `prompt_llm` function, #' such as the LLM provider. #' #' @return A summary of the transcript. @@ -252,7 +252,7 @@ summarise_transcript <- function( } # Interrogate the LLM - interrogate_llm( + prompt_llm( c( system = get_prompts("persona"), user = prompt), @@ -280,7 +280,7 @@ summarise_transcript <- function( args = args ) - interrogate_llm( + prompt_llm( c( system = get_prompts("persona"), user = aggregation_prompt), @@ -328,7 +328,7 @@ summarise_transcript <- function( #' `summarise_transcript` for more details and run `get_prompts()` to see the #' defaults. See `summarise_transcript` for more details. #' @param overwrite Whether to overwrite existing summaries. Default is FALSE. -#' @param ... Additional arguments passed to `interrogate_llm` function, such as +#' @param ... Additional arguments passed to `prompt_llm` function, such as #' the LLM provider. #' #' @return The result tree of the meeting summary. Also saves the results in the @@ -503,7 +503,7 @@ summarise_full_meeting <- function( #' LLM context. #' @param output_file An optional file to save the results to. Default is NULL, #' i.e., the results are not saved to a file. -#' @param ... Additional arguments passed to the `interrogate_llm` function. +#' @param ... Additional arguments passed to the `prompt_llm` function. #' Keep in consideration that this function needs LLMs that manages long #' context and that produce valid JSON outputs. The `force_json` argument is #' used with OpenAI based LLM but it's not accepted by other LLMs; therefore @@ -686,11 +686,11 @@ infer_agenda_from_transcript <- function( } # Attempt to interrogate the LLM - result_json <- try(interrogate_llm( + result_json <- try(prompt_llm( prompt_set, ..., force_json = TRUE - ), silent = TRUE) + ) |> stringr::str_replace_all("\\n", " "), silent = TRUE) # If the interrogation fails due to too long output, retry with a smaller # window @@ -834,7 +834,7 @@ infer_agenda_from_transcript <- function( user = prompt ) - result_json <- interrogate_llm( + result_json <- prompt_llm( prompt_set, ..., force_json = TRUE ) @@ -877,7 +877,7 @@ infer_agenda_from_transcript <- function( #' them. #' @param prompt_only If TRUE, only the prompt is returned, the LLM is not #' interrogated. Default is FALSE. -#' @param ... Additional arguments passed to the `interrogate_llm` function. +#' @param ... Additional arguments passed to the `prompt_llm` function. #' #' @return A vector with the entities found in the text. #' @@ -936,7 +936,7 @@ entity_extractor <- function( return(task) } - interrogate_llm( + prompt_llm( c("system" = get_prompts("persona"), "user" = task), force_json = TRUE, ...) |> jsonlite::fromJSON() |> diff --git a/man/entity_extractor.Rd b/man/entity_extractor.Rd index bc7f51b..07fc1be 100644 --- a/man/entity_extractor.Rd +++ b/man/entity_extractor.Rd @@ -21,7 +21,7 @@ them.} \item{prompt_only}{If TRUE, only the prompt is returned, the LLM is not interrogated. Default is FALSE.} -\item{...}{Additional arguments passed to the \code{interrogate_llm} function.} +\item{...}{Additional arguments passed to the \code{prompt_llm} function.} } \value{ A vector with the entities found in the text. diff --git a/man/infer_agenda_from_transcript.Rd b/man/infer_agenda_from_transcript.Rd index 9c54c77..e7e70a2 100644 --- a/man/infer_agenda_from_transcript.Rd +++ b/man/infer_agenda_from_transcript.Rd @@ -49,7 +49,7 @@ LLM context.} \item{output_file}{An optional file to save the results to. Default is NULL, i.e., the results are not saved to a file.} -\item{...}{Additional arguments passed to the \code{interrogate_llm} function. +\item{...}{Additional arguments passed to the \code{prompt_llm} function. Keep in consideration that this function needs LLMs that manages long context and that produce valid JSON outputs. The \code{force_json} argument is used with OpenAI based LLM but it's not accepted by other LLMs; therefore diff --git a/man/interrogate_llm.Rd b/man/prompt_llm.Rd similarity index 92% rename from man/interrogate_llm.Rd rename to man/prompt_llm.Rd index 15de8e5..471cb35 100644 --- a/man/interrogate_llm.Rd +++ b/man/prompt_llm.Rd @@ -1,10 +1,10 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/LLM_calls.R -\name{interrogate_llm} -\alias{interrogate_llm} -\title{Interrogate Language Model} +\name{prompt_llm} +\alias{prompt_llm} +\title{Interrogate a Language Model} \usage{ -interrogate_llm( +prompt_llm( messages = NULL, provider = getOption("minutemaker_llm_provider"), params = list(temperature = 0), @@ -47,7 +47,7 @@ name pattern: \verb{use__llm}. See the existing functions using the } \examples{ \dontrun{ -response <- interrogate_llm( +response <- prompt_llm( messages = c(user = "Hello there!"), provider = "openai") } diff --git a/man/speech_to_summary_workflow.Rd b/man/speech_to_summary_workflow.Rd index 24b9c8d..2700ffd 100644 --- a/man/speech_to_summary_workflow.Rd +++ b/man/speech_to_summary_workflow.Rd @@ -184,7 +184,7 @@ defaults. See \code{summarise_transcript} for more details.} summarization. See \code{summarise_transcript} for more details.} \item{extra_summarise_args}{Additional arguments passed to the -\code{interrogate_llm} function. See \code{summarise_transcript} for more details.} +\code{prompt_llm} function. See \code{summarise_transcript} for more details.} \item{summarization_window_size}{The size of the summarization window in minutes if the "rolling" method is used. See \code{summarise_transcript} for diff --git a/man/summarise_full_meeting.Rd b/man/summarise_full_meeting.Rd index fd89f6b..e2ce52c 100644 --- a/man/summarise_full_meeting.Rd +++ b/man/summarise_full_meeting.Rd @@ -68,7 +68,7 @@ defaults. See \code{summarise_transcript} for more details.} \item{overwrite}{Whether to overwrite existing summaries. Default is FALSE.} -\item{...}{Additional arguments passed to \code{interrogate_llm} function, such as +\item{...}{Additional arguments passed to \code{prompt_llm} function, such as the LLM provider.} } \value{ diff --git a/man/summarise_transcript.Rd b/man/summarise_transcript.Rd index 4d20d7f..f489344 100644 --- a/man/summarise_transcript.Rd +++ b/man/summarise_transcript.Rd @@ -71,7 +71,7 @@ get_prompts("output_rolling_aggregation") prompts depending on the task.} \item{prompt_only}{If TRUE, only the prompt is returned, the LLM is not interrogated. Default is FALSE.} -\item{...}{Additional arguments passed to the \code{interrogate_llm} function, +\item{...}{Additional arguments passed to the \code{prompt_llm} function, such as the LLM provider.} } \value{ diff --git a/man/use_azure_llm.Rd b/man/use_azure_llm.Rd index 9f3180e..17a2207 100644 --- a/man/use_azure_llm.Rd +++ b/man/use_azure_llm.Rd @@ -9,7 +9,8 @@ use_azure_llm( deployment_id = getOption("minutemaker_azure_deployment_gpt"), resource_name = getOption("minutemaker_azure_resource_gpt"), api_key = getOption("minutemaker_azure_api_key_gpt"), - api_version = getOption("minutemaker_azure_api_version") + api_version = getOption("minutemaker_azure_api_version"), + log_request = getOption("minutemaker_log_requests", TRUE) ) } \arguments{ @@ -25,6 +26,9 @@ options.} \item{api_version}{API version for the Azure language model service. Obtained from R options.} + +\item{log_request}{A boolean to log the request time. Can be set up globally +using the \code{minutemaker_log_requests} option, which defaults to TRUE.} } \value{ The function returns the response from the Azure API. diff --git a/man/use_custom_llm.Rd b/man/use_custom_llm.Rd index 4d37b9c..71845b7 100644 --- a/man/use_custom_llm.Rd +++ b/man/use_custom_llm.Rd @@ -7,7 +7,9 @@ use_custom_llm( body, endpoint = getOption("minutemaker_custom_endpoint_gpt"), - api_key = getOption("minutemaker_custom_api_key") + model = getOption("minutemaker_custom_model_gpt", NULL), + api_key = getOption("minutemaker_custom_api_key"), + log_request = getOption("minutemaker_log_requests", TRUE) ) } \arguments{ @@ -16,8 +18,14 @@ use_custom_llm( \item{endpoint}{The local endpoint for the language model service. Can be obtained from R options.} +\item{model}{Model identifier for the custom API, if needed (some API have +one model per endpoint, some multiple ones). Obtained from R options.} + \item{api_key}{Optional API key for the custom language model services that require it. Obtained from R options.} + +\item{log_request}{A boolean to log the request time. Can be set up globally +using the \code{minutemaker_log_requests} option, which defaults to TRUE.} } \value{ The function returns the response from the local language model diff --git a/man/use_openai_llm.Rd b/man/use_openai_llm.Rd index 32de36d..2833e3b 100644 --- a/man/use_openai_llm.Rd +++ b/man/use_openai_llm.Rd @@ -7,7 +7,8 @@ use_openai_llm( body, model = getOption("minutemaker_openai_model_gpt"), - api_key = getOption("minutemaker_openai_api_key") + api_key = getOption("minutemaker_openai_api_key"), + log_request = getOption("minutemaker_log_requests", TRUE) ) } \arguments{ @@ -16,6 +17,9 @@ use_openai_llm( \item{model}{Model identifier for the OpenAI API. Obtained from R options.} \item{api_key}{API key for the OpenAI service. Obtained from R options.} + +\item{log_request}{A boolean to log the request time. Can be set up globally +using the \code{minutemaker_log_requests} option, which defaults to TRUE.} } \value{ The function returns the response from the OpenAI API. diff --git a/renv.lock b/renv.lock index 65c869a..1abe148 100644 --- a/renv.lock +++ b/renv.lock @@ -1,6 +1,6 @@ { "R": { - "Version": "4.3.2", + "Version": "4.4.0", "Repositories": [ { "Name": "CRAN", @@ -414,13 +414,13 @@ }, "renv": { "Package": "renv", - "Version": "1.0.3", + "Version": "1.0.7", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "utils" ], - "Hash": "41b847654f567341725473431dd0d5ab" + "Hash": "397b7b2a265bc5a7a06852524dabae20" }, "rlang": { "Package": "rlang", diff --git a/renv/activate.R b/renv/activate.R index cb5401f..d13f993 100644 --- a/renv/activate.R +++ b/renv/activate.R @@ -2,11 +2,13 @@ local({ # the requested version of renv - version <- "1.0.3" + version <- "1.0.7" attr(version, "sha") <- NULL # the project directory - project <- getwd() + project <- Sys.getenv("RENV_PROJECT") + if (!nzchar(project)) + project <- getwd() # use start-up diagnostics if enabled diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") @@ -31,6 +33,14 @@ local({ if (!is.null(override)) return(override) + # if we're being run in a context where R_LIBS is already set, + # don't load -- presumably we're being run as a sub-process and + # the parent process has already set up library paths for us + rcmd <- Sys.getenv("R_CMD", unset = NA) + rlibs <- Sys.getenv("R_LIBS", unset = NA) + if (!is.na(rlibs) && !is.na(rcmd)) + return(FALSE) + # next, check environment variables # TODO: prefer using the configuration one in the future envvars <- c( @@ -50,9 +60,22 @@ local({ }) - if (!enabled) + # bail if we're not enabled + if (!enabled) { + + # if we're not enabled, we might still need to manually load + # the user profile here + profile <- Sys.getenv("R_PROFILE_USER", unset = "~/.Rprofile") + if (file.exists(profile)) { + cfg <- Sys.getenv("RENV_CONFIG_USER_PROFILE", unset = "TRUE") + if (tolower(cfg) %in% c("true", "t", "1")) + sys.source(profile, envir = globalenv()) + } + return(FALSE) + } + # avoid recursion if (identical(getOption("renv.autoloader.running"), TRUE)) { warning("ignoring recursive attempt to run renv autoloader") @@ -108,6 +131,21 @@ local({ } + heredoc <- function(text, leave = 0) { + + # remove leading, trailing whitespace + trimmed <- gsub("^\\s*\\n|\\n\\s*$", "", text) + + # split into lines + lines <- strsplit(trimmed, "\n", fixed = TRUE)[[1L]] + + # compute common indent + indent <- regexpr("[^[:space:]]", lines) + common <- min(setdiff(indent, -1L)) - leave + paste(substring(lines, common), collapse = "\n") + + } + startswith <- function(string, prefix) { substring(string, 1, nchar(prefix)) == prefix } @@ -610,6 +648,9 @@ local({ # if the user has requested an automatic prefix, generate it auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (is.na(auto) && getRversion() >= "4.4.0") + auto <- "TRUE" + if (auto %in% c("TRUE", "True", "true", "1")) return(renv_bootstrap_platform_prefix_auto()) @@ -801,24 +842,23 @@ local({ # the loaded version of renv doesn't match the requested version; # give the user instructions on how to proceed - remote <- if (!is.null(description[["RemoteSha"]])) { + dev <- identical(description[["RemoteType"]], "github") + remote <- if (dev) paste("rstudio/renv", description[["RemoteSha"]], sep = "@") - } else { + else paste("renv", description[["Version"]], sep = "@") - } # display both loaded version + sha if available friendly <- renv_bootstrap_version_friendly( version = description[["Version"]], - sha = description[["RemoteSha"]] + sha = if (dev) description[["RemoteSha"]] ) - fmt <- paste( - "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", - "- Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", - "- Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", - sep = "\n" - ) + fmt <- heredoc(" + renv %1$s was loaded from project library, but this project is configured to use renv %2$s. + - Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile. + - Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library. + ") catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) FALSE @@ -1041,7 +1081,7 @@ local({ # if jsonlite is loaded, use that instead if ("jsonlite" %in% loadedNamespaces()) { - json <- catch(renv_json_read_jsonlite(file, text)) + json <- tryCatch(renv_json_read_jsonlite(file, text), error = identity) if (!inherits(json, "error")) return(json) @@ -1050,7 +1090,7 @@ local({ } # otherwise, fall back to the default JSON reader - json <- catch(renv_json_read_default(file, text)) + json <- tryCatch(renv_json_read_default(file, text), error = identity) if (!inherits(json, "error")) return(json) @@ -1063,14 +1103,14 @@ local({ } renv_json_read_jsonlite <- function(file = NULL, text = NULL) { - text <- paste(text %||% read(file), collapse = "\n") + text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") jsonlite::fromJSON(txt = text, simplifyVector = FALSE) } renv_json_read_default <- function(file = NULL, text = NULL) { # find strings in the JSON - text <- paste(text %||% read(file), collapse = "\n") + text <- paste(text %||% readLines(file, warn = FALSE), collapse = "\n") pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' locs <- gregexpr(pattern, text, perl = TRUE)[[1]] @@ -1118,14 +1158,14 @@ local({ map <- as.list(map) # remap strings in object - remapped <- renv_json_remap(json, map) + remapped <- renv_json_read_remap(json, map) # evaluate eval(remapped, envir = baseenv()) } - renv_json_remap <- function(json, map) { + renv_json_read_remap <- function(json, map) { # fix names if (!is.null(names(json))) { @@ -1152,7 +1192,7 @@ local({ # recurse if (is.recursive(json)) { for (i in seq_along(json)) { - json[i] <- list(renv_json_remap(json[[i]], map)) + json[i] <- list(renv_json_read_remap(json[[i]], map)) } }