Creating and deploying custom code

In some cases, you may need to create and deploy custom code as part of your MLOps workflow using vetiver. This could be necessary when you need to:

  • deploy custom models in vetiver
  • deploy unsupported models in vetiver
  • include custom code in vetiver
  • deploy a vetiver model with a custom pipeline

You may also have custom code in a known framework, such as a column transformer for a scikit-learn model.

In these cases, extra steps will be required to successfully create and deploy a VetiverModel object.

Making a custom model

Vetiver supports basic scikit-learn, torch, statsmodels, xgboost, and spacy models. If you need to alter the usage of these models, or deploy a different type of model, you will likely need to create a new model handler.

To create a model handler, you should create a subclass of vetiver’s BaseHandler class. This handler should include the following:

  • model_type: A static method that declares the type of your model.
  • handler_predict(): A method that defines how predictions should be made for your model. This method is used at the /predict endpoint in the VetiverAPI.

Here’s an example of a handler for a model of newmodeltype type. Once you have defined your handler, you can initialize it with your model and pass it to the VetiverModel class.

from vetiver.handlers.base import BaseHandler

class CustomHandler(BaseHandler):
    def __init__(self, model, prototype_data):
        super().__init__(model, prototype_data)

    model_type = staticmethod(lambda: newmodeltype)
    pip_name = "scikit-learn" # package's installation name on pip

    def handler_predict(self, input_data, check_prototype: bool):
        """
        Your code for making predictions using the custom model

        Parameters
        ----------
        input_data:
            Data POSTed to API endpoint
        check_prototype: bool
            Whether the prototype should be enforced
        """
        prediction = model.fancy_new_predict(input_data)

        return prediction

new_model = CustomHandler(model, prototype_data)

VetiverModel(new_model, "custom_model")

If your model is a common type, please consider submitting a pull request.

To deploy custom code, you need to include the necessary source code in your deployment files. If your model or other elements can be imported from a Python package, you can include the relevant packages in a requirements.txt file for deployment. However, if you have custom source code in local files, you will need to include those files in the deployment process.

Deploying custom elements

If your VetiverModel includes custom source code, you need to include that code in your deployment files to build an API in another location. The example below shows a user-created FeatureSelector, which is part of a scikit-learn pipeline.

model.py
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.tree import DecisionTreeClassifier
from sklearn.pipeline import Pipeline

# create custom data preprocessing
class FeatureSelector(BaseEstimator, TransformerMixin):
    def __init__(self, columns):
        self.columns = columns

    def fit(self, X, y=None):
        return self

    def transform(self, X, y=None):
        return X[self.columns]

# create model
model = Pipeline(steps=[
    ('feature_selector', FeatureSelector(features)),
    ('decision_tree', DecisionTreeClassifier())
])

# create deployable model object
from vetiver import VetiverModel, vetiver_pin_write

v = VetiverModel(model, "selected_decision_tree", protoype_data = X)

# pin model to some location, eg, Posit Connect
import pins

board = pins.board_connect(allow_pickle_read=True)
vetiver_pin_write(board, v)

To generate files needed to start a Docker container, you can use the command vetiver.prepare_docker.

vetiver.prepare_docker(board, "selected_decision_tree")

When you run this line, 3 files are generated: a Dockerfile, an app.py file, and a vetiver_requirements.txt. In the app.py file, you’ll need to add an import statement that is formatted from {name of file, excluding .py, that has custom element} import {name of custom element}.

app.py
from vetiver import VetiverModel
import vetiver
import pins
from model import FeatureSelector # add this line to import your custom feature engineering


b = pins.board_connect(allow_pickle_read=True)
v = VetiverModel.from_pin(b, 'selected_decision_tree')

vetiver_api = vetiver.VetiverAPI(v)
api = vetiver_api.app

Add a line to your Dockerfile to copy your source file(s) into your Docker container. The format will be COPY path/to/your/filename.py /vetiver/app/filename.py, where the destination is always in the /vetiver/app/ directory.

Dockerfile
# # Generated by the vetiver package; edit with care
# start with python base image
FROM python:3.10

# create directory in container for vetiver files
WORKDIR /vetiver

# copy  and install requirements
COPY vetiver_requirements.txt /vetiver/requirements.txt

#
RUN pip install --no-cache-dir --upgrade -r /vetiver/requirements.txt

# copy app file
COPY app.py /vetiver/app/app.py

# ADD THIS LINE to copy model source code
COPY model.py /vetiver/app/model.py

# expose port
EXPOSE 8080

# run vetiver API
CMD ["uvicorn", "app.app:api", "--host", "0.0.0.0", "--port", "8080"]

To deploy custom code to Posit Connect, you’ll first start with the command vetiver.write_app.

vetiver.write_app(board, 'selected_decision_tree')

This will generate an app.py file, where you’ll need to add an import statement that is formatted from {name of file, excluding .py, that has custom element} import {name of custom element}.

="app.py"
from vetiver import VetiverModel
import vetiver
import pins
from model import FeatureSelector # add this line to import your custom feature engineering


b = pins.board_connect(allow_pickle_read=True)
v = VetiverModel.from_pin(b, 'selected_decision_tree')

vetiver_api = vetiver.VetiverAPI(v)
api = vetiver_api.app

After editing the app.py file, you can deploy it to Posit Connect using the rsconnect package. Use the rsconnect.api.actions.deploy_python_fastapi() function to deploy the API, specifying the Connect server URL, API key, directory containing the app.py and model.py files, and the entry point of the API.

from rsconnect.api.actions import deploy_python_fastapi
import rsconnect

url = "example.connect.com" # your Posit Connect server url
api_key = os.environ(CONNECT_API_KEY) # your Posit Connect API key

connect_server = rsconnect.api.RSConnectServer(
    url = url,
    api_key = api_key
)

rsconnect.actions.deploy_python_fastapi(
        connect_server = connect_server,
        directory = "./", # path to the directory containing the app.py and model.py files
        entry_point = "app:api" # the API is the app.py file, in a variable named api
    )

Common Pitfalls

When deploying custom code, the most common error is something similar to AttributeError: Can't get attribute 'ExampleModel' on <module '__main__' (built-in)>. There are a few possible causes for this error:

  1. The original ExampleModel may have been pinned from inside a Jupyter Notebook. Because pickling only saves a reference for how to read a class, not the source code, a custom model transformer pinned from a Jupyter Notebook cannot be imported and resolved later. To fix this, repin your model/transformer from inside a Python script.

  2. You may not be uploading the custom code to be used later. When deploying, you’ll want to add the files containing your custom code to the extra_files argument so that it can be imported, eg, vetiver.deploy_rsconnect(connect_server, board, model_name, extra_files=['custom_model.py', 'requirements.txt']) .

Please note that the above steps are a general guide, and you may need to adapt them to your specific use case and deployment environment. If you have any questions, please consider opening an issue.