Files
mlflow--mlflow/tests/entities/test_evaluation_dataset.py
2026-07-13 13:22:34 +08:00

744 lines
23 KiB
Python

import json
from unittest.mock import Mock, patch
import pandas as pd
import pytest
from opentelemetry.sdk.trace import ReadableSpan as OTelReadableSpan
from mlflow.entities.dataset_record import DatasetRecord
from mlflow.entities.dataset_record_source import DatasetRecordSourceType
from mlflow.entities.evaluation_dataset import EvaluationDataset
from mlflow.entities.span import Span, SpanType
from mlflow.entities.trace import Trace
from mlflow.entities.trace_data import TraceData
from mlflow.entities.trace_info import TraceInfo
from mlflow.entities.trace_location import TraceLocation
from mlflow.entities.trace_state import TraceState
from mlflow.exceptions import MlflowException
from mlflow.tracing.utils import build_otel_context
def test_evaluation_dataset_creation():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="abc123",
created_time=123456789,
last_update_time=987654321,
tags={"source": "manual", "type": "HUMAN"},
schema='{"fields": ["input", "output"]}',
profile='{"count": 100}',
created_by="user1",
last_updated_by="user2",
)
assert dataset.dataset_id == "dataset123"
assert dataset.name == "test_dataset"
assert dataset.tags == {"source": "manual", "type": "HUMAN"}
assert dataset.schema == '{"fields": ["input", "output"]}'
assert dataset.profile == '{"count": 100}'
assert dataset.digest == "abc123"
assert dataset.created_by == "user1"
assert dataset.last_updated_by == "user2"
assert dataset.created_time == 123456789
assert dataset.last_update_time == 987654321
dataset.experiment_ids = ["exp1", "exp2"]
assert dataset.experiment_ids == ["exp1", "exp2"]
def test_evaluation_dataset_timestamps_required():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=987654321,
)
assert dataset.created_time == 123456789
assert dataset.last_update_time == 987654321
def test_evaluation_dataset_experiment_ids_setter():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
new_experiment_ids = ["exp1", "exp2"]
dataset.experiment_ids = new_experiment_ids
assert dataset._experiment_ids == new_experiment_ids
assert dataset.experiment_ids == new_experiment_ids
dataset.experiment_ids = []
assert dataset._experiment_ids == []
assert dataset.experiment_ids == []
dataset.experiment_ids = None
assert dataset._experiment_ids == []
assert dataset.experiment_ids == []
def test_evaluation_dataset_to_from_proto():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
tags={"source": "manual", "type": "HUMAN"},
schema='{"fields": ["input", "output"]}',
profile='{"count": 100}',
digest="abc123",
created_time=123456789,
last_update_time=987654321,
created_by="user1",
last_updated_by="user2",
)
dataset.experiment_ids = ["exp1", "exp2"]
proto = dataset.to_proto()
assert proto.name == "test_dataset"
assert proto.tags == '{"source": "manual", "type": "HUMAN"}'
assert proto.schema == '{"fields": ["input", "output"]}'
assert proto.profile == '{"count": 100}'
assert proto.digest == "abc123"
assert proto.created_time == 123456789
assert proto.last_update_time == 987654321
assert proto.created_by == "user1"
assert proto.last_updated_by == "user2"
assert list(proto.experiment_ids) == ["exp1", "exp2"]
dataset2 = EvaluationDataset.from_proto(proto)
assert dataset2.dataset_id == dataset.dataset_id
assert dataset2.name == dataset.name
assert dataset2.tags == dataset.tags
assert dataset2.schema == dataset.schema
assert dataset2.profile == dataset.profile
assert dataset2.digest == dataset.digest
assert dataset2.created_time == dataset.created_time
assert dataset2.last_update_time == dataset.last_update_time
assert dataset2.created_by == dataset.created_by
assert dataset2.last_updated_by == dataset.last_updated_by
assert dataset2._experiment_ids == ["exp1", "exp2"]
assert dataset2.experiment_ids == ["exp1", "exp2"]
def test_evaluation_dataset_to_from_proto_minimal():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
proto = dataset.to_proto()
dataset2 = EvaluationDataset.from_proto(proto)
assert dataset2.dataset_id == "dataset123"
assert dataset2.name == "test_dataset"
assert dataset2.tags is None
assert dataset2.schema is None
assert dataset2.profile is None
assert dataset2.digest == "digest123"
assert dataset2.created_by is None
assert dataset2.last_updated_by is None
assert dataset2._experiment_ids is None
def test_evaluation_dataset_to_from_dict():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
tags={"source": "manual", "type": "HUMAN"},
schema='{"fields": ["input", "output"]}',
profile='{"count": 100}',
digest="abc123",
created_time=123456789,
last_update_time=987654321,
created_by="user1",
last_updated_by="user2",
)
dataset.experiment_ids = ["exp1", "exp2"]
dataset._records = [
DatasetRecord(
dataset_record_id="rec789",
dataset_id="dataset123",
inputs={"question": "What is MLflow?"},
created_time=123456789,
last_update_time=123456789,
)
]
data = dataset.to_dict()
assert data["dataset_id"] == "dataset123"
assert data["name"] == "test_dataset"
assert data["tags"] == {"source": "manual", "type": "HUMAN"}
assert data["schema"] == '{"fields": ["input", "output"]}'
assert data["profile"] == '{"count": 100}'
assert data["digest"] == "abc123"
assert data["created_time"] == 123456789
assert data["last_update_time"] == 987654321
assert data["created_by"] == "user1"
assert data["last_updated_by"] == "user2"
assert data["experiment_ids"] == ["exp1", "exp2"]
assert len(data["records"]) == 1
assert data["records"][0]["inputs"]["question"] == "What is MLflow?"
dataset2 = EvaluationDataset.from_dict(data)
assert dataset2.dataset_id == dataset.dataset_id
assert dataset2.name == dataset.name
assert dataset2.tags == dataset.tags
assert dataset2.schema == dataset.schema
assert dataset2.profile == dataset.profile
assert dataset2.digest == dataset.digest
assert dataset2.created_time == dataset.created_time
assert dataset2.last_update_time == dataset.last_update_time
assert dataset2.created_by == dataset.created_by
assert dataset2.last_updated_by == dataset.last_updated_by
assert dataset2._experiment_ids == ["exp1", "exp2"]
assert dataset2.experiment_ids == ["exp1", "exp2"]
assert len(dataset2._records) == 1
assert dataset2._records[0].inputs["question"] == "What is MLflow?"
def test_evaluation_dataset_to_from_dict_minimal():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
dataset._experiment_ids = []
dataset._records = []
data = dataset.to_dict()
dataset2 = EvaluationDataset.from_dict(data)
assert dataset2.dataset_id == "dataset123"
assert dataset2.name == "test_dataset"
assert dataset2.tags is None
assert dataset2.schema is None
assert dataset2.profile is None
assert dataset2.digest == "digest123"
assert dataset2.created_by is None
assert dataset2.last_updated_by is None
assert dataset2._experiment_ids == []
assert dataset2._records == []
def test_evaluation_dataset_has_records():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
assert dataset.has_records() is False
dataset._records = [
DatasetRecord(
dataset_record_id="rec123",
dataset_id="dataset123",
inputs={"test": "data"},
created_time=123456789,
last_update_time=123456789,
)
]
assert dataset.has_records() is True
dataset._records = []
assert dataset.has_records() is True
def test_evaluation_dataset_proto_with_unloaded_experiment_ids():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
assert dataset._experiment_ids is None
proto = dataset.to_proto()
assert len(proto.experiment_ids) == 0
assert dataset._experiment_ids is None
def test_evaluation_dataset_complex_tags():
complex_tags = {
"source": "automated",
"metadata": {"version": "1.0", "config": {"temperature": 0.7, "max_tokens": 100}},
"labels": ["production", "evaluated"],
}
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
tags=complex_tags,
)
proto = dataset.to_proto()
dataset2 = EvaluationDataset.from_proto(proto)
assert dataset2.tags == complex_tags
dataset._experiment_ids = []
dataset._records = []
data = dataset.to_dict()
dataset3 = EvaluationDataset.from_dict(data)
assert dataset3.tags == complex_tags
def test_evaluation_dataset_to_df():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
# Test empty dataset
df_empty = dataset.to_df()
assert isinstance(df_empty, pd.DataFrame)
expected_columns = [
"inputs",
"outputs",
"expectations",
"tags",
"source_type",
"source_id",
"source",
"created_time",
"dataset_record_id",
]
assert list(df_empty.columns) == expected_columns
assert len(df_empty) == 0
# Test dataset with records
dataset._records = [
DatasetRecord(
dataset_record_id="rec123",
dataset_id="dataset123",
inputs={"question": "What is MLflow?"},
outputs={
"answer": "MLflow is an ML platform for managing machine learning lifecycle",
"key1": "value1",
},
expectations={"answer": "MLflow is an ML platform"},
tags={"source": "manual"},
source_type="HUMAN",
source_id="user123",
created_time=123456789,
last_update_time=123456789,
),
DatasetRecord(
dataset_record_id="rec456",
dataset_id="dataset123",
inputs={"question": "What is Spark?"},
outputs={"answer": "Apache Spark is a unified analytics engine for data processing"},
expectations={"answer": "Spark is a data engine"},
tags={"source": "automated"},
source_type="CODE",
source_id="script456",
created_time=123456790,
last_update_time=123456790,
),
]
df = dataset.to_df()
assert isinstance(df, pd.DataFrame)
assert list(df.columns) == expected_columns
assert len(df) == 2
# Check that outputs column exists and contains actual values
assert "outputs" in df.columns
assert df["outputs"].iloc[0] == {
"answer": "MLflow is an ML platform for managing machine learning lifecycle",
"key1": "value1",
}
assert df["outputs"].iloc[1] == {
"answer": "Apache Spark is a unified analytics engine for data processing"
}
# Check other columns have expected values
assert df["inputs"].iloc[0] == {"question": "What is MLflow?"}
assert df["inputs"].iloc[1] == {"question": "What is Spark?"}
assert df["expectations"].iloc[0] == {"answer": "MLflow is an ML platform"}
assert df["expectations"].iloc[1] == {"answer": "Spark is a data engine"}
assert df["tags"].iloc[0] == {"source": "manual"}
assert df["tags"].iloc[1] == {"source": "automated"}
assert df["source_type"].iloc[0] == "HUMAN"
assert df["source_type"].iloc[1] == "CODE"
assert df["source_id"].iloc[0] == "user123"
assert df["source_id"].iloc[1] == "script456"
assert df["dataset_record_id"].iloc[0] == "rec123"
assert df["dataset_record_id"].iloc[1] == "rec456"
def create_test_span(
span_id=1,
parent_id=None,
name="test_span",
inputs=None,
outputs=None,
span_type=SpanType.UNKNOWN,
):
attributes = {
"mlflow.spanType": json.dumps(span_type),
}
if inputs is not None:
attributes["mlflow.spanInputs"] = json.dumps(inputs)
if outputs is not None:
attributes["mlflow.spanOutputs"] = json.dumps(outputs)
otel_span = OTelReadableSpan(
name=name,
context=build_otel_context(trace_id=123456789, span_id=span_id),
parent=build_otel_context(trace_id=123456789, span_id=parent_id) if parent_id else None,
start_time=100000000,
end_time=200000000,
attributes=attributes,
)
return Span(otel_span)
def create_test_trace(
trace_id="test-trace-123",
inputs=None,
outputs=None,
expectations=None,
trace_metadata=None,
_no_defaults=False,
):
assessments = []
if expectations:
from mlflow.entities.assessment import AssessmentSource, AssessmentSourceType, Expectation
for name, value in expectations.items():
expectation = Expectation(
name=name,
value=value,
source=AssessmentSource(
source_type=AssessmentSourceType.HUMAN, source_id="test_user"
),
)
assessments.append(expectation)
trace_info = TraceInfo(
trace_id=trace_id,
trace_location=TraceLocation.from_experiment_id("0"),
request_time=1234567890,
execution_duration=1000,
state=TraceState.OK,
assessments=assessments,
trace_metadata=trace_metadata or {},
)
default_inputs = {"question": "What is MLflow?"}
default_outputs = {"answer": "MLflow is a platform"}
if _no_defaults:
span_inputs = inputs
span_outputs = outputs
else:
span_inputs = inputs if inputs is not None else default_inputs
span_outputs = outputs if outputs is not None else default_outputs
spans = [
create_test_span(
span_id=1,
parent_id=None,
name="root_span",
inputs=span_inputs,
outputs=span_outputs,
span_type=SpanType.CHAIN,
)
]
trace_data = TraceData(spans=spans)
return Trace(info=trace_info, data=trace_data)
def test_process_trace_records_with_dict_outputs():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
trace = create_test_trace(
trace_id="trace1",
inputs={"question": "What is MLflow?"},
outputs={"answer": "MLflow is a platform", "confidence": 0.95},
)
record_dicts = dataset._process_trace_records([trace])
assert len(record_dicts) == 1
assert record_dicts[0]["inputs"] == {"question": "What is MLflow?"}
assert record_dicts[0]["outputs"] == {"answer": "MLflow is a platform", "confidence": 0.95}
assert record_dicts[0]["expectations"] == {}
assert record_dicts[0]["source"]["source_type"] == DatasetRecordSourceType.TRACE.value
assert record_dicts[0]["source"]["source_data"]["trace_id"] == "trace1"
def test_process_trace_records_with_string_outputs():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
trace = create_test_trace(
trace_id="trace2",
inputs={"query": "Tell me about Python"},
outputs="Python is a programming language",
)
record_dicts = dataset._process_trace_records([trace])
assert len(record_dicts) == 1
assert record_dicts[0]["inputs"] == {"query": "Tell me about Python"}
assert record_dicts[0]["outputs"] == "Python is a programming language"
assert record_dicts[0]["expectations"] == {}
assert record_dicts[0]["source"]["source_type"] == DatasetRecordSourceType.TRACE.value
def test_process_trace_records_with_non_dict_non_string_outputs():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
trace = create_test_trace(
trace_id="trace3", inputs={"x": 1, "y": 2}, outputs=["result1", "result2", "result3"]
)
record_dicts = dataset._process_trace_records([trace])
assert len(record_dicts) == 1
assert record_dicts[0]["inputs"] == {"x": 1, "y": 2}
assert record_dicts[0]["outputs"] == ["result1", "result2", "result3"]
assert record_dicts[0]["source"]["source_type"] == DatasetRecordSourceType.TRACE.value
def test_process_trace_records_with_numeric_outputs():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
trace = create_test_trace(trace_id="trace4", inputs={"number": 42}, outputs=42)
record_dicts = dataset._process_trace_records([trace])
assert len(record_dicts) == 1
assert record_dicts[0]["outputs"] == 42
def test_process_trace_records_with_none_outputs():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
trace = create_test_trace(
trace_id="trace5", inputs={"input": "test"}, outputs=None, _no_defaults=True
)
record_dicts = dataset._process_trace_records([trace])
assert len(record_dicts) == 1
assert record_dicts[0]["outputs"] is None
def test_process_trace_records_with_expectations():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
trace = create_test_trace(
trace_id="trace6",
inputs={"question": "What is 2+2?"},
outputs={"answer": "4"},
expectations={"correctness": True, "tone": "neutral"},
)
record_dicts = dataset._process_trace_records([trace])
assert len(record_dicts) == 1
assert record_dicts[0]["expectations"] == {"correctness": True, "tone": "neutral"}
def test_process_trace_records_multiple_traces():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
traces = [
create_test_trace(trace_id="trace1", outputs={"result": "answer1"}),
create_test_trace(trace_id="trace2", outputs="string answer"),
create_test_trace(trace_id="trace3", outputs=[1, 2, 3]),
]
record_dicts = dataset._process_trace_records(traces)
assert len(record_dicts) == 3
assert record_dicts[0]["outputs"] == {"result": "answer1"}
assert record_dicts[1]["outputs"] == "string answer"
assert record_dicts[2]["outputs"] == [1, 2, 3]
def test_process_trace_records_mixed_types_error():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
trace = create_test_trace(trace_id="trace1")
not_a_trace = {"not": "a trace"}
with pytest.raises(
MlflowException,
match=(
"Mixed types in trace list.*Expected all elements to be Trace objects.*"
"element at index 1 is dict"
),
):
dataset._process_trace_records([trace, not_a_trace])
def test_process_trace_records_preserves_session_metadata():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
# Create trace with session metadata
trace_with_session = create_test_trace(
trace_id="tr-123",
trace_metadata={"mlflow.trace.session": "session_1"},
)
# Create trace without session metadata
trace_without_session = create_test_trace(
trace_id="tr-456",
trace_metadata={},
)
records = dataset._process_trace_records([trace_with_session, trace_without_session])
# Trace with session should have session_id in source_data
assert records[0]["source"]["source_data"]["trace_id"] == "tr-123"
assert records[0]["source"]["source_data"]["session_id"] == "session_1"
# Trace without session should only have trace_id
assert records[1]["source"]["source_data"]["trace_id"] == "tr-456"
assert "session_id" not in records[1]["source"]["source_data"]
def test_to_df_includes_source_column():
from mlflow.entities.dataset_record import DatasetRecord
from mlflow.entities.dataset_record_source import DatasetRecordSource
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
# Manually add a record with source to the dataset
source = DatasetRecordSource(
source_type=DatasetRecordSourceType.TRACE,
source_data={"trace_id": "tr-123"},
)
record = DatasetRecord(
dataset_id="dataset123",
dataset_record_id="record123",
inputs={"question": "test"},
outputs={"answer": "test answer"},
expectations={},
tags={},
created_time=123456789,
last_update_time=123456789,
source=source,
)
dataset._records = [record]
df = dataset.to_df()
assert "source" in df.columns
assert df["source"].notna().all()
assert df["source"].iloc[0] == source
def test_delete_records():
dataset = EvaluationDataset(
dataset_id="dataset123",
name="test_dataset",
digest="digest123",
created_time=123456789,
last_update_time=123456789,
)
# Add some records to cache
dataset._records = [Mock(), Mock()]
mock_store = Mock()
mock_store.delete_dataset_records.return_value = 2
with patch("mlflow.tracking._tracking_service.utils._get_store", return_value=mock_store):
deleted_count = dataset.delete_records(["record1", "record2"])
assert deleted_count == 2
mock_store.delete_dataset_records.assert_called_once_with(
dataset_id="dataset123",
dataset_record_ids=["record1", "record2"],
)
# Verify cache was cleared
assert dataset._records is None