项目文件夹

文件
2026-07-13 13:22:34 +08:00

2306 行
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"