`%||%` <- function(x, y) { if (is.null(x)) y else x } asa_api_default_backend <- "gemini" .asa_api_auth_cache <- new.env(parent = emptyenv()) asa_api_to_bool <- function(value, default = FALSE) { if (is.null(value) || length(value) == 0L) { return(isTRUE(default)) } if (is.logical(value)) { return(isTRUE(value[[1]])) } if (is.numeric(value)) { return(!is.na(value[[1]]) && value[[1]] != 0) } token <- tolower(trimws(as.character(value[[1]]))) if (token %in% c("1", "true", "t", "yes", "y", "on")) { return(TRUE) } if (token %in% c("0", "false", "f", "no", "n", "off")) { return(FALSE) } isTRUE(default) } asa_api_scalar_chr <- function(value, default = "") { if (is.null(value) || length(value) == 0L) { return(default) } text <- as.character(value[[1]]) if (!nzchar(text)) { return(default) } text } asa_api_scalar_num <- function(value, default = NA_real_) { if (is.null(value) || length(value) == 0L) { return(default) } number <- suppressWarnings(as.numeric(value[[1]])) if (is.na(number)) { default } else { number } } asa_api_scalar_int <- function(value, default = NA_integer_) { if (is.null(value) || length(value) == 0L) { return(default) } number <- suppressWarnings(as.integer(value[[1]])) if (is.na(number)) { default } else { number } } asa_api_named_list <- function(value) { if (is.null(value)) { return(list()) } if (is.list(value)) { return(value) } if (is.data.frame(value)) { return(as.list(value)) } list() } asa_api_filter_formals <- function(fun, args) { args <- asa_api_named_list(args) if (!length(args)) { return(list()) } arg_names <- names(args) if (is.null(arg_names) || !length(arg_names)) { return(list()) } allowed <- names(formals(fun)) keep <- intersect(arg_names, allowed) out <- args[keep] out[!vapply(out, is.null, logical(1))] } asa_api_parse_request_body <- function(req) { body <- req$postBody %||% "" if (!nzchar(trimws(body))) { return(list()) } parsed <- tryCatch( jsonlite::fromJSON(body, simplifyVector = FALSE), error = function(e) { stop("Invalid JSON body: ", conditionMessage(e), call. = FALSE) } ) if (!is.list(parsed)) { stop("Request body must be a JSON object.", call. = FALSE) } parsed } asa_api_apply_env_defaults <- function() { backend <- trimws(Sys.getenv("ASA_DEFAULT_BACKEND", unset = "")) model <- trimws(Sys.getenv("ASA_DEFAULT_MODEL", unset = "")) conda_env <- trimws(Sys.getenv("ASA_CONDA_ENV", unset = "")) if (nzchar(backend)) { options(asa.default_backend = backend) } else { active_backend <- trimws(asa_api_scalar_chr(getOption("asa.default_backend", ""), default = "")) if (!nzchar(active_backend)) { options(asa.default_backend = asa_api_default_backend) } } if (nzchar(model)) { options(asa.default_model = model) } if (nzchar(conda_env)) { options(asa.default_conda_env = conda_env) } } asa_api_get_asa_namespace_function <- function(name, optional = FALSE) { if (!requireNamespace("asa", quietly = TRUE)) { stop("Package `asa` is not installed in this environment.", call. = FALSE) } exports <- tryCatch(getNamespaceExports("asa"), error = function(e) character(0)) if (name %in% exports) { return(getExportedValue("asa", name)) } asa_ns <- asNamespace("asa") if (exists(name, envir = asa_ns, inherits = FALSE)) { return(get(name, envir = asa_ns, inherits = FALSE)) } if (isTRUE(optional)) { return(NULL) } stop( sprintf( "Package `asa` does not provide `%s`. Upgrade `asa` or disable direct-provider mode.", name ), call. = FALSE ) } asa_api_get_run_direct_task <- function(optional = FALSE) { asa_api_get_asa_namespace_function("run_direct_task", optional = optional) } asa_api_has_run_direct_task <- function() { !is.null(asa_api_get_run_direct_task(optional = TRUE)) } asa_api_parse_proxy_url <- function(proxy_url) { proxy_url <- trimws(asa_api_scalar_chr(proxy_url, default = "")) if (!nzchar(proxy_url)) { return(NULL) } matches <- regexec("^([A-Za-z][A-Za-z0-9+.-]*)://([^/:?#]+):(\\d+)$", proxy_url) parsed <- regmatches(proxy_url, matches)[[1]] if (length(parsed) != 4L) { return(NULL) } list( scheme = parsed[[2]], host = parsed[[3]], port = asa_api_scalar_int(parsed[[4]], default = NA_integer_) ) } asa_api_port_open <- function(host, port, timeout = 1) { host <- trimws(asa_api_scalar_chr(host, default = "")) port <- asa_api_scalar_int(port, default = NA_integer_) if (!nzchar(host) || is.na(port) || port <= 0L) { return(FALSE) } con <- NULL tryCatch( { con <- socketConnection( host = host, port = as.integer(port), open = "r+b", blocking = TRUE, timeout = as.numeric(timeout) ) TRUE }, error = function(e) FALSE, finally = { if (!is.null(con)) { try(close(con), silent = TRUE) } } ) } asa_api_tor_health <- function() { proxy_url <- trimws(Sys.getenv("ASA_PROXY", unset = "")) proxy_info <- asa_api_parse_proxy_url(proxy_url) control_port <- asa_api_scalar_int(Sys.getenv("TOR_CONTROL_PORT", unset = ""), default = NA_integer_) cookie_path <- trimws(Sys.getenv("ASA_TOR_CONTROL_COOKIE", unset = "")) tor_enabled <- nzchar(proxy_url) tor_proxy_host <- if (!is.null(proxy_info)) proxy_info$host else NULL tor_proxy_port <- if (!is.null(proxy_info)) proxy_info$port else NULL tor_proxy_port_open <- if (!is.null(proxy_info)) { asa_api_port_open(proxy_info$host, proxy_info$port) } else { FALSE } tor_control_port_open <- if (!is.na(control_port) && control_port > 0L) { asa_api_port_open("127.0.0.1", control_port) } else { FALSE } tor_cookie_present <- nzchar(cookie_path) && file.exists(cookie_path) tor_cookie_readable <- tor_cookie_present && file.access(cookie_path, 4L) == 0 tor_ready <- tor_enabled && isTRUE(tor_proxy_port_open) && isTRUE(tor_control_port_open) && isTRUE(tor_cookie_readable) list( tor_enabled = tor_enabled, tor_ready = tor_ready, tor_proxy = if (tor_enabled) proxy_url else NULL, tor_proxy_host = tor_proxy_host, tor_proxy_port = tor_proxy_port, tor_proxy_port_open = tor_proxy_port_open, tor_control_port = if (!is.na(control_port) && control_port > 0L) control_port else NULL, tor_control_port_open = tor_control_port_open, tor_cookie_path = if (nzchar(cookie_path)) cookie_path else NULL, tor_cookie_present = tor_cookie_present, tor_cookie_readable = tor_cookie_readable ) } asa_api_bootstrap <- function() { asa_api_apply_env_defaults() if (!requireNamespace("asa", quietly = TRUE)) { stop("Package `asa` is not installed in this environment.", call. = FALSE) } if (!requireNamespace("sodium", quietly = TRUE)) { stop("Package `sodium` is not installed in this environment.", call. = FALSE) } asa_api_refresh_auth_cache(force = TRUE) invisible(TRUE) } asa_api_get_header <- function(req, key) { headers <- req$HEADERS %||% list() header_names <- names(headers) %||% character(0) value <- "" if (length(header_names)) { header_match <- which(tolower(header_names) == tolower(asa_api_scalar_chr(key, default = ""))) if (length(header_match)) { value <- headers[[header_match[[1]]]] } } if (!nzchar(asa_api_scalar_chr(value, default = ""))) { value <- headers[[key]] %||% headers[[toupper(key)]] %||% "" } asa_api_scalar_chr(value, default = "") } asa_api_clear_auth_cache <- function() { cached_keys <- ls(envir = .asa_api_auth_cache, all.names = TRUE) if (length(cached_keys)) { rm(list = cached_keys, envir = .asa_api_auth_cache) } invisible(TRUE) } asa_api_secret_env_value <- function(name) { trimws(Sys.getenv(asa_api_scalar_chr(name, default = ""), unset = "")) } asa_api_error_fields <- function(message, error_code = NULL, details = NULL) { payload <- list( status = "error", error = asa_api_scalar_chr(message, default = "Request failed.") ) code <- asa_api_scalar_chr(error_code, default = "") if (nzchar(code)) { payload$error_code <- code } if (is.list(details) && length(details)) { payload$details <- details } payload } asa_api_error_status <- function(message) { if (grepl("direct-provider mode|does not provide `run_direct_task`", message, ignore.case = TRUE)) { return(501L) } if (grepl("invalid json|required|must be|non-empty|unauthorized|password|unsupported", message, ignore.case = TRUE)) { return(400L) } 500L } asa_api_drop_nulls <- function(value) { if (!is.list(value)) { return(value) } value[!vapply(value, is.null, logical(1))] } asa_api_make_diagnostic_id <- function(prefix = "asaapi") { sprintf( "%s-%s-%06d", asa_api_scalar_chr(prefix, default = "asaapi"), format(Sys.time(), "%Y%m%dT%H%M%SZ", tz = "UTC"), as.integer(stats::runif(1, min = 0, max = 999999)) ) } asa_api_redact_text_excerpt <- function(text, max_chars = 120L) { text <- asa_api_scalar_chr(text, default = "") if (!nzchar(text)) { return("") } text <- gsub("[[:cntrl:]]+", " ", text) text <- gsub("[[:space:]]+", " ", text) text <- trimws(text) if (!nzchar(text)) { return("") } max_chars <- asa_api_scalar_int(max_chars, default = 120L) if (is.na(max_chars) || max_chars < 16L) { max_chars <- 120L } if (nchar(text, type = "chars") > max_chars) { paste0(substr(text, 1L, max_chars), "...") } else { text } } asa_api_redact_call_text <- function(call_text, max_chars = 240L) { call_text <- asa_api_scalar_chr(call_text, default = "") if (!nzchar(call_text)) { return("") } call_text <- gsub('"[^"]*"', '""', call_text) call_text <- gsub("'[^']*'", "''", call_text) asa_api_redact_text_excerpt(call_text, max_chars = max_chars) } asa_api_prompt_summary <- function(prompt) { prompt <- asa_api_scalar_chr(prompt, default = "") list( chars = nchar(prompt, type = "chars"), excerpt = asa_api_redact_text_excerpt(prompt) ) } asa_api_prompts_summary <- function(prompts) { prompts <- prompts %||% character(0) prompts <- as.character(prompts) prompt_lengths <- nchar(prompts, type = "chars") prompt_lengths[is.na(prompt_lengths)] <- 0L list( count = length(prompts), total_chars = sum(prompt_lengths), max_chars = if (length(prompt_lengths)) max(prompt_lengths) else 0L, first_prompt = if (length(prompts)) asa_api_prompt_summary(prompts[[1]]) else NULL ) } asa_api_runtime_config_summary <- function(config) { if (!is.list(config) || !length(config)) { return(list()) } asa_api_drop_nulls(list( backend = asa_api_scalar_chr(config$backend, default = ""), model = asa_api_scalar_chr(config$model, default = ""), conda_env = asa_api_scalar_chr(config$conda_env, default = ""), use_browser = if (!is.null(config$use_browser)) isTRUE(config$use_browser) else NULL, timeout = asa_api_scalar_int(config$timeout, default = NA_integer_), rate_limit = asa_api_scalar_num(config$rate_limit, default = NA_real_), workers = asa_api_scalar_int(config$workers, default = NA_integer_), memory_folding = if (!is.null(config$memory_folding)) isTRUE(config$memory_folding) else NULL, recursion_limit = asa_api_scalar_int(config$recursion_limit, default = NA_integer_) )) } asa_api_runtime_defaults_summary <- function() { asa_api_drop_nulls(list( backend = getOption("asa.default_backend", NULL), model = getOption("asa.default_model", NULL), conda_env = getOption("asa.default_conda_env", "asa_env"), use_browser = asa_api_to_bool(Sys.getenv("ASA_USE_BROWSER_DEFAULT", unset = "false"), default = FALSE) )) } asa_api_runtime_tor_summary <- function() { tor <- asa_api_tor_health() asa_api_drop_nulls(list( tor_enabled = isTRUE(tor$tor_enabled), tor_ready = isTRUE(tor$tor_ready), tor_proxy_host = tor$tor_proxy_host %||% NULL, tor_proxy_port = tor$tor_proxy_port %||% NULL, tor_proxy_port_open = isTRUE(tor$tor_proxy_port_open), tor_control_port = tor$tor_control_port %||% NULL, tor_control_port_open = isTRUE(tor$tor_control_port_open), tor_cookie_present = isTRUE(tor$tor_cookie_present), tor_cookie_readable = isTRUE(tor$tor_cookie_readable) )) } asa_api_runtime_error_summary <- function(error) { if (is.null(error)) { return(list()) } err_call <- conditionCall(error) call_text <- if (is.null(err_call)) "" else paste(deparse(err_call, width.cutoff = 120L), collapse = " ") asa_api_drop_nulls(list( message = conditionMessage(error), class = as.character(class(error)), call = if (nzchar(call_text)) asa_api_redact_call_text(call_text) else NULL )) } asa_api_runtime_stage <- function(stage_ref, default = "invoke") { if (is.environment(stage_ref)) { return(asa_api_scalar_chr(stage_ref$value, default = default)) } asa_api_scalar_chr(stage_ref, default = default) } asa_api_build_runtime_diagnostic <- function(mode, route, payload = NULL, prompt = NULL, prompts = NULL, config = NULL, forwarded_arg_names = character(0), request_shape = list(), stage = "invoke", error = NULL, diagnostic_id = asa_api_make_diagnostic_id()) { request_shape <- asa_api_named_list(request_shape) request_shape$include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE) request_shape$include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE) if (!is.null(payload$use_direct_provider)) { request_shape$use_direct_provider <- asa_api_to_bool(payload$use_direct_provider, default = FALSE) } request_shape$forwarded_arg_names <- sort(unique(as.character(forwarded_arg_names))) if (!is.null(prompt)) { request_shape$prompt <- asa_api_prompt_summary(prompt) } if (!is.null(prompts)) { request_shape$prompts <- asa_api_prompts_summary(prompts) } asa_api_drop_nulls(list( diagnostic_id = asa_api_scalar_chr(diagnostic_id, default = asa_api_make_diagnostic_id()), timestamp_utc = format(Sys.time(), "%Y-%m-%dT%H:%M:%SZ", tz = "UTC"), mode = asa_api_scalar_chr(mode, default = "unknown"), route = asa_api_scalar_chr(route, default = "unknown"), stage = asa_api_scalar_chr(stage, default = "invoke"), request = asa_api_drop_nulls(request_shape), config = asa_api_runtime_config_summary(config), runtime = list( asa_version = tryCatch(as.character(utils::packageVersion("asa")), error = function(e) NA_character_), defaults = asa_api_runtime_defaults_summary(), direct_provider_available = tryCatch(isTRUE(asa_api_has_run_direct_task()), error = function(e) FALSE), tor = asa_api_runtime_tor_summary() ), error = asa_api_runtime_error_summary(error) )) } asa_api_emit_runtime_diagnostic <- function(diagnostic, log_con = stderr()) { if (requireNamespace("reticulate", quietly = TRUE)) { diagnostic <- tryCatch(reticulate::py_to_r(diagnostic), error = function(e) diagnostic) } diagnostic <- asa_api_named_list(diagnostic) diagnostic_id <- asa_api_scalar_chr(diagnostic$diagnostic_id, default = "unknown") mode <- asa_api_scalar_chr(diagnostic$mode, default = "unknown") route <- asa_api_scalar_chr(diagnostic$route, default = "unknown") stage <- asa_api_scalar_chr(diagnostic$stage, default = "invoke") error_message <- asa_api_scalar_chr((diagnostic$error %||% list())$message, default = "Request failed.") cat( sprintf( "[asa-api] runtime failure diagnostic_id=%s mode=%s route=%s stage=%s error=%s\n", diagnostic_id, mode, route, stage, error_message ), file = log_con ) diagnostic_json <- tryCatch( jsonlite::toJSON(diagnostic, auto_unbox = TRUE, null = "null", force = TRUE, digits = NA), error = function(e) { jsonlite::toJSON( list( diagnostic_id = diagnostic_id, serialization_error = conditionMessage(e) ), auto_unbox = TRUE, null = "null", force = TRUE ) } ) cat( sprintf("[asa-api][diagnostic] %s\n", paste(diagnostic_json, collapse = "")), file = log_con ) invisible(diagnostic) } asa_api_runtime_error_code <- function(mode) { mode <- asa_api_scalar_chr(mode, default = "") if (identical(mode, "asa_agent_batch")) { return("agent_batch_failure") } if (identical(mode, "provider_direct_single")) { return("direct_provider_failure") } "agent_pipeline_failure" } asa_api_runtime_failure_summary <- function(diagnostic) { diagnostic <- asa_api_named_list(diagnostic) asa_api_drop_nulls(list( diagnostic_id = asa_api_scalar_chr(diagnostic$diagnostic_id, default = ""), mode = asa_api_scalar_chr(diagnostic$mode, default = ""), route = asa_api_scalar_chr(diagnostic$route, default = ""), stage = asa_api_scalar_chr(diagnostic$stage, default = "") )) } asa_api_runtime_failure_details <- function(diagnostic) { diagnostic <- asa_api_named_list(diagnostic) asa_api_drop_nulls(list( diagnostic_id = asa_api_scalar_chr(diagnostic$diagnostic_id, default = ""), mode = diagnostic$mode %||% NULL, route = diagnostic$route %||% NULL, stage = diagnostic$stage %||% NULL, request = diagnostic$request %||% NULL, config = diagnostic$config %||% NULL, runtime = diagnostic$runtime %||% NULL, error = diagnostic$error %||% NULL )) } asa_api_condition_status <- function(error) { explicit_status <- asa_api_scalar_int(error$status_code %||% NA_integer_, default = NA_integer_) if (!is.na(explicit_status)) { return(explicit_status) } asa_api_error_status(conditionMessage(error)) } asa_api_raise_runtime_failure <- function(error, diagnostic, mode, status_code = 500L, log_con = stderr()) { diagnostic <- asa_api_emit_runtime_diagnostic(diagnostic, log_con = log_con) diagnostic_id <- asa_api_scalar_chr(diagnostic$diagnostic_id, default = "unknown") message <- sprintf("%s [diagnostic_id=%s]", conditionMessage(error), diagnostic_id) summary_details <- asa_api_runtime_failure_summary(diagnostic) full_details <- asa_api_runtime_failure_details(diagnostic) condition <- simpleError(message) condition$error_code <- asa_api_runtime_error_code(mode) condition$details <- summary_details condition$details_full <- full_details condition$status_code <- asa_api_scalar_int(status_code, default = 500L) class(condition) <- c("asa_api_runtime_error", class(condition)) stop(condition) } asa_api_invoke_with_runtime_diagnostics <- function(fn, mode, route, payload = NULL, prompt = NULL, prompts = NULL, config = NULL, forwarded_arg_names = character(0), request_shape = list(), stage_ref = NULL, status_code = 500L, log_con = stderr()) { tryCatch( fn(), error = function(e) { if (inherits(e, "asa_api_runtime_error")) { stop(e) } if (!identical(asa_api_condition_status(e), 500L)) { stop(e) } diagnostic <- asa_api_build_runtime_diagnostic( mode = mode, route = route, payload = payload, prompt = prompt, prompts = prompts, config = config, forwarded_arg_names = forwarded_arg_names, request_shape = request_shape, stage = asa_api_runtime_stage(stage_ref, default = "invoke"), error = e ) asa_api_raise_runtime_failure( error = e, diagnostic = diagnostic, mode = mode, status_code = status_code, log_con = log_con ) } ) } asa_api_error_failure <- function(error) { message <- conditionMessage(error) failure <- list( status_code = asa_api_error_status(message), message = message ) code <- asa_api_scalar_chr(error$error_code %||% "", default = "") if (nzchar(code)) { failure$error_code <- code } details <- error$details %||% NULL if (requireNamespace("reticulate", quietly = TRUE)) { details <- tryCatch(reticulate::py_to_r(details), error = function(e) details) } if (!is.null(details) && !is.list(details)) { details <- asa_api_to_plain(details) } if (is.list(details) && length(details)) { failure$details <- details } explicit_status <- asa_api_scalar_int(error$status_code %||% NA_integer_, default = NA_integer_) if (!is.na(explicit_status)) { failure$status_code <- explicit_status } failure } asa_api_request_context_route <- function(request_context, default) { context <- asa_api_named_list(request_context) asa_api_scalar_chr(context$route, default = default) } asa_api_request_context_log_con <- function(request_context) { context <- asa_api_named_list(request_context) context$log_con %||% stderr() } asa_api_new_runtime_capture <- function() { capture <- new.env(parent = emptyenv()) capture$prompt <- NULL capture$prompts <- NULL capture$config <- NULL capture$request_shape <- list() capture$forwarded_arg_names <- character(0) capture } asa_api_missing_auth_env_vars <- function() { required <- c("ASA_API_BEARER_TOKEN", "GUI_PASSWORD") required[!nzchar(vapply(required, asa_api_secret_env_value, character(1)))] } asa_api_extract_missing_auth_env_vars <- function(message = NULL) { text <- asa_api_scalar_chr(message, default = "") matches <- regmatches( text, gregexpr("`[^`]+`", text, perl = TRUE) )[[1]] parsed <- if (length(matches) && !identical(matches, character(0))) { gsub("^`|`$", "", matches) } else { character(0) } sort(unique(c(parsed, asa_api_missing_auth_env_vars()))) } asa_api_is_auth_config_error <- function(message = NULL) { grepl( "^Missing required authentication environment variable\\(s\\):", asa_api_scalar_chr(message, default = "") ) } asa_api_boot_failure <- function(boot_error = NULL) { text <- trimws(asa_api_scalar_chr(boot_error, default = "")) if (!nzchar(text)) { return(NULL) } if (isTRUE(asa_api_is_auth_config_error(text))) { return(list( status_code = 503L, message = "Service unavailable: authentication is not configured.", error_code = "auth_config_missing", details = list( missing_env_vars = asa_api_extract_missing_auth_env_vars(text) ) )) } list( status_code = 503L, message = "Service unavailable." ) } asa_api_refresh_auth_cache <- function(force = FALSE) { cached_api_hash <- .asa_api_auth_cache$api_bearer_token_hash %||% "" cached_gui_hash <- .asa_api_auth_cache$gui_password_hash %||% "" if (!isTRUE(force) && is.character(cached_api_hash) && nzchar(cached_api_hash) && is.character(cached_gui_hash) && nzchar(cached_gui_hash)) { return(invisible(TRUE)) } missing_vars <- asa_api_missing_auth_env_vars() if (length(missing_vars)) { asa_api_clear_auth_cache() stop( sprintf( "Missing required authentication environment variable(s): %s.", paste(sprintf("`%s`", missing_vars), collapse = ", ") ), call. = FALSE ) } .asa_api_auth_cache$api_bearer_token_hash <- sodium::password_store( asa_api_secret_env_value("ASA_API_BEARER_TOKEN") ) .asa_api_auth_cache$gui_password_hash <- sodium::password_store( asa_api_secret_env_value("GUI_PASSWORD") ) invisible(TRUE) } asa_api_auth_check_secret <- function(candidate, cache_key, auth_target) { supplied <- asa_api_scalar_chr(candidate, default = "") refresh_error <- NULL auth_result <- tryCatch( { asa_api_refresh_auth_cache() stored_hash <- .asa_api_auth_cache[[asa_api_scalar_chr(cache_key, default = "")]] %||% "" if (nzchar(supplied) && is.character(stored_hash) && nzchar(stored_hash) && isTRUE(sodium::password_verify(stored_hash, supplied))) { return(list(ok = TRUE)) } list( ok = FALSE, status_code = 401L, message = "Unauthorized: provided credential did not match the configured value.", error_code = "credential_mismatch", details = list( auth_target = asa_api_scalar_chr(auth_target, default = "") ) ) }, error = function(e) { refresh_error <<- conditionMessage(e) NULL } ) if (!is.null(auth_result)) { return(auth_result) } boot_failure <- asa_api_boot_failure(refresh_error) if (!is.null(boot_failure)) { return(c(list(ok = FALSE), boot_failure)) } list( ok = FALSE, status_code = 503L, message = "Service unavailable." ) } asa_api_path_requires_bearer_auth <- function(path) { startsWith(asa_api_scalar_chr(path, default = ""), "/v1/") } asa_api_extract_bearer_token <- function(req) { auth_header <- asa_api_get_header(req, "authorization") bearer <- sub("^Bearer\\s+", "", auth_header, ignore.case = TRUE) if (nzchar(trimws(bearer)) && !identical(trimws(bearer), trimws(auth_header))) { return(trimws(bearer)) } "" } asa_api_check_bearer_token <- function(req) { asa_api_auth_check_secret( asa_api_extract_bearer_token(req), "api_bearer_token_hash", "api_bearer_token" ) } asa_api_has_required_bearer_token <- function(req) { isTRUE(asa_api_check_bearer_token(req)$ok) } asa_api_check_gui_password <- function(password) { asa_api_auth_check_secret( password, "gui_password_hash", "gui_password" ) } asa_api_has_required_gui_password <- function(password) { isTRUE(asa_api_check_gui_password(password)$ok) } asa_api_require_prompt <- function(payload) { prompt <- asa_api_scalar_chr(payload$prompt, default = "") if (!nzchar(trimws(prompt))) { stop("`prompt` is required and must be a non-empty string.", call. = FALSE) } prompt } asa_api_require_prompts <- function(payload) { prompts <- payload$prompts %||% NULL if (is.null(prompts)) { stop("`prompts` is required and must be a non-empty array of strings.", call. = FALSE) } prompt_error <- paste0( "`prompts` must be a JSON array of non-empty strings. ", "Structured prompt objects are not supported by `/v1/batch`." ) if (is.character(prompts)) { prompts <- as.character(prompts) } else if (is.list(prompts) && !is.data.frame(prompts)) { prompt_names <- names(prompts) %||% character(0) if (length(prompt_names) && any(nzchar(prompt_names))) { stop(prompt_error, call. = FALSE) } prompts <- vapply( seq_along(prompts), function(i) { item <- prompts[[i]] if (!is.character(item) || length(item) != 1L || is.na(item[[1]])) { stop(prompt_error, call. = FALSE) } as.character(item[[1]]) }, character(1) ) } else { stop(prompt_error, call. = FALSE) } prompts <- trimws(prompts) if (!length(prompts)) { stop("`prompts` must contain at least one non-empty string.", call. = FALSE) } if (any(!nzchar(prompts))) { stop("Each entry in `prompts` must be a non-empty string.", call. = FALSE) } prompts } asa_api_format_key_list <- function(keys) { paste(sprintf("`%s`", sort(unique(as.character(keys)))), collapse = ", ") } asa_api_batch_unsupported_run_keys <- function() { run_formals <- names(formals(asa::run_task)) batch_formals <- names(formals(asa::run_task_batch)) setdiff(run_formals, c(batch_formals, "prompt", "config", "agent")) } asa_api_validate_batch_supported_fields <- function(payload) { payload_names <- names(payload) %||% character(0) unsupported_keys <- asa_api_batch_unsupported_run_keys() run_keys <- names(asa_api_named_list(payload$run)) %||% character(0) unsupported_run_keys <- intersect(run_keys, unsupported_keys) if (length(unsupported_run_keys)) { stop( sprintf( "Unsupported `/v1/batch` `run` keys: %s. `run` must be limited to batch-compatible shared options.", asa_api_format_key_list(unsupported_run_keys) ), call. = FALSE ) } unsupported_top_level_keys <- intersect(payload_names, unsupported_keys) if (length(unsupported_top_level_keys)) { stop( sprintf( "Unsupported `/v1/batch` top-level keys: %s. These keys must be omitted because `/v1/batch` only accepts batch-compatible shared options.", asa_api_format_key_list(unsupported_top_level_keys) ), call. = FALSE ) } invisible(TRUE) } asa_api_build_config <- function(payload) { cfg <- asa_api_named_list(payload$config) cfg_formals <- names(formals(asa::asa_config)) for (name in intersect(names(payload), cfg_formals)) { if (is.null(cfg[[name]])) { cfg[[name]] <- payload[[name]] } } if (is.null(cfg$use_browser)) { cfg$use_browser <- asa_api_to_bool(Sys.getenv("ASA_USE_BROWSER_DEFAULT", unset = "false"), default = FALSE) } env_conda <- trimws(Sys.getenv("ASA_CONDA_ENV", unset = "")) if (is.null(cfg$conda_env) && nzchar(env_conda)) { cfg$conda_env <- env_conda } cfg_args <- asa_api_filter_formals(asa::asa_config, cfg) do.call(asa::asa_config, cfg_args) } asa_api_build_run_args <- function(payload) { run <- asa_api_named_list(payload$run) run_formals <- names(formals(asa::run_task)) for (name in intersect(names(payload), run_formals)) { if (is.null(run[[name]])) { run[[name]] <- payload[[name]] } } run$prompt <- NULL run$config <- NULL run$agent <- NULL if (is.null(run$output_format)) { run$output_format <- "text" } asa_api_filter_formals(asa::run_task, run) } asa_api_build_direct_args <- function(payload, run_direct_task_fun = asa_api_get_run_direct_task()) { direct <- asa_api_build_run_args(payload) direct$config <- NULL direct$agent <- NULL asa_api_filter_formals(run_direct_task_fun, direct) } asa_api_build_batch_args <- function(payload) { batch <- asa_api_named_list(payload$batch) run <- asa_api_named_list(payload$run) if (length(run)) { for (name in names(run)) { if (is.null(batch[[name]])) { batch[[name]] <- run[[name]] } } } batch_formals <- names(formals(asa::run_task_batch)) for (name in intersect(names(payload), batch_formals)) { if (is.null(batch[[name]])) { batch[[name]] <- payload[[name]] } } batch$prompts <- NULL batch$config <- NULL batch$agent <- NULL if (is.null(batch$parallel)) { batch$parallel <- asa_api_to_bool(payload$parallel, default = FALSE) } if (is.null(batch$progress)) { batch$progress <- FALSE } asa_api_filter_formals(asa::run_task_batch, batch) } asa_api_to_plain <- function(value) { if (is.null(value)) { return(NULL) } if (requireNamespace("reticulate", quietly = TRUE)) { value <- tryCatch(reticulate::py_to_r(value), error = function(e) value) } if (inherits(value, "POSIXt")) { return(format(value, tz = "UTC", usetz = TRUE)) } if (is.atomic(value) && length(value) == 1L && is.na(value)) { return(NULL) } tryCatch( jsonlite::fromJSON( jsonlite::toJSON(value, auto_unbox = FALSE, null = "null", force = TRUE, digits = NA), simplifyVector = FALSE ), error = function(e) value ) } asa_api_normalize_result <- function(result, include_raw_output = FALSE, include_trace_json = FALSE) { output <- list( status = asa_api_scalar_chr(result$status, default = "unknown"), message = asa_api_scalar_chr(result$message, default = ""), parsed = asa_api_to_plain(result$parsed), elapsed_time_min = asa_api_scalar_num(result$elapsed_time, default = NA_real_), search_tier = asa_api_scalar_chr(result$search_tier, default = "unknown"), parsing_status = asa_api_to_plain(result$parsing_status), execution = asa_api_to_plain(result$execution) ) if (include_raw_output) { output$raw_output <- asa_api_to_plain(result$raw_output) } if (include_trace_json) { output$trace_json <- asa_api_to_plain(result$trace_json) } output } asa_api_optional_scalar_chr <- function(value) { text <- asa_api_scalar_chr(value, default = "") if (nzchar(text)) { return(text) } NULL } asa_api_optional_scalar_int <- function(value) { number <- asa_api_scalar_int(value, default = NA_integer_) if (!is.na(number)) { return(number) } NULL } asa_api_gui_execution_mode <- function(execution, response_mode = NULL) { response_mode <- asa_api_scalar_chr(response_mode, default = "") if (identical(response_mode, "provider_direct_single")) { return("provider_direct") } execution <- asa_api_named_list(execution) execution_mode <- asa_api_scalar_chr(execution$mode, default = "") if (identical(execution_mode, "provider_direct")) { return("provider_direct") } "asa_agent" } asa_api_gui_execution_summary <- function(execution, response_mode = NULL) { execution <- asa_api_named_list(asa_api_to_plain(execution)) asa_api_drop_nulls(list( mode = asa_api_gui_execution_mode(execution, response_mode = response_mode), thread_id = asa_api_optional_scalar_chr(execution$thread_id), backend_status = asa_api_optional_scalar_chr(execution$backend_status), status_code = asa_api_optional_scalar_int(execution$status_code), tool_calls_used = asa_api_optional_scalar_int(execution$tool_calls_used), search_calls_used = asa_api_optional_scalar_int(execution$search_calls_used), action_step_count = asa_api_optional_scalar_int(execution$action_step_count) )) } asa_api_project_gui_result <- function(result, response_mode = NULL) { if (!is.list(result)) { return(result) } asa_api_drop_nulls(list( status = result$status %||% NULL, message = result$message %||% NULL, parsed = result$parsed %||% NULL, elapsed_time_min = result$elapsed_time_min %||% NULL, search_tier = result$search_tier %||% NULL, parsing_status = result$parsing_status %||% NULL, execution = asa_api_gui_execution_summary(result$execution, response_mode = response_mode) )) } asa_api_is_result_like <- function(value) { inherits(value, "asa_result") || (is.list(value) && !is.null(value$status) && !is.null(value$message)) } asa_api_single_mode <- function(payload, allow_direct_provider = FALSE) { use_direct_provider <- isTRUE(allow_direct_provider) && asa_api_to_bool(payload$use_direct_provider, default = FALSE) if (use_direct_provider) { return("provider_direct_single") } "asa_agent_single" } asa_api_run_single_via_asa <- function(payload, request_context = NULL) { route <- asa_api_request_context_route(request_context, "/v1/run") log_con <- asa_api_request_context_log_con(request_context) stage_ref <- new.env(parent = emptyenv()) stage_ref$value <- "apply_env_defaults" capture <- asa_api_new_runtime_capture() asa_api_invoke_with_runtime_diagnostics( fn = function() { stage_ref$value <- "apply_env_defaults" asa_api_apply_env_defaults() stage_ref$value <- "require_prompt" capture$prompt <- asa_api_require_prompt(payload) stage_ref$value <- "build_config" capture$config <- asa_api_build_config(payload) stage_ref$value <- "build_run_args" run_args <- asa_api_build_run_args(payload) capture$forwarded_arg_names <- names(run_args) capture$request_shape <- asa_api_drop_nulls(list( output_format = asa_api_scalar_chr(run_args$output_format, default = "text"), performance_profile = asa_api_scalar_chr(run_args$performance_profile, default = ""), use_plan_mode = if (!is.null(run_args$use_plan_mode)) isTRUE(run_args$use_plan_mode) else NULL )) stage_ref$value <- "prepare_args" args <- c(list(prompt = capture$prompt, config = capture$config), run_args) args <- asa_api_filter_formals(asa::run_task, args) stage_ref$value <- "invoke" result <- do.call(asa::run_task, args) include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE) include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE) stage_ref$value <- "normalize_result" asa_api_normalize_result( result, include_raw_output = include_raw_output, include_trace_json = include_trace_json ) }, mode = "asa_agent_single", route = route, payload = payload, prompt = capture$prompt, config = capture$config, forwarded_arg_names = capture$forwarded_arg_names, request_shape = capture$request_shape, stage_ref = stage_ref, log_con = log_con ) } asa_api_run_single_via_direct <- function(payload, request_context = NULL) { route <- asa_api_request_context_route(request_context, "/gui/query") log_con <- asa_api_request_context_log_con(request_context) stage_ref <- new.env(parent = emptyenv()) stage_ref$value <- "apply_env_defaults" capture <- asa_api_new_runtime_capture() asa_api_invoke_with_runtime_diagnostics( fn = function() { stage_ref$value <- "apply_env_defaults" asa_api_apply_env_defaults() stage_ref$value <- "resolve_direct_function" run_direct_task_fun <- asa_api_get_run_direct_task() stage_ref$value <- "require_prompt" capture$prompt <- asa_api_require_prompt(payload) stage_ref$value <- "build_config" capture$config <- asa_api_build_config(payload) stage_ref$value <- "build_direct_args" direct_args <- asa_api_build_direct_args(payload, run_direct_task_fun = run_direct_task_fun) capture$forwarded_arg_names <- names(direct_args) capture$request_shape <- asa_api_drop_nulls(list( output_format = asa_api_scalar_chr(direct_args$output_format, default = "text"), performance_profile = asa_api_scalar_chr(direct_args$performance_profile, default = ""), use_plan_mode = if (!is.null(direct_args$use_plan_mode)) isTRUE(direct_args$use_plan_mode) else NULL )) stage_ref$value <- "prepare_args" args <- c(list(prompt = capture$prompt, config = capture$config), direct_args) args <- asa_api_filter_formals(run_direct_task_fun, args) stage_ref$value <- "invoke" result <- do.call(run_direct_task_fun, args) include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE) include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE) stage_ref$value <- "normalize_result" asa_api_normalize_result( result, include_raw_output = include_raw_output, include_trace_json = include_trace_json ) }, mode = "provider_direct_single", route = route, payload = payload, prompt = capture$prompt, config = capture$config, forwarded_arg_names = capture$forwarded_arg_names, request_shape = capture$request_shape, stage_ref = stage_ref, log_con = log_con ) } asa_api_run_single <- function(payload, allow_direct_provider = FALSE, request_context = NULL) { if (identical(asa_api_single_mode(payload, allow_direct_provider = allow_direct_provider), "provider_direct_single")) { return(asa_api_run_single_via_direct(payload, request_context = request_context)) } asa_api_run_single_via_asa(payload, request_context = request_context) } asa_api_run_batch <- function(payload, request_context = NULL) { route <- asa_api_request_context_route(request_context, "/v1/batch") log_con <- asa_api_request_context_log_con(request_context) stage_ref <- new.env(parent = emptyenv()) stage_ref$value <- "apply_env_defaults" capture <- asa_api_new_runtime_capture() asa_api_invoke_with_runtime_diagnostics( fn = function() { stage_ref$value <- "apply_env_defaults" asa_api_apply_env_defaults() stage_ref$value <- "require_prompts" capture$prompts <- asa_api_require_prompts(payload) stage_ref$value <- "validate_batch_fields" asa_api_validate_batch_supported_fields(payload) stage_ref$value <- "build_config" capture$config <- asa_api_build_config(payload) stage_ref$value <- "build_batch_args" batch_args <- asa_api_build_batch_args(payload) capture$forwarded_arg_names <- names(batch_args) capture$request_shape <- asa_api_drop_nulls(list( output_format = asa_api_scalar_chr(batch_args$output_format, default = "text"), parallel = if (!is.null(batch_args$parallel)) isTRUE(batch_args$parallel) else NULL, progress = if (!is.null(batch_args$progress)) isTRUE(batch_args$progress) else NULL, performance_profile = asa_api_scalar_chr(batch_args$performance_profile, default = "") )) stage_ref$value <- "prepare_args" args <- c(list(prompts = capture$prompts, config = capture$config), batch_args) args <- asa_api_filter_formals(asa::run_task_batch, args) stage_ref$value <- "invoke" raw <- do.call(asa::run_task_batch, args) include_raw_output <- asa_api_to_bool(payload$include_raw_output, default = FALSE) include_trace_json <- asa_api_to_bool(payload$include_trace_json, default = FALSE) stage_ref$value <- "normalize_batch_result" if (is.data.frame(raw) && "asa_result" %in% names(raw)) { items <- lapply(raw$asa_result, asa_api_normalize_result, include_raw_output = include_raw_output, include_trace_json = include_trace_json ) } else if (is.list(raw) && length(raw) && all(vapply(raw, asa_api_is_result_like, logical(1)))) { items <- lapply(raw, asa_api_normalize_result, include_raw_output = include_raw_output, include_trace_json = include_trace_json ) } else if (is.list(raw) && length(raw) == 0L) { items <- list() } else { items <- list(asa_api_to_plain(raw)) } list( status = "success", results = items, count = length(items), circuit_breaker_aborted = isTRUE(attr(raw, "circuit_breaker_aborted")) ) }, mode = "asa_agent_batch", route = route, payload = payload, prompts = capture$prompts, config = capture$config, forwarded_arg_names = capture$forwarded_arg_names, request_shape = capture$request_shape, stage_ref = stage_ref, log_con = log_con ) } asa_api_run_gui_query <- function(payload, request_context = NULL) { route <- asa_api_request_context_route(request_context, "/gui/query") log_con <- asa_api_request_context_log_con(request_context) stage_ref <- new.env(parent = emptyenv()) stage_ref$value <- "run_single" response_mode <- asa_api_single_mode(payload, allow_direct_provider = TRUE) capture <- asa_api_new_runtime_capture() capture$prompt <- asa_api_scalar_chr(payload$prompt, default = "") run_payload <- payload run_payload$include_raw_output <- FALSE run_payload$include_trace_json <- FALSE run_args <- asa_api_named_list(run_payload$run) capture$request_shape <- asa_api_drop_nulls(list( output_format = asa_api_scalar_chr(run_args$output_format %||% run_payload$output_format, default = "text"), use_direct_provider = asa_api_to_bool(run_payload$use_direct_provider, default = FALSE) )) asa_api_invoke_with_runtime_diagnostics( fn = function() { stage_ref$value <- "run_single" result <- asa_api_run_single( run_payload, allow_direct_provider = TRUE, request_context = list(route = route, log_con = log_con) ) stage_ref$value <- "project_gui_result" asa_api_project_gui_result(result, response_mode = response_mode) }, mode = response_mode, route = route, payload = run_payload, prompt = capture$prompt, request_shape = capture$request_shape, stage_ref = stage_ref, log_con = log_con ) } asa_api_health_payload <- function(boot_error = NULL) { asa_installed <- requireNamespace("asa", quietly = TRUE) direct_provider_available <- asa_installed && isTRUE(asa_api_has_run_direct_task()) direct_provider_note <- NULL if (!asa_installed) { direct_provider_note <- "Direct provider mode unavailable: package `asa` is not installed." } else if (!direct_provider_available) { direct_provider_note <- "Direct provider mode unavailable: installed `asa` does not provide `run_direct_task`." } has_boot_error <- is.character(boot_error) && nzchar(trimws(boot_error)) tor_health <- asa_api_tor_health() healthy <- asa_installed && !has_boot_error && (!isTRUE(tor_health$tor_enabled) || isTRUE(tor_health$tor_ready)) c( list( status = if (healthy) "ok" else "degraded", service = "asa-api", time_utc = format(Sys.time(), tz = "UTC", usetz = TRUE), asa_installed = asa_installed, direct_provider_available = direct_provider_available, boot_error = if (has_boot_error) boot_error else NULL ), if (!is.null(direct_provider_note)) { list(direct_provider_note = direct_provider_note) }, list( defaults = list( backend = getOption("asa.default_backend", NULL), model = getOption("asa.default_model", NULL), conda_env = getOption("asa.default_conda_env", "asa_env"), use_browser = asa_api_to_bool(Sys.getenv("ASA_USE_BROWSER_DEFAULT", unset = "false"), default = FALSE) ) ), tor_health ) }