Repository: mlflow/mlflow

Issue:
[DOC-FIX] LLM page at docs site pointing to work link
### Willingness to contribute

Yes. I would be willing to contribute a document fix with guidance from the MLflow community.

### URL(s) with the issue

In the documentation for LLMs at: https://mlflow.org/docs/latest/llms/index.html

The link to the MLflow examples in the [Getting Started Examples for each Provider](https://mlflow.org/docs/latest/llms/index.html#id2) is broken. Currently this points to https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/mlflow_serving/README.md

Github returns a 404 - page not found error

### Description of proposal (what needs changing)

It seems that the correct link is either:

https://github.com/mlflow/mlflow/tree/master/examples/deployments/deployments_server/mlflow_models
 or
https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/mlflow_models/README.md

(Note the wrong link points to **mlflow_serving** while the working link points to **mlflow_models**)

Discussion:
github-actions[bot]: <!-- documentation preview -->


Documentation preview for 1c15a3d92dedde3f45b3dd161bf767360d828ee4 will be available [here](https://output.circle-artifacts.com/output/job/1c936c4e-f6cc-4107-9f9b-90a77c4b6ec4/artifacts/0/docs/build/html/index.html) when [this CircleCI job](https://circleci.com/gh/mlflow/mlflow/84703) completes successfully.

<details>
<summary>More info</summary>

- Ignore this comment if this PR does not change the documentation.
- It takes a few minutes for the preview to be available.
- The preview is updated when a new commit is pushed to this PR.
- This comment was created by https://github.com/mlflow/mlflow/actions/runs/7401452433.

</details>


Patch:
diff --git a/docs/source/llms/index.rst b/docs/source/llms/index.rst
index 6edbd8f18..1987e8211 100644
--- a/docs/source/llms/index.rst
+++ b/docs/source/llms/index.rst
@@ -127,12 +127,12 @@ with your favorite provider!
 * `MosaicML quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/mosaicml/README.md>`_
 * `Anthropic quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/anthropic/README.md>`_
 * `Cohere quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/cohere/README.md>`_
-* `MLflow quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/mlflow_serving/README.md>`_
+* `MLflow quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/mlflow_models/README.md>`_
 * `AWS Bedrock quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/bedrock/README.md>`_
-* `AI21 Labs quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/ai21labs/README.md>`_ 
+* `AI21 Labs quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/ai21_labs/README.md>`_ 
 * `PaLM 2 quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/palm/README.md>`_
 * `Azure OpenAI quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/azure_openai/README.md>`_
-* `Hugging Face Text Generation Interface (TGI) quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/huggingface/readme.md>`_
+* `Hugging Face Text Generation Interface (TGI) quickstart <https://github.com/mlflow/mlflow/blob/master/examples/deployments/deployments_server/huggingface/README.md>`_
 
 .. note::
     The **MLflow** and **Hugging Face TGI** providers are for self-hosted LLM serving of either foundation open-source LLM models, fine-tuned open-source 
diff --git a/docs/source/models.rst b/docs/source/models.rst
index 3c3c99d14..bce0dfd6c 100644
--- a/docs/source/models.rst
+++ b/docs/source/models.rst
@@ -875,9 +875,13 @@ To include an input example with your model, add it to the appropriate log_model
 :py:func:`sklearn.log_model() <mlflow.sklearn.log_model>`. Input examples are also used to infer
 model signatures in log_model calls when signatures aren't specified.
 
-Similar to model signatures, model inputs can be column-based (i.e DataFrames) or tensor-based
-(i.e numpy.ndarrays). We offer support for input_example with params by using tuple to combine model
-inputs and params. See examples below:
+By default, if input example is a dictionary, we convert it to pandas DataFrame format when saving.
+Note that for langchain, openai, pyfunc and transformers flavors, input example could be saved without
+conversion by setting ``example_no_conversion`` to ``False``.
+
+Similar to model signatures, model inputs can be column-based (i.e DataFrames), tensor-based
+(i.e numpy.ndarrays) or json object (i.e python dictionary). We offer support for input_example 
+with params by using tuple to combine model inputs and params. See examples below:
 
 How To Log Model With Column-based Example
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -929,6 +933,27 @@ The following example demonstrates how you can log a tensor-based input example
     )
     mlflow.tensorflow.log_model(..., input_example=input_example)
 
+How To Log Model With Json Object
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+For models accepting python dictionary inputs instead of pandas DataFrame, we support saving the example
+directly as it is. To enable this, ``example_no_conversion`` should be set to ``True`` when logging the model. 
+This feature is only supported for langchain, openai, pyfunc and transformers flavors, where saving the example
+directly is useful for inference and model serving.
+By default, ``example_no_conversion`` is set to ``False`` for backwards compatibility.
+
+The following example demonstrates how you can log a json object input example with your model:
+
+.. code-block:: python
+
+    input_example = {
+        "messages": [
+            {"role": "system", "content": "You are a helpful assistant."},
+            {"role": "assistant", "content": "What would you like to ask?"},
+            {"role": "user", "content": "Who owns MLflow?"},
+        ]
+    }
+    mlflow.langchain.log_model(..., input_example=input_example, example_no_conversion=True)
+
 How To Log Model With Example Containing Params
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 For models that require additional parameters during inference, you can include an input_example 
diff --git a/examples/deployments/deployments_server/bedrock/README.md b/examples/deployments/deployments_server/bedrock/README.md
index f60dba3be..22f82f3bc 100644
--- a/examples/deployments/deployments_server/bedrock/README.md
+++ b/examples/deployments/deployments_server/bedrock/README.md
@@ -1,6 +1,6 @@
-## Example endpoint configuration for AWS Bedrcok
+## Example endpoint configuration for AWS Bedrock
 
-To view an example of a Bedrock endpoint configurations, see [the configuration example](config.yaml) YAML file.
+To view an example of a Bedrock endpoint configuration, see [the configuration example](config.yaml) YAML file.
 
 ## Credentials
 
diff --git a/mlflow/java/spark/pom.xml b/mlflow/java/spark/pom.xml
index a895df808..eed2f4b5d 100644
--- a/mlflow/java/spark/pom.xml
+++ b/mlflow/java/spark/pom.xml
@@ -41,21 +41,6 @@
       <artifactId>spark-catalyst_${scala.compat.version}</artifactId>
       <version>3.5.0</version>
     </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.dataformat</groupId>
-      <artifactId>jackson-dataformat-yaml</artifactId>
-      <version>2.15.2</version>
-    </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
-      <version>2.15.2</version>
-    </dependency>
-    <dependency>
-      <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-annotations</artifactId>
-      <version>2.15.2</version>
-    </dependency>
 
     <!-- Test -->
     <dependency>
@@ -70,14 +55,6 @@
       <version>1.10.19</version>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-api</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>slf4j-simple</artifactId>
-    </dependency>
     <dependency>
       <groupId>org.scalatest</groupId>
       <artifactId>scalatest_${scala.compat.version}</artifactId>
diff --git a/mlflow/langchain/__init__.py b/mlflow/langchain/__init__.py
index 04134fc80..752d7f15d 100644
--- a/mlflow/langchain/__init__.py
+++ b/mlflow/langchain/__init__.py
@@ -98,6 +98,7 @@ def save_model(
     metadata=None,
     loader_fn=None,
     persist_dir=None,
+    example_no_conversion=False,
 ):
     """
     Save a LangChain model to a path on the local file system.
@@ -184,6 +185,7 @@ def save_model(
                                 )
 
                         See a complete example in examples/langchain/retrieval_qa_chain.py.
+    :param example_no_conversion: {{ example_no_conversion }}
     """
     import langchain
     from langchain.schema import BaseRetriever
@@ -236,7 +238,7 @@ def save_model(
         mlflow_model.signature = signature
 
     if input_example is not None:
-        _save_example(mlflow_model, input_example, path)
+        _save_example(mlflow_model, input_example, path, example_no_conversion)
     if metadata is not None:
         mlflow_model.metadata = metadata
 
@@ -306,6 +308,7 @@ def log_model(
     metadata=None,
     loader_fn=None,
     persist_dir=None,
+    example_no_conversion=False,
 ):
     """
     Log a LangChain model as an MLflow artifact for the current run.
@@ -402,6 +405,7 @@ def log_model(
                                 )
 
                         See a complete example in examples/langchain/retrieval_qa_chain.py.
+    :param example_no_conversion: {{ example_no_conversion }}
     :return: A :py:class:`ModelInfo <mlflow.models.model.ModelInfo>` instance that contains the
              metadata of the logged model.
     """
@@ -422,6 +426,7 @@ def log_model(
         metadata=metadata,
         loader_fn=loader_fn,
         persist_dir=persist_dir,
+        example_no_conversion=example_no_conversion,
     )
 
 
diff --git a/mlflow/models/utils.py b/mlflow/models/utils.py
index 6ff1a1adc..e6f39b911 100644
--- a/mlflow/models/utils.py
+++ b/mlflow/models/utils.py
@@ -37,6 +37,7 @@ except ImportError:
 INPUT_EXAMPLE_PATH = "artifact_path"
 EXAMPLE_DATA_KEY = "inputs"
 EXAMPLE_PARAMS_KEY = "params"
+EXAMPLE_FILENAME = "input_example.json"
 
 ModelInputExample = Union[
     pd.DataFrame, np.ndarray, dict, list, "csr_matrix", "csc_matrix", str, bytes, tuple
@@ -199,9 +200,8 @@ class _Example:
                 del result["columns"]
             return result
 
-        example_filename = "input_example.json"
         self.info = {
-            INPUT_EXAMPLE_PATH: example_filename,
+            INPUT_EXAMPLE_PATH: EXAMPLE_FILENAME,
         }
         # Avoid changing the variable passed in
         input_example = deepcopy(input_example)
@@ -320,7 +320,9 @@ def _contains_params(input_example):
     )
 
 
-def _save_example(mlflow_model: Model, input_example: ModelInputExample, path: str):
+def _save_example(
+    mlflow_model: Model, input_example: ModelInputExample, path: str, no_conversion=False
+):
     """
     Save example to a file on the given path and updates passed Model with example metadata.
 
@@ -336,9 +338,25 @@ def _save_example(mlflow_model: Model, input_example: ModelInputExample, path: s
     :param mlflow_model: Model metadata that will get updated with the example metadata.
     :param path: Where to store the example file. Should be model the model directory.
     """
-    example = _Example(input_example)
-    example.save(path)
-    mlflow_model.saved_input_example_info = example.info
+    if no_conversion:
+        example_info = {
+            INPUT_EXAMPLE_PATH: EXAMPLE_FILENAME,
+            "type": "json_object",
+        }
+        try:
+            with open(os.path.join(path, example_info[INPUT_EXAMPLE_PATH]), "w") as f:
+                json.dump(input_example, f, cls=NumpyEncoder)
+        except Exception as e:
+            raise MlflowException.invalid_parameter_value(
+                "Failed to save input example. Please make sure the input example is jsonable "
+                f"when no_conversion is True. Got error: {e}"
+            ) from e
+        else:
+            mlflow_model.saved_input_example_info = example_info
+    else:
+        example = _Example(input_example)
+        example.save(path)
+        mlflow_model.saved_input_example_info = example.info
 
 
 def _get_mlflow_model_input_example_dict(mlflow_model: Model, path: str):
@@ -352,7 +370,13 @@ def _get_mlflow_model_input_example_dict(mlflow_model: Model, path: str):
     if mlflow_model.saved_input_example_info is None:
         return None
     example_type = mlflow_model.saved_input_example_info["type"]
-    if example_type not in ["dataframe", "ndarray", "sparse_matrix_csc", "sparse_matrix_csr"]:
+    if example_type not in [
+        "dataframe",
+        "ndarray",
+        "sparse_matrix_csc",
+        "sparse_matrix_csr",
+        "json_object",
+    ]:
         raise MlflowException(f"This version of mlflow can not load example of type {example_type}")
     path = os.path.join(path, mlflow_model.saved_input_example_info["artifact_path"])
     with open(path) as handle:
@@ -377,6 +401,8 @@ def _read_example(mlflow_model: Model, path: str):
     input_schema = mlflow_model.signature.inputs if mlflow_model.signature is not None else None
     if mlflow_model.saved_input_example_info.get(EXAMPLE_PARAMS_KEY, None):
         input_example = input_example[EXAMPLE_DATA_KEY]
+    if example_type == "json_object":
+        return input_example
     if example_type == "ndarray":
         return _read_tensor_input_from_json(input_example, schema=input_schema)
     if example_type in ["sparse_matrix_csc", "sparse_matrix_csr"]:
diff --git a/mlflow/openai/__init__.py b/mlflow/openai/__init__.py
index 131ceb698..f786a5332 100644
--- a/mlflow/openai/__init__.py
+++ b/mlflow/openai/__init__.py
@@ -259,6 +259,7 @@ def save_model(
     pip_requirements=None,
     extra_pip_requirements=None,
     metadata=None,
+    example_no_conversion=False,
     **kwargs,
 ):
     """
@@ -296,6 +297,7 @@ def save_model(
 
             .. Note:: Experimental: This parameter may change or be removed in a future
                                     release without warning.
+        example_no_conversion: {{ example_no_conversion }}
         kwargs: Keyword arguments specific to the OpenAI task, such as the ``messages`` (see
             :ref:`mlflow.openai.messages` for more details on this parameter)
             or ``top_p`` value to use for chat completion.
@@ -372,7 +374,7 @@ def save_model(
         )
 
     if input_example is not None:
-        _save_example(mlflow_model, input_example, path)
+        _save_example(mlflow_model, input_example, path, example_no_conversion)
     if metadata is not None:
         mlflow_model.metadata = metadata
     model_data_path = os.path.join(path, MODEL_FILENAME)
@@ -457,6 +459,7 @@ def log_model(
     pip_requirements=None,
     extra_pip_requirements=None,
     metadata=None,
+    example_no_conversion=False,
     **kwargs,
 ):
     """
@@ -500,6 +503,7 @@ def log_model(
 
             .. Note:: Experimental: This parameter may change or be removed in a future
                                     release without warning.
+        example_no_conversion: {{ example_no_conversion }}
         kwargs: Keyword arguments specific to the OpenAI task, such as the ``messages`` (see
             :ref:`mlflow.openai.messages` for more details on this parameter)
             or ``top_p`` value to use for chat completion.
@@ -550,6 +554,7 @@ def log_model(
         pip_requirements=pip_requirements,
         extra_pip_requirements=extra_pip_requirements,
         metadata=metadata,
+        example_no_conversion=example_no_conversion,
         **kwargs,
     )
 
diff --git a/mlflow/pyfunc/__init__.py b/mlflow/pyfunc/__init__.py
index c0a99a23f..efa0f3788 100644
--- a/mlflow/pyfunc/__init__.py
+++ b/mlflow/pyfunc/__init__.py
@@ -1741,6 +1741,7 @@ def save_model(
     extra_pip_requirements=None,
     metadata=None,
     model_config=None,
+    example_no_conversion=False,
     **kwargs,
 ):
     """
@@ -1880,6 +1881,7 @@ def save_model(
 
                      .. Note:: Experimental: This parameter may change or be removed in a future
                                              release without warning.
+    :param example_no_conversion: {{ example_no_conversion }}
     """
     _validate_env_arguments(conda_env, pip_requirements, extra_pip_requirements)
     _validate_pyfunc_model_config(model_config)
@@ -1963,7 +1965,7 @@ def save_model(
                     _logger.warning(f"Failed to infer model signature from input example. {e}")
 
     if input_example is not None:
-        _save_example(mlflow_model, input_example, path)
+        _save_example(mlflow_model, input_example, path, example_no_conversion)
     if metadata is not None:
         mlflow_model.metadata = metadata
 
@@ -2012,6 +2014,7 @@ def log_model(
     extra_pip_requirements=None,
     metadata=None,
     model_config=None,
+    example_no_conversion=False,
 ):
     """
     Log a Pyfunc model with custom inference logic and optional data dependencies as an MLflow
@@ -2159,6 +2162,7 @@ def log_model(
 
                      .. Note:: Experimental: This parameter may change or be removed in a future
                                              release without warning.
+    :param example_no_conversion: {{ example_no_conversion }}
     :return: A :py:class:`ModelInfo <mlflow.models.model.ModelInfo>` instance that contains the
              metadata of the logged model.
     """
@@ -2179,6 +2183,7 @@ def log_model(
         extra_pip_requirements=extra_pip_requirements,
         metadata=metadata,
         model_config=model_config,
+        example_no_conversion=example_no_conversion,
     )
 
 
diff --git a/mlflow/transformers/__init__.py b/mlflow/transformers/__init__.py
index 1e2a01782..85f8970bf 100644
--- a/mlflow/transformers/__init__.py
+++ b/mlflow/transformers/__init__.py
@@ -234,6 +234,7 @@ def save_model(
     conda_env=None,
     metadata: Optional[Dict[str, Any]] = None,
     model_config: Optional[Dict[str, Any]] = None,
+    example_no_conversion: bool = False,
     **kwargs,  # pylint: disable=unused-argument
 ) -> None:
     """
@@ -407,7 +408,7 @@ def save_model(
 
                      .. Note:: Experimental: This parameter may change or be removed in a future
                                              release without warning.
-
+    :param example_no_conversion: {{ example_no_conversion }}
     :param kwargs: Optional additional configurations for transformers serialization.
     :return: None
     """
@@ -453,7 +454,7 @@ def save_model(
         mlflow_model.signature = signature
     if input_example is not None:
         input_example = _format_input_example_for_special_cases(input_example, built_pipeline)
-        _save_example(mlflow_model, input_example, str(path))
+        _save_example(mlflow_model, input_example, str(path), example_no_conversion)
     if metadata is not None:
         mlflow_model.metadata = metadata
 
@@ -592,6 +593,7 @@ def log_model(
     conda_env=None,
     metadata: Optional[Dict[str, Any]] = None,
     model_config: Optional[Dict[str, Any]] = None,
+    example_no_conversion: bool = False,
     **kwargs,
 ):
     """
@@ -774,6 +776,7 @@ def log_model(
 
                      .. Note:: Experimental: This parameter may change or be removed in a future
                                              release without warning.
+    :param example_no_conversion: {{ example_no_conversion }}
     :param kwargs: Additional arguments for :py:class:`mlflow.models.model.Model`
     """
     return Model.log(
@@ -794,6 +797,7 @@ def log_model(
         pip_requirements=pip_requirements,
         extra_pip_requirements=extra_pip_requirements,
         model_config=model_config,
+        example_no_conversion=example_no_conversion,
         **kwargs,
     )
 
diff --git a/mlflow/utils/docstring_utils.py b/mlflow/utils/docstring_utils.py
index db01bfcaa..8c5aafce9 100644
--- a/mlflow/utils/docstring_utils.py
+++ b/mlflow/utils/docstring_utils.py
@@ -242,6 +242,13 @@ DataFrame and then serialized to json using the Pandas split-oriented
 format, or a numpy array where the example will be serialized to json
 by converting it to a list. Bytes are base64-encoded. When the ``signature`` parameter is
 ``None``, the input example is used to infer a model signature.
+"""
+        ),
+        "example_no_conversion": (
+            """If ``True``, the input example will not be converted to a Pandas DataFrame
+format when saving. This is useful when the model expects a non-DataFrame input and the
+input example could be passed directly to the model. Defaults to ``False`` for backwards
+compatibility.
 """
         ),
     }


Test patch:
diff --git a/tests/models/test_model.py b/tests/models/test_model.py
index 48ba53ea1..13a82eacb 100644
--- a/tests/models/test_model.py
+++ b/tests/models/test_model.py
@@ -453,3 +453,29 @@ def test_validate_schema(sklearn_knn_model, iris_data, tmp_path):
     reloaded_model = mlflow.sklearn.load_model(sk_model_path)
     np.testing.assert_array_equal(prediction, reloaded_model.predict(X))
     validate_schema(prediction, signature.outputs)
+
+
+def test_save_load_input_example_without_conversion(tmp_path):
+    class MyModel(mlflow.pyfunc.PythonModel):
+        def predict(self, context, model_input, params=None):
+            return model_input
+
+    input_example = {
+        "messages": [
+            {"role": "user", "content": "Hello!"},
+        ]
+    }
+    with mlflow.start_run() as run:
+        mlflow.pyfunc.log_model(
+            python_model=MyModel(),
+            artifact_path="test_model",
+            input_example=input_example,
+            example_no_conversion=True,
+        )
+        local_path = _download_artifact_from_uri(
+            f"runs:/{run.info.run_id}/test_model", output_path=tmp_path
+        )
+    loaded_model = Model.load(os.path.join(local_path, "MLmodel"))
+    assert loaded_model.saved_input_example_info["type"] == "json_object"
+    loaded_example = loaded_model.load_input_example(local_path)
+    assert loaded_example == input_example