| `%||%` <- 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 |
| ) |
| } |
|
|