asa-api / R /asa_api_helpers.R
cjerzak's picture
add files
1307edc
`%||%` <- 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('"[^"]*"', '"<redacted>"', call_text)
call_text <- gsub("'[^']*'", "'<redacted>'", 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
)
}