Spaces:
Runtime error
Runtime error
Upload 4 files
Browse files- 00_db_helper.R +25 -0
- Dockerfile +59 -0
- plumber.R +139 -0
- run_backend.R +1 -0
00_db_helper.R
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 00_db_helper.R
|
| 2 |
+
library(DBI)
|
| 3 |
+
library(RPostgres)
|
| 4 |
+
|
| 5 |
+
# Cek apakah file .env ada di folder (biasanya saat di laptop lokal)
|
| 6 |
+
# Jika tidak ada (seperti di Hugging Face), lewati saja agar tidak error
|
| 7 |
+
if (file.exists(".env")) {
|
| 8 |
+
library(dotenv)
|
| 9 |
+
load_dot_env(".env")
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# Fungsi sentral untuk koneksi ke Supabase
|
| 14 |
+
connect_supabase <- function() {
|
| 15 |
+
dbConnect(
|
| 16 |
+
RPostgres::Postgres(),
|
| 17 |
+
host = Sys.getenv("DB_HOST1"),
|
| 18 |
+
dbname = Sys.getenv("DB_NAME1"),
|
| 19 |
+
user = Sys.getenv("DB_USER1"),
|
| 20 |
+
password = Sys.getenv("DB_PSWD1"),
|
| 21 |
+
port = as.integer(Sys.getenv("DB_PORT1")),
|
| 22 |
+
bigint = "integer",
|
| 23 |
+
options = "-c search_path=mlops"
|
| 24 |
+
)
|
| 25 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# Dockerfile untuk Deployment
|
| 3 |
+
# ============================================================================
|
| 4 |
+
|
| 5 |
+
FROM rocker/r-ver:4.5.0
|
| 6 |
+
|
| 7 |
+
# Install system dependencies
|
| 8 |
+
# Pasang dependensi sistem Linux yang dibutuhkan
|
| 9 |
+
RUN apt-get update && apt-get install -y \
|
| 10 |
+
libcurl4-openssl-dev \
|
| 11 |
+
libssl-dev \
|
| 12 |
+
libxml2-dev \
|
| 13 |
+
libfontconfig1-dev \
|
| 14 |
+
libharfbuzz-dev \
|
| 15 |
+
libfribidi-dev \
|
| 16 |
+
libfreetype6-dev \
|
| 17 |
+
libpng-dev \
|
| 18 |
+
libtiff5-dev \
|
| 19 |
+
libjpeg-dev \
|
| 20 |
+
cmake \
|
| 21 |
+
libpq-dev \
|
| 22 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 23 |
+
|
| 24 |
+
# Set working directory
|
| 25 |
+
WORKDIR /app
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# Install R packages
|
| 29 |
+
# RUN R -e "install.packages(c( \
|
| 30 |
+
# 'tidymodels', \
|
| 31 |
+
# 'vetiver', \
|
| 32 |
+
# 'pins', \
|
| 33 |
+
# 'plumber', \
|
| 34 |
+
# 'ranger', \
|
| 35 |
+
# 'httr', \
|
| 36 |
+
# 'jsonlite', \
|
| 37 |
+
# 'dplyr', \
|
| 38 |
+
# 'ggplot2' \
|
| 39 |
+
# ), repos='https://cloud.r-project.org/')"
|
| 40 |
+
|
| 41 |
+
# 5. Salin semua file dari proyek lokal Anda ke dalam kontainer
|
| 42 |
+
COPY . .
|
| 43 |
+
|
| 44 |
+
# COPY renv.lock ./
|
| 45 |
+
# RUN R -e "install.packages('renv'); renv::restore()"
|
| 46 |
+
|
| 47 |
+
# Pasang paket R versi biner (Cepat dan Anti-Gagal Kompilasi)
|
| 48 |
+
# Kita menggunakan repositori 'noble' karena rocker:4.5.0 berbasis Ubuntu 24.04
|
| 49 |
+
RUN R -e "options(repos = c(CRAN = 'https://packagemanager.posit.co/cran/__linux__/noble/latest')); install.packages(c('plumber', 'vetiver', 'pins', 'DBI', 'RPostgres', 'jsonlite', 'dotenv', 'purrr', 'dplyr', 'ranger', 'tidymodels'))"
|
| 50 |
+
|
| 51 |
+
# 6. Atur hak akses agar kontainer bisa menulis file (misal saat mengunduh model)
|
| 52 |
+
RUN chmod -R 777 /app
|
| 53 |
+
|
| 54 |
+
# 7. Berikan instruksi port dinamis (Hugging Face default adalah 7860)
|
| 55 |
+
ENV PORT=7860
|
| 56 |
+
EXPOSE 7860
|
| 57 |
+
|
| 58 |
+
# 8. Jalankan eksekusi API Server menggunakan skrip yang sudah kita buat
|
| 59 |
+
CMD ["R", "-e", "source('run_backend.R')"]
|
plumber.R
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
library(plumber)
|
| 2 |
+
library(dplyr)
|
| 3 |
+
library(DBI)
|
| 4 |
+
library(RPostgres)
|
| 5 |
+
library(httr2)
|
| 6 |
+
library(jsonlite)
|
| 7 |
+
|
| 8 |
+
source("00_db_helper.R")
|
| 9 |
+
# source("backend/00_db_helper.R")
|
| 10 |
+
|
| 11 |
+
# Baca statistik training sekali saja saat API start
|
| 12 |
+
train_stats <- NULL
|
| 13 |
+
if (file.exists("models/train_statistics.rds")) {
|
| 14 |
+
train_stats <- readRDS("models/train_statistics.rds")
|
| 15 |
+
# print(train_stats)
|
| 16 |
+
} else {
|
| 17 |
+
con <- connect_supabase()
|
| 18 |
+
train_stats <- dbGetQuery(con, "SELECT * FROM mlops.train_statistics ORDER BY create_date DESC LIMIT 1")
|
| 19 |
+
DBI::dbDisconnect(con)
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
#* @filter cors
|
| 23 |
+
function(req, res) {
|
| 24 |
+
res$setHeader("Access-Control-Allow-Origin", "*")
|
| 25 |
+
res$setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
| 26 |
+
res$setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
| 27 |
+
|
| 28 |
+
if (req$REQUEST_METHOD == "OPTIONS") {
|
| 29 |
+
res$status <- 200
|
| 30 |
+
return(list())
|
| 31 |
+
}
|
| 32 |
+
plumber::forward()
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
#* Mengambil ringkasan performa model
|
| 36 |
+
#* @param window:int Ukuran window data
|
| 37 |
+
#* @get /performance
|
| 38 |
+
function(window = 100) {
|
| 39 |
+
con <- connect_supabase()
|
| 40 |
+
db_res <- dbGetQuery(con,
|
| 41 |
+
"SELECT predicted_value FROM mlops.predictions ORDER BY timestamp DESC LIMIT $1",
|
| 42 |
+
list(as.integer(window))
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
total_db_res <- dbGetQuery(con, "SELECT count(predicted_value) as total FROM mlops.predictions")
|
| 46 |
+
dbDisconnect(con)
|
| 47 |
+
|
| 48 |
+
if(nrow(db_res) == 0) {
|
| 49 |
+
return(list(total = 0, mean_pred = NA, sd_pred = NA))
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
list(
|
| 53 |
+
total = total_db_res$total,
|
| 54 |
+
window = window,
|
| 55 |
+
mean_pred = mean(db_res$predicted_value, na.rm = TRUE),
|
| 56 |
+
sd_pred = sd(db_res$predicted_value, na.rm = TRUE),
|
| 57 |
+
min_pred = min(db_res$predicted_value, na.rm = TRUE),
|
| 58 |
+
max_pred = max(db_res$predicted_value, na.rm = TRUE)
|
| 59 |
+
)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
#* Mengambil data logs dan drift yang sudah diproses
|
| 63 |
+
#* @param window:int Ukuran window data
|
| 64 |
+
#* @param drift_feature:str Fitur yang ingin dihitung Z-Score nya
|
| 65 |
+
#* @get /logs-drift
|
| 66 |
+
function(window = 100) {
|
| 67 |
+
con <- connect_supabase()
|
| 68 |
+
query_sql <- "
|
| 69 |
+
WITH exploded_data AS (
|
| 70 |
+
SELECT
|
| 71 |
+
request_id,
|
| 72 |
+
timestamp,
|
| 73 |
+
variant, version, row_id,
|
| 74 |
+
predicted_value,
|
| 75 |
+
(input_features->>'displ')::numeric AS displ,
|
| 76 |
+
(input_features->>'year')::numeric AS year,
|
| 77 |
+
(input_features->>'cyl')::numeric AS cyl,
|
| 78 |
+
(input_features->>'class')::text as class,
|
| 79 |
+
status,
|
| 80 |
+
COALESCE(error_message::text, '') AS error_message
|
| 81 |
+
FROM mlops.predictions
|
| 82 |
+
WHERE status = 'SUCCESS'
|
| 83 |
+
ORDER BY timestamp DESC
|
| 84 |
+
LIMIT %d
|
| 85 |
+
)
|
| 86 |
+
SELECT
|
| 87 |
+
*,
|
| 88 |
+
ABS(displ - %f) / %f AS z_displ,
|
| 89 |
+
ABS(year - %f) / %f AS z_year,
|
| 90 |
+
ABS(cyl - %f) / %f AS z_cyl
|
| 91 |
+
FROM exploded_data;
|
| 92 |
+
"
|
| 93 |
+
# print(train_stats)
|
| 94 |
+
query_filled <- sprintf(
|
| 95 |
+
query_sql,
|
| 96 |
+
as.integer(window),
|
| 97 |
+
train_stats$displ_mean, train_stats$displ_sd,
|
| 98 |
+
train_stats$year_mean, train_stats$year_sd,
|
| 99 |
+
train_stats$cyl_mean, train_stats$cyl_sd
|
| 100 |
+
)
|
| 101 |
+
# print(query_filled)
|
| 102 |
+
|
| 103 |
+
final_data <- dbGetQuery(con, query_filled)
|
| 104 |
+
# print("final_data:")
|
| 105 |
+
# print(head(final_data))
|
| 106 |
+
# print(str(final_data))
|
| 107 |
+
|
| 108 |
+
# Jika query kosong, langsung kembalikan data frame kosong
|
| 109 |
+
if(nrow(final_data) == 0) {
|
| 110 |
+
dbDisconnect(con)
|
| 111 |
+
return(list(data = data.frame()))
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
actuals_db <- tryCatch({
|
| 115 |
+
dbGetQuery(con, "SELECT request_id, row_id, actual_value FROM mlops.actuals;")
|
| 116 |
+
}, error = function(e) {
|
| 117 |
+
data.frame(request_id = character(), row_id = character(), actual_value = numeric())
|
| 118 |
+
})
|
| 119 |
+
dbDisconnect(con)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# Gabung Actual
|
| 123 |
+
# print(nrow(actuals_db) > 0)
|
| 124 |
+
# print("row_id" %in% names(final_data))
|
| 125 |
+
if (nrow(actuals_db) > 0 && "row_id" %in% names(final_data)) {
|
| 126 |
+
actuals_db$request_id <- as.character(actuals_db$request_id)
|
| 127 |
+
actuals_db$row_id <- as.character(actuals_db$row_id)
|
| 128 |
+
final_data <- final_data %>%
|
| 129 |
+
left_join(actuals_db, by = c("request_id", "row_id"))
|
| 130 |
+
# print(final_data)
|
| 131 |
+
} else {
|
| 132 |
+
final_data$actual_value <- NA_real_
|
| 133 |
+
}
|
| 134 |
+
# print("final_data:")
|
| 135 |
+
# print(nrow(final_data))
|
| 136 |
+
# print(str(final_data))
|
| 137 |
+
|
| 138 |
+
return(final_data)
|
| 139 |
+
}
|
run_backend.R
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
plumber::plumb("backend/plumber.R")$run(port=7860)
|