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

2526 lines
78 KiB
Python

import json
import time
from unittest import mock
from unittest.mock import AsyncMock, MagicMock, patch
import pandas as pd
import pytest
import sklearn.neighbors as knn
from click.testing import CliRunner
from fastapi import Request
import mlflow
from mlflow import MlflowClient
from mlflow.entities import (
EvaluationDataset,
Expectation,
Feedback,
GatewayEndpointModelConfig,
IssueSeverity,
IssueStatus,
Metric,
Param,
RunTag,
)
from mlflow.entities.assessment_source import AssessmentSource, AssessmentSourceType
from mlflow.entities.gateway_budget_policy import (
BudgetAction,
BudgetDuration,
BudgetDurationUnit,
BudgetTargetScope,
BudgetUnit,
)
from mlflow.entities.gateway_endpoint import GatewayModelLinkageType
from mlflow.entities.gateway_guardrail import GuardrailAction, GuardrailStage
from mlflow.entities.trace import Trace
from mlflow.entities.webhook import WebhookAction, WebhookEntity, WebhookEvent
from mlflow.gateway.cli import start
from mlflow.gateway.constants import MLFLOW_GATEWAY_CALLER_HEADER
from mlflow.gateway.schemas import chat
from mlflow.genai.datasets import create_dataset
from mlflow.genai.discovery.entities import _TriageResult
from mlflow.genai.discovery.pipeline import discover_issues
from mlflow.genai.judges import make_judge
from mlflow.genai.judges.base import AlignmentOptimizer
from mlflow.genai.scorers import scorer
from mlflow.genai.scorers.base import Scorer
from mlflow.genai.scorers.builtin_scorers import (
Completeness,
Guidelines,
RelevanceToQuery,
Safety,
UserFrustration,
)
from mlflow.genai.simulators import ConversationSimulator
from mlflow.pyfunc.model import (
ResponsesAgent,
ResponsesAgentRequest,
ResponsesAgentResponse,
)
from mlflow.server.gateway_api import chat_completions, invocations
from mlflow.store.tracking.gateway.entities import GatewayEndpointConfig, GatewayModelConfig
from mlflow.store.tracking.sqlalchemy_store import SqlAlchemyStore
from mlflow.telemetry.client import TelemetryClient
from mlflow.telemetry.events import (
AiCommandRunEvent,
AlignJudgeEvent,
AutologgingEvent,
CreateDatasetEvent,
CreateExperimentEvent,
CreateLoggedModelEvent,
CreateModelVersionEvent,
CreatePromptEvent,
CreateRegisteredModelEvent,
CreateRunEvent,
CreateWebhookEvent,
DiscoverIssuesEvent,
EvaluateEvent,
GatewayCreateBudgetPolicyEvent,
GatewayCreateEndpointEvent,
GatewayCreateGuardrailEvent,
GatewayCreateModelDefinitionEvent,
GatewayCreateSecretEvent,
GatewayDeleteBudgetPolicyEvent,
GatewayDeleteEndpointEvent,
GatewayDeleteGuardrailEvent,
GatewayDeleteSecretEvent,
GatewayGetEndpointEvent,
GatewayInvocationEvent,
GatewayListBudgetPoliciesEvent,
GatewayListEndpointsEvent,
GatewayListSecretsEvent,
GatewayStartEvent,
GatewayUpdateBudgetPolicyEvent,
GatewayUpdateEndpointEvent,
GatewayUpdateGuardrailEvent,
GatewayUpdateSecretEvent,
GenAIEvaluateEvent,
GetLoggedModelEvent,
GitModelVersioningEvent,
InvokeCustomJudgeModelEvent,
LoadPromptEvent,
LogAssessmentEvent,
LogBatchEvent,
LogDatasetEvent,
LogMetricEvent,
LogParamEvent,
MakeJudgeEvent,
McpRunEvent,
MergeRecordsEvent,
PromptOptimizationEvent,
ScorerCallEvent,
SimulateConversationEvent,
StartTraceEvent,
TracingContextPropagation,
TrackingServerStartEvent,
UpdateIssueEvent,
)
from mlflow.tracing.distributed import (
get_tracing_context_headers_for_http_request,
set_tracing_context_from_http_request_headers,
)
from mlflow.tracking.fluent import (
_create_dataset_input,
_create_logged_model,
_initialize_logged_model,
)
from mlflow.utils.os import is_windows
from tests.telemetry.helper_functions import validate_telemetry_record
class TestModel(mlflow.pyfunc.PythonModel):
def predict(self, model_input: list[str]) -> str:
return "test"
@pytest.fixture
def mlflow_client():
return MlflowClient()
@pytest.fixture(autouse=True)
def mock_get_telemetry_client(mock_telemetry_client: TelemetryClient):
with mock.patch(
"mlflow.telemetry.track.get_telemetry_client",
return_value=mock_telemetry_client,
):
yield
def test_create_logged_model(mock_requests, mock_telemetry_client: TelemetryClient):
event_name = CreateLoggedModelEvent.name
mlflow.create_external_model(name="model")
validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, {"flavor": "external"}
)
mlflow.initialize_logged_model(name="model", tags={"key": "value"})
validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, {"flavor": "initialize"}
)
_initialize_logged_model(name="model", flavor="keras")
validate_telemetry_record(mock_telemetry_client, mock_requests, event_name, {"flavor": "keras"})
mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"flavor": "pyfunc.CustomPythonModel"},
)
mlflow.sklearn.log_model(
knn.KNeighborsClassifier(),
name="model",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"flavor": "sklearn", "serialization_format": "skops"},
)
mlflow.sklearn.log_model(
knn.KNeighborsClassifier(),
name="model",
serialization_format=mlflow.sklearn.SERIALIZATION_FORMAT_PICKLE,
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"flavor": "sklearn", "serialization_format": "pickle"},
)
class SimpleResponsesAgent(ResponsesAgent):
def predict(self, request: ResponsesAgentRequest) -> ResponsesAgentResponse:
mock_response = {
"output": [
{
"type": "message",
"id": "1234",
"status": "completed",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": request.input[0].content,
}
],
}
],
}
return ResponsesAgentResponse(**mock_response)
mlflow.pyfunc.log_model(
name="model",
python_model=SimpleResponsesAgent(),
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"flavor": "pyfunc.ResponsesAgent"},
)
_create_logged_model(name="model", flavor="pyfunc", uses_uv=True)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"flavor": "pyfunc", "uses_uv": True},
)
_create_logged_model(name="model", flavor="pyfunc", uses_uv=False)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"flavor": "pyfunc"},
)
def test_create_experiment(mock_requests, mlflow_client, mock_telemetry_client: TelemetryClient):
event_name = CreateExperimentEvent.name
exp_id = mlflow.create_experiment(name="test_experiment")
validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, {"experiment_id": exp_id}
)
exp_id = mlflow_client.create_experiment(name="test_experiment1")
validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, {"experiment_id": exp_id}
)
def test_create_run(mock_requests, mlflow_client, mock_telemetry_client: TelemetryClient):
event_name = CreateRunEvent.name
exp_id = mlflow.create_experiment(name="test_experiment")
with mlflow.start_run(experiment_id=exp_id):
record = validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, check_params=False
)
assert json.loads(record["params"])["experiment_id"] == exp_id
mlflow_client.create_run(experiment_id=exp_id)
validate_telemetry_record(mock_telemetry_client, mock_requests, event_name, check_params=False)
exp_id = mlflow.create_experiment(name="test_experiment2")
mlflow.set_experiment(experiment_id=exp_id)
with mlflow.start_run():
record = validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, check_params=False
)
params = json.loads(record["params"])
assert params["mlflow_experiment_id"] == exp_id
def test_create_run_with_imports(mock_requests, mock_telemetry_client: TelemetryClient):
event_name = CreateRunEvent.name
import pyspark.ml # noqa: F401
with mlflow.start_run():
data = validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, check_params=False
)
assert "pyspark.ml" in json.loads(data["params"])["imports"]
def test_create_registered_model(
mock_requests, mlflow_client, mock_telemetry_client: TelemetryClient
):
event_name = CreateRegisteredModelEvent.name
mlflow_client.create_registered_model(name="test_model1")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"is_prompt": False},
)
mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
registered_model_name="test_model",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"is_prompt": False},
)
def test_create_model_version(mock_requests, mlflow_client, mock_telemetry_client: TelemetryClient):
event_name = CreateModelVersionEvent.name
mlflow_client.create_registered_model(name="test_model")
mlflow_client.create_model_version(
name="test_model", source="test_source", run_id="test_run_id"
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"is_prompt": False},
)
mlflow.pyfunc.log_model(
name="model",
python_model=TestModel(),
registered_model_name="test_model",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"is_prompt": False},
)
mlflow.genai.register_prompt(
name="ai_assistant_prompt",
template="Respond to the user's message as a {{style}} AI. {{greeting}}",
commit_message="Initial version of AI assistant",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
event_name,
{"is_prompt": True},
)
def test_start_trace(mock_requests, mlflow_client, mock_telemetry_client: TelemetryClient):
event_name = StartTraceEvent.name
with mlflow.start_span(name="test_span"):
pass
validate_telemetry_record(mock_telemetry_client, mock_requests, event_name, check_params=False)
@mlflow.trace
def test_func():
pass
test_func()
validate_telemetry_record(mock_telemetry_client, mock_requests, event_name, check_params=False)
trace_id = mlflow_client.start_trace(name="test_trace").trace_id
mlflow_client.end_trace(trace_id=trace_id)
validate_telemetry_record(mock_telemetry_client, mock_requests, event_name, check_params=False)
import openai # noqa: F401
test_func()
data = validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, check_params=False
)
params = json.loads(data["params"])
assert "openai" in params["imports"]
assert params["format"] == "native"
def test_start_trace_genai_semconv(
mock_requests, monkeypatch, mock_telemetry_client: TelemetryClient
):
monkeypatch.setenv("MLFLOW_ENABLE_OTEL_GENAI_SEMCONV", "true")
event_name = StartTraceEvent.name
@mlflow.trace
def test_func():
pass
test_func()
data = validate_telemetry_record(
mock_telemetry_client, mock_requests, event_name, check_params=False
)
assert json.loads(data["params"])["format"] == "genai_semconv"
def test_create_prompt(mock_requests, mlflow_client, mock_telemetry_client: TelemetryClient):
mlflow_client.create_prompt(name="test_prompt")
validate_telemetry_record(mock_telemetry_client, mock_requests, CreatePromptEvent.name)
# OSS prompt registry uses create_registered_model with a special tag
mlflow.genai.register_prompt(
name="greeting_prompt",
template="Respond to the user's message as a {{style}} AI. {{greeting}}",
)
expected_params = {"is_prompt": True}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
CreateRegisteredModelEvent.name,
expected_params,
)
def test_log_assessment(mock_requests, mock_telemetry_client: TelemetryClient):
with mlflow.start_span(name="test_span") as span:
feedback = Feedback(
name="faithfulness",
value=0.9,
rationale="The model is faithful to the input.",
metadata={"model": "gpt-4o-mini"},
)
mlflow.log_assessment(trace_id=span.trace_id, assessment=feedback)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogAssessmentEvent.name,
{"type": "feedback", "source_type": "CODE"},
)
mlflow.log_feedback(trace_id=span.trace_id, value=0.9, name="faithfulness")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogAssessmentEvent.name,
{"type": "feedback", "source_type": "CODE"},
)
with mlflow.start_span(name="test_span2") as span:
expectation = Expectation(
name="expected_answer",
value="MLflow",
)
mlflow.log_assessment(trace_id=span.trace_id, assessment=expectation)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogAssessmentEvent.name,
{"type": "expectation", "source_type": "HUMAN"},
)
mlflow.log_expectation(trace_id=span.trace_id, value="MLflow", name="expected_answer")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogAssessmentEvent.name,
{"type": "expectation", "source_type": "HUMAN"},
)
def test_evaluate(mock_requests, mock_telemetry_client: TelemetryClient):
mlflow.models.evaluate(
data=pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]}),
model=lambda x: x["x"] * 2,
extra_metrics=[mlflow.metrics.latency()],
)
validate_telemetry_record(mock_telemetry_client, mock_requests, EvaluateEvent.name)
def test_create_webhook(mock_requests, mock_telemetry_client: TelemetryClient):
client = MlflowClient()
client.create_webhook(
name="test_webhook",
url="https://example.com/webhook",
events=[WebhookEvent(WebhookEntity.MODEL_VERSION, WebhookAction.CREATED)],
)
expected_params = {"events": ["model_version.created"]}
validate_telemetry_record(
mock_telemetry_client, mock_requests, CreateWebhookEvent.name, expected_params
)
def test_genai_evaluate(mock_requests, mock_telemetry_client: TelemetryClient):
@mlflow.genai.scorer
def decorator_scorer():
return 1.0
instructions_judge = make_judge(
name="quality_judge",
instructions="Evaluate if {{ outputs }} is high quality",
model="openai:/gpt-4",
)
session_level_instruction_judge = make_judge(
name="conversation_quality",
instructions="Evaluate if the {{ conversation }} is engaging and coherent",
model="openai:/gpt-4",
)
guidelines_scorer = Guidelines(
name="politeness",
guidelines=["Be polite", "Be respectful"],
)
builtin_scorer = RelevanceToQuery(name="relevance_check")
session_level_builtin_scorer = UserFrustration(name="frustration_check")
data = [
{
"inputs": {"model_input": ["What is MLflow?"]},
"outputs": "MLflow is an open source platform.",
}
]
model = TestModel()
with (
mock.patch("mlflow.genai.judges.utils.invocation_utils.invoke_judge_model"),
mock.patch("mlflow.genai.judges.builtin.invoke_judge_model"),
mock.patch("mlflow.genai.judges.instructions_judge.invoke_judge_model"),
):
# Test with all scorer kinds and scopes, without predict_fn
mlflow.genai.evaluate(
data=data,
scorers=[
decorator_scorer,
instructions_judge,
session_level_instruction_judge,
guidelines_scorer,
builtin_scorer,
session_level_builtin_scorer,
],
)
expected_params = {
"predict_fn_provided": False,
"scorer_info": [
{
"class": "UserDefinedScorer",
"kind": "decorator",
"scope": "trace",
},
{
"class": "UserDefinedScorer",
"kind": "instructions",
"scope": "trace",
},
{
"class": "UserDefinedScorer",
"kind": "instructions",
"scope": "session",
},
{"class": "Guidelines", "kind": "guidelines", "scope": "trace"},
{"class": "RelevanceToQuery", "kind": "builtin", "scope": "trace"},
{"class": "UserFrustration", "kind": "builtin", "scope": "session"},
],
"eval_data_type": "list[dict]",
"eval_data_size": 1,
"eval_data_provided_fields": ["inputs", "outputs"],
}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GenAIEvaluateEvent.name,
expected_params,
)
# Test with predict_fn
mlflow.genai.evaluate(
data=data,
scorers=[builtin_scorer, guidelines_scorer],
predict_fn=model.predict,
)
expected_params = {
"predict_fn_provided": True,
"scorer_info": [
{"class": "RelevanceToQuery", "kind": "builtin", "scope": "trace"},
{"class": "Guidelines", "kind": "guidelines", "scope": "trace"},
],
"eval_data_type": "list[dict]",
"eval_data_size": 1,
"eval_data_provided_fields": ["inputs", "outputs"],
}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GenAIEvaluateEvent.name,
expected_params,
)
def test_genai_evaluate_telemetry_data_fields(
mock_requests, mock_telemetry_client: TelemetryClient
):
@mlflow.genai.scorer
def sample_scorer():
return 1.0
with mock.patch("mlflow.genai.judges.utils.invocation_utils.invoke_judge_model"):
# Test with list of dicts
data_list = [
{
"inputs": {"question": "Q1"},
"outputs": "A1",
"expectations": {"answer": "Expected1"},
},
{
"inputs": {"question": "Q2"},
"outputs": "A2",
"expectations": {"answer": "Expected2"},
},
]
mlflow.genai.evaluate(data=data_list, scorers=[sample_scorer])
expected_params = {
"predict_fn_provided": False,
"scorer_info": [
{
"class": "UserDefinedScorer",
"kind": "decorator",
"scope": "trace",
},
],
"eval_data_type": "list[dict]",
"eval_data_size": 2,
"eval_data_provided_fields": ["expectations", "inputs", "outputs"],
}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GenAIEvaluateEvent.name,
expected_params,
)
# Test with pandas DataFrame
df_data = pd.DataFrame([
{"inputs": {"question": "Q1"}, "outputs": "A1"},
{"inputs": {"question": "Q2"}, "outputs": "A2"},
{"inputs": {"question": "Q3"}, "outputs": "A3"},
])
mlflow.genai.evaluate(data=df_data, scorers=[sample_scorer])
expected_params = {
"predict_fn_provided": False,
"scorer_info": [
{
"class": "UserDefinedScorer",
"kind": "decorator",
"scope": "trace",
},
],
"eval_data_type": "pd.DataFrame",
"eval_data_size": 3,
"eval_data_provided_fields": ["inputs", "outputs"],
}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GenAIEvaluateEvent.name,
expected_params,
)
# Test with list of Traces
trace_ids = []
for i in range(2):
with mlflow.start_span(name=f"test_span_{i}") as span:
span.set_inputs({"question": f"Q{i}"})
span.set_outputs({"answer": f"A{i}"})
trace_ids.append(span.trace_id)
traces = [mlflow.get_trace(trace_id) for trace_id in trace_ids]
mlflow.genai.evaluate(data=traces, scorers=[sample_scorer])
expected_params = {
"predict_fn_provided": False,
"scorer_info": [
{
"class": "UserDefinedScorer",
"kind": "decorator",
"scope": "trace",
},
],
"eval_data_type": "list[Trace]",
"eval_data_size": 2,
"eval_data_provided_fields": ["inputs", "outputs", "trace"],
}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GenAIEvaluateEvent.name,
expected_params,
)
# Test with EvaluationDataset
from mlflow.genai.datasets import create_dataset
dataset = create_dataset("test_dataset")
dataset_data = [
{
"inputs": {"question": "Q1"},
"outputs": "A1",
"expectations": {"answer": "Expected1"},
},
{
"inputs": {"question": "Q2"},
"outputs": "A2",
"expectations": {"answer": "Expected2"},
},
]
dataset.merge_records(dataset_data)
mlflow.genai.evaluate(data=dataset, scorers=[sample_scorer])
expected_params = {
"predict_fn_provided": False,
"scorer_info": [
{
"class": "UserDefinedScorer",
"kind": "decorator",
"scope": "trace",
},
],
"eval_data_type": "EvaluationDataset",
"eval_data_size": 2,
"eval_data_provided_fields": ["expectations", "inputs", "outputs"],
}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GenAIEvaluateEvent.name,
expected_params,
)
def test_simulate_conversation(mock_requests, mock_telemetry_client: TelemetryClient):
simulator = ConversationSimulator(
test_cases=[
{"goal": "Learn about MLflow"},
{"goal": "Debug an issue"},
],
max_turns=2,
)
def mock_predict_fn(input, **kwargs):
return {"role": "assistant", "content": "Mock response"}
mock_trace = mock.Mock()
with (
mock.patch(
"mlflow.genai.simulators.simulator.invoke_model_without_tracing",
return_value="Mock user message",
),
mock.patch(
"mlflow.genai.simulators.simulator.ConversationSimulator._check_goal_achieved",
return_value=False,
),
mock.patch(
"mlflow.genai.simulators.simulator.mlflow.get_trace",
return_value=mock_trace,
),
):
result = simulator.simulate(predict_fn=mock_predict_fn)
assert len(result) == 2
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
SimulateConversationEvent.name,
{
"callsite": "conversation_simulator",
"simulated_conversation_info": [
{"turn_count": len(result[0])},
{"turn_count": len(result[1])},
],
},
)
def test_simulate_conversation_from_genai_evaluate(
mock_requests, mock_telemetry_client: TelemetryClient
):
simulator = ConversationSimulator(
test_cases=[
{"goal": "Learn about MLflow"},
],
max_turns=1,
)
def mock_predict_fn(input, **kwargs):
return {"role": "assistant", "content": "Mock response"}
@scorer
def simple_scorer(outputs) -> bool:
return len(outputs) > 0
with (
mock.patch(
"mlflow.genai.simulators.simulator.invoke_model_without_tracing",
return_value="Mock user message",
),
mock.patch(
"mlflow.genai.simulators.simulator.ConversationSimulator._check_goal_achieved",
return_value=True,
),
):
mlflow.genai.evaluate(data=simulator, predict_fn=mock_predict_fn, scorers=[simple_scorer])
mock_telemetry_client.flush()
simulate_events = [
record
for record in mock_requests
if record["data"]["event_name"] == SimulateConversationEvent.name
]
assert len(simulate_events) == 1
event_params = json.loads(simulate_events[0]["data"]["params"])
assert event_params == {
"callsite": "genai_evaluate",
"simulated_conversation_info": [{"turn_count": 1}],
}
def test_prompt_optimization(mock_requests, mock_telemetry_client: TelemetryClient):
from mlflow.genai.optimize import optimize_prompts
from mlflow.genai.optimize.optimizers import BasePromptOptimizer
from mlflow.genai.optimize.types import PromptOptimizerOutput
class MockAdapter(BasePromptOptimizer):
def __init__(self):
self.model_name = "openai:/gpt-4o-mini"
def optimize(self, eval_fn, train_data, target_prompts, enable_tracking):
return PromptOptimizerOutput(optimized_prompts=target_prompts)
sample_prompt = mlflow.genai.register_prompt(
name="test_prompt_for_adaptation",
template="Translate {{input_text}} to {{language}}",
)
sample_data = [
{"inputs": {"input_text": "Hello", "language": "Spanish"}, "outputs": "Hola"},
{"inputs": {"input_text": "World", "language": "French"}, "outputs": "Monde"},
]
@mlflow.genai.scorers.scorer
def exact_match_scorer(outputs, expectations):
return 1.0 if outputs == expectations["expected_response"] else 0.0
def predict_fn(input_text, language):
mlflow.genai.load_prompt(f"prompts:/{sample_prompt.name}/{sample_prompt.version}")
return "translated"
optimize_prompts(
predict_fn=predict_fn,
train_data=sample_data,
prompt_uris=[f"prompts:/{sample_prompt.name}/{sample_prompt.version}"],
optimizer=MockAdapter(),
scorers=[exact_match_scorer],
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
PromptOptimizationEvent.name,
{
"optimizer_type": "MockAdapter",
"prompt_count": 1,
"scorer_count": 1,
"custom_aggregation": False,
},
)
def test_create_dataset(mock_requests, mock_telemetry_client: TelemetryClient):
with mock.patch("mlflow.tracking._tracking_service.utils._get_store") as mock_store:
mock_store_instance = mock.MagicMock()
mock_store.return_value = mock_store_instance
mock_store_instance.create_dataset.return_value = mock.MagicMock(
dataset_id="test-dataset-id", name="test_dataset", tags={"test": "value"}
)
create_dataset(name="test_dataset", tags={"test": "value"})
validate_telemetry_record(mock_telemetry_client, mock_requests, CreateDatasetEvent.name)
def test_merge_records(mock_requests, mock_telemetry_client: TelemetryClient):
with mock.patch("mlflow.tracking._tracking_service.utils._get_store") as mock_store:
mock_store_instance = mock.MagicMock()
mock_store.return_value = mock_store_instance
mock_store_instance.get_dataset.return_value = mock.MagicMock(dataset_id="test-id")
mock_store_instance.upsert_dataset_records.return_value = {
"inserted": 2,
"updated": 0,
}
evaluation_dataset = EvaluationDataset(
dataset_id="test-id",
name="test",
digest="digest",
created_time=123,
last_update_time=456,
)
records = [
{"inputs": {"q": "Q1"}, "expectations": {"a": "A1"}},
{"inputs": {"q": "Q2"}, "expectations": {"a": "A2"}},
]
evaluation_dataset.merge_records(records)
expected_params = {
"record_count": 2,
"input_type": "list[dict]",
"dataset_type": "trace",
}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
MergeRecordsEvent.name,
expected_params,
)
def test_log_dataset(mock_requests, mock_telemetry_client: TelemetryClient):
with mlflow.start_run() as run:
dataset = mlflow.data.from_pandas(pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]}))
mlflow.log_input(dataset)
validate_telemetry_record(mock_telemetry_client, mock_requests, LogDatasetEvent.name)
mlflow.log_inputs(datasets=[dataset], contexts=["training"], tags_list=[None])
validate_telemetry_record(mock_telemetry_client, mock_requests, LogDatasetEvent.name)
client = MlflowClient()
client.log_inputs(run_id=run.info.run_id, datasets=[_create_dataset_input(dataset)])
validate_telemetry_record(mock_telemetry_client, mock_requests, LogDatasetEvent.name)
def test_log_metric(mock_requests, mock_telemetry_client: TelemetryClient):
with mlflow.start_run():
mlflow.log_metric("test_metric", 1.0)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogMetricEvent.name,
{"synchronous": True},
)
mlflow.log_metric("test_metric", 1.0, synchronous=False)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogMetricEvent.name,
{"synchronous": False},
)
client = MlflowClient()
client.log_metric(
run_id=mlflow.active_run().info.run_id,
key="test_metric",
value=1.0,
timestamp=int(time.time()),
step=0,
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogMetricEvent.name,
{"synchronous": True},
)
client.log_metric(
run_id=mlflow.active_run().info.run_id,
key="test_metric",
value=1.0,
timestamp=int(time.time()),
step=0,
synchronous=False,
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogMetricEvent.name,
{"synchronous": False},
)
def test_log_param(mock_requests, mock_telemetry_client: TelemetryClient):
with mlflow.start_run():
mlflow.log_param("test_param", "test_value")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogParamEvent.name,
{"synchronous": True},
)
mlflow.log_param("test_param", "test_value", synchronous=False)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogParamEvent.name,
{"synchronous": False},
)
client = mlflow.MlflowClient()
client.log_param(
run_id=mlflow.active_run().info.run_id,
key="test_param",
value="test_value",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogParamEvent.name,
{"synchronous": True},
)
def test_log_batch(mock_requests, mock_telemetry_client: TelemetryClient):
with mlflow.start_run():
mlflow.log_params(params={"test_param": "test_value"})
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogBatchEvent.name,
{"metrics": False, "params": True, "tags": False, "synchronous": True},
)
mlflow.log_params(params={"test_param": "test_value"}, synchronous=False)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogBatchEvent.name,
{"metrics": False, "params": True, "tags": False, "synchronous": False},
)
mlflow.log_metrics(metrics={"test_metric": 1.0})
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogBatchEvent.name,
{"metrics": True, "params": False, "tags": False, "synchronous": True},
)
mlflow.log_metrics(metrics={"test_metric": 1.0}, synchronous=False)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogBatchEvent.name,
{"metrics": True, "params": False, "tags": False, "synchronous": False},
)
mlflow.set_tags(tags={"test_tag": "test_value"})
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogBatchEvent.name,
{"metrics": False, "params": False, "tags": True, "synchronous": True},
)
mlflow.set_tags(tags={"test_tag": "test_value"}, synchronous=False)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogBatchEvent.name,
{"metrics": False, "params": False, "tags": True, "synchronous": False},
)
client = mlflow.MlflowClient()
client.log_batch(
run_id=mlflow.active_run().info.run_id,
metrics=[Metric(key="test_metric", value=1.0, timestamp=int(time.time()), step=0)],
params=[Param(key="test_param", value="test_value")],
tags=[RunTag(key="test_tag", value="test_value")],
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LogBatchEvent.name,
{"metrics": True, "params": True, "tags": True, "synchronous": True},
)
def test_get_logged_model(mock_requests, mock_telemetry_client: TelemetryClient, tmp_path):
model_info = mlflow.sklearn.log_model(
knn.KNeighborsClassifier(),
name="model",
)
mock_telemetry_client.flush()
mlflow.sklearn.load_model(model_info.model_uri)
data = validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GetLoggedModelEvent.name,
check_params=False,
)
assert "sklearn" in json.loads(data["params"])["imports"]
mlflow.pyfunc.load_model(model_info.model_uri)
data = validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GetLoggedModelEvent.name,
check_params=False,
)
model_def = """
import mlflow
from mlflow.models import set_model
class TestModel(mlflow.pyfunc.PythonModel):
def predict(self, context, model_input: list[str], params=None) -> list[str]:
return model_input
set_model(TestModel())
"""
model_path = tmp_path / "model.py"
model_path.write_text(model_def)
model_info = mlflow.pyfunc.log_model(
name="model",
python_model=model_path,
)
mock_telemetry_client.flush()
mlflow.pyfunc.load_model(model_info.model_uri)
data = validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GetLoggedModelEvent.name,
check_params=False,
)
# test load model after registry
mlflow.register_model(model_info.model_uri, name="test")
mock_telemetry_client.flush()
mlflow.pyfunc.load_model("models:/test/1")
data = validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GetLoggedModelEvent.name,
check_params=False,
)
def test_mcp_run(mock_requests, mock_telemetry_client: TelemetryClient):
from mlflow.mcp.cli import run
runner = CliRunner(catch_exceptions=False)
with mock.patch("mlflow.mcp.cli.run_server") as mock_run_server:
runner.invoke(run)
mock_run_server.assert_called_once()
mock_telemetry_client.flush()
validate_telemetry_record(mock_telemetry_client, mock_requests, McpRunEvent.name)
@pytest.mark.skipif(is_windows(), reason="Windows does not support gateway start")
def test_gateway_start(tmp_path, mock_requests, mock_telemetry_client: TelemetryClient):
config = tmp_path.joinpath("config.yml")
config.write_text(
"""
endpoints:
- name: test-endpoint
endpoint_type: llm/v1/completions
model:
provider: openai
name: gpt-3.5-turbo
config:
openai_api_key: test-key
"""
)
def assert_event_recorded_before_run_app(**kwargs):
mock_telemetry_client.flush()
validate_telemetry_record(mock_telemetry_client, mock_requests, GatewayStartEvent.name)
runner = CliRunner(catch_exceptions=False)
with mock.patch("mlflow.gateway.cli.run_app", side_effect=assert_event_recorded_before_run_app):
runner.invoke(start, ["--config-path", str(config)])
@pytest.mark.parametrize(
("cli_args", "expected_params"),
[
(
["--backend-store-uri", "sqlite:///test.db"],
{
"auth_enabled": False,
"app_name": None,
"backend_store_type": "sqlite",
"serve_artifacts": True,
"artifacts_only": False,
"expose_prometheus": False,
"enable_workspaces": False,
"workers": None,
"dev": False,
},
),
(
["--backend-store-uri", "sqlite:///test.db", "--app-name", "basic-auth"],
{
"auth_enabled": True,
"app_name": "basic-auth",
"backend_store_type": "sqlite",
"serve_artifacts": True,
"artifacts_only": False,
"expose_prometheus": False,
"enable_workspaces": False,
"workers": None,
"dev": False,
},
),
(
[
"--backend-store-uri",
"sqlite:///test.db",
"--no-serve-artifacts",
"--expose-prometheus",
"/tmp/metrics",
"--enable-workspaces",
],
{
"auth_enabled": False,
"app_name": None,
"backend_store_type": "sqlite",
"serve_artifacts": False,
"artifacts_only": False,
"expose_prometheus": True,
"enable_workspaces": True,
"workers": None,
"dev": False,
},
),
],
)
def test_tracking_server_start(
tmp_path,
mock_requests,
mock_telemetry_client: TelemetryClient,
monkeypatch,
cli_args,
expected_params,
):
from mlflow.cli import server
# Isolate env vars that server() mutates so they don't leak into other tests
for key in (
"MLFLOW_ENABLE_WORKSPACES",
"MLFLOW_TRACE_ARCHIVAL_CONFIG",
"MLFLOW_WORKSPACE_STORE_URI",
"MLFLOW_SERVER_DISABLE_SECURITY_MIDDLEWARE",
"MLFLOW_SERVER_ALLOWED_HOSTS",
"MLFLOW_SERVER_CORS_ALLOWED_ORIGINS",
"MLFLOW_SERVER_X_FRAME_OPTIONS",
):
monkeypatch.delenv(key, raising=False)
def assert_event_recorded_before_run_server(**kwargs):
mock_telemetry_client.flush()
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
TrackingServerStartEvent.name,
expected_params,
)
runner = CliRunner(catch_exceptions=False)
with (
mock.patch(
"mlflow.server._run_server", side_effect=assert_event_recorded_before_run_server
),
mock.patch("mlflow.server.handlers.initialize_backend_stores"),
):
runner.invoke(server, cli_args)
def test_ai_command_run(mock_requests, mock_telemetry_client: TelemetryClient):
from mlflow.ai_commands import commands
runner = CliRunner(catch_exceptions=False)
# Test CLI context
with mock.patch("mlflow.ai_commands.get_command", return_value="---\ntest\n---\nTest command"):
result = runner.invoke(commands, ["run", "test_command"])
assert result.exit_code == 0
mock_telemetry_client.flush()
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
AiCommandRunEvent.name,
{"command_key": "test_command", "context": "cli"},
)
def test_git_model_versioning(mock_requests, mock_telemetry_client):
from mlflow.genai import enable_git_model_versioning
with enable_git_model_versioning():
pass
mock_telemetry_client.flush()
validate_telemetry_record(mock_telemetry_client, mock_requests, GitModelVersioningEvent.name)
@pytest.mark.parametrize(
("model_uri", "expected_provider"),
[
("databricks:/llama-3.1-70b", "databricks"),
("openai:/gpt-4o-mini", "openai"),
("endpoints:/my-endpoint", "endpoints"),
("anthropic:/claude-3-opus", "anthropic"),
],
)
def test_invoke_custom_judge_model(
mock_requests,
mock_telemetry_client: TelemetryClient,
model_uri,
expected_provider,
):
from mlflow.genai.judges.adapters.gateway_adapter import InvokeOutput
from mlflow.genai.judges.utils import invoke_judge_model
mock_response = json.dumps({"result": 0.8, "rationale": "Test rationale"})
mock_target = (
"mlflow.genai.judges.adapters.gateway_adapter._invoke_via_gateway"
if expected_provider == "endpoints"
else "mlflow.genai.judges.adapters.gateway_adapter.GatewayAdapter._invoke_and_handle_tools"
)
mock_return = (
mock_response
if expected_provider == "endpoints"
else InvokeOutput(
response=mock_response,
request_id=None,
num_prompt_tokens=None,
num_completion_tokens=None,
)
)
with mock.patch(mock_target, return_value=mock_return):
invoke_judge_model(
model_uri=model_uri,
prompt="Test prompt",
assessment_name="test_assessment",
)
expected_params = {"model_provider": expected_provider}
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
InvokeCustomJudgeModelEvent.name,
expected_params,
)
def test_make_judge(mock_requests, mock_telemetry_client: TelemetryClient):
make_judge(
name="test_judge",
instructions="Evaluate the {{ inputs }} and {{ outputs }}",
model="openai:/gpt-4",
feedback_value_type=str,
)
expected_params = {"model_provider": "openai"}
validate_telemetry_record(
mock_telemetry_client, mock_requests, MakeJudgeEvent.name, expected_params
)
make_judge(
name="test_judge",
instructions="Evaluate the {{ inputs }} and {{ outputs }}",
feedback_value_type=str,
)
expected_params = {"model_provider": None}
validate_telemetry_record(
mock_telemetry_client, mock_requests, MakeJudgeEvent.name, expected_params
)
def test_align_judge(mock_requests, mock_telemetry_client: TelemetryClient):
judge = make_judge(
name="test_judge",
instructions="Evaluate the {{ inputs }} and {{ outputs }}",
model="openai:/gpt-4",
feedback_value_type=str,
)
traces = [
mock.MagicMock(spec=Trace),
mock.MagicMock(spec=Trace),
]
class MockOptimizer(AlignmentOptimizer):
def align(self, judge, traces):
return judge
custom_optimizer = MockOptimizer()
judge.align(traces, optimizer=custom_optimizer)
expected_params = {"trace_count": 2, "optimizer_type": "MockOptimizer"}
validate_telemetry_record(
mock_telemetry_client, mock_requests, AlignJudgeEvent.name, expected_params
)
def test_discover_issues(mock_requests, mock_telemetry_client: TelemetryClient):
traces = [
mock.MagicMock(spec=Trace),
mock.MagicMock(spec=Trace),
mock.MagicMock(spec=Trace),
]
mock_triage_run_id = "abc123"
mock_eval_result = mock.MagicMock()
mock_eval_result.run_id = mock_triage_run_id
with (
patch("mlflow.genai.discovery.pipeline.get_session_id", return_value=None),
patch("mlflow.genai.discovery.pipeline.verify_scorer"),
patch(
"mlflow.genai.discovery.pipeline.mlflow.genai.evaluate",
return_value=mock_eval_result,
),
patch(
"mlflow.genai.discovery.pipeline.extract_failing_traces",
return_value=_TriageResult([], {}, {}),
),
patch("mlflow.genai.discovery.pipeline.mlflow.MlflowClient"),
patch("mlflow.genai.discovery.pipeline.mlflow.set_experiment"),
):
discover_issues(
traces=traces,
model="openai:/gpt-4",
categories=["hallucination", "accuracy"],
)
expected_params = {
"model": "openai:/gpt-4",
"trace_count": 3,
"categories": ["hallucination", "accuracy"],
"source_run_id": None,
"issue_count": 0,
"total_traces_analyzed": 3,
"total_cost_usd": None,
"triage_run_id": mock_triage_run_id,
}
validate_telemetry_record(
mock_telemetry_client, mock_requests, DiscoverIssuesEvent.name, expected_params
)
def test_autologging(mock_requests, mock_telemetry_client: TelemetryClient):
try:
mlflow.openai.autolog()
mlflow.autolog()
mock_telemetry_client.flush()
data = [record["data"] for record in mock_requests]
params = [event["params"] for event in data if event["event_name"] == AutologgingEvent.name]
assert (
json.dumps({
"flavor": mlflow.openai.FLAVOR_NAME,
"log_traces": True,
"disable": False,
})
in params
)
assert json.dumps({"flavor": "all", "log_traces": True, "disable": False}) in params
finally:
mlflow.autolog(disable=True)
def test_load_prompt(mock_requests, mock_telemetry_client: TelemetryClient):
# Register a prompt first
prompt = mlflow.genai.register_prompt(
name="test_prompt",
template="Hello {{name}}",
)
mock_telemetry_client.flush()
# Set an alias for testing
mlflow.genai.set_prompt_alias(name="test_prompt", version=prompt.version, alias="production")
# Test load_prompt with version (no alias)
mlflow.genai.load_prompt(name_or_uri="test_prompt", version=prompt.version)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LoadPromptEvent.name,
{"uses_alias": False},
)
# Test load_prompt with URI and version (no alias)
mlflow.genai.load_prompt(name_or_uri=f"prompts:/test_prompt/{prompt.version}")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
LoadPromptEvent.name,
{"uses_alias": False},
)
# Test load_prompt with alias
mlflow.genai.load_prompt(name_or_uri="prompts:/test_prompt@production")
validate_telemetry_record(
mock_telemetry_client, mock_requests, LoadPromptEvent.name, {"uses_alias": True}
)
# Test load_prompt with @latest (special alias)
mlflow.genai.load_prompt(name_or_uri="prompts:/test_prompt@latest")
validate_telemetry_record(
mock_telemetry_client, mock_requests, LoadPromptEvent.name, {"uses_alias": True}
)
def test_scorer_call_direct(mock_requests, mock_telemetry_client: TelemetryClient):
@scorer
def custom_scorer(outputs) -> bool:
return len(outputs) > 0
result = custom_scorer(outputs="test output")
assert result is True
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "UserDefinedScorer",
"scorer_kind": "decorator",
"scope": "trace",
"callsite": "direct_scorer_call",
"has_feedback_error": False,
},
)
safety_scorer = Safety()
mock_feedback = Feedback(
name="test_feedback",
value="yes",
rationale="Test rationale",
)
with mock.patch(
"mlflow.genai.judges.builtin.invoke_judge_model",
return_value=mock_feedback,
):
safety_scorer(outputs="test output")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "Safety",
"scorer_kind": "builtin",
"scope": "trace",
"callsite": "direct_scorer_call",
"has_feedback_error": False,
},
)
mock_requests.clear()
guidelines_scorer = Guidelines(guidelines="The response must be in English")
with mock.patch(
"mlflow.genai.judges.builtin.invoke_judge_model",
return_value=mock_feedback,
):
guidelines_scorer(
inputs={"question": "What is MLflow?"}, outputs="MLflow is an ML platform"
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "Guidelines",
"scorer_kind": "guidelines",
"scope": "trace",
"callsite": "direct_scorer_call",
"has_feedback_error": False,
},
)
mock_requests.clear()
class CustomClassScorer(Scorer):
name: str = "custom_class"
def __call__(self, *, outputs) -> bool:
return len(outputs) > 0
custom_class_scorer = CustomClassScorer()
result = custom_class_scorer(outputs="test output")
assert result is True
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "UserDefinedScorer",
"scorer_kind": "class",
"scope": "trace",
"callsite": "direct_scorer_call",
"has_feedback_error": False,
},
)
def test_scorer_call_from_genai_evaluate(mock_requests, mock_telemetry_client: TelemetryClient):
@scorer
def simple_length_checker(outputs) -> bool:
return len(outputs) > 0
session_judge = make_judge(
name="conversation_quality",
instructions="Evaluate if the {{ conversation }} is engaging and coherent",
model="openai:/gpt-4",
)
# Create traces with session metadata for session-level scorer testing
@mlflow.trace(span_type=mlflow.entities.SpanType.CHAT_MODEL)
def model(question, session_id):
mlflow.update_current_trace(metadata={"mlflow.trace.session": session_id})
return f"Answer to: {question}"
model("What is MLflow?", session_id="test_session")
trace_1 = mlflow.get_trace(mlflow.get_last_active_trace_id())
model("How does MLflow work?", session_id="test_session")
trace_2 = mlflow.get_trace(mlflow.get_last_active_trace_id())
test_data = pd.DataFrame([
{
"trace": trace_1,
},
{
"trace": trace_2,
},
])
mock_feedback = Feedback(
name="test_feedback",
value="yes",
rationale="Test",
)
with mock.patch(
"mlflow.genai.judges.instructions_judge.invoke_judge_model",
return_value=mock_feedback,
):
mlflow.genai.evaluate(data=test_data, scorers=[simple_length_checker, session_judge])
mock_telemetry_client.flush()
scorer_call_events = [
record for record in mock_requests if record["data"]["event_name"] == ScorerCallEvent.name
]
# Should have 3 events: 2 response-level calls (one per trace)
# + 1 session-level call (one per session)
assert len(scorer_call_events) == 3
event_params = [json.loads(event["data"]["params"]) for event in scorer_call_events]
# Validate response-level scorer was called twice (once per trace)
response_level_events = [
params
for params in event_params
if params["scorer_class"] == "UserDefinedScorer"
and params["scorer_kind"] == "decorator"
and params["scope"] == "trace"
and params["callsite"] == "genai_evaluate"
and params["has_feedback_error"] is False
]
assert len(response_level_events) == 2
# Validate session-level scorer was called once (once per session)
session_level_events = [
params
for params in event_params
if params["scorer_class"] == "UserDefinedScorer"
and params["scorer_kind"] == "instructions"
and params["scope"] == "session"
and params["callsite"] == "genai_evaluate"
and params["has_feedback_error"] is False
]
assert len(session_level_events) == 1
mock_requests.clear()
@pytest.mark.parametrize(
("job_name", "expected_callsite"),
[
("run_online_trace_scorer", "online_scoring"),
("run_online_session_scorer", "online_scoring"),
# Counterexample: non-online-scoring job should be treated as direct call
("invoke_scorer", "direct_scorer_call"),
],
)
def test_scorer_call_online_scoring_callsite(
mock_requests, mock_telemetry_client: TelemetryClient, monkeypatch, job_name, expected_callsite
):
# Import here to avoid circular imports
from mlflow.server.jobs.utils import MLFLOW_SERVER_JOB_NAME_ENV_VAR
monkeypatch.setenv(MLFLOW_SERVER_JOB_NAME_ENV_VAR, job_name)
@scorer
def custom_scorer(outputs: str) -> bool:
return True
custom_scorer(outputs="test output")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "UserDefinedScorer",
"scorer_kind": "decorator",
"scope": "trace",
"callsite": expected_callsite,
"has_feedback_error": False,
},
)
def test_scorer_call_tracks_feedback_errors(mock_requests, mock_telemetry_client: TelemetryClient):
error_judge = make_judge(
name="quality_judge",
instructions="Evaluate if {{ outputs }} is high quality",
model="openai:/gpt-4",
)
error_feedback = Feedback(
name="quality_judge",
error="Model invocation failed",
source=AssessmentSource(
source_type=AssessmentSourceType.LLM_JUDGE, source_id="openai:/gpt-4"
),
)
with mock.patch(
"mlflow.genai.judges.instructions_judge.invoke_judge_model",
return_value=error_feedback,
):
result = error_judge(outputs="test output")
assert result.error is not None
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "UserDefinedScorer",
"scorer_kind": "instructions",
"scope": "trace",
"callsite": "direct_scorer_call",
"has_feedback_error": True,
},
)
mock_requests.clear()
# Test Scorer returns list of Feedback with mixed errors
@scorer
def multi_feedback_scorer(outputs) -> list[Feedback]:
return [
Feedback(name="feedback1", value=1.0),
Feedback(name="feedback2", error=ValueError("Error in feedback 2")),
Feedback(name="feedback3", value=0.5),
]
multi_feedback_scorer(outputs="test")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "UserDefinedScorer",
"scorer_kind": "decorator",
"scope": "trace",
"callsite": "direct_scorer_call",
"has_feedback_error": True,
},
)
mock_requests.clear()
# Test Scorer returns primitive type (no Feedback error possible)
@scorer
def primitive_scorer(outputs) -> bool:
return True
primitive_scorer(outputs="test")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "UserDefinedScorer",
"scorer_kind": "decorator",
"scope": "trace",
"callsite": "direct_scorer_call",
"has_feedback_error": False,
},
)
def test_scorer_call_wrapped_builtin_scorer_direct(
mock_requests, mock_telemetry_client: TelemetryClient
):
completeness_scorer = Completeness()
mock_feedback = Feedback(
name="completeness",
value="yes",
rationale="Test rationale",
)
with mock.patch(
"mlflow.genai.judges.instructions_judge.invoke_judge_model",
return_value=mock_feedback,
):
completeness_scorer(inputs={"question": "What is MLflow?"}, outputs="MLflow is a platform")
mock_telemetry_client.flush()
# Verify exactly 1 scorer_call event was created
# (only top-level Completeness, not nested InstructionsJudge)
scorer_call_events = [
record for record in mock_requests if record["data"]["event_name"] == ScorerCallEvent.name
]
assert len(scorer_call_events) == 1, (
f"Expected 1 scorer call event for Completeness scorer (nested calls should be skipped), "
f"got {len(scorer_call_events)}"
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "Completeness",
"scorer_kind": "builtin",
"scope": "trace",
"callsite": "direct_scorer_call",
"has_feedback_error": False,
},
)
def test_scorer_call_wrapped_builtin_scorer_from_genai_evaluate(
mock_requests, mock_telemetry_client: TelemetryClient
):
user_frustration_scorer = UserFrustration()
@mlflow.trace(span_type=mlflow.entities.SpanType.CHAT_MODEL)
def model(question, session_id):
mlflow.update_current_trace(metadata={"mlflow.trace.session": session_id})
return f"Answer to: {question}"
model("What is MLflow?", session_id="test_session")
trace_1 = mlflow.get_trace(mlflow.get_last_active_trace_id())
model("How does MLflow work?", session_id="test_session")
trace_2 = mlflow.get_trace(mlflow.get_last_active_trace_id())
test_data = pd.DataFrame([
{"trace": trace_1},
{"trace": trace_2},
])
mock_feedback = Feedback(
name="user_frustration",
value="no",
rationale="Test rationale",
)
with mock.patch(
"mlflow.genai.judges.instructions_judge.invoke_judge_model",
return_value=mock_feedback,
):
mlflow.genai.evaluate(data=test_data, scorers=[user_frustration_scorer])
mock_telemetry_client.flush()
# Verify exactly 1 scorer_call event was created for the session-level scorer
# (one call at the session level and no nested InstructionsJudge event)
scorer_call_events = [
record for record in mock_requests if record["data"]["event_name"] == ScorerCallEvent.name
]
assert len(scorer_call_events) == 1, (
f"Expected 1 scorer call event for UserFrustration scorer "
f"(nested calls should be skipped), got {len(scorer_call_events)}"
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
ScorerCallEvent.name,
{
"scorer_class": "UserFrustration",
"scorer_kind": "builtin",
"scope": "session",
"callsite": "genai_evaluate",
"has_feedback_error": False,
},
)
def test_gateway_crud_telemetry(mock_requests, mock_telemetry_client: TelemetryClient, tmp_path):
db_path = tmp_path / "mlflow.db"
store = SqlAlchemyStore(f"sqlite:///{db_path}", tmp_path.as_posix())
secret = store.create_gateway_secret(
secret_name="test-secret",
secret_value={"api_key": "test-api-key"},
provider="openai",
created_by="test-user",
)
model_def = store.create_gateway_model_definition(
name="test-model",
provider="openai",
model_name="gpt-4",
secret_id=secret.secret_id,
created_by="test-user",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayCreateModelDefinitionEvent.name,
{"model_name": "gpt-4", "provider": "openai"},
)
model_config = GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=100,
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[model_config],
created_by="test-user",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayCreateEndpointEvent.name,
{
"has_fallback_config": False,
"routing_strategy": None,
"num_model_configs": 1,
"usage_tracking": True,
},
)
store.get_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayGetEndpointEvent.name,
)
store.list_gateway_endpoints()
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayListEndpointsEvent.name,
{"filter_by_provider": False},
)
store.list_gateway_endpoints(provider="openai")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayListEndpointsEvent.name,
{"filter_by_provider": True},
)
store.update_gateway_endpoint(
endpoint_id=endpoint.endpoint_id,
name="updated-endpoint",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayUpdateEndpointEvent.name,
{
"has_fallback_config": False,
"routing_strategy": None,
"num_model_configs": None,
"usage_tracking": None,
},
)
store.delete_gateway_endpoint(endpoint_id=endpoint.endpoint_id)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayDeleteEndpointEvent.name,
)
def test_gateway_secret_crud_telemetry(
mock_requests, mock_telemetry_client: TelemetryClient, tmp_path
):
db_path = tmp_path / "mlflow.db"
store = SqlAlchemyStore(f"sqlite:///{db_path}", tmp_path.as_posix())
secret = store.create_gateway_secret(
secret_name="test-secret",
secret_value={"api_key": "test-api-key"},
provider="openai",
created_by="test-user",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayCreateSecretEvent.name,
{"provider": "openai"},
)
secret2 = store.create_gateway_secret(
secret_name="test-secret-2",
secret_value={"api_key": "test-api-key-2"},
created_by="test-user",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayCreateSecretEvent.name,
{"provider": None},
)
store.list_secret_infos()
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayListSecretsEvent.name,
{"filter_by_provider": False},
)
store.list_secret_infos(provider="openai")
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayListSecretsEvent.name,
{"filter_by_provider": True},
)
store.update_gateway_secret(
secret_id=secret.secret_id,
secret_value={"api_key": "updated-api-key"},
updated_by="test-user",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayUpdateSecretEvent.name,
)
store.delete_gateway_secret(secret_id=secret.secret_id)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayDeleteSecretEvent.name,
)
store.delete_gateway_secret(secret_id=secret2.secret_id)
def test_gateway_budget_policy_crud_telemetry(
mock_requests, mock_telemetry_client: TelemetryClient, tmp_path
):
db_path = tmp_path / "mlflow.db"
store = SqlAlchemyStore(f"sqlite:///{db_path}", tmp_path.as_posix())
policy = store.create_budget_policy(
budget_unit=BudgetUnit.USD,
budget_amount=100.0,
duration=BudgetDuration(unit=BudgetDurationUnit.DAYS, value=30),
target_scope=BudgetTargetScope.GLOBAL,
budget_action=BudgetAction.ALERT,
created_by="test-user",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayCreateBudgetPolicyEvent.name,
{
"budget_unit": "USD",
"duration_unit": "DAYS",
"target_scope": "GLOBAL",
"budget_action": "ALERT",
},
)
store.list_budget_policies()
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayListBudgetPoliciesEvent.name,
)
store.update_budget_policy(
budget_policy_id=policy.budget_policy_id,
budget_amount=200.0,
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayUpdateBudgetPolicyEvent.name,
)
store.delete_budget_policy(budget_policy_id=policy.budget_policy_id)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayDeleteBudgetPolicyEvent.name,
)
def test_gateway_guardrail_crud_telemetry(
mock_requests, mock_telemetry_client: TelemetryClient, tmp_path
):
db_path = tmp_path / "mlflow.db"
store = SqlAlchemyStore(f"sqlite:///{db_path}", tmp_path.as_posix())
secret = store.create_gateway_secret(
secret_name="test-secret",
secret_value={"api_key": "test-api-key"},
provider="openai",
created_by="test-user",
)
model_def = store.create_gateway_model_definition(
name="test-model",
provider="openai",
model_name="gpt-4",
secret_id=secret.secret_id,
created_by="test-user",
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=100,
)
],
created_by="test-user",
usage_tracking=False,
)
scorer_experiment_id = store.create_experiment("guardrail-scorer-exp")
serialized_scorer = json.dumps({
"instructions_judge_pydantic_data": {
"model": "gateway:/test-endpoint",
"instructions": "Is this input safe?",
}
})
scorer = store.register_scorer(
experiment_id=scorer_experiment_id,
name="safety-judge",
serialized_scorer=serialized_scorer,
)
guardrail = store.create_gateway_guardrail(
name="guardrail-1",
scorer_id=scorer.scorer_id,
scorer_version=scorer.scorer_version,
stage=GuardrailStage.BEFORE,
action=GuardrailAction.VALIDATION,
created_by="test-user",
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayCreateGuardrailEvent.name,
{
"stage": "BEFORE",
"action": "VALIDATION",
},
)
# Guardrail update telemetry is emitted by endpoint guardrail config updates.
store.add_guardrail_to_endpoint(endpoint.endpoint_id, guardrail.guardrail_id, execution_order=1)
store.update_endpoint_guardrail_config(
endpoint_id=endpoint.endpoint_id,
guardrail_id=guardrail.guardrail_id,
execution_order=2,
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayUpdateGuardrailEvent.name,
{"stage": None, "action": None},
)
store.delete_gateway_guardrail(guardrail.guardrail_id)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayDeleteGuardrailEvent.name,
)
@pytest.mark.asyncio
async def test_gateway_invocation_telemetry(
mock_requests, mock_telemetry_client: TelemetryClient, tmp_path
):
db_path = tmp_path / "mlflow.db"
store = SqlAlchemyStore(f"sqlite:///{db_path}", tmp_path.as_posix())
secret = store.create_gateway_secret(
secret_name="test-secret",
secret_value={"api_key": "test-api-key"},
provider="openai",
created_by="test-user",
)
mock_telemetry_client.flush()
mock_requests.clear()
model_def = store.create_gateway_model_definition(
name="test-model",
provider="openai",
model_name="gpt-4",
secret_id=secret.secret_id,
created_by="test-user",
)
endpoint = store.create_gateway_endpoint(
name="test-endpoint",
model_configs=[
GatewayEndpointModelConfig(
model_definition_id=model_def.model_definition_id,
linkage_type=GatewayModelLinkageType.PRIMARY,
weight=100,
)
],
created_by="test-user",
)
mock_telemetry_client.flush()
mock_requests.clear()
mock_response = chat.ResponsePayload(
id="test-id",
object="chat.completion",
created=1234567890,
model="gpt-4",
choices=[
chat.Choice(
index=0,
message=chat.ResponseMessage(role="assistant", content="Hello!"),
finish_reason="stop",
)
],
usage=chat.ChatUsage(prompt_tokens=10, completion_tokens=5, total_tokens=15),
)
mock_model = GatewayModelConfig(
model_definition_id="test-model-def",
provider="openai",
model_name="gpt-4",
secret_value={"api_key": "test"},
linkage_type=GatewayModelLinkageType.PRIMARY,
)
# Test invocations endpoint (chat)
mock_request = MagicMock(spec=Request)
mock_request.headers = {}
mock_request.json = AsyncMock(
return_value={
"messages": [{"role": "user", "content": "Hi"}],
"temperature": 0.7,
"stream": False,
}
)
with (
patch("mlflow.server.gateway_api._get_store", return_value=store),
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
):
mock_provider = MagicMock()
mock_provider.chat = AsyncMock(return_value=mock_response)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id,
endpoint_name=endpoint.name,
models=[mock_model],
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
await invocations(endpoint.name, mock_request)
data = validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayInvocationEvent.name,
check_params=False,
)
params = json.loads(data["params"])
assert params["is_streaming"] is False
assert params["invocation_type"] == "mlflow_invocations"
assert params["has_traceparent"] is False
assert params["auth_enabled"] is False
assert params["endpoint_id"] == endpoint.endpoint_id
assert params["provider"] == "openai"
# Non-streaming includes timing fields
assert "provider_duration_ms" in params
assert "gateway_overhead_ms" in params
# Test chat_completions endpoint
mock_request = MagicMock(spec=Request)
mock_request.headers = {}
mock_request.json = AsyncMock(
return_value={
"model": endpoint.name,
"messages": [{"role": "user", "content": "Hi"}],
"temperature": 0.7,
"stream": False,
}
)
with (
patch("mlflow.server.gateway_api._get_store", return_value=store),
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
):
mock_provider = MagicMock()
mock_provider.chat = AsyncMock(return_value=mock_response)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id,
endpoint_name=endpoint.name,
models=[mock_model],
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
await chat_completions(mock_request)
data = validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayInvocationEvent.name,
check_params=False,
)
params = json.loads(data["params"])
assert params["is_streaming"] is False
assert params["invocation_type"] == "mlflow_chat_completions"
assert params["endpoint_id"] == endpoint.endpoint_id
assert params["provider"] == "openai"
# Test streaming invocation — timing fields should be absent
mock_request = MagicMock(spec=Request)
mock_request.headers = {}
mock_request.json = AsyncMock(
return_value={
"model": endpoint.name,
"messages": [{"role": "user", "content": "Hi"}],
"stream": True,
}
)
async def mock_stream():
yield chat.StreamResponsePayload(
id="test-id",
object="chat.completion.chunk",
created=1234567890,
model="gpt-4",
choices=[
chat.StreamChoice(
index=0,
delta=chat.StreamDelta(role="assistant", content="Hello"),
finish_reason=None,
)
],
)
with (
patch("mlflow.server.gateway_api._get_store", return_value=store),
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
):
mock_provider = MagicMock()
mock_provider.chat_stream = MagicMock(return_value=mock_stream())
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id,
endpoint_name=endpoint.name,
models=[mock_model],
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
await chat_completions(mock_request)
data = validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayInvocationEvent.name,
check_params=False,
)
params = json.loads(data["params"])
assert params["is_streaming"] is True
assert params["invocation_type"] == "mlflow_chat_completions"
# Streaming responses should NOT include timing fields
assert "provider_duration_ms" not in params
assert "gateway_overhead_ms" not in params
# Test that caller header and traceparent are included in telemetry when present
mock_request = MagicMock(spec=Request)
mock_request.json = AsyncMock(
return_value={
"model": endpoint.name,
"messages": [{"role": "user", "content": "Hi"}],
"stream": False,
}
)
mock_request.headers = {MLFLOW_GATEWAY_CALLER_HEADER: "judge", "traceparent": "00-abc-def-01"}
mock_auth_module = MagicMock()
mock_auth_module.is_auth_enabled = MagicMock(return_value=True)
with (
patch("mlflow.server.gateway_api._get_store", return_value=store),
patch(
"mlflow.server.gateway_api._create_provider_from_endpoint_name"
) as mock_create_provider,
patch.dict("sys.modules", {"mlflow.server.auth": mock_auth_module}),
):
mock_provider = MagicMock()
mock_provider.chat = AsyncMock(return_value=mock_response)
mock_endpoint_config = GatewayEndpointConfig(
endpoint_id=endpoint.endpoint_id,
endpoint_name=endpoint.name,
models=[mock_model],
)
mock_create_provider.return_value = (mock_provider, mock_endpoint_config)
await chat_completions(mock_request)
data = validate_telemetry_record(
mock_telemetry_client,
mock_requests,
GatewayInvocationEvent.name,
check_params=False,
)
params = json.loads(data["params"])
assert params["is_streaming"] is False
assert params["invocation_type"] == "mlflow_chat_completions"
assert params["caller"] == "judge"
assert params["has_traceparent"] is True
assert params["auth_enabled"] is True
def test_tracing_context_propagation_get_and_set_success(
mock_requests, mock_telemetry_client: TelemetryClient
):
with mock.patch(
"mlflow.telemetry.track.get_telemetry_client", return_value=mock_telemetry_client
):
with mlflow.start_span("client span"):
headers = get_tracing_context_headers_for_http_request()
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
TracingContextPropagation.name,
)
with mock.patch(
"mlflow.telemetry.track.get_telemetry_client", return_value=mock_telemetry_client
):
with set_tracing_context_from_http_request_headers(headers):
with mlflow.start_span("server span"):
pass
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
TracingContextPropagation.name,
)
def test_update_issue_telemetry(mock_requests, mock_telemetry_client: TelemetryClient, db_uri):
store = SqlAlchemyStore(db_uri, "/tmp")
exp_id = store.create_experiment("test-exp")
issue = store.create_issue(
experiment_id=exp_id,
name="Original name",
description="Original description",
status=IssueStatus.PENDING,
)
mock_telemetry_client.flush()
mock_requests.clear()
store.update_issue(
issue_id=issue.issue_id,
status=IssueStatus.RESOLVED,
name="Updated name",
description="Updated description",
severity=IssueSeverity.HIGH,
)
validate_telemetry_record(
mock_telemetry_client,
mock_requests,
UpdateIssueEvent.name,
{
"status": "resolved",
"has_name": True,
"has_description": True,
"severity": "high",
"source_run_id": None,
},
)