rmachine-learningdeploymentplumber

How to send curl variables to plumber function dynamically?


I want to dynamically call the plumber API based on any number of input variables. I need to map the curl input to the input of a function name. For example if the function has an input hi then, curl -s --data 'hi=2' means that hi=2 should be passed as an input parameter to the function. This can be done directly in R with match.call() but it is failing while calling it through the plumber API.

Take the function

#' @post /API
#' @serializer unboxedJSON
tmp <- function(hi) {

  out <- list(hi=hi)

  out <- toJSON(out, pretty = TRUE, auto_unbox = TRUE)

  return(out)

}

tmp(hi=2)
out: {hi:2}

Then

curl -s --data 'hi=10' http://127.0.0.1/8081/API
out: {\n  \"hi\": \"2\"\n}

Everything looks good. However, take the function

#' @post /API
#' @serializer unboxedJSON
tmp <- function(...) {

  out <- match.call() %>%
         as.list() %>%
         .[2:length(.)] # %>%

  out <- toJSON(out, pretty = TRUE, auto_unbox = TRUE)

  return(out)

}
tmp(hi=2)
out: {hi:2}

Then

curl -s --data 'hi=10' http://127.0.0.1/8081/API
out: {"error":"500 - Internal server error","message":"Error: No method asJSON S3 class: R6\n"}

In practice what I really want to do is load my ML model to predict a score with the plumber API. For example

model <- readRDS('model.rds') # Load model as a global variable

predict_score <- function(...) {
    
    df_in <- match.call() %>%
        as.list() %>%
        .[2:length(.)] %>%
        as.data.frame()

    json_out <- list(
        score_out = predict(model, df_in) %>%
        toJSON(., pretty = T, auto_unbox = T)

    return(json_out)
}

This function works as expected when running locally, but running through the API via curl -s --data 'var1=1&var2=2...etc' http://listen_address

I get the following error:

{"error":"500 - Internal server error","message":"Error in as.data.frame.default(x[[i]], optional = TRUE): cannot coerce class "c("PlumberResponse", "R6")" to a data.frame\n"}


Solution

  • Internally plumber match parameters in your request to the name of the parameters in your function. There are special arguments that you could use to explore all args in the request. If you have an argument named req, it will give you an environnement containing the entire request metadata, one of which is req$args. Which you could then parse. The first two args are self reference to special arguments req and res. They are environment and should not be serialized. I would not advise doing what is shown here in any production code as it opens up the api to abuse.

    model <- readRDS('model.rds') # Load model as a global variable
    
    #' @post /API
    #' @serializer unboxedJSON
    predict_score <- function(req) {
    
        df_in <- as.data.frame(req$args[-(1:2)])
    
        json_out <- list(
            score_out = predict(model, df_in)
    
        return(json_out)
    }
    

    But for your use case, what I would actually advise is having a single parameter named df_in. Here is how you would set that up.

    model <- readRDS('model.rds') # Load model as a global variable
    
    #' @post /API
    #' @param df_in
    #' @serializer unboxedJSON
    predict_score <- function(df_in) {
    
        json_out <- list(
            score_out = predict(model, df_in)
    
        return(json_out)
    }
    
    

    Then with curl

    curl --header "Content-Type: application/json" \
      --request POST \
      --data '{"df_in":{"hi":2, "othercrap":4}}' \
      http://listen_address
    

    When the body of request starts with "{" plumber will parse the content of the body with jsonlite:fromJSON and use the name of the parsed objects to maps to parameters in your function.

    Currently both CRAN and master branch on github do not handle this correctly via the swagger api but it will works just fine via curl or other direct calling method. Next plumber version will handle all that and more I believe.

    See a similar answer to this of question here : https://github.com/rstudio/plumber/issues/512#issuecomment-605735332