662 lines
21 KiB
Python
662 lines
21 KiB
Python
import json
|
|
import pathlib
|
|
import pickle
|
|
import uuid
|
|
from dataclasses import asdict
|
|
|
|
import pytest
|
|
|
|
import mlflow
|
|
from mlflow.exceptions import MlflowException
|
|
from mlflow.models.model import Model
|
|
from mlflow.models.signature import ModelSignature
|
|
from mlflow.models.utils import load_serving_example
|
|
from mlflow.pyfunc.loaders.chat_model import _ChatModelPyfuncWrapper
|
|
from mlflow.tracing.constant import TraceTagKey
|
|
from mlflow.tracking.artifact_utils import _download_artifact_from_uri
|
|
from mlflow.types.llm import (
|
|
CHAT_MODEL_INPUT_SCHEMA,
|
|
CHAT_MODEL_OUTPUT_SCHEMA,
|
|
ChatChoice,
|
|
ChatChoiceDelta,
|
|
ChatChunkChoice,
|
|
ChatCompletionChunk,
|
|
ChatCompletionResponse,
|
|
ChatMessage,
|
|
ChatParams,
|
|
FunctionToolCallArguments,
|
|
FunctionToolDefinition,
|
|
ToolParamsSchema,
|
|
)
|
|
from mlflow.types.schema import ColSpec, DataType, Schema
|
|
|
|
from tests.helper_functions import (
|
|
expect_status_code,
|
|
pyfunc_serve_and_score_model,
|
|
)
|
|
from tests.tracing.helper import get_traces
|
|
|
|
# `None`s (`max_tokens` and `stop`) are excluded
|
|
DEFAULT_PARAMS = {
|
|
"temperature": 1.0,
|
|
"n": 1,
|
|
"stream": False,
|
|
}
|
|
|
|
|
|
def get_mock_streaming_response(message, is_last_chunk=False):
|
|
if is_last_chunk:
|
|
return {
|
|
"id": "123",
|
|
"model": "MyChatModel",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"delta": {
|
|
"role": None,
|
|
"content": None,
|
|
},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {
|
|
"prompt_tokens": 10,
|
|
"completion_tokens": 10,
|
|
"total_tokens": 20,
|
|
},
|
|
}
|
|
else:
|
|
return {
|
|
"id": "123",
|
|
"model": "MyChatModel",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"delta": {
|
|
"role": "assistant",
|
|
"content": message,
|
|
},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {
|
|
"prompt_tokens": 10,
|
|
"completion_tokens": 10,
|
|
"total_tokens": 20,
|
|
},
|
|
}
|
|
|
|
|
|
def get_mock_response(messages, params):
|
|
return {
|
|
"id": "123",
|
|
"model": "MyChatModel",
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": json.dumps([m.to_dict() for m in messages]),
|
|
},
|
|
"finish_reason": "stop",
|
|
},
|
|
{
|
|
"index": 1,
|
|
"message": {
|
|
"role": "user",
|
|
"content": json.dumps(params.to_dict()),
|
|
},
|
|
"finish_reason": "stop",
|
|
},
|
|
],
|
|
"usage": {
|
|
"prompt_tokens": 10,
|
|
"completion_tokens": 10,
|
|
"total_tokens": 20,
|
|
},
|
|
}
|
|
|
|
|
|
class SimpleChatModel(mlflow.pyfunc.ChatModel):
|
|
def predict(
|
|
self, context, messages: list[ChatMessage], params: ChatParams
|
|
) -> ChatCompletionResponse:
|
|
mock_response = get_mock_response(messages, params)
|
|
return ChatCompletionResponse.from_dict(mock_response)
|
|
|
|
def predict_stream(self, context, messages: list[ChatMessage], params: ChatParams):
|
|
num_chunks = 10
|
|
for i in range(num_chunks):
|
|
mock_response = get_mock_streaming_response(
|
|
f"message {i}", is_last_chunk=(i == num_chunks - 1)
|
|
)
|
|
yield ChatCompletionChunk.from_dict(mock_response)
|
|
|
|
|
|
class ChatModelWithContext(mlflow.pyfunc.ChatModel):
|
|
def load_context(self, context):
|
|
predict_path = pathlib.Path(context.artifacts["predict_fn"])
|
|
self.predict_fn = pickle.loads(predict_path.read_bytes())
|
|
|
|
def predict(
|
|
self, context, messages: list[ChatMessage], params: ChatParams
|
|
) -> ChatCompletionResponse:
|
|
message = ChatMessage(role="assistant", content=self.predict_fn())
|
|
return ChatCompletionResponse.from_dict(get_mock_response([message], params))
|
|
|
|
|
|
class ChatModelWithTrace(mlflow.pyfunc.ChatModel):
|
|
@mlflow.trace
|
|
def predict(
|
|
self, context, messages: list[ChatMessage], params: ChatParams
|
|
) -> ChatCompletionResponse:
|
|
mock_response = get_mock_response(messages, params)
|
|
return ChatCompletionResponse.from_dict(mock_response)
|
|
|
|
|
|
class ChatModelWithMetadata(mlflow.pyfunc.ChatModel):
|
|
def predict(
|
|
self, context, messages: list[ChatMessage], params: ChatParams
|
|
) -> ChatCompletionResponse:
|
|
mock_response = get_mock_response(messages, params)
|
|
return ChatCompletionResponse(
|
|
**mock_response,
|
|
custom_outputs=params.custom_inputs,
|
|
)
|
|
|
|
|
|
class ChatModelWithToolCalling(mlflow.pyfunc.ChatModel):
|
|
def predict(
|
|
self, context, messages: list[ChatMessage], params: ChatParams
|
|
) -> ChatCompletionResponse:
|
|
tools = params.tools
|
|
|
|
# call the first tool with some value for all the required params
|
|
tool_name = tools[0].function.name
|
|
tool_params = tools[0].function.parameters
|
|
arguments = {}
|
|
for param in tool_params.required:
|
|
param_type = tool_params.properties[param].type
|
|
if param_type == "string":
|
|
arguments[param] = "some_value"
|
|
elif param_type == "number":
|
|
arguments[param] = 123
|
|
elif param_type == "boolean":
|
|
arguments[param] = True
|
|
else:
|
|
# keep the test example simple
|
|
raise ValueError(f"Unsupported param type: {param_type}")
|
|
|
|
tool_call = FunctionToolCallArguments(
|
|
name=tool_name,
|
|
arguments=json.dumps(arguments),
|
|
).to_tool_call(id=uuid.uuid4().hex)
|
|
|
|
tool_message = ChatMessage(
|
|
role="assistant",
|
|
tool_calls=[tool_call],
|
|
)
|
|
|
|
return ChatCompletionResponse(choices=[ChatChoice(index=0, message=tool_message)])
|
|
|
|
|
|
def test_chat_model_save_load(tmp_path):
|
|
model = SimpleChatModel()
|
|
mlflow.pyfunc.save_model(python_model=model, path=tmp_path)
|
|
|
|
loaded_model = mlflow.pyfunc.load_model(tmp_path)
|
|
assert isinstance(loaded_model._model_impl, _ChatModelPyfuncWrapper)
|
|
input_schema = loaded_model.metadata.get_input_schema()
|
|
output_schema = loaded_model.metadata.get_output_schema()
|
|
assert input_schema == CHAT_MODEL_INPUT_SCHEMA
|
|
assert output_schema == CHAT_MODEL_OUTPUT_SCHEMA
|
|
|
|
|
|
def test_chat_model_with_trace(tmp_path):
|
|
model = ChatModelWithTrace()
|
|
mlflow.pyfunc.save_model(python_model=model, path=tmp_path)
|
|
|
|
# predict() call during saving chat model should not generate a trace
|
|
assert len(get_traces()) == 0
|
|
|
|
loaded_model = mlflow.pyfunc.load_model(tmp_path)
|
|
messages = [
|
|
{"role": "system", "content": "You are a helpful assistant"},
|
|
{"role": "user", "content": "Hello!"},
|
|
]
|
|
loaded_model.predict({"messages": messages})
|
|
|
|
traces = get_traces()
|
|
assert len(traces) == 1
|
|
assert traces[0].info.tags[TraceTagKey.TRACE_NAME] == "predict"
|
|
request = json.loads(traces[0].data.request)
|
|
assert request["messages"] == [asdict(ChatMessage.from_dict(msg)) for msg in messages]
|
|
|
|
|
|
def test_chat_model_save_throws_with_signature(tmp_path):
|
|
model = SimpleChatModel()
|
|
|
|
with pytest.raises(MlflowException, match="Please remove the `signature` parameter"):
|
|
mlflow.pyfunc.save_model(
|
|
python_model=model,
|
|
path=tmp_path,
|
|
signature=ModelSignature(
|
|
Schema([ColSpec(name="test", type=DataType.string)]),
|
|
Schema([ColSpec(name="test", type=DataType.string)]),
|
|
),
|
|
)
|
|
|
|
|
|
def mock_predict():
|
|
return "hello"
|
|
|
|
|
|
def test_chat_model_with_context_saves_successfully(tmp_path):
|
|
model_path = tmp_path / "model"
|
|
predict_path = tmp_path / "predict.pkl"
|
|
predict_path.write_bytes(pickle.dumps(mock_predict))
|
|
|
|
model = ChatModelWithContext()
|
|
mlflow.pyfunc.save_model(
|
|
python_model=model,
|
|
path=model_path,
|
|
artifacts={"predict_fn": str(predict_path)},
|
|
)
|
|
|
|
loaded_model = mlflow.pyfunc.load_model(model_path)
|
|
messages = [{"role": "user", "content": "test"}]
|
|
|
|
response = loaded_model.predict({"messages": messages})
|
|
expected_response = json.dumps([{"role": "assistant", "content": "hello"}])
|
|
assert response["choices"][0]["message"]["content"] == expected_response
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ret",
|
|
[
|
|
"not a ChatCompletionResponse",
|
|
{"dict": "with", "bad": "keys"},
|
|
{
|
|
"id": "1",
|
|
"created": 1,
|
|
"model": "m",
|
|
"choices": [{"bad": "choice"}],
|
|
"usage": {
|
|
"prompt_tokens": 10,
|
|
"completion_tokens": 10,
|
|
"total_tokens": 20,
|
|
},
|
|
},
|
|
],
|
|
)
|
|
def test_save_throws_on_invalid_output(tmp_path, ret):
|
|
class BadChatModel(mlflow.pyfunc.ChatModel):
|
|
def predict(self, context, messages, params) -> ChatCompletionResponse:
|
|
return ret
|
|
|
|
model = BadChatModel()
|
|
with pytest.raises(
|
|
MlflowException,
|
|
match=(
|
|
"Failed to save ChatModel. Please ensure that the model's "
|
|
r"predict\(\) method returns a ChatCompletionResponse object"
|
|
),
|
|
):
|
|
mlflow.pyfunc.save_model(python_model=model, path=tmp_path)
|
|
|
|
|
|
# test that we can predict with the model
|
|
def test_chat_model_predict(tmp_path):
|
|
model = SimpleChatModel()
|
|
mlflow.pyfunc.save_model(python_model=model, path=tmp_path)
|
|
|
|
loaded_model = mlflow.pyfunc.load_model(tmp_path)
|
|
messages = [
|
|
{"role": "system", "content": "You are a helpful assistant"},
|
|
{"role": "user", "content": "Hello!"},
|
|
]
|
|
|
|
response = loaded_model.predict({"messages": messages})
|
|
assert response["choices"][0]["message"]["content"] == json.dumps(messages)
|
|
assert json.loads(response["choices"][1]["message"]["content"]) == DEFAULT_PARAMS
|
|
|
|
# override all params
|
|
params_override = {
|
|
"temperature": 0.5,
|
|
"max_tokens": 10,
|
|
"stop": ["\n"],
|
|
"n": 2,
|
|
"stream": True,
|
|
"top_p": 0.1,
|
|
"top_k": 20,
|
|
"frequency_penalty": 0.5,
|
|
"presence_penalty": -0.5,
|
|
}
|
|
response = loaded_model.predict({"messages": messages, **params_override})
|
|
assert response["choices"][0]["message"]["content"] == json.dumps(messages)
|
|
assert json.loads(response["choices"][1]["message"]["content"]) == params_override
|
|
|
|
# override a subset of params
|
|
params_subset = {
|
|
"max_tokens": 100,
|
|
}
|
|
response = loaded_model.predict({"messages": messages, **params_subset})
|
|
assert response["choices"][0]["message"]["content"] == json.dumps(messages)
|
|
assert json.loads(response["choices"][1]["message"]["content"]) == {
|
|
**DEFAULT_PARAMS,
|
|
**params_subset,
|
|
}
|
|
|
|
|
|
def test_chat_model_works_in_serving():
|
|
model = SimpleChatModel()
|
|
messages = [
|
|
{"role": "system", "content": "You are a helpful assistant"},
|
|
{"role": "user", "content": "Hello!"},
|
|
]
|
|
params_subset = {
|
|
"max_tokens": 100,
|
|
}
|
|
with mlflow.start_run():
|
|
model_info = mlflow.pyfunc.log_model(
|
|
name="model",
|
|
python_model=model,
|
|
input_example=(messages, params_subset),
|
|
)
|
|
|
|
inference_payload = load_serving_example(model_info.model_uri)
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_info.model_uri,
|
|
data=inference_payload,
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
|
|
expect_status_code(response, 200)
|
|
choices = json.loads(response.content)["choices"]
|
|
assert choices[0]["message"]["content"] == json.dumps(messages)
|
|
assert json.loads(choices[1]["message"]["content"]) == {
|
|
**DEFAULT_PARAMS,
|
|
**params_subset,
|
|
}
|
|
|
|
|
|
def test_chat_model_works_with_infer_signature_input_example(tmp_path):
|
|
model = SimpleChatModel()
|
|
params_subset = {
|
|
"max_tokens": 100,
|
|
}
|
|
input_example = {
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": "What is Retrieval-augmented Generation?",
|
|
}
|
|
],
|
|
**params_subset,
|
|
}
|
|
with mlflow.start_run():
|
|
model_info = mlflow.pyfunc.log_model(
|
|
name="model", python_model=model, input_example=input_example
|
|
)
|
|
assert model_info.signature.inputs == CHAT_MODEL_INPUT_SCHEMA
|
|
assert model_info.signature.outputs == CHAT_MODEL_OUTPUT_SCHEMA
|
|
mlflow_model = Model.load(model_info.model_uri)
|
|
local_path = _download_artifact_from_uri(model_info.model_uri)
|
|
assert mlflow_model.load_input_example(local_path) == {
|
|
"messages": input_example["messages"],
|
|
**params_subset,
|
|
}
|
|
|
|
inference_payload = load_serving_example(model_info.model_uri)
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_info.model_uri,
|
|
data=inference_payload,
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
|
|
expect_status_code(response, 200)
|
|
choices = json.loads(response.content)["choices"]
|
|
assert choices[0]["message"]["content"] == json.dumps(input_example["messages"])
|
|
assert json.loads(choices[1]["message"]["content"]) == {
|
|
**DEFAULT_PARAMS,
|
|
**params_subset,
|
|
}
|
|
|
|
|
|
def test_chat_model_logs_default_metadata_task(tmp_path):
|
|
model = SimpleChatModel()
|
|
params_subset = {
|
|
"max_tokens": 100,
|
|
}
|
|
input_example = {
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": "What is Retrieval-augmented Generation?",
|
|
}
|
|
],
|
|
**params_subset,
|
|
}
|
|
with mlflow.start_run():
|
|
model_info = mlflow.pyfunc.log_model(
|
|
name="model", python_model=model, input_example=input_example
|
|
)
|
|
assert model_info.signature.inputs == CHAT_MODEL_INPUT_SCHEMA
|
|
assert model_info.signature.outputs == CHAT_MODEL_OUTPUT_SCHEMA
|
|
assert model_info.metadata["task"] == "agent/v1/chat"
|
|
|
|
with mlflow.start_run():
|
|
model_info_with_override = mlflow.pyfunc.log_model(
|
|
name="model", python_model=model, input_example=input_example, metadata={"task": None}
|
|
)
|
|
assert model_info_with_override.metadata["task"] is None
|
|
|
|
|
|
def test_chat_model_works_with_chat_message_input_example(tmp_path):
|
|
model = SimpleChatModel()
|
|
input_example = [
|
|
ChatMessage(role="user", content="What is Retrieval-augmented Generation?", name="chat")
|
|
]
|
|
with mlflow.start_run():
|
|
model_info = mlflow.pyfunc.log_model(
|
|
name="model", python_model=model, input_example=input_example
|
|
)
|
|
assert model_info.signature.inputs == CHAT_MODEL_INPUT_SCHEMA
|
|
assert model_info.signature.outputs == CHAT_MODEL_OUTPUT_SCHEMA
|
|
mlflow_model = Model.load(model_info.model_uri)
|
|
local_path = _download_artifact_from_uri(model_info.model_uri)
|
|
assert mlflow_model.load_input_example(local_path) == {
|
|
"messages": [message.to_dict() for message in input_example],
|
|
}
|
|
|
|
inference_payload = load_serving_example(model_info.model_uri)
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_info.model_uri,
|
|
data=inference_payload,
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
|
|
expect_status_code(response, 200)
|
|
choices = json.loads(response.content)["choices"]
|
|
assert choices[0]["message"]["content"] == json.dumps(json.loads(inference_payload)["messages"])
|
|
|
|
|
|
def test_chat_model_works_with_infer_signature_multi_input_example(tmp_path):
|
|
model = SimpleChatModel()
|
|
params_subset = {
|
|
"max_tokens": 100,
|
|
}
|
|
input_example = {
|
|
"messages": [
|
|
{
|
|
"role": "assistant",
|
|
"content": "You are in helpful assistant!",
|
|
},
|
|
{
|
|
"role": "user",
|
|
"content": "What is Retrieval-augmented Generation?",
|
|
},
|
|
],
|
|
**params_subset,
|
|
}
|
|
with mlflow.start_run():
|
|
model_info = mlflow.pyfunc.log_model(
|
|
name="model", python_model=model, input_example=input_example
|
|
)
|
|
assert model_info.signature.inputs == CHAT_MODEL_INPUT_SCHEMA
|
|
assert model_info.signature.outputs == CHAT_MODEL_OUTPUT_SCHEMA
|
|
mlflow_model = Model.load(model_info.model_uri)
|
|
local_path = _download_artifact_from_uri(model_info.model_uri)
|
|
assert mlflow_model.load_input_example(local_path) == {
|
|
"messages": input_example["messages"],
|
|
**params_subset,
|
|
}
|
|
|
|
inference_payload = load_serving_example(model_info.model_uri)
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_info.model_uri,
|
|
data=inference_payload,
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
|
|
expect_status_code(response, 200)
|
|
choices = json.loads(response.content)["choices"]
|
|
assert choices[0]["message"]["content"] == json.dumps(input_example["messages"])
|
|
assert json.loads(choices[1]["message"]["content"]) == {
|
|
**DEFAULT_PARAMS,
|
|
**params_subset,
|
|
}
|
|
|
|
|
|
def test_chat_model_predict_stream(tmp_path):
|
|
model = SimpleChatModel()
|
|
mlflow.pyfunc.save_model(python_model=model, path=tmp_path)
|
|
|
|
loaded_model = mlflow.pyfunc.load_model(tmp_path)
|
|
messages = [
|
|
{"role": "system", "content": "You are a helpful assistant"},
|
|
{"role": "user", "content": "Hello!"},
|
|
]
|
|
|
|
responses = list(loaded_model.predict_stream({"messages": messages}))
|
|
for i, resp in enumerate(responses[:-1]):
|
|
assert resp["choices"][0]["delta"]["content"] == f"message {i}"
|
|
|
|
assert responses[-1]["choices"][0]["delta"] == {}
|
|
|
|
|
|
def test_chat_model_can_receive_and_return_metadata():
|
|
messages = [{"role": "user", "content": "Hello!"}]
|
|
params = {
|
|
"custom_inputs": {"image_url": "example", "detail": "high", "other_dict": {"key": "value"}},
|
|
}
|
|
input_example = {
|
|
"messages": messages,
|
|
**params,
|
|
}
|
|
|
|
model = ChatModelWithMetadata()
|
|
with mlflow.start_run():
|
|
model_info = mlflow.pyfunc.log_model(
|
|
name="model",
|
|
python_model=model,
|
|
input_example=input_example,
|
|
)
|
|
|
|
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
|
|
# test that it works for normal pyfunc predict
|
|
response = loaded_model.predict({"messages": messages, **params})
|
|
assert response["custom_outputs"] == params["custom_inputs"]
|
|
|
|
# test that it works in serving
|
|
inference_payload = load_serving_example(model_info.model_uri)
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_info.model_uri,
|
|
data=inference_payload,
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
|
|
serving_response = json.loads(response.content)
|
|
assert serving_response["custom_outputs"] == params["custom_inputs"]
|
|
|
|
|
|
def test_chat_model_can_use_tool_calls():
|
|
messages = [{"role": "user", "content": "What's the weather?"}]
|
|
|
|
weather_tool = (
|
|
FunctionToolDefinition(
|
|
name="get_weather",
|
|
description="Get the weather for your current location",
|
|
parameters=ToolParamsSchema(
|
|
{
|
|
"city": {
|
|
"type": "string",
|
|
"description": "The city to get the weather for",
|
|
},
|
|
"unit": {"type": "string", "enum": ["F", "C"]},
|
|
},
|
|
required=["city", "unit"],
|
|
),
|
|
)
|
|
.to_tool_definition()
|
|
.to_dict()
|
|
)
|
|
|
|
example = {
|
|
"messages": messages,
|
|
"tools": [weather_tool],
|
|
}
|
|
|
|
model = ChatModelWithToolCalling()
|
|
with mlflow.start_run():
|
|
model_info = mlflow.pyfunc.log_model(
|
|
name="model",
|
|
python_model=model,
|
|
input_example=example,
|
|
)
|
|
|
|
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
response = loaded_model.predict(example)
|
|
|
|
model_tool_calls = response["choices"][0]["message"]["tool_calls"]
|
|
assert json.loads(model_tool_calls[0]["function"]["arguments"]) == {
|
|
"city": "some_value",
|
|
"unit": "some_value",
|
|
}
|
|
|
|
|
|
def test_chat_model_without_context_in_predict():
|
|
response = ChatCompletionResponse(
|
|
choices=[ChatChoice(message=ChatMessage(role="assistant", content="hi"))]
|
|
)
|
|
chunk_response = ChatCompletionChunk(
|
|
choices=[ChatChunkChoice(delta=ChatChoiceDelta(role="assistant", content="hi"))]
|
|
)
|
|
|
|
class Model(mlflow.pyfunc.ChatModel):
|
|
def predict(self, messages: list[ChatMessage], params: ChatParams):
|
|
return response
|
|
|
|
def predict_stream(self, messages: list[ChatMessage], params: ChatParams):
|
|
yield chunk_response
|
|
|
|
model = Model()
|
|
messages = [ChatMessage(role="user", content="hello?", name="chat")]
|
|
assert model.predict(messages, ChatParams()) == response
|
|
assert next(iter(model.predict_stream(messages, ChatParams()))) == chunk_response
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.pyfunc.log_model(
|
|
name="model", python_model=model, input_example=messages
|
|
)
|
|
pyfunc_model = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
input_data = {"messages": [{"role": "user", "content": "hello"}]}
|
|
assert pyfunc_model.predict(input_data) == response.to_dict()
|
|
assert next(iter(pyfunc_model.predict_stream(input_data))) == chunk_response.to_dict()
|