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

2306 lines
76 KiB
Python

import json
import os
import sys
import warnings
from unittest import mock
import pandas as pd
import pytest
import mlflow
from mlflow.data import Dataset
from mlflow.data.pyfunc_dataset_mixin import PyFuncConvertibleDatasetMixin
from mlflow.entities.dataset_record_source import DatasetRecordSourceType
from mlflow.entities.evaluation_dataset import (
EvaluationDataset as EntityEvaluationDataset,
)
from mlflow.exceptions import MlflowException
from mlflow.genai.datasets import (
EvaluationDataset,
create_dataset,
delete_dataset,
delete_dataset_tag,
get_dataset,
search_datasets,
set_dataset_tags,
)
from mlflow.genai.datasets.evaluation_dataset import (
EvaluationDataset as WrapperEvaluationDataset,
)
from mlflow.store.entities.paged_list import PagedList
from mlflow.store.tracking import SEARCH_EVALUATION_DATASETS_MAX_RESULTS
from mlflow.tracking import MlflowClient
from mlflow.utils.mlflow_tags import MLFLOW_USER
@pytest.fixture
def mock_client():
with (
mock.patch("mlflow.tracking.client.MlflowClient") as mock_client_class,
mock.patch("mlflow.genai.datasets.MlflowClient", mock_client_class),
):
mock_client_instance = mock_client_class.return_value
yield mock_client_instance
@pytest.fixture
def mock_databricks_environment():
with mock.patch("mlflow.genai.datasets.is_databricks_uri", return_value=True):
yield
@pytest.fixture
def client(db_uri):
original_tracking_uri = mlflow.get_tracking_uri()
mlflow.set_tracking_uri(db_uri)
yield MlflowClient(tracking_uri=db_uri)
mlflow.set_tracking_uri(original_tracking_uri)
@pytest.fixture
def experiments(client):
exp1 = client.create_experiment("test_exp_1")
exp2 = client.create_experiment("test_exp_2")
exp3 = client.create_experiment("test_exp_3")
return [exp1, exp2, exp3]
@pytest.fixture
def experiment(client):
return client.create_experiment("test_trace_experiment")
def test_create_dataset(mock_client):
expected_dataset = EntityEvaluationDataset(
dataset_id="test_id",
name="test_dataset",
digest="abc123",
created_time=123456789,
last_update_time=123456789,
tags={"environment": "production", "version": "1.0"},
)
mock_client.create_dataset.return_value = expected_dataset
result = create_dataset(
name="test_dataset",
experiment_id=["exp1", "exp2"],
tags={"environment": "production", "version": "1.0"},
)
assert result == expected_dataset
mock_client.create_dataset.assert_called_once_with(
name="test_dataset",
experiment_id=["exp1", "exp2"],
tags={"environment": "production", "version": "1.0"},
)
def test_create_dataset_single_experiment_id(mock_client):
expected_dataset = EntityEvaluationDataset(
dataset_id="test_id",
name="test_dataset",
digest="abc123",
created_time=123456789,
last_update_time=123456789,
)
mock_client.create_dataset.return_value = expected_dataset
result = create_dataset(
name="test_dataset",
experiment_id="exp1",
)
assert result == expected_dataset
mock_client.create_dataset.assert_called_once_with(
name="test_dataset",
experiment_id=["exp1"],
tags=None,
)
def test_create_dataset_with_empty_tags(mock_client):
expected_dataset = EntityEvaluationDataset(
dataset_id="test_id",
name="test_dataset",
digest="abc123",
created_time=123456789,
last_update_time=123456789,
tags={},
)
mock_client.create_dataset.return_value = expected_dataset
result = create_dataset(
name="test_dataset",
experiment_id=["exp1"],
tags={},
)
assert result == expected_dataset
mock_client.create_dataset.assert_called_once_with(
name="test_dataset",
experiment_id=["exp1"],
tags={},
)
def test_create_dataset_databricks(mock_databricks_environment):
mock_dataset = mock.Mock()
with mock.patch.dict(
"sys.modules",
{
"databricks.agents.datasets": mock.Mock(
create_dataset=mock.Mock(return_value=mock_dataset)
)
},
):
result = create_dataset(
name="catalog.schema.table",
experiment_id=["exp1", "exp2"],
)
sys.modules["databricks.agents.datasets"].create_dataset.assert_called_once_with(
"catalog.schema.table", ["exp1", "exp2"]
)
assert isinstance(result, EvaluationDataset)
def test_get_dataset(mock_client):
expected_dataset = EntityEvaluationDataset(
dataset_id="test_id",
name="test_dataset",
digest="abc123",
created_time=123456789,
last_update_time=123456789,
)
mock_client.get_dataset.return_value = expected_dataset
result = get_dataset(dataset_id="test_id")
assert result == expected_dataset
mock_client.get_dataset.assert_called_once_with("test_id")
def test_get_dataset_missing_id():
with pytest.raises(ValueError, match="Either 'name' or 'dataset_id' must be provided"):
get_dataset()
def test_get_dataset_databricks(mock_databricks_environment):
mock_dataset = mock.Mock()
with mock.patch.dict(
"sys.modules",
{"databricks.agents.datasets": mock.Mock(get_dataset=mock.Mock(return_value=mock_dataset))},
):
result = get_dataset(name="catalog.schema.table")
sys.modules["databricks.agents.datasets"].get_dataset.assert_called_once_with(
"catalog.schema.table"
)
assert isinstance(result, EvaluationDataset)
def test_get_dataset_databricks_missing_name(mock_databricks_environment):
with pytest.raises(ValueError, match="Parameter 'name' is required"):
get_dataset(dataset_id="test_id")
def test_get_dataset_by_name_oss(experiments):
dataset = create_dataset(
name="unique_dataset_name",
experiment_id=experiments[0],
tags={"test": "get_by_name"},
)
retrieved = get_dataset(name="unique_dataset_name")
assert retrieved.dataset_id == dataset.dataset_id
assert retrieved.name == "unique_dataset_name"
assert retrieved.tags["test"] == "get_by_name"
def test_get_dataset_by_name_not_found(client):
with pytest.raises(MlflowException, match="Dataset with name 'nonexistent_dataset' not found"):
get_dataset(name="nonexistent_dataset")
def test_get_dataset_by_name_multiple_matches(experiments):
create_dataset(
name="duplicate_name",
experiment_id=experiments[0],
)
create_dataset(
name="duplicate_name",
experiment_id=experiments[1],
)
with pytest.raises(MlflowException, match="Multiple datasets found with name 'duplicate_name'"):
get_dataset(name="duplicate_name")
def test_get_dataset_both_name_and_id_error(experiments):
dataset = create_dataset(
name="test_dataset_both",
experiment_id=experiments[0],
)
with pytest.raises(ValueError, match="Cannot specify both 'name' and 'dataset_id'"):
get_dataset(name="test_dataset_both", dataset_id=dataset.dataset_id)
def test_get_dataset_neither_name_nor_id_error(client):
with pytest.raises(ValueError, match="Either 'name' or 'dataset_id' must be provided"):
get_dataset()
@pytest.mark.parametrize(
"name",
[
"dataset's_with_single_quote",
'dataset"with_double_quote',
],
)
def test_get_dataset_name_with_quotes(experiments, name):
dataset = create_dataset(name=name, experiment_id=experiments[0])
retrieved = get_dataset(name=name)
assert retrieved.dataset_id == dataset.dataset_id
assert retrieved.name == name
def test_delete_dataset(mock_client):
delete_dataset(dataset_id="test_id")
mock_client.delete_dataset.assert_called_once_with("test_id")
def test_delete_dataset_missing_id():
with pytest.raises(ValueError, match="Parameter 'dataset_id' is required"):
delete_dataset()
def test_delete_dataset_databricks(mock_databricks_environment):
with mock.patch.dict(
"sys.modules",
{"databricks.agents.datasets": mock.Mock(delete_dataset=mock.Mock())},
):
delete_dataset(name="catalog.schema.table")
sys.modules["databricks.agents.datasets"].delete_dataset.assert_called_once_with(
"catalog.schema.table"
)
def test_search_datasets_with_mock(mock_client):
datasets = [
EntityEvaluationDataset(
dataset_id="id1",
name="dataset1",
digest="digest1",
created_time=123456789,
last_update_time=123456789,
),
EntityEvaluationDataset(
dataset_id="id2",
name="dataset2",
digest="digest2",
created_time=123456789,
last_update_time=123456789,
),
]
mock_client.search_datasets.return_value = PagedList(datasets, None)
result = search_datasets(
experiment_ids=["exp1", "exp2"],
filter_string="name LIKE 'test%'",
max_results=100,
order_by=["created_time DESC"],
)
assert len(result) == 2
assert isinstance(result, list)
mock_client.search_datasets.assert_called_once_with(
experiment_ids=["exp1", "exp2"],
filter_string="name LIKE 'test%'",
max_results=50,
order_by=["created_time DESC"],
page_token=None,
)
def test_search_datasets_single_experiment_id(mock_client):
datasets = [
EntityEvaluationDataset(
dataset_id="id1",
name="dataset1",
digest="digest1",
created_time=123456789,
last_update_time=123456789,
)
]
mock_client.search_datasets.return_value = PagedList(datasets, None)
# When no max_results is specified, it defaults to None which means get all
# Mock time to have a consistent filter_string
with mock.patch("time.time", return_value=1234567890):
search_datasets(experiment_ids="exp1")
# The pagination wrapper will use SEARCH_EVALUATION_DATASETS_MAX_RESULTS as the page size
# Now the function adds default filter (last 7 days) and order_by when not specified
seven_days_ago = int((1234567890 - 7 * 24 * 60 * 60) * 1000)
mock_client.search_datasets.assert_called_once_with(
experiment_ids=["exp1"],
filter_string=f"created_time >= {seven_days_ago}",
max_results=SEARCH_EVALUATION_DATASETS_MAX_RESULTS, # Page size
order_by=["created_time DESC"],
page_token=None,
)
def test_search_datasets_pagination_handling(mock_client):
page1_datasets = [
EntityEvaluationDataset(
dataset_id=f"id{i}",
name=f"dataset{i}",
digest=f"digest{i}",
created_time=123456789,
last_update_time=123456789,
)
for i in range(3)
]
page2_datasets = [
EntityEvaluationDataset(
dataset_id=f"id{i}",
name=f"dataset{i}",
digest=f"digest{i}",
created_time=123456789,
last_update_time=123456789,
)
for i in range(3, 5)
]
mock_client.search_datasets.side_effect = [
PagedList(page1_datasets, "token1"),
PagedList(page2_datasets, None),
]
result = search_datasets(experiment_ids=["exp1"], max_results=10)
assert len(result) == 5
assert isinstance(result, list)
assert mock_client.search_datasets.call_count == 2
first_call = mock_client.search_datasets.call_args_list[0]
assert first_call[1]["page_token"] is None
second_call = mock_client.search_datasets.call_args_list[1]
assert second_call[1]["page_token"] == "token1"
def test_search_datasets_single_page(mock_client):
datasets = [
EntityEvaluationDataset(
dataset_id="id1",
name="dataset1",
digest="digest1",
created_time=123456789,
last_update_time=123456789,
)
]
mock_client.search_datasets.return_value = PagedList(datasets, None)
result = search_datasets(max_results=10)
assert len(result) == 1
assert isinstance(result, list)
assert mock_client.search_datasets.call_count == 1
def test_search_datasets_databricks(mock_databricks_environment, mock_client):
datasets = [
EntityEvaluationDataset(
dataset_id="id1",
name="dataset1",
digest="digest1",
created_time=123456789,
last_update_time=123456789,
),
]
mock_client.search_datasets.return_value = PagedList(datasets, None)
result = search_datasets(experiment_ids=["exp1"])
assert len(result) == 1
assert isinstance(result, list)
# Verify that default filter_string and order_by are NOT set for Databricks
# (since these parameters may not be supported by all Databricks backends)
mock_client.search_datasets.assert_called_once()
call_kwargs = mock_client.search_datasets.call_args.kwargs
assert call_kwargs.get("filter_string") is None
assert call_kwargs.get("order_by") is None
def test_databricks_import_error():
with (
mock.patch("mlflow.genai.datasets.is_databricks_uri", return_value=True),
mock.patch.dict("sys.modules", {"databricks.agents.datasets": None}),
mock.patch("builtins.__import__", side_effect=ImportError("No module")),
):
with pytest.raises(ImportError, match="databricks-agents"):
create_dataset(name="test", experiment_id="exp1")
def test_databricks_profile_uri_support():
mock_dataset = mock.Mock()
with (
mock.patch(
"mlflow.genai.datasets.get_tracking_uri",
return_value="databricks://profilename",
),
mock.patch.dict(
"sys.modules",
{
"databricks.agents.datasets": mock.Mock(
get_dataset=mock.Mock(return_value=mock_dataset),
create_dataset=mock.Mock(return_value=mock_dataset),
delete_dataset=mock.Mock(),
)
},
),
):
result = get_dataset(name="catalog.schema.table")
sys.modules["databricks.agents.datasets"].get_dataset.assert_called_once_with(
"catalog.schema.table"
)
assert isinstance(result, EvaluationDataset)
result2 = create_dataset(name="catalog.schema.table2", experiment_id=["exp1"])
sys.modules["databricks.agents.datasets"].create_dataset.assert_called_once_with(
"catalog.schema.table2", ["exp1"]
)
assert isinstance(result2, EvaluationDataset)
delete_dataset(name="catalog.schema.table3")
sys.modules["databricks.agents.datasets"].delete_dataset.assert_called_once_with(
"catalog.schema.table3"
)
def test_databricks_profile_env_var_set_from_uri(monkeypatch):
mock_dataset = mock.Mock()
profile_values_during_calls = []
def mock_get_dataset(name):
profile_values_during_calls.append((
"get_dataset",
os.environ.get("DATABRICKS_CONFIG_PROFILE"),
))
return mock_dataset
def mock_create_dataset(name, experiment_ids):
profile_values_during_calls.append((
"create_dataset",
os.environ.get("DATABRICKS_CONFIG_PROFILE"),
))
return mock_dataset
def mock_delete_dataset(name):
profile_values_during_calls.append((
"delete_dataset",
os.environ.get("DATABRICKS_CONFIG_PROFILE"),
))
mock_agents_module = mock.Mock(
get_dataset=mock_get_dataset,
create_dataset=mock_create_dataset,
delete_dataset=mock_delete_dataset,
)
monkeypatch.setitem(sys.modules, "databricks.agents.datasets", mock_agents_module)
monkeypatch.setattr("mlflow.genai.datasets.get_tracking_uri", lambda: "databricks://myprofile")
assert "DATABRICKS_CONFIG_PROFILE" not in os.environ
get_dataset(name="catalog.schema.table")
create_dataset(name="catalog.schema.table", experiment_id="exp1")
delete_dataset(name="catalog.schema.table")
assert "DATABRICKS_CONFIG_PROFILE" not in os.environ
assert profile_values_during_calls == [
("get_dataset", "myprofile"),
("create_dataset", "myprofile"),
("delete_dataset", "myprofile"),
]
def test_databricks_profile_env_var_overridden_and_restored(monkeypatch):
mock_dataset = mock.Mock()
profile_during_call = None
def mock_get_dataset(name):
nonlocal profile_during_call
profile_during_call = os.environ.get("DATABRICKS_CONFIG_PROFILE")
return mock_dataset
mock_agents_module = mock.Mock(get_dataset=mock_get_dataset)
monkeypatch.setitem(sys.modules, "databricks.agents.datasets", mock_agents_module)
monkeypatch.setattr("mlflow.genai.datasets.get_tracking_uri", lambda: "databricks://myprofile")
monkeypatch.setenv("DATABRICKS_CONFIG_PROFILE", "original_profile")
assert os.environ.get("DATABRICKS_CONFIG_PROFILE") == "original_profile"
get_dataset(name="catalog.schema.table")
assert os.environ.get("DATABRICKS_CONFIG_PROFILE") == "original_profile"
assert profile_during_call == "myprofile"
def test_databricks_dataset_merge_records_uses_profile(monkeypatch):
profile_during_merge = None
profile_during_to_df = None
mock_inner_dataset = mock.Mock()
mock_inner_dataset.digest = "test_digest"
mock_inner_dataset.name = "catalog.schema.table"
mock_inner_dataset.dataset_id = "dataset-123"
def mock_merge_records(records):
nonlocal profile_during_merge
profile_during_merge = os.environ.get("DATABRICKS_CONFIG_PROFILE")
return mock_inner_dataset
def mock_to_df():
nonlocal profile_during_to_df
profile_during_to_df = os.environ.get("DATABRICKS_CONFIG_PROFILE")
import pandas as pd
return pd.DataFrame({"test": [1, 2, 3]})
mock_inner_dataset.merge_records = mock_merge_records
mock_inner_dataset.to_df = mock_to_df
def mock_get_dataset(name):
return mock_inner_dataset
mock_agents_module = mock.Mock(get_dataset=mock_get_dataset)
monkeypatch.setitem(sys.modules, "databricks.agents.datasets", mock_agents_module)
monkeypatch.setattr("mlflow.genai.datasets.get_tracking_uri", lambda: "databricks://myprofile")
assert "DATABRICKS_CONFIG_PROFILE" not in os.environ
dataset = get_dataset(name="catalog.schema.table")
assert "DATABRICKS_CONFIG_PROFILE" not in os.environ
dataset.merge_records([{"inputs": {"q": "test"}}])
assert profile_during_merge == "myprofile"
assert "DATABRICKS_CONFIG_PROFILE" not in os.environ
dataset.to_df()
assert profile_during_to_df == "myprofile"
assert "DATABRICKS_CONFIG_PROFILE" not in os.environ
def test_create_dataset_with_user_tag(experiments):
dataset = create_dataset(
name="test_user_attribution",
experiment_id=experiments[0],
tags={"environment": "test", MLFLOW_USER: "john_doe"},
)
assert dataset.name == "test_user_attribution"
assert dataset.tags[MLFLOW_USER] == "john_doe"
assert dataset.created_by == "john_doe"
dataset2 = create_dataset(
name="test_no_user",
experiment_id=experiments[0],
tags={"environment": "test"},
)
assert dataset2.name == "test_no_user"
assert isinstance(dataset2.tags[MLFLOW_USER], str)
assert dataset2.created_by == dataset2.tags[MLFLOW_USER]
def test_create_and_get_dataset(experiments):
dataset = create_dataset(
name="qa_evaluation_v1",
experiment_id=[experiments[0], experiments[1]],
tags={"source": "manual_curation", "environment": "test"},
)
assert dataset.name == "qa_evaluation_v1"
assert dataset.tags["source"] == "manual_curation"
assert dataset.tags["environment"] == "test"
assert len(dataset.experiment_ids) == 2
assert dataset.dataset_id is not None
retrieved = get_dataset(dataset_id=dataset.dataset_id)
assert retrieved.dataset_id == dataset.dataset_id
assert retrieved.name == dataset.name
assert retrieved.tags == dataset.tags
assert set(retrieved.experiment_ids) == {experiments[0], experiments[1]}
def test_create_dataset_minimal_params(client):
dataset = create_dataset(name="minimal_dataset")
assert dataset.name == "minimal_dataset"
assert "mlflow.user" not in dataset.tags or isinstance(dataset.tags.get("mlflow.user"), str)
assert dataset.experiment_ids == ["0"]
def test_active_record_pattern_merge_records(experiments):
dataset = create_dataset(
name="active_record_test",
experiment_id=experiments[0],
)
records_batch1 = [
{
"inputs": {"question": "What is MLflow?"},
"outputs": {
"answer": "MLflow is an open source platform for managing the ML lifecycle",
"key1": "value1",
},
"expectations": {
"answer": "MLflow is an open source platform",
"key2": "value2",
},
"tags": {"difficulty": "easy"},
},
{
"inputs": {"question": "What is Python?"},
"outputs": {"answer": "Python is a versatile programming language"},
"expectations": {"answer": "Python is a programming language"},
"tags": {"difficulty": "easy"},
},
]
records_batch2 = [
{
"inputs": {"question": "What is MLflow?"},
"outputs": {"answer": "MLflow is a popular ML lifecycle platform"},
"expectations": {"answer": "MLflow is an ML lifecycle platform"},
"tags": {"category": "ml"},
},
{
"inputs": {"question": "What is Docker?"},
"outputs": {"answer": "Docker is a popular containerization platform"},
"expectations": {"answer": "Docker is a containerization platform"},
"tags": {"difficulty": "medium"},
},
]
dataset.merge_records(records_batch1)
df1 = dataset.to_df()
assert len(df1) == 2
mlflow_record = df1[df1["inputs"].apply(lambda x: x.get("question") == "What is MLflow?")].iloc[
0
]
assert mlflow_record["expectations"] == {
"answer": "MLflow is an open source platform",
"key2": "value2",
}
assert mlflow_record["outputs"] == {
"answer": "MLflow is an open source platform for managing the ML lifecycle",
"key1": "value1",
}
assert mlflow_record["tags"]["difficulty"] == "easy"
assert "category" not in mlflow_record["tags"]
dataset.merge_records(records_batch2)
df2 = dataset.to_df()
assert len(df2) == 3
mlflow_record_updated = df2[
df2["inputs"].apply(lambda x: x.get("question") == "What is MLflow?")
].iloc[0]
assert mlflow_record_updated["expectations"] == {
"answer": "MLflow is an ML lifecycle platform",
"key2": "value2",
}
assert mlflow_record_updated["outputs"] == {
"answer": "MLflow is a popular ML lifecycle platform"
}
assert mlflow_record_updated["tags"]["difficulty"] == "easy"
assert mlflow_record_updated["tags"]["category"] == "ml"
# Verify that the new Docker record also has outputs
docker_record = df2[df2["inputs"].apply(lambda x: x.get("question") == "What is Docker?")].iloc[
0
]
assert docker_record["outputs"]["answer"] == "Docker is a popular containerization platform"
assert docker_record["expectations"]["answer"] == "Docker is a containerization platform"
assert docker_record["tags"]["difficulty"] == "medium"
def test_dataset_with_dataframe_records(experiments):
dataset = create_dataset(
name="dataframe_test",
experiment_id=experiments[0],
tags={"source": "csv", "file": "test_data.csv"},
)
df = pd.DataFrame([
{
"inputs": {"text": "The movie was amazing!", "model": "sentiment-v1"},
"expectations": {"sentiment": "positive", "confidence": 0.95},
"tags": {"source": "imdb"},
},
{
"inputs": {"text": "Terrible experience", "model": "sentiment-v1"},
"expectations": {"sentiment": "negative", "confidence": 0.88},
"tags": {"source": "yelp"},
},
])
dataset.merge_records(df)
result_df = dataset.to_df()
assert len(result_df) == 2
assert all(col in result_df.columns for col in ["inputs", "expectations", "tags"])
# Check that all expected records are present (order-agnostic)
texts = {record["inputs"]["text"] for _, record in result_df.iterrows()}
expected_texts = {"The movie was amazing!", "Terrible experience"}
assert texts == expected_texts
sentiments = {record["expectations"]["sentiment"] for _, record in result_df.iterrows()}
expected_sentiments = {"positive", "negative"}
assert sentiments == expected_sentiments
def test_search_datasets(experiments):
for i in range(5):
create_dataset(
name=f"search_test_{i}",
experiment_id=[experiments[i % len(experiments)]],
tags={"type": "human" if i % 2 == 0 else "trace", "index": str(i)},
)
all_results = search_datasets()
assert len(all_results) == 5
exp0_results = search_datasets(experiment_ids=experiments[0])
assert len(exp0_results) == 2
human_results = search_datasets(filter_string="name LIKE 'search_test_%'")
assert len(human_results) == 5
limited_results = search_datasets(max_results=2)
assert len(limited_results) == 2
more_results = search_datasets(max_results=4)
assert len(more_results) == 4
def test_delete_dataset(experiments):
dataset = create_dataset(
name="to_be_deleted",
experiment_id=[experiments[0], experiments[1]],
tags={"env": "test", "version": "1.0"},
)
dataset_id = dataset.dataset_id
dataset.merge_records([{"inputs": {"q": "test"}, "expectations": {"a": "answer"}}])
retrieved = get_dataset(dataset_id=dataset_id)
assert retrieved is not None
assert len(retrieved.to_df()) == 1
delete_dataset(dataset_id=dataset_id)
with pytest.raises(MlflowException, match="Could not find|not found"):
get_dataset(dataset_id=dataset_id)
search_results = search_datasets(experiment_ids=[experiments[0], experiments[1]])
found_ids = [d.dataset_id for d in search_results]
assert dataset_id not in found_ids
def test_dataset_lifecycle_workflow(experiments):
dataset = create_dataset(
name="qa_eval_prod_v1",
experiment_id=[experiments[0], experiments[1]],
tags={"source": "qa_team_annotations", "team": "qa", "env": "prod"},
)
initial_cases = [
{
"inputs": {"question": "What is the capital of France?"},
"expectations": {"answer": "Paris", "confidence": "high"},
"tags": {"category": "geography", "difficulty": "easy"},
},
{
"inputs": {"question": "Explain quantum computing"},
"expectations": {"answer": "Quantum computing uses quantum mechanics principles"},
"tags": {"category": "science", "difficulty": "hard"},
},
]
dataset.merge_records(initial_cases)
dataset_id = dataset.dataset_id
retrieved = get_dataset(dataset_id=dataset_id)
df = retrieved.to_df()
assert len(df) == 2
additional_cases = [
{
"inputs": {"question": "What is 2+2?"},
"expectations": {"answer": "4", "confidence": "high"},
"tags": {"category": "math", "difficulty": "easy"},
},
]
retrieved.merge_records(additional_cases)
found = search_datasets(
experiment_ids=experiments[0],
filter_string="name LIKE 'qa_eval%'",
)
assert len(found) == 1
assert found[0].dataset_id == dataset_id
final_dataset = get_dataset(dataset_id=dataset_id)
final_df = final_dataset.to_df()
assert len(final_df) == 3
categories = set()
for _, row in final_df.iterrows():
if row["tags"] and "category" in row["tags"]:
categories.add(row["tags"]["category"])
assert categories == {"geography", "science", "math"}
def test_error_handling_filestore_backend(tmp_path):
pytest.skip("FileStore is no longer supported.")
file_uri = f"file://{tmp_path}"
mlflow.set_tracking_uri(file_uri)
with pytest.raises(MlflowException, match="not supported with FileStore") as exc:
create_dataset(name="test")
assert exc.value.error_code == "FEATURE_DISABLED"
with pytest.raises(MlflowException, match="not supported with FileStore") as exc:
get_dataset(dataset_id="test_id")
assert exc.value.error_code == "FEATURE_DISABLED"
with pytest.raises(MlflowException, match="not supported with FileStore") as exc:
search_datasets()
assert exc.value.error_code == "FEATURE_DISABLED"
with pytest.raises(MlflowException, match="not supported with FileStore") as exc:
delete_dataset(dataset_id="test_id")
assert exc.value.error_code == "FEATURE_DISABLED"
def test_single_experiment_id_handling(experiments):
dataset = create_dataset(
name="single_exp_test",
experiment_id=experiments[0],
)
assert isinstance(dataset.experiment_ids, list)
assert dataset.experiment_ids == [experiments[0]]
results = search_datasets(experiment_ids=experiments[0])
found_ids = [d.dataset_id for d in results]
assert dataset.dataset_id in found_ids
def test_trace_to_evaluation_dataset_integration(experiments):
trace_inputs = [
{"question": "What is MLflow?", "context": "ML platforms"},
{"question": "What is Python?", "context": "programming"},
{"question": "What is MLflow?", "context": "ML platforms"},
]
created_trace_ids = []
for i, inputs in enumerate(trace_inputs):
with mlflow.start_run(experiment_id=experiments[i % 2]):
with mlflow.start_span(name=f"qa_trace_{i}") as span:
span.set_inputs(inputs)
span.set_outputs({"answer": f"Answer for {inputs['question']}"})
span.set_attributes({"model": "test-model", "temperature": "0.7"})
trace_id = span.trace_id
created_trace_ids.append(trace_id)
mlflow.log_expectation(
trace_id=trace_id,
name="expected_answer",
value=f"Detailed answer for {inputs['question']}",
)
mlflow.log_expectation(
trace_id=trace_id,
name="quality_score",
value=0.85 + i * 0.05,
)
traces = mlflow.search_traces(
locations=[experiments[0], experiments[1]],
max_results=10,
return_type="list",
)
assert len(traces) == 3
dataset = create_dataset(
name="trace_eval_dataset",
experiment_id=[experiments[0], experiments[1]],
tags={"source": "test_traces", "type": "trace_integration"},
)
dataset.merge_records(traces)
df = dataset.to_df()
assert len(df) == 2
for _, record in df.iterrows():
assert "inputs" in record
assert "question" in record["inputs"]
assert "context" in record["inputs"]
assert record.get("source_type") == "TRACE"
assert record.get("source_id") is not None
mlflow_records = df[df["inputs"].apply(lambda x: x.get("question") == "What is MLflow?")]
assert len(mlflow_records) == 1
with mlflow.start_run(experiment_id=experiments[0]):
with mlflow.start_span(name="additional_trace") as span:
span.set_inputs({"question": "What is Docker?", "context": "containers"})
span.set_outputs({"answer": "Docker is a containerization platform"})
span.set_attributes({"model": "test-model"})
all_traces = mlflow.search_traces(
locations=[experiments[0], experiments[1]], max_results=10, return_type="list"
)
assert len(all_traces) == 4
new_trace = None
for trace in all_traces:
root_span = trace.data._get_root_span() if hasattr(trace, "data") else None
if root_span and root_span.inputs and root_span.inputs.get("question") == "What is Docker?":
new_trace = trace
break
assert new_trace is not None
dataset.merge_records([new_trace])
final_df = dataset.to_df()
assert len(final_df) == 3
retrieved = get_dataset(dataset_id=dataset.dataset_id)
retrieved_df = retrieved.to_df()
assert len(retrieved_df) == 3
delete_dataset(dataset_id=dataset.dataset_id)
with pytest.raises(MlflowException, match="Could not find|not found"):
get_dataset(dataset_id=dataset.dataset_id)
search_results = search_datasets(
experiment_ids=[experiments[0], experiments[1]], max_results=100
)
found_dataset_ids = [d.dataset_id for d in search_results]
assert dataset.dataset_id not in found_dataset_ids
all_datasets = search_datasets(max_results=100)
all_dataset_ids = [d.dataset_id for d in all_datasets]
assert dataset.dataset_id not in all_dataset_ids
def test_search_traces_dataframe_to_dataset_integration(experiments):
for i in range(3):
with mlflow.start_run(experiment_id=experiments[0]):
with mlflow.start_span(name=f"test_span_{i}") as span:
span.set_inputs({"question": f"Question {i}?", "temperature": 0.7})
span.set_outputs({"answer": f"Answer {i}"})
mlflow.log_expectation(
trace_id=span.trace_id,
name="expected_answer",
value=f"Expected answer {i}",
)
mlflow.log_expectation(
trace_id=span.trace_id,
name="min_score",
value=0.8,
)
traces_df = mlflow.search_traces(
locations=[experiments[0]],
)
assert "trace" in traces_df.columns
assert "assessments" in traces_df.columns
assert len(traces_df) == 3
dataset = create_dataset(
name="traces_dataframe_dataset",
experiment_id=experiments[0],
tags={"source": "search_traces", "format": "dataframe"},
)
dataset.merge_records(traces_df)
result_df = dataset.to_df()
assert len(result_df) == 3
for idx, row in result_df.iterrows():
assert "inputs" in row
assert "expectations" in row
assert "source_type" in row
assert row["source_type"] == "TRACE"
assert "question" in row["inputs"]
question_text = row["inputs"]["question"]
assert question_text.startswith("Question ")
assert question_text.endswith("?")
question_num = int(question_text.replace("Question ", "").replace("?", ""))
assert 0 <= question_num <= 2
assert "expected_answer" in row["expectations"]
assert f"Expected answer {question_num}" == row["expectations"]["expected_answer"]
assert "min_score" in row["expectations"]
assert row["expectations"]["min_score"] == 0.8
def test_trace_to_dataset_with_assessments(client, experiment):
trace_data = [
{
"inputs": {"question": "What is MLflow?", "context": "ML platforms"},
"outputs": {"answer": "MLflow is an open source platform for ML lifecycle"},
"expectations": {
"correctness": True,
"completeness": 0.8,
},
},
{
"inputs": {
"question": "What is Python?",
"context": "programming languages",
},
"outputs": {"answer": "Python is a high-level programming language"},
"expectations": {
"correctness": True,
},
},
{
"inputs": {"question": "What is Docker?", "context": "containerization"},
"outputs": {"answer": "Docker is a container platform"},
"expectations": {},
},
]
created_traces = []
for i, data in enumerate(trace_data):
with mlflow.start_run(experiment_id=experiment):
with mlflow.start_span(name=f"qa_trace_{i}") as span:
span.set_inputs(data["inputs"])
span.set_outputs(data["outputs"])
span.set_attributes({"model": "test-model", "temperature": 0.7})
trace_id = span.trace_id
for name, value in data["expectations"].items():
mlflow.log_expectation(
trace_id=trace_id,
name=name,
value=value,
span_id=span.span_id,
)
trace = client.get_trace(trace_id)
created_traces.append(trace)
dataset = create_dataset(
name="trace_assessment_dataset",
experiment_id=[experiment],
tags={"source": "trace_integration_test", "version": "1.0"},
)
dataset.merge_records(created_traces)
df = dataset.to_df()
assert len(df) == 3
mlflow_record = df[df["inputs"].apply(lambda x: x.get("question") == "What is MLflow?")].iloc[0]
assert mlflow_record["inputs"]["question"] == "What is MLflow?"
assert mlflow_record["inputs"]["context"] == "ML platforms"
assert "expectations" in mlflow_record
assert mlflow_record["expectations"]["correctness"] is True
assert mlflow_record["expectations"]["completeness"] == 0.8
assert mlflow_record["source_type"] == "TRACE"
assert mlflow_record["source_id"] is not None
python_record = df[df["inputs"].apply(lambda x: x.get("question") == "What is Python?")].iloc[0]
assert python_record["expectations"]["correctness"] is True
assert len(python_record["expectations"]) == 1
docker_record = df[df["inputs"].apply(lambda x: x.get("question") == "What is Docker?")].iloc[0]
assert docker_record["expectations"] is None or docker_record["expectations"] == {}
retrieved = get_dataset(dataset_id=dataset.dataset_id)
assert retrieved.tags["source"] == "trace_integration_test"
assert retrieved.tags["version"] == "1.0"
assert set(retrieved.experiment_ids) == {experiment}
def test_trace_deduplication_with_assessments(client, experiment):
trace_ids = []
for i in range(3):
with mlflow.start_run(experiment_id=experiment):
with mlflow.start_span(name=f"duplicate_trace_{i}") as span:
span.set_inputs({"question": "What is AI?", "model": "gpt-4"})
span.set_outputs({"answer": f"AI is artificial intelligence (version {i})"})
trace_id = span.trace_id
trace_ids.append(trace_id)
mlflow.log_expectation(
trace_id=trace_id,
name="quality",
value=0.5 + i * 0.2,
span_id=span.span_id,
)
traces = [client.get_trace(tid) for tid in trace_ids]
dataset = create_dataset(
name="dedup_test",
experiment_id=experiment,
tags={"test": "deduplication"},
)
dataset.merge_records(traces)
df = dataset.to_df()
assert len(df) == 1
record = df.iloc[0]
assert record["inputs"]["question"] == "What is AI?"
assert record["expectations"]["quality"] == 0.9
assert record["source_id"] in trace_ids
def test_mixed_record_types_with_traces(client, experiment):
with mlflow.start_run(experiment_id=experiment):
with mlflow.start_span(name="mixed_test_trace") as span:
span.set_inputs({"question": "What is ML?", "context": "machine learning"})
span.set_outputs({"answer": "ML stands for Machine Learning"})
trace_id = span.trace_id
mlflow.log_expectation(
trace_id=trace_id,
name="accuracy",
value=0.95,
span_id=span.span_id,
)
trace = client.get_trace(trace_id)
dataset = create_dataset(
name="mixed_records_test",
experiment_id=experiment,
tags={"type": "mixed", "test": "true"},
)
manual_records = [
{
"inputs": {"question": "What is AI?"},
"expectations": {"correctness": True},
"tags": {"source": "manual"},
},
{
"inputs": {"question": "What is Python?"},
"expectations": {"correctness": True},
"tags": {"source": "manual"},
},
]
dataset.merge_records(manual_records)
df1 = dataset.to_df()
assert len(df1) == 2
dataset.merge_records([trace])
df2 = dataset.to_df()
assert len(df2) == 3
ml_record = df2[df2["inputs"].apply(lambda x: x.get("question") == "What is ML?")].iloc[0]
assert ml_record["expectations"]["accuracy"] == 0.95
assert ml_record["source_type"] == "TRACE"
manual_questions = {"What is AI?", "What is Python?"}
manual_records_df = df2[df2["inputs"].apply(lambda x: x.get("question") in manual_questions)]
assert len(manual_records_df) == 2
for _, record in manual_records_df.iterrows():
assert record.get("source_type") != "TRACE"
def test_trace_without_root_span_inputs(client, experiment):
with mlflow.start_run(experiment_id=experiment):
with mlflow.start_span(name="no_inputs_trace") as span:
span.set_outputs({"result": "some output"})
trace_id = span.trace_id
trace = client.get_trace(trace_id)
dataset = create_dataset(
name="no_inputs_test",
experiment_id=experiment,
)
dataset.merge_records([trace])
df = dataset.to_df()
assert len(df) == 1
assert df.iloc[0]["inputs"] == {}
assert df.iloc[0]["expectations"] is None or df.iloc[0]["expectations"] == {}
def test_error_handling_invalid_trace_types(client, experiment):
dataset = create_dataset(
name="error_test",
experiment_id=experiment,
)
with mlflow.start_run(experiment_id=experiment):
with mlflow.start_span(name="valid_trace") as span:
span.set_inputs({"q": "test"})
trace_id = span.trace_id
valid_trace = client.get_trace(trace_id)
with pytest.raises(MlflowException, match="Mixed types in trace list"):
dataset.merge_records([valid_trace, {"inputs": {"q": "dict record"}}])
with pytest.raises(MlflowException, match="Mixed types in trace list"):
dataset.merge_records([valid_trace, "not a trace"])
def test_trace_integration_end_to_end(client, experiment):
traces_to_create = [
{
"name": "successful_qa",
"inputs": {"question": "What is the capital of France?", "language": "en"},
"outputs": {"answer": "Paris", "confidence": 0.99},
"expectations": {"correctness": True, "confidence_threshold": 0.8},
},
{
"name": "incorrect_qa",
"inputs": {"question": "What is 2+2?", "language": "en"},
"outputs": {"answer": "5", "confidence": 0.5},
"expectations": {"correctness": False},
},
{
"name": "multilingual_qa",
"inputs": {"question": "¿Cómo estás?", "language": "es"},
"outputs": {"answer": "I'm doing well, thank you!", "confidence": 0.9},
"expectations": {"language_match": False, "politeness": True},
},
]
created_trace_ids = []
for trace_config in traces_to_create:
with mlflow.start_run(experiment_id=experiment):
with mlflow.start_span(name=trace_config["name"]) as span:
span.set_inputs(trace_config["inputs"])
span.set_outputs(trace_config["outputs"])
span.set_attributes({
"model": "test-llm-v1",
"temperature": 0.7,
"max_tokens": 100,
})
trace_id = span.trace_id
created_trace_ids.append(trace_id)
for exp_name, exp_value in trace_config["expectations"].items():
mlflow.log_expectation(
trace_id=trace_id,
name=exp_name,
value=exp_value,
span_id=span.span_id,
metadata={"trace_name": trace_config["name"]},
)
dataset = create_dataset(
name="comprehensive_trace_test",
experiment_id=[experiment],
tags={
"test_type": "end_to_end",
"model": "test-llm-v1",
"language": "multilingual",
},
)
traces = [client.get_trace(tid) for tid in created_trace_ids]
dataset.merge_records(traces)
df = dataset.to_df()
assert len(df) == 3
french_record = df[df["inputs"].apply(lambda x: "France" in str(x.get("question", "")))].iloc[0]
assert french_record["expectations"]["correctness"] is True
assert french_record["expectations"]["confidence_threshold"] == 0.8
math_record = df[df["inputs"].apply(lambda x: "2+2" in str(x.get("question", "")))].iloc[0]
assert math_record["expectations"]["correctness"] is False
spanish_record = df[df["inputs"].apply(lambda x: x.get("language") == "es")].iloc[0]
assert spanish_record["expectations"]["language_match"] is False
assert spanish_record["expectations"]["politeness"] is True
retrieved_dataset = get_dataset(dataset_id=dataset.dataset_id)
retrieved_df = retrieved_dataset.to_df()
assert len(retrieved_df) == 3
assert retrieved_dataset.tags["model"] == "test-llm-v1"
additional_records = [
{
"inputs": {"question": "What is Python?", "language": "en"},
"expectations": {"technical_accuracy": True},
"tags": {"source": "manual_addition"},
}
]
retrieved_dataset.merge_records(additional_records)
final_df = retrieved_dataset.to_df()
assert len(final_df) == 4
trace_records = final_df[final_df["source_type"] == "TRACE"]
assert len(trace_records) == 3
manual_records = final_df[final_df["source_type"] != "TRACE"]
assert len(manual_records) == 1
def test_dataset_pagination_transparency_large_records(experiments):
dataset = create_dataset(
name="test_pagination_transparency",
experiment_id=experiments[0],
tags={"test": "large_dataset"},
)
large_records = [
{
"inputs": {"question": f"Question {i}", "index": i},
"expectations": {"answer": f"Answer {i}", "score": i * 0.01},
}
for i in range(150)
]
dataset.merge_records(large_records)
all_records = dataset._mlflow_dataset.records
assert len(all_records) == 150
record_indices = {record.inputs["index"] for record in all_records}
expected_indices = set(range(150))
assert record_indices == expected_indices
record_scores = {record.expectations["score"] for record in all_records}
expected_scores = {i * 0.01 for i in range(150)}
assert record_scores == expected_scores
df = dataset.to_df()
assert len(df) == 150
df_indices = {row["index"] for row in df["inputs"]}
assert df_indices == expected_indices
assert not hasattr(dataset, "page_token")
assert not hasattr(dataset, "next_page_token")
assert not hasattr(dataset, "max_results")
second_access = dataset._mlflow_dataset.records
assert second_access is all_records
dataset._mlflow_dataset._records = None
refreshed_records = dataset._mlflow_dataset.records
assert len(refreshed_records) == 150
def test_dataset_internal_pagination_with_mock(experiments):
from mlflow.tracking._tracking_service.utils import _get_store
dataset = create_dataset(
name="test_internal_pagination",
experiment_id=experiments[0],
tags={"test": "pagination_mock"},
)
records = [
{"inputs": {"question": f"Q{i}", "id": i}, "expectations": {"answer": f"A{i}"}}
for i in range(75)
]
dataset.merge_records(records)
dataset._mlflow_dataset._records = None
store = _get_store()
with mock.patch.object(
store, "_load_dataset_records", wraps=store._load_dataset_records
) as mock_load:
accessed_records = dataset._mlflow_dataset.records
mock_load.assert_called_once_with(dataset.dataset_id, max_results=None)
assert len(accessed_records) == 75
dataset._mlflow_dataset._records = None
with mock.patch.object(
store, "_load_dataset_records", wraps=store._load_dataset_records
) as mock_load:
df = dataset.to_df()
mock_load.assert_called_once_with(dataset.dataset_id, max_results=None)
assert len(df) == 75
def test_dataset_experiment_associations(experiments):
from mlflow.genai.datasets import (
add_dataset_to_experiments,
remove_dataset_from_experiments,
)
dataset = create_dataset(
name="test_associations",
experiment_id=experiments[0],
tags={"test": "associations"},
)
initial_exp_ids = dataset.experiment_ids
assert experiments[0] in initial_exp_ids
updated = add_dataset_to_experiments(
dataset_id=dataset.dataset_id, experiment_ids=[experiments[1], experiments[2]]
)
assert experiments[0] in updated.experiment_ids
assert experiments[1] in updated.experiment_ids
assert experiments[2] in updated.experiment_ids
assert len(updated.experiment_ids) == 3
result = add_dataset_to_experiments(
dataset_id=dataset.dataset_id, experiment_ids=[experiments[1], experiments[2]]
)
assert len(result.experiment_ids) == 3
assert all(exp in result.experiment_ids for exp in experiments)
removed = remove_dataset_from_experiments(
dataset_id=dataset.dataset_id, experiment_ids=[experiments[1], experiments[2]]
)
assert experiments[1] not in removed.experiment_ids
assert experiments[2] not in removed.experiment_ids
assert experiments[0] in removed.experiment_ids
assert len(removed.experiment_ids) == 1
with mock.patch("mlflow.store.tracking.sqlalchemy_store._logger.warning") as mock_warning:
idempotent = remove_dataset_from_experiments(
dataset_id=dataset.dataset_id,
experiment_ids=[experiments[1], experiments[2]],
)
assert mock_warning.call_count == 2
assert "was not associated" in mock_warning.call_args_list[0][0][0]
assert len(idempotent.experiment_ids) == 1
def test_dataset_associations_filestore_blocking(tmp_path):
pytest.skip("FileStore is no longer supported.")
from mlflow.genai.datasets import (
add_dataset_to_experiments,
remove_dataset_from_experiments,
)
mlflow.set_tracking_uri(tmp_path.as_uri())
with pytest.raises(NotImplementedError, match="not supported with FileStore"):
add_dataset_to_experiments(dataset_id="d-test123", experiment_ids=["1", "2"])
with pytest.raises(NotImplementedError, match="not supported with FileStore"):
remove_dataset_from_experiments(dataset_id="d-test123", experiment_ids=["1"])
def test_evaluation_dataset_tags_crud_workflow(experiments):
dataset = create_dataset(
name="test_tags_crud",
experiment_id=experiments[0],
)
initial_tags = dataset.tags.copy()
set_dataset_tags(
dataset_id=dataset.dataset_id,
tags={
"team": "ml-platform",
"project": "evaluation",
"priority": "high",
},
)
dataset = get_dataset(dataset_id=dataset.dataset_id)
expected_tags = initial_tags.copy()
expected_tags.update({
"team": "ml-platform",
"project": "evaluation",
"priority": "high",
})
assert dataset.tags == expected_tags
set_dataset_tags(
dataset_id=dataset.dataset_id,
tags={
"priority": "medium",
"status": "active",
},
)
dataset = get_dataset(dataset_id=dataset.dataset_id)
expected_tags = initial_tags.copy()
expected_tags.update({
"team": "ml-platform",
"project": "evaluation",
"priority": "medium",
"status": "active",
})
assert dataset.tags == expected_tags
delete_dataset_tag(
dataset_id=dataset.dataset_id,
key="priority",
)
dataset = get_dataset(dataset_id=dataset.dataset_id)
expected_tags = initial_tags.copy()
expected_tags.update({
"team": "ml-platform",
"project": "evaluation",
"status": "active",
})
assert dataset.tags == expected_tags
delete_dataset(dataset_id=dataset.dataset_id)
with pytest.raises(MlflowException, match="Could not find|not found"):
get_dataset(dataset_id=dataset.dataset_id)
with pytest.raises(MlflowException, match="Could not find|not found"):
set_dataset_tags(
dataset_id=dataset.dataset_id,
tags={"should": "fail"},
)
delete_dataset_tag(dataset_id=dataset.dataset_id, key="status")
def test_set_dataset_tags_databricks(mock_databricks_environment):
with pytest.raises(NotImplementedError, match="tag operations are not available"):
set_dataset_tags(dataset_id="test", tags={"key": "value"})
def test_delete_dataset_tag_databricks(mock_databricks_environment):
with pytest.raises(NotImplementedError, match="tag operations are not available"):
delete_dataset_tag(dataset_id="test", key="key")
def test_dataset_schema_evolution_and_log_input(experiments):
dataset = create_dataset(
name="schema_evolution_test",
experiment_id=[experiments[0]],
tags={"test": "schema_evolution", "mlflow.user": "test_user"},
)
stage1_records = [
{
"inputs": {"prompt": "What is MLflow?"},
"expectations": {"response": "MLflow is a platform"},
}
]
dataset.merge_records(stage1_records)
ds1 = get_dataset(dataset_id=dataset.dataset_id)
schema1 = json.loads(ds1.schema)
assert schema1 is not None
assert "prompt" in schema1["inputs"]
assert schema1["inputs"]["prompt"] == "string"
assert len(schema1["inputs"]) == 1
assert len(schema1["expectations"]) == 1
stage2_records = [
{
"inputs": {
"prompt": "Explain Python",
"temperature": 0.7,
"max_length": 500,
"top_p": 0.95,
},
"expectations": {
"response": "Python is a programming language",
"quality_score": 0.85,
"token_count": 127,
},
}
]
dataset.merge_records(stage2_records)
ds2 = get_dataset(dataset_id=dataset.dataset_id)
schema2 = json.loads(ds2.schema)
assert "temperature" in schema2["inputs"]
assert schema2["inputs"]["temperature"] == "float"
assert "max_length" in schema2["inputs"]
assert schema2["inputs"]["max_length"] == "integer"
assert len(schema2["inputs"]) == 4
assert len(schema2["expectations"]) == 3
stage3_records = [
{
"inputs": {
"prompt": "Complex query",
"streaming": True,
"stop_sequences": ["\n\n", "END"],
"config": {"model": "gpt-4", "version": "1.0"},
},
"expectations": {
"response": "Complex response",
"is_valid": True,
"citations": ["source1", "source2"],
"metadata": {"confidence": 0.9},
},
}
]
dataset.merge_records(stage3_records)
ds3 = get_dataset(dataset_id=dataset.dataset_id)
schema3 = json.loads(ds3.schema)
assert schema3["inputs"]["streaming"] == "boolean"
assert schema3["inputs"]["stop_sequences"] == "array"
assert schema3["inputs"]["config"] == "object"
assert schema3["expectations"]["is_valid"] == "boolean"
assert schema3["expectations"]["citations"] == "array"
assert schema3["expectations"]["metadata"] == "object"
assert "prompt" in schema3["inputs"]
assert "temperature" in schema3["inputs"]
assert "quality_score" in schema3["expectations"]
with mlflow.start_run(experiment_id=experiments[0]) as run:
mlflow.log_input(dataset, context="evaluation")
mlflow.log_metrics({"accuracy": 0.92, "f1_score": 0.89})
run_data = mlflow.get_run(run.info.run_id)
assert run_data.inputs is not None
assert run_data.inputs.dataset_inputs is not None
assert len(run_data.inputs.dataset_inputs) > 0
dataset_input = run_data.inputs.dataset_inputs[0]
assert dataset_input.dataset.name == "schema_evolution_test"
assert dataset_input.dataset.source_type == "mlflow_evaluation_dataset"
tag_dict = {tag.key: tag.value for tag in dataset_input.tags}
assert "mlflow.data.context" in tag_dict
assert tag_dict["mlflow.data.context"] == "evaluation"
final_dataset = get_dataset(dataset_id=dataset.dataset_id)
final_schema = json.loads(final_dataset.schema)
assert "inputs" in final_schema
assert "expectations" in final_schema
assert "version" in final_schema
assert final_schema["version"] == "1.0"
profile = json.loads(final_dataset.profile)
assert profile is not None
assert profile["num_records"] == 3
consistency_records = [
{
"inputs": {"prompt": "Another test", "temperature": 0.5, "max_length": 200},
"expectations": {"response": "Another response", "quality_score": 0.75},
}
]
dataset.merge_records(consistency_records)
consistent_dataset = get_dataset(dataset_id=dataset.dataset_id)
consistent_schema = json.loads(consistent_dataset.schema)
assert set(consistent_schema["inputs"].keys()) == set(final_schema["inputs"].keys())
assert set(consistent_schema["expectations"].keys()) == set(final_schema["expectations"].keys())
consistent_profile = json.loads(consistent_dataset.profile)
assert consistent_profile["num_records"] == 4
delete_dataset_tag(dataset_id="test", key="key")
def test_deprecated_parameter_substitution(experiment):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
dataset = create_dataset(
uc_table_name="test_dataset_deprecated",
experiment_id=experiment,
tags={"test": "deprecated_parameter"},
)
assert len(w) == 1
assert issubclass(w[0].category, FutureWarning)
assert "uc_table_name" in str(w[0].message)
assert "deprecated" in str(w[0].message).lower()
assert "name" in str(w[0].message)
assert dataset.name == "test_dataset_deprecated"
assert dataset.tags["test"] == "deprecated_parameter"
with pytest.raises(ValueError, match="Cannot specify both.*uc_table_name.*and.*name"):
create_dataset(
uc_table_name="old_name",
name="new_name",
experiment_id=experiment,
)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
with pytest.raises(ValueError, match="name.*only supported in Databricks"):
delete_dataset(uc_table_name="test_dataset_deprecated")
assert len(w) == 1
assert issubclass(w[0].category, FutureWarning)
assert "uc_table_name" in str(w[0].message)
delete_dataset(dataset_id=dataset.dataset_id)
def test_create_dataset_uses_active_experiment_when_not_specified(client):
exp_id = mlflow.create_experiment("test_active_experiment")
mlflow.set_experiment(experiment_id=exp_id)
dataset = create_dataset(name="test_with_active_exp")
assert dataset.experiment_ids == [exp_id]
from mlflow.tracking import fluent
fluent._active_experiment_id = None
def test_create_dataset_with_no_active_experiment(client):
from mlflow.tracking import fluent
fluent._active_experiment_id = None
dataset = create_dataset(name="test_no_active_exp")
assert dataset.experiment_ids == ["0"]
def test_create_dataset_explicit_overrides_active_experiment(client):
active_exp = mlflow.create_experiment("active_exp")
explicit_exp = mlflow.create_experiment("explicit_exp")
mlflow.set_experiment(experiment_id=active_exp)
dataset = create_dataset(name="test_explicit_override", experiment_id=explicit_exp)
assert dataset.experiment_ids == [explicit_exp]
from mlflow.tracking import fluent
fluent._active_experiment_id = None
def test_create_dataset_none_uses_active_experiment(client):
exp_id = mlflow.create_experiment("test_none_experiment")
mlflow.set_experiment(experiment_id=exp_id)
dataset = create_dataset(name="test_none_exp", experiment_id=None)
assert dataset.experiment_ids == [exp_id]
from mlflow.tracking import fluent
fluent._active_experiment_id = None
def test_source_type_inference():
exp = mlflow.create_experiment("test_source_inference")
dataset = create_dataset(
name="test_source_inference",
experiment_id=exp,
tags={"test": "source_inference"},
)
human_records = [
{
"inputs": {"question": "What is MLflow?"},
"expectations": {"answer": "MLflow is an ML platform", "quality": 0.9},
},
{
"inputs": {"question": "How to track experiments?"},
"expectations": {"answer": "Use mlflow.start_run()", "quality": 0.85},
},
]
dataset.merge_records(human_records)
df = dataset.to_df()
human_sources = df[df["source_type"] == DatasetRecordSourceType.HUMAN.value]
assert len(human_sources) == 2
code_records = [{"inputs": {"question": f"Generated question {i}"}} for i in range(3)]
dataset.merge_records(code_records)
df = dataset.to_df()
code_sources = df[df["source_type"] == DatasetRecordSourceType.CODE.value]
assert len(code_sources) == 3
explicit_records = [
{
"inputs": {"question": "Document-based question"},
"expectations": {"answer": "From document"},
"source": {
"source_type": DatasetRecordSourceType.DOCUMENT.value,
"source_data": {"source_id": "doc123", "page": 5},
},
}
]
dataset.merge_records(explicit_records)
df = dataset.to_df()
doc_sources = df[df["source_type"] == DatasetRecordSourceType.DOCUMENT.value]
assert len(doc_sources) == 1
assert doc_sources.iloc[0]["source_id"] == "doc123"
empty_exp_records = [{"inputs": {"question": "Has empty expectations"}, "expectations": {}}]
dataset.merge_records(empty_exp_records)
df = dataset.to_df()
last_record = df.iloc[-1]
assert last_record["source_type"] not in [
DatasetRecordSourceType.HUMAN.value,
DatasetRecordSourceType.CODE.value,
]
explicit_trace = [
{
"inputs": {"question": "From trace"},
"source": {
"source_type": DatasetRecordSourceType.TRACE.value,
"source_data": {"trace_id": "trace123"},
},
}
]
dataset.merge_records(explicit_trace)
df = dataset.to_df()
trace_sources = df[df["source_type"] == DatasetRecordSourceType.TRACE.value]
assert len(trace_sources) == 1, f"Expected 1 TRACE source, got {len(trace_sources)}"
assert trace_sources.iloc[0]["source_id"] == "trace123"
source_counts = df["source_type"].value_counts()
assert source_counts.get(DatasetRecordSourceType.HUMAN.value, 0) == 2
assert source_counts.get(DatasetRecordSourceType.CODE.value, 0) == 3
assert source_counts.get(DatasetRecordSourceType.DOCUMENT.value, 0) == 1
assert source_counts.get(DatasetRecordSourceType.TRACE.value, 0) == 1
delete_dataset(dataset_id=dataset.dataset_id)
def test_trace_source_type_detection():
exp = mlflow.create_experiment("test_trace_source_detection")
trace_ids = []
for i in range(3):
with mlflow.start_run(experiment_id=exp):
with mlflow.start_span(name=f"test_span_{i}") as span:
span.set_inputs({"question": f"Question {i}", "context": f"Context {i}"})
span.set_outputs({"answer": f"Answer {i}"})
trace_ids.append(span.trace_id)
if i < 2:
mlflow.log_expectation(
trace_id=span.trace_id,
name="quality",
value=0.8 + i * 0.05,
span_id=span.span_id,
)
dataset = create_dataset(
name="test_trace_sources",
experiment_id=exp,
tags={"test": "trace_source_detection"},
)
client = mlflow.MlflowClient()
traces = [client.get_trace(tid) for tid in trace_ids]
dataset.merge_records(traces)
df = dataset.to_df()
trace_sources = df[df["source_type"] == DatasetRecordSourceType.TRACE.value]
assert len(trace_sources) == 3
for trace_id in trace_ids:
matching_records = df[df["source_id"] == trace_id]
assert len(matching_records) == 1
dataset2 = create_dataset(
name="test_trace_sources_df",
experiment_id=exp,
tags={"test": "trace_source_df"},
)
traces_df = mlflow.search_traces(locations=[exp])
assert not traces_df.empty
dataset2.merge_records(traces_df)
df2 = dataset2.to_df()
trace_sources2 = df2[df2["source_type"] == DatasetRecordSourceType.TRACE.value]
assert len(trace_sources2) == len(traces_df)
dataset3 = create_dataset(
name="test_trace_sources_list",
experiment_id=exp,
tags={"test": "trace_source_list"},
)
traces_list = mlflow.search_traces(locations=[exp], return_type="list")
assert len(traces_list) > 0
dataset3.merge_records(traces_list)
df3 = dataset3.to_df()
trace_sources3 = df3[df3["source_type"] == DatasetRecordSourceType.TRACE.value]
assert len(trace_sources3) == len(traces_list)
df_with_expectations = df[df["expectations"].apply(lambda x: bool(x) and len(x) > 0)]
assert len(df_with_expectations) == 2
delete_dataset(dataset_id=dataset.dataset_id)
delete_dataset(dataset_id=dataset2.dataset_id)
delete_dataset(dataset_id=dataset3.dataset_id)
def test_create_dataset_empty_list_stays_empty(client):
exp_id = mlflow.create_experiment("test_empty_list")
mlflow.set_experiment(experiment_id=exp_id)
dataset = create_dataset(name="test_empty_list", experiment_id=[])
assert dataset.experiment_ids == []
from mlflow.tracking import fluent
fluent._active_experiment_id = None
def test_search_datasets_filter_string_edge_cases(client):
exp_id = mlflow.create_experiment("test_filter_edge_cases")
dataset = create_dataset(name="test_dataset", experiment_id=exp_id, tags={"test": "value"})
with mock.patch("mlflow.tracking.client.MlflowClient.search_datasets") as mock_search:
mock_search.return_value = mock.MagicMock(token=None, items=[dataset])
search_datasets(experiment_ids=exp_id, filter_string=None)
call_args = mock_search.call_args
filter_arg = call_args.kwargs.get("filter_string")
assert "created_time >=" in filter_arg
mock_search.reset_mock()
search_datasets(experiment_ids=exp_id, filter_string=[])
call_args = mock_search.call_args
filter_arg = call_args.kwargs.get("filter_string")
assert "created_time >=" in filter_arg
mock_search.reset_mock()
search_datasets(experiment_ids=exp_id, filter_string="")
call_args = mock_search.call_args
filter_arg = call_args.kwargs.get("filter_string")
assert "created_time >=" in filter_arg
mock_search.reset_mock()
search_datasets(experiment_ids=exp_id, filter_string='name = "test"')
call_args = mock_search.call_args
filter_arg = call_args.kwargs.get("filter_string")
assert filter_arg == 'name = "test"'
def test_wrapper_type_is_actually_returned_not_entity(experiments):
dataset = create_dataset(
name="test_wrapper",
experiment_id=experiments[0],
tags={"test": "wrapper_check"},
)
assert isinstance(dataset, WrapperEvaluationDataset)
assert not isinstance(dataset, EntityEvaluationDataset)
assert hasattr(dataset, "_mlflow_dataset")
assert dataset._mlflow_dataset is not None
assert isinstance(dataset._mlflow_dataset, EntityEvaluationDataset)
def test_wrapper_delegates_all_properties_correctly(experiments):
dataset = create_dataset(
name="test_delegation",
experiment_id=experiments[0],
tags={"env": "test", "version": "1.0"},
)
assert dataset.name == "test_delegation"
assert dataset.dataset_id.startswith("d-")
assert dataset.tags["env"] == "test"
assert dataset.tags["version"] == "1.0"
assert experiments[0] in dataset.experiment_ids
assert dataset.created_time > 0
assert dataset.last_update_time > 0
assert dataset.digest is not None
assert hasattr(dataset, "source")
assert dataset.source._get_source_type() == "mlflow_evaluation_dataset"
def test_get_and_search_return_wrapper_not_entity(experiments):
created = create_dataset(
name="test_get_wrapper",
experiment_id=experiments[0],
tags={"test": "get"},
)
retrieved = get_dataset(dataset_id=created.dataset_id)
assert isinstance(retrieved, WrapperEvaluationDataset)
assert not isinstance(retrieved, EntityEvaluationDataset)
assert retrieved.dataset_id == created.dataset_id
assert retrieved.name == created.name
results = search_datasets(
experiment_ids=experiments[0],
filter_string="name = 'test_get_wrapper'",
)
assert len(results) == 1
assert isinstance(results[0], WrapperEvaluationDataset)
assert not isinstance(results[0], EntityEvaluationDataset)
def test_wrapper_vs_direct_client_usage(experiments):
client = MlflowClient()
entity_dataset = client.create_dataset(
name="test_client_direct",
experiment_id=experiments[0],
tags={"direct": "client"},
)
assert isinstance(entity_dataset, EntityEvaluationDataset)
assert not isinstance(entity_dataset, WrapperEvaluationDataset)
wrapped_dataset = create_dataset(
name="test_wrapped",
experiment_id=experiments[0],
tags={"wrapped": "fluent"},
)
assert isinstance(wrapped_dataset, WrapperEvaluationDataset)
assert not isinstance(wrapped_dataset, EntityEvaluationDataset)
assert wrapped_dataset._mlflow_dataset is not None
wrapped_from_entity = WrapperEvaluationDataset(entity_dataset)
assert wrapped_from_entity == entity_dataset
def test_wrapper_works_with_mlflow_log_input_integration(experiments):
dataset = create_dataset(
name="test_log_input",
experiment_id=experiments[0],
)
records = [
{
"inputs": {"question": "Test question"},
"expectations": {"answer": "Test answer"},
}
]
dataset.merge_records(records)
with mlflow.start_run(experiment_id=experiments[0]) as run:
mlflow.log_input(dataset, context="evaluation")
run_data = mlflow.get_run(run.info.run_id)
assert len(run_data.inputs.dataset_inputs) == 1
dataset_input = run_data.inputs.dataset_inputs[0]
assert dataset_input.dataset.name == "test_log_input"
assert dataset_input.dataset.digest == dataset.digest
def test_wrapper_isinstance_checks_for_dataset_interfaces(experiments):
dataset = create_dataset(
name="test_isinstance",
experiment_id=experiments[0],
)
assert isinstance(dataset, Dataset)
assert isinstance(dataset, PyFuncConvertibleDatasetMixin)
assert isinstance(dataset, WrapperEvaluationDataset)
assert not isinstance(dataset, EntityEvaluationDataset)
assert isinstance(dataset, (WrapperEvaluationDataset, EntityEvaluationDataset))
@pytest.mark.parametrize(
"records",
[
[
{"inputs": {"persona": "Student", "goal": "Find articles"}},
{
"inputs": {
"persona": "Researcher",
"goal": "Review",
"context": {"dept": "CS"},
}
},
{"inputs": {"goal": "Single goal"}, "expectations": {"output": "expected"}},
],
[
{"inputs": {"goal": "Learn ML", "simulation_guidelines": "Be concise"}},
{
"inputs": {
"persona": "Engineer",
"goal": "Debug",
"simulation_guidelines": "Focus on logs",
}
},
{
"inputs": {
"persona": "Student",
"goal": "Study",
"context": {"course": "CS101"},
"simulation_guidelines": "Ask clarifying questions",
}
},
],
],
)
def test_multiturn_valid_formats(experiments, records):
dataset = create_dataset(name="multiturn_test", experiment_id=experiments[0])
dataset.merge_records(records)
df = dataset.to_df()
assert len(df) == 3
for _, row in df.iterrows():
assert any(
key in row["inputs"] for key in ["persona", "goal", "context", "simulation_guidelines"]
)
@pytest.mark.parametrize(
("records", "error_pattern"),
[
# Top-level session fields
(
[{"persona": "Student", "goal": "Find articles", "custom_field": "value"}],
"Each record must have an 'inputs' field",
),
# Mixed fields in inputs
(
[
{
"inputs": {
"persona": "Student",
"goal": "Find",
"custom_field": "value",
}
}
],
"Invalid input schema.*cannot mix session fields",
),
# Inconsistent batch schema
(
[
{"inputs": {"persona": "Student", "goal": "Find articles"}},
{"inputs": {"question": "What is MLflow?"}},
],
"must use the same granularity.*Found",
),
# Empty inputs in batch with session records
(
[
{"inputs": {"goal": "Find articles"}},
{"inputs": {}},
],
"Empty inputs are not allowed for session records.*'goal' field is required",
),
],
)
def test_multiturn_validation_errors(experiments, records, error_pattern):
dataset = create_dataset(name="multiturn_error_test", experiment_id=experiments[0])
with pytest.raises(MlflowException, match=error_pattern):
dataset.merge_records(records)
@pytest.mark.parametrize(
("existing_records", "new_records"),
[
# Multiturn then custom
(
[{"inputs": {"persona": "Student", "goal": "Find articles"}}],
[{"inputs": {"question": "What is MLflow?", "model": "gpt-4"}}],
),
# Custom then multiturn
(
[{"inputs": {"question": "What is MLflow?", "model": "gpt-4"}}],
[{"inputs": {"persona": "Student", "goal": "Find articles"}}],
),
],
)
def test_multiturn_schema_compatibility(experiments, existing_records, new_records):
dataset = create_dataset(name="multiturn_compat_test", experiment_id=experiments[0])
dataset.merge_records(existing_records)
with pytest.raises(MlflowException, match="Cannot mix granularities"):
dataset.merge_records(new_records)
def test_multiturn_with_expectations_and_tags(experiments):
dataset = create_dataset(name="multiturn_full_test", experiment_id=experiments[0])
records = [
{
"inputs": {
"persona": "Graduate Student",
"goal": "Find peer-reviewed articles on machine learning",
"context": {"user_id": "U0001", "department": "CS"},
"simulation_guidelines": "Be thorough and cite sources",
},
"expectations": {"expected_output": "relevant articles", "quality": "high"},
"tags": {"difficulty": "medium"},
},
{
"inputs": {
"persona": "Librarian",
"goal": "Help with inter-library loan",
},
"expectations": {"expected_output": "loan information"},
},
]
dataset.merge_records(records)
df = dataset.to_df()
assert len(df) == 2
grad_record = df[df["inputs"].apply(lambda x: x.get("persona") == "Graduate Student")].iloc[0]
assert grad_record["expectations"]["expected_output"] == "relevant articles"
assert grad_record["expectations"]["quality"] == "high"
assert grad_record["tags"]["difficulty"] == "medium"
assert grad_record["inputs"]["context"] == {"user_id": "U0001", "department": "CS"}
assert grad_record["inputs"]["simulation_guidelines"] == "Be thorough and cite sources"