624 lines
20 KiB
Python
624 lines
20 KiB
Python
import importlib
|
|
import json
|
|
from unittest import mock
|
|
|
|
import dspy
|
|
import dspy.teleprompt
|
|
import pytest
|
|
from dspy.utils.dummies import DummyLM, dummy_rm
|
|
from packaging.version import Version
|
|
|
|
import mlflow
|
|
from mlflow.exceptions import MlflowException
|
|
from mlflow.models import Model, ModelSignature
|
|
from mlflow.types.schema import ColSpec, Schema
|
|
|
|
from tests.helper_functions import (
|
|
_assert_pip_requirements,
|
|
_compare_logged_code_paths,
|
|
_mlflow_major_version_string,
|
|
expect_status_code,
|
|
pyfunc_serve_and_score_model,
|
|
)
|
|
|
|
_DSPY_VERSION = Version(importlib.metadata.version("dspy"))
|
|
|
|
_DSPY_UNDER_2_6 = _DSPY_VERSION < Version("2.6.0")
|
|
|
|
_DSPY_2_6_23_OR_OLDER = _DSPY_VERSION <= Version("2.6.23")
|
|
skip_if_2_6_23_or_older = pytest.mark.skipif(
|
|
_DSPY_2_6_23_OR_OLDER,
|
|
reason="Streaming API is only supported in dspy 2.6.24 or later.",
|
|
)
|
|
|
|
_REASONING_KEYWORD = "rationale" if _DSPY_UNDER_2_6 else "reasoning"
|
|
|
|
|
|
@pytest.fixture
|
|
def dummy_model():
|
|
return DummyLM([
|
|
{"answer": answer, _REASONING_KEYWORD: "reason"} for answer in ["4", "6", "8", "10"]
|
|
])
|
|
|
|
|
|
class CoT(dspy.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.prog = dspy.ChainOfThought("question -> answer")
|
|
|
|
def forward(self, question):
|
|
return self.prog(question=question)
|
|
|
|
|
|
class NumericalCoT(dspy.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.prog = dspy.ChainOfThought("question -> answer: int")
|
|
|
|
def forward(self, question):
|
|
return self.prog(question=question).answer
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_dspy_settings():
|
|
yield
|
|
|
|
dspy.settings.configure(lm=None, rm=None)
|
|
|
|
|
|
use_dspy_model_save_param = pytest.param(
|
|
True,
|
|
marks=pytest.mark.skipif(
|
|
Version(dspy.__version__) <= Version("3.1.0"),
|
|
reason="dspy<=3.1.0 does not support 'use_dspy_model_save' param.",
|
|
),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("use_dspy_model_save", [use_dspy_model_save_param, False])
|
|
def test_basic_save(use_dspy_model_save):
|
|
if use_dspy_model_save and _DSPY_VERSION <= Version("2.6.0"):
|
|
pytest.skip("'use_dspy_model_save' = True does not support dspy <= 2.6.0")
|
|
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dspy.LM(model="openai/gpt-4o-mini", max_tokens=250))
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model,
|
|
name="model",
|
|
use_dspy_model_save=use_dspy_model_save,
|
|
)
|
|
|
|
# Clear the lm setting to test the loading logic.
|
|
dspy.settings.configure(lm=None)
|
|
|
|
loaded_model = mlflow.dspy.load_model(model_info.model_uri)
|
|
|
|
# Check that the global settings is popped back.
|
|
assert dspy.settings.lm.model == "openai/gpt-4o-mini"
|
|
assert isinstance(loaded_model, CoT)
|
|
|
|
|
|
@pytest.mark.parametrize("use_dspy_model_save", [use_dspy_model_save_param, False])
|
|
def test_save_compiled_model(dummy_model, use_dspy_model_save):
|
|
train_data = [
|
|
"What is 2 + 2?",
|
|
"What is 3 + 3?",
|
|
"What is 4 + 4?",
|
|
"What is 5 + 5?",
|
|
]
|
|
train_label = ["4", "6", "8", "10"]
|
|
trainset = [
|
|
dspy.Example(question=q, answer=a).with_inputs("question")
|
|
for q, a in zip(train_data, train_label)
|
|
]
|
|
|
|
def dummy_metric(program):
|
|
return 1.0
|
|
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
dspy_model = CoT()
|
|
optimizer = dspy.teleprompt.BootstrapFewShot(metric=dummy_metric)
|
|
optimized_cot = optimizer.compile(dspy_model, trainset=trainset)
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
optimized_cot, name="model", use_dspy_model_save=use_dspy_model_save
|
|
)
|
|
|
|
# Clear the lm setting to test the loading logic.
|
|
dspy.settings.configure(lm=None)
|
|
|
|
loaded_model = mlflow.dspy.load_model(model_info.model_uri)
|
|
|
|
assert isinstance(loaded_model, CoT)
|
|
assert loaded_model.prog.predictors()[0].demos == optimized_cot.prog.predictors()[0].demos
|
|
|
|
|
|
@pytest.mark.parametrize("use_dspy_model_save", [use_dspy_model_save_param, False])
|
|
def test_dspy_save_preserves_object_state(use_dspy_model_save):
|
|
class GenerateAnswer(dspy.Signature):
|
|
"""Answer questions with short factoid answers."""
|
|
|
|
context = dspy.InputField(desc="may contain relevant facts")
|
|
question = dspy.InputField()
|
|
answer = dspy.OutputField(desc="often between 1 and 5 words")
|
|
|
|
class RAG(dspy.Module):
|
|
def __init__(self, num_passages=3):
|
|
super().__init__()
|
|
|
|
self.retrieve = dspy.Retrieve(k=num_passages)
|
|
self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
|
|
|
|
def forward(self, question):
|
|
assert question == "What is 2 + 2?"
|
|
context = self.retrieve(question).passages
|
|
prediction = self.generate_answer(context=context, question=question)
|
|
return dspy.Prediction(context=context, answer=prediction.answer)
|
|
|
|
def dummy_metric(*args, **kwargs):
|
|
return 1.0
|
|
|
|
model = DummyLM([{"answer": answer, "reasoning": "reason"} for answer in ["4", "6", "8", "10"]])
|
|
rm = dummy_rm(passages=["dummy1", "dummy2", "dummy3"])
|
|
dspy.settings.configure(lm=model, rm=rm)
|
|
|
|
train_data = [
|
|
"What is 2 + 2?",
|
|
"What is 3 + 3?",
|
|
"What is 4 + 4?",
|
|
"What is 5 + 5?",
|
|
]
|
|
train_label = ["4", "6", "8", "10"]
|
|
trainset = [
|
|
dspy.Example(question=q, answer=a).with_inputs("question").with_inputs("reasoning")
|
|
for q, a in zip(train_data, train_label)
|
|
]
|
|
|
|
dspy_model = RAG()
|
|
optimizer = dspy.teleprompt.BootstrapFewShot(metric=dummy_metric)
|
|
optimized_cot = optimizer.compile(dspy_model, trainset=trainset)
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
optimized_cot, name="model", use_dspy_model_save=use_dspy_model_save
|
|
)
|
|
|
|
original_settings = dict(dspy.settings.config)
|
|
original_settings["traces"] = None
|
|
|
|
# Clear the lm setting to test the loading logic.
|
|
dspy.settings.configure(lm=None)
|
|
|
|
model_url = model_info.model_uri
|
|
|
|
input_examples = {"inputs": ["What is 2 + 2?"]}
|
|
# test that the model can be served
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_url,
|
|
data=json.dumps(input_examples),
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
expect_status_code(response, 200)
|
|
|
|
loaded_model = mlflow.dspy.load_model(model_url)
|
|
assert isinstance(loaded_model, RAG)
|
|
assert loaded_model.retrieve is not None
|
|
assert (
|
|
loaded_model.generate_answer.predictors()[0].demos
|
|
== optimized_cot.generate_answer.predictors()[0].demos
|
|
)
|
|
|
|
loaded_settings = dict(dspy.settings.config)
|
|
loaded_settings["traces"] = None
|
|
|
|
assert loaded_settings["lm"].model == original_settings["lm"].model
|
|
assert loaded_settings["lm"].model_type == original_settings["lm"].model_type
|
|
assert loaded_settings["rm"].__dict__ == original_settings["rm"].__dict__
|
|
|
|
del (
|
|
loaded_settings["lm"],
|
|
original_settings["lm"],
|
|
loaded_settings["rm"],
|
|
original_settings["rm"],
|
|
)
|
|
|
|
assert original_settings == loaded_settings
|
|
|
|
|
|
@pytest.mark.parametrize("use_dspy_model_save", [use_dspy_model_save_param, False])
|
|
def test_load_logged_model_in_native_dspy(dummy_model, use_dspy_model_save):
|
|
dspy_model = CoT()
|
|
# Arbitrary set the demo to test saving/loading has no data loss.
|
|
dspy_model.prog.predictors()[0].demos = [
|
|
"What is 2 + 2?",
|
|
"What is 3 + 3?",
|
|
"What is 4 + 4?",
|
|
"What is 5 + 5?",
|
|
]
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model, name="model", use_dspy_model_save=use_dspy_model_save
|
|
)
|
|
loaded_dspy_model = mlflow.dspy.load_model(model_info.model_uri)
|
|
|
|
assert isinstance(loaded_dspy_model, CoT)
|
|
assert loaded_dspy_model.prog.predictors()[0].demos == dspy_model.prog.predictors()[0].demos
|
|
|
|
|
|
def test_serving_logged_model(dummy_model):
|
|
class CoT(dspy.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.prog = dspy.ChainOfThought("question -> answer")
|
|
|
|
def forward(self, question):
|
|
assert question == "What is 2 + 2?"
|
|
return self.prog(question=question)
|
|
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
input_examples = {"inputs": ["What is 2 + 2?"]}
|
|
input_schema = Schema([ColSpec("string")])
|
|
output_schema = Schema([ColSpec("string")])
|
|
signature = ModelSignature(inputs=input_schema, outputs=output_schema)
|
|
|
|
artifact_path = "model"
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model,
|
|
name=artifact_path,
|
|
signature=signature,
|
|
input_example=["What is 2 + 2?"],
|
|
)
|
|
model_uri = model_info.model_uri
|
|
dspy.settings.configure(lm=None)
|
|
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_uri,
|
|
data=json.dumps(input_examples),
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
|
|
expect_status_code(response, 200)
|
|
|
|
json_response = json.loads(response.content)
|
|
|
|
assert _REASONING_KEYWORD in json_response["predictions"]
|
|
assert "answer" in json_response["predictions"]
|
|
|
|
|
|
def test_log_model_multi_inputs(dummy_model):
|
|
class MultiInputCoT(dspy.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.prog = dspy.ChainOfThought("question, hint -> answer")
|
|
|
|
def forward(self, question, hint):
|
|
assert question == "What is 2 + 2?"
|
|
assert hint == "Hint: 2 + 2 = ?"
|
|
return self.prog(question=question, hint=hint)
|
|
|
|
dspy_model = MultiInputCoT()
|
|
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
input_example = {"question": "What is 2 + 2?", "hint": "Hint: 2 + 2 = ?"}
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model,
|
|
name="model",
|
|
input_example=input_example,
|
|
)
|
|
|
|
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
assert loaded_model.predict(input_example) == {"answer": "6", _REASONING_KEYWORD: "reason"}
|
|
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_info.model_uri,
|
|
data=json.dumps({"inputs": [input_example]}),
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
|
|
expect_status_code(response, 200)
|
|
|
|
json_response = json.loads(response.content)
|
|
|
|
assert _REASONING_KEYWORD in json_response["predictions"]
|
|
assert "answer" in json_response["predictions"]
|
|
|
|
|
|
def test_save_chat_model_with_string_output(dummy_model):
|
|
class CoT(dspy.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.prog = dspy.ChainOfThought("question -> answer")
|
|
|
|
def forward(self, inputs):
|
|
# DSPy chat model's inputs is a list of dict with keys roles (optional) and content.
|
|
# And here we output a single string.
|
|
return self.prog(question=inputs[0]["content"]).answer
|
|
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
input_examples = {"messages": [{"role": "user", "content": "What is 2 + 2?"}]}
|
|
|
|
artifact_path = "model"
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model,
|
|
name=artifact_path,
|
|
task="llm/v1/chat",
|
|
input_example=input_examples,
|
|
)
|
|
loaded_pyfunc = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
response = loaded_pyfunc.predict(input_examples)
|
|
|
|
assert "choices" in response
|
|
assert len(response["choices"]) == 1
|
|
assert "message" in response["choices"][0]
|
|
# The content should just be a string.
|
|
assert response["choices"][0]["message"]["content"] == "4"
|
|
|
|
|
|
def test_serve_chat_model(dummy_model):
|
|
class CoT(dspy.Module):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.prog = dspy.ChainOfThought("question -> answer")
|
|
|
|
def forward(self, inputs):
|
|
return self.prog(question=inputs[0]["content"])
|
|
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
input_examples = {"messages": [{"role": "user", "content": "What is 2 + 2?"}]}
|
|
|
|
artifact_path = "model"
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model,
|
|
name=artifact_path,
|
|
task="llm/v1/chat",
|
|
input_example=input_examples,
|
|
)
|
|
dspy.settings.configure(lm=None)
|
|
|
|
response = pyfunc_serve_and_score_model(
|
|
model_uri=model_info.model_uri,
|
|
data=json.dumps(input_examples),
|
|
content_type="application/json",
|
|
extra_args=["--env-manager", "local"],
|
|
)
|
|
|
|
expect_status_code(response, 200)
|
|
|
|
json_response = json.loads(response.content)
|
|
|
|
assert "choices" in json_response
|
|
assert len(json_response["choices"]) == 1
|
|
assert "message" in json_response["choices"][0]
|
|
assert _REASONING_KEYWORD in json_response["choices"][0]["message"]["content"]
|
|
assert "answer" in json_response["choices"][0]["message"]["content"]
|
|
|
|
|
|
def test_code_paths_is_used():
|
|
artifact_path = "model"
|
|
dspy_model = CoT()
|
|
with (
|
|
mlflow.start_run(),
|
|
mock.patch("mlflow.dspy.load._add_code_from_conf_to_system_path") as add_mock,
|
|
):
|
|
model_info = mlflow.dspy.log_model(dspy_model, name=artifact_path, code_paths=[__file__])
|
|
_compare_logged_code_paths(__file__, model_info.model_uri, "dspy")
|
|
mlflow.dspy.load_model(model_info.model_uri)
|
|
add_mock.assert_called()
|
|
|
|
|
|
def test_additional_pip_requirements():
|
|
expected_mlflow_version = _mlflow_major_version_string()
|
|
artifact_path = "model"
|
|
dspy_model = CoT()
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model, name=artifact_path, extra_pip_requirements=["dummy"]
|
|
)
|
|
|
|
_assert_pip_requirements(model_info.model_uri, [expected_mlflow_version, "dummy"])
|
|
|
|
|
|
def test_infer_signature_from_input_examples(dummy_model):
|
|
artifact_path = "model"
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dummy_model)
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model, name=artifact_path, input_example="what is 2 + 2?"
|
|
)
|
|
|
|
loaded_model = Model.load(model_info.model_uri)
|
|
assert loaded_model.signature.inputs == Schema([ColSpec("string")])
|
|
assert loaded_model.signature.outputs == Schema([
|
|
ColSpec(name="answer", type="string"),
|
|
ColSpec(name=_REASONING_KEYWORD, type="string"),
|
|
])
|
|
|
|
|
|
@skip_if_2_6_23_or_older
|
|
def test_predict_stream_unsupported_schema(dummy_model):
|
|
dspy_model = NumericalCoT()
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
model_info = mlflow.dspy.log_model(dspy_model, name="model")
|
|
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
|
|
assert not loaded_model._model_meta.flavors["python_function"]["streamable"]
|
|
output = loaded_model.predict_stream({"question": "What is 2 + 2?"})
|
|
with pytest.raises(
|
|
mlflow.exceptions.MlflowException,
|
|
match="This model does not support predict_stream method.",
|
|
):
|
|
next(output)
|
|
|
|
|
|
@skip_if_2_6_23_or_older
|
|
def test_predict_stream_success(dummy_model):
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model, name="model", input_example={"question": "what is 2 + 2?"}
|
|
)
|
|
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
|
|
assert loaded_model._model_meta.flavors["python_function"]["streamable"]
|
|
results = []
|
|
|
|
def dummy_streamify(*args, **kwargs):
|
|
# In dspy>=3, `StreamResponse` requires `is_last_chunk` argument.
|
|
# https://github.com/stanfordnlp/dspy/pull/8587
|
|
extra_kwargs = {"is_last_chunk": False} if _DSPY_VERSION.major >= 3 else {}
|
|
yield dspy.streaming.StreamResponse(
|
|
predict_name="prog.predict",
|
|
signature_field_name="answer",
|
|
chunk="2",
|
|
**extra_kwargs,
|
|
)
|
|
extra_kwargs = {"is_last_chunk": True} if _DSPY_VERSION.major >= 3 else {}
|
|
yield dspy.streaming.StreamResponse(
|
|
predict_name="prog.predict",
|
|
signature_field_name=_REASONING_KEYWORD,
|
|
chunk="reason",
|
|
**extra_kwargs,
|
|
)
|
|
|
|
with mock.patch("dspy.streamify", return_value=dummy_streamify):
|
|
output = loaded_model.predict_stream({"question": "What is 2 + 2?"})
|
|
for o in output:
|
|
results.append(o)
|
|
|
|
assert len(results) == 2
|
|
extra_kwargs = {"is_last_chunk": False} if _DSPY_VERSION.major >= 3 else {}
|
|
assert results[0] == {
|
|
"predict_name": "prog.predict",
|
|
"signature_field_name": "answer",
|
|
"chunk": "2",
|
|
**extra_kwargs,
|
|
}
|
|
extra_kwargs = {"is_last_chunk": True} if _DSPY_VERSION.major >= 3 else {}
|
|
assert results[1] == {
|
|
"predict_name": "prog.predict",
|
|
"signature_field_name": _REASONING_KEYWORD,
|
|
"chunk": "reason",
|
|
**extra_kwargs,
|
|
}
|
|
|
|
|
|
def test_predict_output(dummy_model):
|
|
class MockModelReturningNonPrediction(dspy.Module):
|
|
def forward(self, question):
|
|
# Return a plain dict instead of dspy.Prediction
|
|
return {"answer": "4", "custom_field": "custom_value"}
|
|
|
|
class MockModelReturningPrediction(dspy.Module):
|
|
def forward(self, question):
|
|
# Return a dspy.Prediction
|
|
prediction = dspy.Prediction()
|
|
prediction.answer = "4"
|
|
prediction.custom_field = "custom_value"
|
|
return prediction
|
|
|
|
dspy.settings.configure(lm=dummy_model)
|
|
|
|
non_prediction_model = MockModelReturningNonPrediction()
|
|
model_info = mlflow.dspy.log_model(non_prediction_model, name="non_prediction_model")
|
|
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
result = loaded_model.predict("What is 2 + 2?")
|
|
|
|
assert isinstance(result, dict)
|
|
assert result == {"answer": "4", "custom_field": "custom_value"}
|
|
|
|
prediction_model = MockModelReturningPrediction()
|
|
model_info = mlflow.dspy.log_model(prediction_model, name="prediction_model")
|
|
loaded_model = mlflow.pyfunc.load_model(model_info.model_uri)
|
|
result = loaded_model.predict("What is 2 + 2?")
|
|
|
|
assert isinstance(result, dict)
|
|
assert result == {"answer": "4", "custom_field": "custom_value"}
|
|
|
|
|
|
def test_load_model_disallows_pickle_deserialization_legacy_pkl(monkeypatch):
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dspy.LM(model="openai/gpt-4o-mini", max_tokens=250))
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model,
|
|
name="model",
|
|
use_dspy_model_save=False,
|
|
)
|
|
|
|
monkeypatch.setenv("MLFLOW_ALLOW_PICKLE_DESERIALIZATION", "false")
|
|
with pytest.raises(MlflowException, match="MLFLOW_ALLOW_PICKLE_DESERIALIZATION"):
|
|
mlflow.dspy.load_model(model_info.model_uri)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
_DSPY_VERSION <= Version("3.1.0"),
|
|
reason="'use_dspy_model_save' = True does not support dspy <= 3.1.0",
|
|
)
|
|
@pytest.mark.parametrize(("env_value", "expected_allow_pickle"), [("false", False), ("true", True)])
|
|
def test_load_model_forwards_allow_pickle_to_dspy(env_value, expected_allow_pickle, monkeypatch):
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dspy.LM(model="openai/gpt-4o-mini", max_tokens=250))
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model,
|
|
name="model",
|
|
use_dspy_model_save=True,
|
|
)
|
|
|
|
monkeypatch.setenv("MLFLOW_ALLOW_PICKLE_DESERIALIZATION", env_value)
|
|
with mock.patch("dspy.load") as mock_load:
|
|
try:
|
|
mlflow.dspy.load_model(model_info.model_uri)
|
|
except Exception:
|
|
pass # downstream code may fail on the MagicMock; only the kwarg matters here
|
|
|
|
mock_load.assert_called_once()
|
|
assert mock_load.call_args.kwargs["allow_pickle"] is expected_allow_pickle
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
_DSPY_VERSION <= Version("3.1.0"),
|
|
reason="'use_dspy_model_save' = True does not support dspy <= 3.1.0",
|
|
)
|
|
def test_load_model_wraps_dspy_error_when_pickle_disabled(monkeypatch):
|
|
dspy_model = CoT()
|
|
dspy.settings.configure(lm=dspy.LM(model="openai/gpt-4o-mini", max_tokens=250))
|
|
|
|
with mlflow.start_run():
|
|
model_info = mlflow.dspy.log_model(
|
|
dspy_model,
|
|
name="model",
|
|
use_dspy_model_save=True,
|
|
)
|
|
|
|
monkeypatch.setenv("MLFLOW_ALLOW_PICKLE_DESERIALIZATION", "false")
|
|
with mock.patch("dspy.load", side_effect=ValueError("pickle disallowed by dspy")):
|
|
with pytest.raises(MlflowException, match="MLFLOW_ALLOW_PICKLE_DESERIALIZATION"):
|
|
mlflow.dspy.load_model(model_info.model_uri)
|