# ============================================================================ # Script 2: Deployment API dengan Plumber dan Vetiver # ============================================================================ library(plumber) library(vetiver) library(pins) library(dplyr) library(purrr) library(jsonlite) library(RPostgres) library(tidymodels) library(ranger) # Cek apakah file .env ada di folder (biasanya saat di laptop lokal) # Jika tidak ada (seperti di Hugging Face), lewati saja agar tidak error if (file.exists(".env")) { library(dotenv) load_dot_env(".env") } source("00_db_helper.R") # Load model and metadata model_board <- NULL v <- NULL train_stats <- NULL model_metadata <- NULL tryCatch({ model_board <- board_folder("models", versioned = TRUE) v <- vetiver_pin_read(model_board, "mpg_highway_model") # print(v) # 🟢 PENYESUAIAN HUGGING FACE: # Karena 03_run_server.R sudah mengunduh file .rds langsung ke folder "models", # kita tidak perlu menggunakan board_folder() yang mencari struktur folder timestamp. # Kita cukup membaca file RDS-nya secara langsung! # Membaca model # v <- readRDS("models/mpg_highway_model.rds") # Load training statistics if (file.exists("models/train_statistics.rds")) { train_stats <- readRDS("models/train_statistics.rds") } # Load model metadata if (file.exists("models/model_metadata.rds")) { model_metadata <- readRDS("models/model_metadata.rds") } message("✓ Model loaded successfully") }, error = function(e) { message("⚠️ No model found. Please run 01_train_and_version.R first") message(" API will start in limited mode") }) # ============================================================================ # CREATE API # ============================================================================ #* @apiTitle MPG Highway Prediction API #* @apiDescription API untuk prediksi highway mileage dengan monitoring api <- pr() %>% # --- VETIVER ENDPOINTS (OTOMATIS) --- # Ini akan otomatis membuat: # - POST /predict : untuk prediksi standar (tanpa row_id) # - GET /ping : health check vetiver_api(v) %>% # 2. PAKSA ubah Judul dan Deskripsi menggunakan fungsi R (Bukan komentar) pr_set_api_spec(function(spec) { spec$info$title <- "MPG Highway Prediction API" spec$info$version <- "1.0.0" spec$info$description <- "API untuk prediksi highway mileage dengan monitoring" # Tambahkan deskripsi untuk endpoint /ping (Health Check) if (!is.null(spec$paths$`/ping`$get)) { spec$paths$`/ping`$get$summary <- "REST API Health Check" spec$paths$`/ping`$get$description <- "Memastikan bahwa server API berjalan dengan baik dan model siap digunakan." } # Tambahkan deskripsi untuk endpoint /predict (Bawaan Vetiver) if (!is.null(spec$paths$`/predict`$post)) { spec$paths$`/predict`$post$summary <- "Prediksi Standar Vetiver" spec$paths$`/predict`$post$description <- "Melakukan prediksi konsumsi bahan bakar (highway) menggunakan skema standar Vetiver tanpa menyertakan row_id." # PAKSA masukkan skema parameter JSON Body ke Swagger spec$paths$`/predict`$post$requestBody <- list( required = TRUE, content = list( `application/json` = list( schema = list( type = "object", properties = list( displ = list(type = "number", example = 2.4, description = "Engine displacement in liters"), year = list(type = "integer", example = 2008, description = "Year of manufacture"), cyl = list(type = "integer", example = 4, description = "Number of cylinders"), class = list(type = "string", example = "compact", description = "Vehicle class") ), required = c("displ", "year", "cyl", "class") # Opsional: Tandai kolom yang wajib diisi ) ) ) ) } # --------------------------------------------------------- # Deskripsi untuk Endpoint Kustom (DI SINI KUNCINYA!) # --------------------------------------------------------- if (!is.null(spec$paths$`/predict_custom`$post)) { spec$paths$`/predict_custom`$post$summary <- "Prediksi Kustom & Logging ke Database" spec$paths$`/predict_custom`$post$description <- "Endpoint khusus untuk melakukan prediksi yang secara otomatis memisahkan 'row_id' sebelum diprediksi dan menyimpannya bersama payload ke log SQLite. Digunakan untuk siklus monitoring penuh." spec$paths$`/predict_custom`$post$requestBody <- list( required = TRUE, content = list( `application/json` = list( schema = list( type = "object", properties = list( row_id = list(type = "string", example = "1", description = "ID Baris kustom untuk pemetaan data aktual"), displ = list(type = "number", example = 2.4), year = list(type = "integer", example = 2008), cyl = list(type = "integer", example = 4), class = list(type = "string", example = "compact") ) ) ) ) ) } if (!is.null(spec$paths$`/metadata`$get)) { spec$paths$`/metadata`$get$summary <- "Informasi Detail Model & Metrik Evaluasi" spec$paths$`/metadata`$get$description <- "Menampilkan nama model, deskripsi, tanggal training, jumlah baris data latih, dan metrik hasil evaluasi (RMSE, Rsq, MAE)." } if (!is.null(spec$paths$`/prototype`$get)) { spec$paths$`/prototype`$get$summary <- "Akses Dashboard Prototype" spec$paths$`/prototype`$get$description <- "Mengembalikan tampilan antarmuka (UI) sederhana untuk mencoba prediksi secara interaktif langsung dari peramban." } spec }) %>% # --- CUSTOM ENDPOINTS --- #* Prediksi Kustom dengan Pencatatan row_id ke SQLite #* @post /predict_custom #* @summary Prediksi Kustom & Logging ke Database #* @description Endpoint khusus untuk melakukan prediksi yang secara otomatis memisahkan 'row_id' sebelum diprediksi dan menyimpannya bersama payload ke log SQLite. Digunakan untuk siklus monitoring penuh. #* @serializer unboxedJSON #* @parser json list(simplifyDataFrame = FALSE) pr_post("/predict_custom", function(req) { # 1. Parsing body JSON otomatis menjadi Data Frame utuh oleh Plumber if(!is.data.frame(req$body)){ input_data <- as.data.frame(req$body) } else { input_data <- req$body } num_rows <- nrow(input_data) # Generate Request ID Unik req_id <- paste0("REQ-", format(Sys.time(), "%y%m%d%H%M%S"), "-", sample(1000:9999, 1)) # Ambil prototype dari objek vetiver model_prototype <- v$prototype # Pisahkan row_id agar tidak ikut divalidasi atau diprediksi data_for_pred <- input_data[, names(input_data) %in% names(model_prototype), drop = FALSE] # 2. Bungkus seluruh proses dalam satu tryCatch (All-or-Nothing) response <- tryCatch({ # 🕵️‍♂️ VALIDASI OTOMATIS RESMI: # Kita ambil ptype (prototype) resmi dari objek vetiver prototype_data <- v$prototype # Gunakan vctrs untuk memaksa validasi tipe data secara massal # Jika ada kolom yang tipenya salah (misal character ke numeric), ini akan langsung melempar eror! valid_data <- vctrs::vec_cast(data_for_pred, prototype_data) # EKSEKUSI PREDIKSI MASSAL (Menggunakan data yang sudah tervalidasi) pred_result <- predict(v, valid_data) predictions_vector <- as.numeric(pred_result$.pred) # 3. Siapkan Data untuk Bulk Insert ke SQLite # 3. Siapkan Data untuk Bulk Insert ke SQLite # Siapkan Data untuk Bulk Insert ke SQLite batch_db_data <- data.frame( request_id = paste0(req_id, "-", 1:num_rows), timestamp = rep(as.character(Sys.time()), num_rows), variant = rep("standard", num_rows), version = rep(v$metadata$version[1], num_rows), # 🔑 Tambahkan ini agar masuk ke database! row_id = input_data$row_id, input_features = sapply(1:num_rows, function(i) { jsonlite::toJSON(as.list(input_data[i, , drop = FALSE]), auto_unbox = TRUE) }), predicted_value = predictions_vector, status = "SUCCESS", error_message = NA_character_ ) # Eksekusi Bulk Insert con <- connect_supabase() # 1. Pastikan R mengarah ke schema mlops DBI::dbExecute(con, "SET search_path TO mlops") DBI::dbBegin(con) tryCatch({ # 1. Buat Query INSERT Standar secara dinamis # Kita susun teks SQL: INSERT INTO predictions (col1, col2) VALUES ($1, $2) placeholders <- paste0("$", 1:ncol(batch_db_data), collapse = ", ") columns <- paste(names(batch_db_data), collapse = ", ") sql_query <- paste0("INSERT INTO mlops.predictions (", columns, ") VALUES (", placeholders, ")") # 2. Eksekusi query secara aman menggunakan dbExecute # Ini mengirimkan data murni tanpa memicu fungsi COPY bawaan RPostgres for (i in 1:nrow(batch_db_data)) { DBI::dbExecute(con, sql_query, unname(as.list(batch_db_data[i, ]))) } DBI::dbCommit(con) cat("🎉 Sukses murni! Log berhasil disimpan ke Supabase.\n") }, error = function(e) { DBI::dbRollback(con) cat("❌ Gagal menyimpan log ke Supabase:", e$message, "\n") }) DBI::dbDisconnect(con) # Bentuk respon sukses untuk Client results_list <- lapply(1:num_rows, function(i) { list( row_id = input_data$row_id[i], status = "SUCCESS", prediction = round(predictions_vector[i], 4) ) }) list( request_id = req_id, status = "COMPLETED", results = results_list ) }, error = function(e) { # Ambil pesan error asli raw_msg <- e$message # Deteksi jika error disebabkan oleh ketidakcocokan tipe data (vctrs) clean_msg <- if (grepl("Can't convert", raw_msg)) { # Ekstrak nama kolom dari pesan error vctrs menggunakan regex col_match <- regmatches(raw_msg, regexec("`data_for_pred\\$(.*?)`", raw_msg))[[1]][2] paste0("❌ Gagal Validasi: Tipe data pada kolom [", col_match, "] tidak sesuai dengan standar model.") } else { paste0("❌ Kesalahan Sistem: ", raw_msg) } # Kembalikan respon gagal total yang sudah rapi list( request_id = req_id, status = "FAILED", message = clean_msg ) }) return(response) }) %>% #* Model metadata #* @get /metadata #* @summary Informasi Detail Model & Metrik Evaluasi #* @description Menampilkan nama model, deskripsi, tanggal training, jumlah baris data latih, dan metrik hasil evaluasi (RMSE, Rsq, MAE). Membantu Anda mengidentifikasi performa baseline model yang sedang aktif. #* @serializer unboxedJSON pr_get("/metadata", function() { if (is.null(v)) { return(list( error = "No model loaded", message = "Please train model first" )) } result <- list( model_name = v$model_name, description = v$description ) if (!is.null(model_metadata)) { result$train_date <- as.character(model_metadata$train_date) result$train_rows <- model_metadata$train_rows result$test_rmse <- model_metadata$test_rmse result$test_rsq <- model_metadata$test_rsq result$test_mae <- model_metadata$test_mae } return(result) }) %>% #* @assets ./index.html / #* @get / pr_get("/", function(req, res) { res$status <- 200 res$setHeader("Content-Type", "text/html") include_html("index.html", res) }) api