cjerzak commited on
Commit
7f40cff
·
1 Parent(s): 8f895b0

add files

Browse files
Files changed (5) hide show
  1. Dockerfile +7 -3
  2. R/asa_api_helpers.R +28 -14
  3. README.md +3 -0
  4. Tests/api_contract_smoke.R +11 -0
  5. www/index.html +27 -2
Dockerfile CHANGED
@@ -43,7 +43,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
43
  r-cran-remotes \
44
  && rm -rf /var/lib/apt/lists/*
45
 
46
- ARG DOCKERFILE_REV=2026-03-04-r2u24-cxxabi-fix-1
 
 
47
 
48
  RUN echo "asa-api docker revision: ${DOCKERFILE_REV}"; \
49
  set -eux; \
@@ -68,8 +70,10 @@ ARG ASA_SOFTWARE_REF=main
68
 
69
  RUN git clone --depth 1 --branch "${ASA_SOFTWARE_REF}" "${ASA_SOFTWARE_REPO}" /opt/asa-software \
70
  && R -q -e "remotes::install_local('/opt/asa-software/asa', dependencies = TRUE, upgrade = 'never')" \
71
- && R -q -e "asa::build_backend(conda_env='asa_env', python_version='3.12', check_browser=FALSE, fix_browser=FALSE)" \
72
- && R -q -e "reticulate::use_condaenv('asa_env', required = TRUE); cfg <- reticulate::py_config(); cat('reticulate py_config python:', cfg[['python']], '\n'); reticulate::py_run_string('import sys; print(sys.version)')" \
 
 
73
  && R -q -e "gate <- c('asa','plumber','jsonlite','reticulate'); missing <- gate[!vapply(gate, function(p) requireNamespace(p, quietly = TRUE), logical(1))]; if (length(missing)) stop(sprintf('Post-asa gate failed; missing: %s', paste(missing, collapse = ', ')), call. = FALSE); cat('Post-asa package gate passed\n')" \
74
  && rm -rf /opt/asa-software/.git
75
 
 
43
  r-cran-remotes \
44
  && rm -rf /var/lib/apt/lists/*
45
 
46
+ ARG DOCKERFILE_REV=2026-03-10-openssl-hotfix-1
47
+ ARG ASA_CONDA_PYTHON_VERSION=3.12.3
48
+ ARG ASA_CONDA_OPENSSL_VERSION=3.0.13
49
 
50
  RUN echo "asa-api docker revision: ${DOCKERFILE_REV}"; \
51
  set -eux; \
 
70
 
71
  RUN git clone --depth 1 --branch "${ASA_SOFTWARE_REF}" "${ASA_SOFTWARE_REPO}" /opt/asa-software \
72
  && R -q -e "remotes::install_local('/opt/asa-software/asa', dependencies = TRUE, upgrade = 'never')" \
73
+ && /opt/conda/bin/conda create -n asa_env "python=${ASA_CONDA_PYTHON_VERSION}" "openssl=${ASA_CONDA_OPENSSL_VERSION}" pip setuptools \
74
+ && R -q -e "asa::build_backend(conda_env='asa_env', python_version='3.12', force=FALSE, check_browser=FALSE, fix_browser=FALSE)" \
75
+ && /opt/conda/bin/conda run -n asa_env python -c "import ssl, sys; print(sys.version); print(ssl.OPENSSL_VERSION)" \
76
+ && R -q -e "reticulate::use_condaenv('asa_env', required = TRUE); cfg <- reticulate::py_config(); cat('reticulate py_config python:', cfg[['python']], '\n'); reticulate::py_run_string('import ssl, sys; print(sys.version); print(ssl.OPENSSL_VERSION)')" \
77
  && R -q -e "gate <- c('asa','plumber','jsonlite','reticulate'); missing <- gate[!vapply(gate, function(p) requireNamespace(p, quietly = TRUE), logical(1))]; if (length(missing)) stop(sprintf('Post-asa gate failed; missing: %s', paste(missing, collapse = ', ')), call. = FALSE); cat('Post-asa package gate passed\n')" \
78
  && rm -rf /opt/asa-software/.git
79
 
R/asa_api_helpers.R CHANGED
@@ -651,22 +651,36 @@ asa_api_run_batch <- function(payload) {
651
  asa_api_health_payload <- function(boot_error = NULL) {
652
  asa_installed <- requireNamespace("asa", quietly = TRUE)
653
  direct_provider_available <- asa_installed && isTRUE(asa_api_has_run_direct_task())
 
 
 
 
 
 
654
  has_boot_error <- is.character(boot_error) && nzchar(trimws(boot_error))
655
  tor_health <- asa_api_tor_health()
656
  healthy <- asa_installed && !has_boot_error && (!isTRUE(tor_health$tor_enabled) || isTRUE(tor_health$tor_ready))
657
 
658
- c(list(
659
- status = if (healthy) "ok" else "degraded",
660
- service = "asa-api",
661
- time_utc = format(Sys.time(), tz = "UTC", usetz = TRUE),
662
- asa_installed = asa_installed,
663
- direct_provider_available = direct_provider_available,
664
- boot_error = if (has_boot_error) boot_error else NULL,
665
- defaults = list(
666
- backend = getOption("asa.default_backend", NULL),
667
- model = getOption("asa.default_model", NULL),
668
- conda_env = getOption("asa.default_conda_env", "asa_env"),
669
- use_browser = asa_api_to_bool(Sys.getenv("ASA_USE_BROWSER_DEFAULT", unset = "false"), default = FALSE)
670
- )
671
- ), tor_health)
 
 
 
 
 
 
 
 
672
  }
 
651
  asa_api_health_payload <- function(boot_error = NULL) {
652
  asa_installed <- requireNamespace("asa", quietly = TRUE)
653
  direct_provider_available <- asa_installed && isTRUE(asa_api_has_run_direct_task())
654
+ direct_provider_note <- NULL
655
+ if (!asa_installed) {
656
+ direct_provider_note <- "Direct provider mode unavailable: package `asa` is not installed."
657
+ } else if (!direct_provider_available) {
658
+ direct_provider_note <- "Direct provider mode unavailable: installed `asa` does not provide `run_direct_task`."
659
+ }
660
  has_boot_error <- is.character(boot_error) && nzchar(trimws(boot_error))
661
  tor_health <- asa_api_tor_health()
662
  healthy <- asa_installed && !has_boot_error && (!isTRUE(tor_health$tor_enabled) || isTRUE(tor_health$tor_ready))
663
 
664
+ c(
665
+ list(
666
+ status = if (healthy) "ok" else "degraded",
667
+ service = "asa-api",
668
+ time_utc = format(Sys.time(), tz = "UTC", usetz = TRUE),
669
+ asa_installed = asa_installed,
670
+ direct_provider_available = direct_provider_available,
671
+ boot_error = if (has_boot_error) boot_error else NULL
672
+ ),
673
+ if (!is.null(direct_provider_note)) {
674
+ list(direct_provider_note = direct_provider_note)
675
+ },
676
+ list(
677
+ defaults = list(
678
+ backend = getOption("asa.default_backend", NULL),
679
+ model = getOption("asa.default_model", NULL),
680
+ conda_env = getOption("asa.default_conda_env", "asa_env"),
681
+ use_browser = asa_api_to_bool(Sys.getenv("ASA_USE_BROWSER_DEFAULT", unset = "false"), default = FALSE)
682
+ )
683
+ ),
684
+ tor_health
685
+ )
686
  }
README.md CHANGED
@@ -188,6 +188,7 @@ R dependency strategy in this image:
188
  - Base image is `rocker/r2u:24.04`.
189
  - Core R runtime packages (`plumber`, `jsonlite`, `reticulate`, `remotes`) are installed as binary apt packages (`r-cran-*`), not compiled from CRAN source.
190
  - This avoids source-compile failures such as `sodium` -> `plumber` install breaks.
 
191
  - Runtime linker guardrails are set so `reticulate` prefers conda environment libraries (`/opt/conda/envs/asa_env/lib` and `/opt/conda/lib`) to avoid C++ ABI loader mismatches.
192
  - Tor is installed in the image and started before the API. If Tor does not become ready, the container exits instead of serving direct search traffic.
193
 
@@ -195,6 +196,8 @@ Build args:
195
 
196
  - `ASA_SOFTWARE_REPO` (default: `https://github.com/cjerzak/asa-software`)
197
  - `ASA_SOFTWARE_REF` (default: `main`)
 
 
198
 
199
  Local build:
200
 
 
188
  - Base image is `rocker/r2u:24.04`.
189
  - Core R runtime packages (`plumber`, `jsonlite`, `reticulate`, `remotes`) are installed as binary apt packages (`r-cran-*`), not compiled from CRAN source.
190
  - This avoids source-compile failures such as `sodium` -> `plumber` install breaks.
191
+ - The Docker build pre-creates `asa_env` with pinned `python=3.12.3` and `openssl=3.0.13` before calling `asa::build_backend()`, reducing rebuild drift that can break `reticulate` SSL imports on Ubuntu 24.04 / Hugging Face rebuilds.
192
  - Runtime linker guardrails are set so `reticulate` prefers conda environment libraries (`/opt/conda/envs/asa_env/lib` and `/opt/conda/lib`) to avoid C++ ABI loader mismatches.
193
  - Tor is installed in the image and started before the API. If Tor does not become ready, the container exits instead of serving direct search traffic.
194
 
 
196
 
197
  - `ASA_SOFTWARE_REPO` (default: `https://github.com/cjerzak/asa-software`)
198
  - `ASA_SOFTWARE_REF` (default: `main`)
199
+ - `ASA_CONDA_PYTHON_VERSION` (default: `3.12.3`)
200
+ - `ASA_CONDA_OPENSSL_VERSION` (default: `3.0.13`)
201
 
202
  Local build:
203
 
Tests/api_contract_smoke.R CHANGED
@@ -115,6 +115,17 @@ assert_true(
115
  identical(isTRUE(health_payload$direct_provider_available), isTRUE(asa_api_has_run_direct_task())),
116
  "Health payload should report direct-provider availability from the same capability check."
117
  )
 
 
 
 
 
 
 
 
 
 
 
118
 
119
  {
120
  original_asa <- asa_api_run_single_via_asa
 
115
  identical(isTRUE(health_payload$direct_provider_available), isTRUE(asa_api_has_run_direct_task())),
116
  "Health payload should report direct-provider availability from the same capability check."
117
  )
118
+ if (isTRUE(health_payload$direct_provider_available)) {
119
+ assert_true(
120
+ is.null(health_payload$direct_provider_note),
121
+ "Health payload should omit the direct-provider note when the capability is available."
122
+ )
123
+ } else {
124
+ assert_true(
125
+ is.character(health_payload$direct_provider_note) && nzchar(trimws(health_payload$direct_provider_note)),
126
+ "Health payload should explain why direct-provider mode is unavailable."
127
+ )
128
+ }
129
 
130
  {
131
  original_asa <- asa_api_run_single_via_asa
www/index.html CHANGED
@@ -241,6 +241,11 @@
241
  color: rgba(16, 34, 46, 0.66);
242
  }
243
 
 
 
 
 
 
244
  .api-guide {
245
  margin-top: 2px;
246
  padding: 12px;
@@ -270,11 +275,19 @@
270
  font-weight: 500;
271
  }
272
 
 
 
 
 
273
  .toggle input {
274
  width: auto;
275
  margin: 0;
276
  }
277
 
 
 
 
 
278
  .mode-badge {
279
  display: inline-flex;
280
  align-items: center;
@@ -359,6 +372,7 @@
359
  const promptInput = document.getElementById("prompt");
360
  const outputFormatInput = document.getElementById("output-format");
361
  const useDirectProviderInput = document.getElementById("use-direct-provider");
 
362
  const directProviderNoteEl = document.getElementById("direct-provider-note");
363
  const outputPre = document.getElementById("output");
364
  const statusEl = document.getElementById("status");
@@ -381,15 +395,26 @@
381
  function setDirectProviderAvailability(available, note) {
382
  if (available) {
383
  useDirectProviderInput.disabled = false;
 
 
 
 
384
  directProviderNoteEl.hidden = true;
385
  directProviderNoteEl.textContent = "";
 
386
  return;
387
  }
388
 
 
389
  useDirectProviderInput.checked = false;
390
  useDirectProviderInput.disabled = true;
 
 
 
 
391
  directProviderNoteEl.hidden = false;
392
- directProviderNoteEl.textContent = note || "Direct provider mode requires an asa build that provides run_direct_task.";
 
393
  }
394
 
395
  async function loadCapabilities() {
@@ -405,7 +430,7 @@
405
 
406
  const data = await response.json();
407
  if (data && data.direct_provider_available === false) {
408
- setDirectProviderAvailability(false);
409
  } else if (data && data.direct_provider_available === true) {
410
  setDirectProviderAvailability(true);
411
  }
 
241
  color: rgba(16, 34, 46, 0.66);
242
  }
243
 
244
+ .hint.warn {
245
+ color: var(--err);
246
+ font-weight: 500;
247
+ }
248
+
249
  .api-guide {
250
  margin-top: 2px;
251
  padding: 12px;
 
275
  font-weight: 500;
276
  }
277
 
278
+ .toggle.disabled {
279
+ opacity: 0.62;
280
+ }
281
+
282
  .toggle input {
283
  width: auto;
284
  margin: 0;
285
  }
286
 
287
+ .toggle input:disabled {
288
+ cursor: not-allowed;
289
+ }
290
+
291
  .mode-badge {
292
  display: inline-flex;
293
  align-items: center;
 
372
  const promptInput = document.getElementById("prompt");
373
  const outputFormatInput = document.getElementById("output-format");
374
  const useDirectProviderInput = document.getElementById("use-direct-provider");
375
+ const directProviderToggleEl = useDirectProviderInput.closest(".toggle");
376
  const directProviderNoteEl = document.getElementById("direct-provider-note");
377
  const outputPre = document.getElementById("output");
378
  const statusEl = document.getElementById("status");
 
395
  function setDirectProviderAvailability(available, note) {
396
  if (available) {
397
  useDirectProviderInput.disabled = false;
398
+ if (directProviderToggleEl) {
399
+ directProviderToggleEl.classList.remove("disabled");
400
+ directProviderToggleEl.removeAttribute("title");
401
+ }
402
  directProviderNoteEl.hidden = true;
403
  directProviderNoteEl.textContent = "";
404
+ directProviderNoteEl.classList.remove("warn");
405
  return;
406
  }
407
 
408
+ const message = note || "Direct provider mode is unavailable on this server because the installed `asa` build does not provide run_direct_task().";
409
  useDirectProviderInput.checked = false;
410
  useDirectProviderInput.disabled = true;
411
+ if (directProviderToggleEl) {
412
+ directProviderToggleEl.classList.add("disabled");
413
+ directProviderToggleEl.title = message;
414
+ }
415
  directProviderNoteEl.hidden = false;
416
+ directProviderNoteEl.textContent = message;
417
+ directProviderNoteEl.classList.add("warn");
418
  }
419
 
420
  async function loadCapabilities() {
 
430
 
431
  const data = await response.json();
432
  if (data && data.direct_provider_available === false) {
433
+ setDirectProviderAvailability(false, data.direct_provider_note);
434
  } else if (data && data.direct_provider_available === true) {
435
  setDirectProviderAvailability(true);
436
  }