R/documentation.R

Defines functions .document_session_facts .document_normalise_metadata .document_format_details .document_format_entries .document_record_event document_end document_start

Documented in document_end document_start

# --- FILE: localLLM/R/documentation.R ---

#' @importFrom digest digest
NULL

.localllm_doc_env <- new.env(parent = emptyenv())
.localllm_doc_env$active <- FALSE
.localllm_doc_env$entries <- list()
.localllm_doc_env$path <- NULL
.localllm_doc_env$append <- FALSE
.localllm_doc_env$start_time <- NULL
.localllm_doc_env$last_hash <- NULL

#' Start automatic run documentation
#'
#' Calling `document_start()` enables automatic logging for subsequent
#' `localLLM` calls. Information such as timestamps, models, and generation
#' settings are buffered in-memory until [document_end()] is invoked, at which
#' point a human-readable text report is written to disk.
#'
#' @param path Optional log file path. Defaults to
#'   `localLLM_run_<timestamp>.txt` in the working directory.
#' @param metadata Optional named list of user-defined metadata to include in the
#'   log header (e.g. project name, dataset id).
#' @param append When `TRUE`, entries are appended to an existing file instead of
#'   overwriting it.
#' @return The path that will be written when [document_end()] is called.
#' @importFrom utils capture.output str
#' @export
document_start <- function(path = NULL, metadata = list(), append = FALSE) {
  if (isTRUE(.localllm_doc_env$active)) {
    stop("document_start() has already been called. Invoke document_end() before starting a new session.", call. = FALSE)
  }

  start_time <- Sys.time()
  if (is.null(path)) {
    path <- file.path(getwd(), sprintf("localLLM_run_%s.txt", format(start_time, "%Y%m%d_%H%M%S")))
  }
  dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE)

  metadata <- .document_normalise_metadata(metadata)

  .localllm_doc_env$active <- TRUE
  .localllm_doc_env$path <- path
  .localllm_doc_env$append <- isTRUE(append)
  .localllm_doc_env$start_time <- start_time
  .localllm_doc_env$entries <- list()
  .localllm_doc_env$last_hash <- NULL

  base_details <- c(.document_session_facts(), metadata)
  .document_record_event("document_start", base_details, timestamp = start_time)

  invisible(path)
}

#' Finish automatic run documentation
#'
#' Flushes the buffered log entries assembled since the matching
#' [document_start()] call and writes them to the configured text file.
#' A SHA-256 hash of the written content is appended to the log so runs
#' can be compared or referenced succinctly.
#'
#' @return Invisibly returns the log file path with attribute `hash` containing
#'   the SHA-256 digest of the run contents.
#' @export
document_end <- function() {
  if (!isTRUE(.localllm_doc_env$active)) {
    stop("document_start() has not been called.", call. = FALSE)
  }

  end_time <- Sys.time()
  entries <- .localllm_doc_env$entries
  duration <- as.numeric(difftime(end_time, .localllm_doc_env$start_time, units = "secs"))
  entries[[length(entries) + 1L]] <- list(
    type = "document_end",
    timestamp = end_time,
    details = list(duration_seconds = duration, total_events = length(entries))
  )

  lines <- .document_format_entries(entries, .localllm_doc_env$start_time, end_time, .localllm_doc_env$path)

  hash_input <- paste(lines, collapse = "\n")
  run_hash <- digest::digest(hash_input, algo = "sha256")
  lines_to_write <- c(lines, sprintf("Hash (SHA-256): %s", run_hash))

  path <- .localllm_doc_env$path
  mode <- if (isTRUE(.localllm_doc_env$append) && file.exists(path)) "a" else "w"
  con <- file(path, open = mode, encoding = "UTF-8")
  on.exit(close(con), add = TRUE)
  if (mode == "a") {
    writeLines("", con)
    writeLines(strrep("=", 60), con)
  }
  writeLines(lines_to_write, con)

  .localllm_message("localLLM run log written to: ", path, " (hash: ", run_hash, ")")

  .localllm_doc_env$active <- FALSE
  .localllm_doc_env$entries <- list()
  .localllm_doc_env$path <- NULL
  .localllm_doc_env$start_time <- NULL
  .localllm_doc_env$last_hash <- run_hash

  result <- structure(path, hash = run_hash)
  invisible(result)
}

.document_record_event <- function(event, details = list(), timestamp = Sys.time()) {
  if (!isTRUE(.localllm_doc_env$active)) {
    return(invisible(FALSE))
  }

  entry <- list(
    type = event,
    timestamp = timestamp,
    details = details
  )

  .localllm_doc_env$entries[[length(.localllm_doc_env$entries) + 1L]] <- entry
  invisible(TRUE)
}

.document_format_entries <- function(entries, start_time, end_time, path) {
  tz <- Sys.timezone()
  duration <- as.numeric(difftime(end_time, start_time, units = "secs"))

  header <- c(
    "localLLM Run Log",
    sprintf("File: %s", normalizePath(path, winslash = "/", mustWork = FALSE)),
    sprintf("Started: %s", format(start_time, tz = tz, usetz = TRUE)),
    sprintf("Ended: %s", format(end_time, tz = tz, usetz = TRUE)),
    sprintf("Duration: %.2f seconds", duration),
    "",
    "Events:"
  )

  event_lines <- unlist(lapply(entries, function(entry) {
    block <- c(sprintf("- [%s] %s", format(entry$timestamp, tz = tz, usetz = TRUE), entry$type))
    if (length(entry$details)) {
      detail_lines <- .document_format_details(entry$details)
      block <- c(block, paste0("    ", detail_lines))
    }
    c(block, "")
  }), use.names = FALSE)

  c(header, event_lines)
}

.document_format_details <- function(details) {
  tryCatch({
    json <- jsonlite::toJSON(details, auto_unbox = TRUE, null = "null", digits = NA, pretty = TRUE)
    strsplit(json, "\n", fixed = TRUE)[[1]]
  }, error = function(e) {
    capture.output(str(details, give.attr = FALSE))
  })
}

.document_normalise_metadata <- function(metadata) {
  if (is.null(metadata)) {
    return(list())
  }
  if (!is.list(metadata)) {
    metadata <- as.list(metadata)
  }
  metadata
}

.document_session_facts <- function() {
  sys <- Sys.info()
  list(
    package_version = as.character(utils::packageVersion("localLLM")),
    r_version = as.character(getRversion()),
    platform = R.version$platform,
    os = sys[["sysname"]],
    release = sys[["release"]],
    user = sys[["user"]] %||% Sys.getenv("USER", unset = NA_character_),
    working_directory = getwd()
  )
}

Try the localLLM package in your browser

Any scripts or data that you put into this service are public.

localLLM documentation built on Nov. 3, 2026, 1:09 a.m.