192 lines
6.1 KiB
Python
192 lines
6.1 KiB
Python
import numpy as np
|
|
import pandas as pd
|
|
|
|
from mlflow.entities.dataset_record_source import DatasetRecordSource, DatasetRecordSourceType
|
|
from mlflow.genai.evaluation.entities import EvalItem, EvaluationResult
|
|
from mlflow.genai.judges import CategoricalRating
|
|
|
|
|
|
def test_eval_item_from_dataset_row_extracts_source():
|
|
source = DatasetRecordSource(
|
|
source_type=DatasetRecordSourceType.TRACE,
|
|
source_data={"trace_id": "tr-123", "session_id": "session_1"},
|
|
)
|
|
|
|
row = {
|
|
"inputs": {"question": "test"},
|
|
"outputs": "answer",
|
|
"expectations": {},
|
|
"source": source,
|
|
}
|
|
|
|
eval_item = EvalItem.from_dataset_row(row)
|
|
|
|
assert eval_item.source == source
|
|
assert eval_item.source.source_data["session_id"] == "session_1"
|
|
assert eval_item.inputs == {"question": "test"}
|
|
assert eval_item.outputs == "answer"
|
|
|
|
|
|
def test_eval_item_from_dataset_row_handles_missing_source():
|
|
row = {
|
|
"inputs": {"question": "test"},
|
|
"outputs": "answer",
|
|
"expectations": {},
|
|
}
|
|
|
|
eval_item = EvalItem.from_dataset_row(row)
|
|
|
|
assert eval_item.source is None
|
|
assert eval_item.inputs == {"question": "test"}
|
|
assert eval_item.outputs == "answer"
|
|
|
|
|
|
def test_all_passing():
|
|
df = pd.DataFrame([
|
|
{"scorer_a/value": True, "scorer_a/rationale": None},
|
|
{"scorer_a/value": True, "scorer_a/rationale": None},
|
|
])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert result.passed
|
|
assert result.reason == ""
|
|
|
|
|
|
def test_with_failures():
|
|
df = pd.DataFrame([
|
|
{
|
|
"scorer_a/value": True,
|
|
"scorer_a/rationale": None,
|
|
"scorer_b/value": False,
|
|
"scorer_b/rationale": "bad output",
|
|
}
|
|
])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert not result.passed
|
|
assert "scorer_b" in result.reason
|
|
|
|
|
|
def test_string_yes_no():
|
|
df = pd.DataFrame([
|
|
{"scorer_a/value": "yes", "scorer_a/rationale": None},
|
|
{"scorer_a/value": "no", "scorer_a/rationale": "failed check"},
|
|
])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert not result.passed
|
|
assert "scorer_a" in result.reason
|
|
|
|
|
|
def test_no_rating_fails_cleanly_without_pass_if_hint():
|
|
# "no" is a recognized rating, so it fails without nagging about pass_if.
|
|
df = pd.DataFrame([{"scorer_a/value": "no", "scorer_a/rationale": None}])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert not result.passed
|
|
assert "value='no'" in result.reason
|
|
assert "pass_if" not in result.reason
|
|
|
|
|
|
def test_unrecognized_string_gets_pass_if_hint():
|
|
# A non-yes/no string is not a recognized rating; surface the pass_if hint.
|
|
df = pd.DataFrame([{"scorer_a/value": "pass", "scorer_a/rationale": None}])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert not result.passed
|
|
assert "'pass'" in result.reason
|
|
assert "pass_if" in result.reason
|
|
|
|
|
|
def test_none_result_df():
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=None)
|
|
assert result.passed
|
|
|
|
|
|
def test_categorical_rating_value():
|
|
df = pd.DataFrame([
|
|
{"scorer_a/value": CategoricalRating.YES, "scorer_a/rationale": None},
|
|
{"scorer_a/value": CategoricalRating.NO, "scorer_a/rationale": "nope"},
|
|
])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert not result.passed
|
|
assert "scorer_a" in result.reason
|
|
|
|
|
|
def test_error_message_fails_with_detail():
|
|
df = pd.DataFrame([
|
|
{
|
|
"scorer_a/value": None,
|
|
"scorer_a/rationale": None,
|
|
"scorer_a/error_message": "scorer blew up",
|
|
}
|
|
])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert not result.passed
|
|
assert "scorer blew up" in result.reason
|
|
|
|
|
|
def test_numeric_value_without_pass_if_fails_loudly():
|
|
df = pd.DataFrame([{"scorer_a/value": 0.7, "scorer_a/rationale": None}])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert not result.passed
|
|
assert "pass_if" in result.reason
|
|
|
|
|
|
def test_numeric_value_rationale_does_not_suppress_pass_if_hint():
|
|
df = pd.DataFrame([{"scorer_a/value": 0.7, "scorer_a/rationale": "looks good"}])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert not result.passed
|
|
assert "looks good" in result.reason
|
|
assert "pass_if" in result.reason
|
|
|
|
|
|
def test_pass_if_predicate_gates_numeric_value():
|
|
df = pd.DataFrame([{"scorer_a/value": 0.7, "scorer_a/rationale": None}])
|
|
|
|
lenient = EvaluationResult(
|
|
run_id="r1", metrics={}, result_df=df, pass_criteria={"scorer_a": lambda v: v >= 0.6}
|
|
)
|
|
assert lenient.passed
|
|
|
|
strict = EvaluationResult(
|
|
run_id="r1", metrics={}, result_df=df, pass_criteria={"scorer_a": lambda v: v >= 0.8}
|
|
)
|
|
assert not strict.passed
|
|
assert "scorer_a" in strict.reason
|
|
|
|
|
|
def test_pass_if_raising_is_reported_not_propagated():
|
|
df = pd.DataFrame([{"scorer_a/value": "weird", "scorer_a/rationale": None}])
|
|
|
|
def boom(v):
|
|
raise RuntimeError("bad predicate")
|
|
|
|
result = EvaluationResult(
|
|
run_id="r1", metrics={}, result_df=df, pass_criteria={"scorer_a": boom}
|
|
)
|
|
assert not result.passed
|
|
assert "pass_if raised" in result.reason
|
|
|
|
|
|
def test_numpy_scalar_values():
|
|
# Regression for np.bool_ / np.float64 scalars from DataFrame.iterrows().
|
|
df = pd.DataFrame([
|
|
{
|
|
"flag/value": np.bool_(True),
|
|
"score/value": np.float64(0.95),
|
|
}
|
|
])
|
|
result = EvaluationResult(
|
|
run_id="r1", metrics={}, result_df=df, pass_criteria={"score": lambda v: v >= 0.9}
|
|
)
|
|
assert result.passed, result.reason
|
|
|
|
df_fail = pd.DataFrame([{"flag/value": np.bool_(False)}])
|
|
assert not EvaluationResult(run_id="r1", metrics={}, result_df=df_fail).passed
|
|
|
|
|
|
def test_sparse_columns_are_skipped():
|
|
# Different rows run different scorers, so each row has NaN for the other's column.
|
|
df = pd.DataFrame([
|
|
{"scorer_a/value": True},
|
|
{"scorer_b/value": True},
|
|
])
|
|
result = EvaluationResult(run_id="r1", metrics={}, result_df=df)
|
|
assert result.passed, result.reason
|