Files
2026-07-13 13:22:34 +08:00

224 lines
6.7 KiB
Python

from typing import Any, Union
import pytest
from pydantic import BaseModel
from mlflow.entities.assessment import Feedback
from mlflow.exceptions import MlflowException
from mlflow.genai.judges import CategoricalRating
from mlflow.genai.optimize.util import (
create_metric_from_scorers,
infer_type_from_value,
validate_train_data,
)
from mlflow.genai.scorers import scorer
@pytest.mark.parametrize(
("input_value", "expected_type"),
[
(None, type(None)),
(True, bool),
(42, int),
(3.14, float),
("hello", str),
],
)
def test_infer_primitive_types(input_value, expected_type):
assert infer_type_from_value(input_value) == expected_type
@pytest.mark.parametrize(
("input_list", "expected_type"),
[
([], list[Any]),
([1, 2, 3], list[int]),
(["a", "b", "c"], list[str]),
([1.0, 2.0, 3.0], list[float]),
([True, False, True], list[bool]),
([1, "hello", True], list[Union[int, str, bool]]), # noqa: UP007
([1, "hello", True], list[int | str | bool]),
([1, 2.0], list[int | float]),
([[1, 2], [3, 4]], list[list[int]]),
([["a"], ["b", "c"]], list[list[str]]),
],
)
def test_infer_list_types(input_list, expected_type):
assert infer_type_from_value(input_list) == expected_type
@pytest.mark.parametrize(
("input_dict", "expected_fields"),
[
({"name": "John", "age": 30, "active": True}, {"name": str, "age": int, "active": bool}),
({"score": 95.5, "passed": True}, {"score": float, "passed": bool}),
],
)
def test_infer_simple_dict(input_dict, expected_fields):
result = infer_type_from_value(input_dict)
assert isinstance(result, type)
assert issubclass(result, BaseModel)
for field_name, expected_type in expected_fields.items():
assert result.__annotations__[field_name] == expected_type
def test_infer_nested_dict():
data = {
"user": {"name": "John", "scores": [85, 90, 95]},
"settings": {"enabled": True, "theme": "dark"},
}
result = infer_type_from_value(data)
assert isinstance(result, type)
assert issubclass(result, BaseModel)
# Check nested model types
user_model = result.__annotations__["user"]
settings_model = result.__annotations__["settings"]
assert issubclass(user_model, BaseModel)
assert issubclass(settings_model, BaseModel)
# Check nested field types
assert user_model.__annotations__["name"] == str
assert user_model.__annotations__["scores"] == list[int]
assert settings_model.__annotations__["enabled"] == bool
assert settings_model.__annotations__["theme"] == str
@pytest.mark.parametrize(
("model_class", "model_data"),
[
(
type("UserModel", (BaseModel,), {"__annotations__": {"name": str, "age": int}}),
{"name": "John", "age": 30},
),
(
type("ProductModel", (BaseModel,), {"__annotations__": {"id": int, "price": float}}),
{"id": 1, "price": 99.99},
),
],
)
def test_infer_pydantic_model(model_class, model_data):
model = model_class(**model_data)
result = infer_type_from_value(model)
assert result == model_class
@pytest.mark.parametrize(
"type_to_infer",
[
type("CustomClass", (), {}),
type("AnotherClass", (), {"custom_attr": 42}),
],
)
def test_infer_unsupported_type(type_to_infer):
obj = type_to_infer()
assert infer_type_from_value(obj) == Any
@pytest.mark.parametrize(
("input_dict", "model_name"),
[
({"name": "John", "age": 30}, "UserData"),
({"id": 1, "value": "test"}, "TestModel"),
],
)
def test_model_name_parameter(input_dict, model_name):
result = infer_type_from_value(input_dict, model_name=model_name)
assert result.__name__ == model_name
@pytest.mark.parametrize(
("score", "expected_score"),
[
(CategoricalRating.YES, 1.0),
(CategoricalRating.NO, 0.0),
("yes", 1.0),
("no", 0.0),
(True, 1.0),
(False, 0.0),
(1, 1.0),
(0, 0.0),
(1.0, 1.0),
(0.0, 0.0),
],
)
def test_create_metric_from_scorers_with_single_score(score, expected_score):
@scorer(name="test_scorer")
def test_scorer(inputs, outputs):
return Feedback(name="test_scorer", value=score, rationale="test rationale")
metric = create_metric_from_scorers([test_scorer])
result = metric({"input": "test"}, {"output": "result"}, {}, None)
assert result[0] == expected_score
assert result[1] == {"test_scorer": "test rationale"}
assert result[2] == {"test_scorer": expected_score}
def test_create_metric_from_scorers_with_multiple_categorical_ratings():
@scorer(name="scorer1")
def scorer1(inputs, outputs):
return Feedback(name="scorer1", value=CategoricalRating.YES, rationale="rationale1")
@scorer(name="scorer2")
def scorer2(inputs, outputs):
return Feedback(name="scorer2", value=CategoricalRating.YES, rationale="rationale2")
metric = create_metric_from_scorers([scorer1, scorer2])
# Should average: (1.0 + 1.0) / 2 = 1.0
result = metric({"input": "test"}, {"output": "result"}, {}, None)
assert result[0] == 1.0
assert result[1] == {"scorer1": "rationale1", "scorer2": "rationale2"}
assert result[2] == {"scorer1": 1.0, "scorer2": 1.0}
@pytest.mark.parametrize(
("train_data", "scorers", "expected_error"),
[
# Empty inputs
(
[{"inputs": {}, "outputs": "result"}],
[],
"Record 0 is missing required 'inputs' field or it is empty",
),
# Missing inputs
(
[{"outputs": "result"}],
[],
"Record 0 is missing required 'inputs' field or it is empty",
),
],
)
def test_validate_train_data_errors(train_data, scorers, expected_error):
import pandas as pd
with pytest.raises(MlflowException, match=expected_error):
validate_train_data(pd.DataFrame(train_data), scorers, lambda **kwargs: None)
@pytest.mark.parametrize(
"train_data",
[
# Valid with outputs
[{"inputs": {"text": "hello"}, "outputs": "result"}],
# Valid with expectations
[{"inputs": {"text": "hello"}, "expectations": {"expected": "result"}}],
# Multiple valid records
[
{"inputs": {"text": "hello"}, "outputs": "result1"},
{"inputs": {"text": "world"}, "expectations": {"expected": "result2"}},
],
# Falsy but valid values: False as output
[{"inputs": {"text": "hello"}, "outputs": False}],
],
)
def test_validate_train_data_success(train_data):
import pandas as pd
validate_train_data(pd.DataFrame(train_data), [], lambda **kwargs: None)