File size: 11,994 Bytes
1465190
 
 
 
 
 
 
 
 
 
 
7a3dfe0
 
1465190
f8e41c0
 
 
 
 
 
 
1465190
 
 
 
 
 
 
 
 
d11bd0e
 
1465190
 
 
 
 
 
 
 
d11bd0e
1465190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f8e41c0
 
 
1a750a0
f8e41c0
 
 
 
1465190
1a750a0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
# ============================================================================
# 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